2026-01-08 00:36:00 -05:00
|
|
|
#include "audiopreviewwidget.h"
|
|
|
|
|
#include <QHeaderView>
|
|
|
|
|
#include <QtEndian>
|
|
|
|
|
#include <QPainter>
|
|
|
|
|
#include <QPen>
|
|
|
|
|
#include <QDir>
|
|
|
|
|
#include <QShowEvent>
|
|
|
|
|
#include <QResizeEvent>
|
|
|
|
|
#include <QTimer>
|
|
|
|
|
|
|
|
|
|
AudioPreviewWidget::AudioPreviewWidget(QWidget *parent)
|
|
|
|
|
: QWidget(parent)
|
|
|
|
|
, mPlayer(new QMediaPlayer(this))
|
|
|
|
|
, mAudioOutput(new QAudioOutput(this))
|
|
|
|
|
, mAudioBuffer(nullptr)
|
|
|
|
|
, mPositionTimer(new QTimer(this))
|
|
|
|
|
, mDuration(0)
|
|
|
|
|
, mCalculatedDuration(0)
|
|
|
|
|
, mSampleRate(0)
|
|
|
|
|
, mChannels(0)
|
|
|
|
|
, mBitsPerSample(0)
|
|
|
|
|
, mDataSize(0)
|
|
|
|
|
, mAudioFormat(0)
|
|
|
|
|
, mBigEndian(false)
|
|
|
|
|
{
|
|
|
|
|
mPlayer->setAudioOutput(mAudioOutput);
|
|
|
|
|
mAudioOutput->setVolume(0.5);
|
|
|
|
|
|
|
|
|
|
// Timer for updating position slider during playback
|
|
|
|
|
mPositionTimer->setInterval(50); // 50ms = 20 updates per second
|
|
|
|
|
connect(mPositionTimer, &QTimer::timeout, this, &AudioPreviewWidget::onUpdatePosition);
|
|
|
|
|
|
|
|
|
|
// Create UI
|
|
|
|
|
auto *mainLayout = new QVBoxLayout(this);
|
|
|
|
|
mainLayout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
|
|
|
|
|
|
// Splitter for preview and metadata
|
|
|
|
|
auto *splitter = new QSplitter(Qt::Horizontal, this);
|
|
|
|
|
|
|
|
|
|
// Left side - audio controls
|
|
|
|
|
auto *controlsWidget = new QWidget(splitter);
|
|
|
|
|
controlsWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
|
|
|
|
|
auto *controlsLayout = new QVBoxLayout(controlsWidget);
|
|
|
|
|
controlsLayout->setContentsMargins(4, 4, 4, 4);
|
|
|
|
|
|
|
|
|
|
mFilenameLabel = new QLabel(this);
|
|
|
|
|
mFilenameLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
|
|
|
|
mFilenameLabel->setStyleSheet("QLabel { background-color: #252526; color: #888; padding: 4px 8px; font-size: 11px; }");
|
|
|
|
|
controlsLayout->addWidget(mFilenameLabel);
|
|
|
|
|
|
|
|
|
|
// Waveform display - expands vertically
|
|
|
|
|
mWaveformLabel = new QLabel(this);
|
|
|
|
|
mWaveformLabel->setMinimumHeight(80);
|
|
|
|
|
mWaveformLabel->setMinimumWidth(400);
|
|
|
|
|
mWaveformLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
|
|
|
|
mWaveformLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
|
|
|
|
mWaveformLabel->setStyleSheet("QLabel { background-color: #1e1e1e; border: 1px solid #333; }");
|
|
|
|
|
controlsLayout->addWidget(mWaveformLabel, 1);
|
|
|
|
|
|
|
|
|
|
// Time and position
|
|
|
|
|
auto *positionLayout = new QHBoxLayout();
|
|
|
|
|
mTimeLabel = new QLabel("00:00 / 00:00", this);
|
|
|
|
|
mPositionSlider = new QSlider(Qt::Horizontal, this);
|
|
|
|
|
mPositionSlider->setRange(0, 0);
|
|
|
|
|
mPositionSlider->setSingleStep(100); // 100ms per arrow key
|
|
|
|
|
mPositionSlider->setPageStep(1000); // 1 second per page up/down
|
|
|
|
|
mPositionSlider->setTracking(true); // Update while dragging
|
|
|
|
|
positionLayout->addWidget(mPositionSlider, 1);
|
|
|
|
|
positionLayout->addWidget(mTimeLabel);
|
|
|
|
|
controlsLayout->addLayout(positionLayout);
|
|
|
|
|
|
|
|
|
|
// Playback controls
|
|
|
|
|
auto *buttonLayout = new QHBoxLayout();
|
|
|
|
|
mPlayButton = new QPushButton("Play", this);
|
|
|
|
|
mStopButton = new QPushButton("Stop", this);
|
|
|
|
|
mVolumeSlider = new QSlider(Qt::Horizontal, this);
|
|
|
|
|
mVolumeSlider->setRange(0, 100);
|
|
|
|
|
mVolumeSlider->setValue(50);
|
|
|
|
|
mVolumeSlider->setMaximumWidth(100);
|
|
|
|
|
|
|
|
|
|
auto *volumeLabel = new QLabel("Vol:", this);
|
|
|
|
|
buttonLayout->addWidget(mPlayButton);
|
|
|
|
|
buttonLayout->addWidget(mStopButton);
|
|
|
|
|
buttonLayout->addStretch();
|
|
|
|
|
buttonLayout->addWidget(volumeLabel);
|
|
|
|
|
buttonLayout->addWidget(mVolumeSlider);
|
|
|
|
|
controlsLayout->addLayout(buttonLayout);
|
|
|
|
|
|
|
|
|
|
splitter->addWidget(controlsWidget);
|
|
|
|
|
|
|
|
|
|
// Right side - metadata tree
|
|
|
|
|
mMetadataTree = new QTreeWidget(splitter);
|
|
|
|
|
mMetadataTree->setHeaderLabels({"Property", "Value"});
|
|
|
|
|
mMetadataTree->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
|
|
|
|
|
mMetadataTree->header()->setSectionResizeMode(1, QHeaderView::Stretch);
|
|
|
|
|
mMetadataTree->setAlternatingRowColors(true);
|
|
|
|
|
splitter->addWidget(mMetadataTree);
|
|
|
|
|
|
|
|
|
|
splitter->setSizes({400, 200});
|
|
|
|
|
mainLayout->addWidget(splitter);
|
|
|
|
|
|
|
|
|
|
// Connect signals
|
|
|
|
|
connect(mPlayButton, &QPushButton::clicked, this, &AudioPreviewWidget::onPlayPause);
|
|
|
|
|
connect(mStopButton, &QPushButton::clicked, this, &AudioPreviewWidget::onStop);
|
|
|
|
|
connect(mPositionSlider, &QSlider::sliderMoved, this, &AudioPreviewWidget::onSliderMoved);
|
|
|
|
|
connect(mVolumeSlider, &QSlider::valueChanged, this, [this](int value) {
|
|
|
|
|
mAudioOutput->setVolume(value / 100.0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
connect(mPlayer, &QMediaPlayer::positionChanged, this, &AudioPreviewWidget::onPositionChanged);
|
|
|
|
|
connect(mPlayer, &QMediaPlayer::durationChanged, this, &AudioPreviewWidget::onDurationChanged);
|
|
|
|
|
connect(mPlayer, &QMediaPlayer::mediaStatusChanged, this, &AudioPreviewWidget::onMediaStatusChanged);
|
|
|
|
|
|
|
|
|
|
// Connect to theme changes
|
|
|
|
|
connect(&Settings::instance(), &Settings::themeChanged, this, &AudioPreviewWidget::applyTheme);
|
|
|
|
|
|
|
|
|
|
// Apply current theme
|
|
|
|
|
applyTheme(Settings::instance().theme());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AudioPreviewWidget::~AudioPreviewWidget()
|
|
|
|
|
{
|
|
|
|
|
mPlayer->stop();
|
|
|
|
|
if (mAudioBuffer) {
|
|
|
|
|
mAudioBuffer->close();
|
|
|
|
|
delete mAudioBuffer;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AudioPreviewWidget::parseWavHeader(const QByteArray &data)
|
|
|
|
|
{
|
|
|
|
|
if (data.size() < 44) return;
|
|
|
|
|
|
|
|
|
|
const uchar *d = reinterpret_cast<const uchar*>(data.constData());
|
|
|
|
|
|
|
|
|
|
// Check RIFF header - RIFF = little-endian, RIFX = big-endian
|
|
|
|
|
mBigEndian = false;
|
|
|
|
|
if (data.left(4) == "RIFX") {
|
|
|
|
|
mBigEndian = true;
|
|
|
|
|
} else if (data.left(4) != "RIFF") {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (data.mid(8, 4) != "WAVE") return;
|
|
|
|
|
|
|
|
|
|
// Helper lambdas for endian-aware reading
|
|
|
|
|
auto readU16 = [d, this](int offset) -> quint16 {
|
|
|
|
|
return mBigEndian ? qFromBigEndian<quint16>(d + offset) : qFromLittleEndian<quint16>(d + offset);
|
|
|
|
|
};
|
|
|
|
|
auto readU32 = [d, this](int offset) -> quint32 {
|
|
|
|
|
return mBigEndian ? qFromBigEndian<quint32>(d + offset) : qFromLittleEndian<quint32>(d + offset);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Find fmt chunk
|
|
|
|
|
int pos = 12;
|
|
|
|
|
while (pos < data.size() - 8) {
|
|
|
|
|
QString chunkId = QString::fromLatin1(data.mid(pos, 4));
|
|
|
|
|
quint32 chunkSize = readU32(pos + 4);
|
|
|
|
|
|
|
|
|
|
if (chunkId == "fmt ") {
|
|
|
|
|
if (static_cast<qint64>(pos) + 8 + chunkSize <= data.size()) {
|
|
|
|
|
mAudioFormat = readU16(pos + 8);
|
|
|
|
|
mChannels = readU16(pos + 10);
|
|
|
|
|
mSampleRate = readU32(pos + 12);
|
|
|
|
|
mBitsPerSample = readU16(pos + 22);
|
|
|
|
|
}
|
|
|
|
|
} else if (chunkId == "data") {
|
|
|
|
|
mDataSize = chunkSize;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pos += 8 + chunkSize;
|
|
|
|
|
if (chunkSize % 2) pos++; // Padding
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool AudioPreviewWidget::loadFromData(const QByteArray &data, const QString &filename)
|
|
|
|
|
{
|
|
|
|
|
mFilename = filename;
|
|
|
|
|
mFilenameLabel->setText(filename);
|
|
|
|
|
mAudioData = data;
|
|
|
|
|
|
|
|
|
|
// Parse WAV header for info
|
|
|
|
|
parseWavHeader(data);
|
|
|
|
|
|
|
|
|
|
// Add WAV info to metadata tree
|
|
|
|
|
mMetadataTree->clear();
|
|
|
|
|
|
|
|
|
|
auto *formatItem = new QTreeWidgetItem(mMetadataTree);
|
|
|
|
|
formatItem->setText(0, "Format");
|
|
|
|
|
QString formatStr = mBigEndian ? "WAV (RIFX/BE)" : "WAV (RIFF/LE)";
|
|
|
|
|
formatItem->setText(1, formatStr);
|
|
|
|
|
|
|
|
|
|
if (mAudioFormat > 0) {
|
|
|
|
|
auto *codecItem = new QTreeWidgetItem(mMetadataTree);
|
|
|
|
|
codecItem->setText(0, "Audio Codec");
|
|
|
|
|
QString codecStr;
|
|
|
|
|
switch (mAudioFormat) {
|
|
|
|
|
case 1: codecStr = "PCM"; break;
|
|
|
|
|
case 2: codecStr = "MS ADPCM"; break;
|
|
|
|
|
case 6: codecStr = "A-law"; break;
|
|
|
|
|
case 7: codecStr = "u-law"; break;
|
|
|
|
|
case 17: codecStr = "IMA ADPCM"; break;
|
|
|
|
|
case 85: codecStr = "MP3"; break;
|
|
|
|
|
case 0x165: codecStr = "XMA"; break;
|
|
|
|
|
case 0x166: codecStr = "XMA2"; break;
|
|
|
|
|
default: codecStr = QString("0x%1").arg(mAudioFormat, 0, 16); break;
|
|
|
|
|
}
|
|
|
|
|
codecItem->setText(1, codecStr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mSampleRate > 0) {
|
|
|
|
|
auto *srItem = new QTreeWidgetItem(mMetadataTree);
|
|
|
|
|
srItem->setText(0, "Sample Rate");
|
|
|
|
|
srItem->setText(1, QString("%1 Hz").arg(mSampleRate));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mChannels > 0) {
|
|
|
|
|
auto *chItem = new QTreeWidgetItem(mMetadataTree);
|
|
|
|
|
chItem->setText(0, "Channels");
|
|
|
|
|
chItem->setText(1, mChannels == 1 ? "Mono" : (mChannels == 2 ? "Stereo" : QString::number(mChannels)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mBitsPerSample > 0) {
|
|
|
|
|
auto *bpsItem = new QTreeWidgetItem(mMetadataTree);
|
|
|
|
|
bpsItem->setText(0, "Bit Depth");
|
|
|
|
|
bpsItem->setText(1, QString("%1-bit").arg(mBitsPerSample));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mDataSize > 0) {
|
|
|
|
|
auto *sizeItem = new QTreeWidgetItem(mMetadataTree);
|
|
|
|
|
sizeItem->setText(0, "Data Size");
|
|
|
|
|
sizeItem->setText(1, QString("%1 bytes").arg(mDataSize));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculate duration from WAV header data
|
|
|
|
|
mCalculatedDuration = 0;
|
|
|
|
|
// Always try to play - QMediaPlayer/Windows Media Foundation may support more formats
|
|
|
|
|
bool canPlay = true;
|
|
|
|
|
|
|
|
|
|
if (mSampleRate > 0 && mChannels > 0 && mDataSize > 0 && mBitsPerSample > 0) {
|
|
|
|
|
// Calculate duration assuming PCM - many "XMA2" labeled files contain PCM data
|
|
|
|
|
int bytesPerSample = mBitsPerSample / 8;
|
|
|
|
|
mCalculatedDuration = static_cast<double>(mDataSize) / (mSampleRate * mChannels * bytesPerSample);
|
|
|
|
|
|
|
|
|
|
auto *durItem = new QTreeWidgetItem(mMetadataTree);
|
|
|
|
|
durItem->setText(0, "Duration");
|
|
|
|
|
int mins = static_cast<int>(mCalculatedDuration) / 60;
|
|
|
|
|
int secs = static_cast<int>(mCalculatedDuration) % 60;
|
|
|
|
|
int ms = static_cast<int>((mCalculatedDuration - static_cast<int>(mCalculatedDuration)) * 1000);
|
|
|
|
|
durItem->setText(1, QString("%1:%2.%3")
|
|
|
|
|
.arg(mins, 2, 10, QChar('0'))
|
|
|
|
|
.arg(secs, 2, 10, QChar('0'))
|
|
|
|
|
.arg(ms, 3, 10, QChar('0')));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Disable playback for unsupported formats
|
|
|
|
|
if (!canPlay) {
|
|
|
|
|
mPlayButton->setEnabled(false);
|
|
|
|
|
mPlayButton->setText("N/A");
|
|
|
|
|
mPlayButton->setToolTip("XMA/XMA2 playback not supported");
|
|
|
|
|
mStopButton->setEnabled(false);
|
|
|
|
|
mPositionSlider->setEnabled(false);
|
|
|
|
|
} else {
|
|
|
|
|
mPlayButton->setEnabled(true);
|
|
|
|
|
mPlayButton->setText("Play");
|
|
|
|
|
mPlayButton->setToolTip("");
|
|
|
|
|
mStopButton->setEnabled(true);
|
|
|
|
|
mPositionSlider->setEnabled(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto *fileSizeItem = new QTreeWidgetItem(mMetadataTree);
|
|
|
|
|
fileSizeItem->setText(0, "File Size");
|
|
|
|
|
fileSizeItem->setText(1, QString("%1 bytes").arg(data.size()));
|
|
|
|
|
|
|
|
|
|
// Debug: show first 32 bytes as hex (includes fmt chunk start)
|
|
|
|
|
auto *headerItem = new QTreeWidgetItem(mMetadataTree);
|
|
|
|
|
headerItem->setText(0, "Header (hex)");
|
|
|
|
|
QString headerHex;
|
|
|
|
|
for (int i = 0; i < qMin(32, data.size()); i++) {
|
|
|
|
|
headerHex += QString("%1 ").arg(static_cast<uchar>(data[i]), 2, 16, QChar('0'));
|
|
|
|
|
}
|
|
|
|
|
headerItem->setText(1, headerHex.trimmed());
|
|
|
|
|
|
|
|
|
|
// Debug: show raw format value
|
|
|
|
|
auto *rawFmtItem = new QTreeWidgetItem(mMetadataTree);
|
|
|
|
|
rawFmtItem->setText(0, "Raw Format");
|
|
|
|
|
rawFmtItem->setText(1, QString("0x%1 (%2)").arg(mAudioFormat, 4, 16, QChar('0')).arg(mAudioFormat));
|
|
|
|
|
|
|
|
|
|
// Setup audio buffer for playback
|
|
|
|
|
if (mAudioBuffer) {
|
|
|
|
|
mAudioBuffer->close();
|
|
|
|
|
delete mAudioBuffer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mAudioBuffer = new QBuffer(this);
|
|
|
|
|
mAudioBuffer->setData(mAudioData);
|
|
|
|
|
mAudioBuffer->open(QIODevice::ReadOnly);
|
|
|
|
|
|
|
|
|
|
mPlayer->setSourceDevice(mAudioBuffer);
|
|
|
|
|
|
|
|
|
|
// Initialize slider and time display with calculated duration (authoritative)
|
|
|
|
|
if (mCalculatedDuration > 0) {
|
|
|
|
|
mDuration = static_cast<qint64>(mCalculatedDuration * 1000);
|
|
|
|
|
mPositionSlider->setRange(0, static_cast<int>(mDuration));
|
|
|
|
|
mPositionSlider->setValue(0);
|
|
|
|
|
}
|
|
|
|
|
updateTimeLabel();
|
|
|
|
|
|
|
|
|
|
// Draw waveform visualization
|
|
|
|
|
drawWaveform();
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AudioPreviewWidget::drawWaveform()
|
|
|
|
|
{
|
|
|
|
|
if (mAudioData.isEmpty()) return;
|
|
|
|
|
|
|
|
|
|
// Use actual label size for dynamic sizing
|
|
|
|
|
int width = qMax(400, mWaveformLabel->width());
|
|
|
|
|
int height = qMax(80, mWaveformLabel->height());
|
|
|
|
|
|
|
|
|
|
mWaveformPixmap = QPixmap(width, height);
|
|
|
|
|
mWaveformPixmap.fill(mBgColor);
|
|
|
|
|
|
|
|
|
|
QPainter painter(&mWaveformPixmap);
|
|
|
|
|
painter.setRenderHint(QPainter::Antialiasing);
|
|
|
|
|
|
|
|
|
|
int centerY = height / 2;
|
|
|
|
|
int amplitudeHeight = height / 4; // Half of available space (25% above + 25% below center)
|
|
|
|
|
|
|
|
|
|
// Draw center line
|
|
|
|
|
painter.setPen(mBorderColor);
|
|
|
|
|
painter.drawLine(0, centerY, width, centerY);
|
|
|
|
|
|
|
|
|
|
// Find data section
|
|
|
|
|
int dataStart = 44; // Standard WAV header
|
|
|
|
|
if (mAudioData.size() > dataStart)
|
|
|
|
|
{
|
|
|
|
|
const qint16 *samples = reinterpret_cast<const qint16*>(mAudioData.constData() + dataStart);
|
|
|
|
|
int numSamples = (mAudioData.size() - dataStart) / 2;
|
|
|
|
|
int samplesPerPixel = qMax(1, numSamples / width);
|
|
|
|
|
|
|
|
|
|
painter.setPen(mAccentColor); // Theme accent color
|
|
|
|
|
|
|
|
|
|
for (int x = 0; x < width && x * samplesPerPixel < numSamples; x++)
|
|
|
|
|
{
|
|
|
|
|
qint16 minVal = 0, maxVal = 0;
|
|
|
|
|
for (int i = 0; i < samplesPerPixel && (x * samplesPerPixel + i) < numSamples; i++)
|
|
|
|
|
{
|
|
|
|
|
qint16 sample = samples[x * samplesPerPixel + i];
|
|
|
|
|
minVal = qMin(minVal, sample);
|
|
|
|
|
maxVal = qMax(maxVal, sample);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Scale to full amplitude height
|
|
|
|
|
int yMin = centerY - (maxVal * amplitudeHeight / 32768);
|
|
|
|
|
int yMax = centerY - (minVal * amplitudeHeight / 32768);
|
|
|
|
|
painter.drawLine(x, yMin, x, yMax);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
painter.end();
|
|
|
|
|
updateWaveformPosition();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AudioPreviewWidget::applyTheme(const Theme &theme)
|
|
|
|
|
{
|
|
|
|
|
mAccentColor = QColor(theme.accentColor);
|
|
|
|
|
mBgColor = QColor(theme.backgroundColor);
|
|
|
|
|
mPanelColor = QColor(theme.panelColor);
|
|
|
|
|
mTextColor = QColor(theme.textColor);
|
|
|
|
|
mTextColorMuted = QColor(theme.textColorMuted);
|
|
|
|
|
mBorderColor = QColor(theme.borderColor);
|
|
|
|
|
|
|
|
|
|
// Update UI element styles
|
|
|
|
|
mFilenameLabel->setStyleSheet(QString(
|
|
|
|
|
"QLabel { background-color: %1; color: %2; padding: 4px 8px; font-size: 11px; }"
|
|
|
|
|
).arg(theme.panelColor, theme.textColorMuted));
|
|
|
|
|
|
|
|
|
|
mWaveformLabel->setStyleSheet(QString(
|
|
|
|
|
"QLabel { background-color: %1; border: 1px solid %2; }"
|
|
|
|
|
).arg(theme.backgroundColor, theme.borderColor));
|
|
|
|
|
|
|
|
|
|
mMetadataTree->setStyleSheet(QString(
|
|
|
|
|
"QTreeWidget { background-color: %1; color: %2; border: none; }"
|
|
|
|
|
"QTreeWidget::item:selected { background-color: %3; color: white; }"
|
|
|
|
|
"QTreeWidget::item:alternate { background-color: %4; }"
|
|
|
|
|
"QHeaderView::section { background-color: %4; color: %5; padding: 4px; border: none; }"
|
|
|
|
|
).arg(theme.backgroundColor, theme.textColor, theme.accentColor, theme.panelColor, theme.textColorMuted));
|
|
|
|
|
|
|
|
|
|
// Redraw waveform with new colors
|
|
|
|
|
if (!mAudioData.isEmpty()) {
|
|
|
|
|
drawWaveform();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AudioPreviewWidget::updateWaveformPosition()
|
|
|
|
|
{
|
|
|
|
|
if (mWaveformPixmap.isNull()) return;
|
|
|
|
|
|
|
|
|
|
QPixmap displayPixmap = mWaveformPixmap.copy();
|
|
|
|
|
QPainter painter(&displayPixmap);
|
|
|
|
|
|
|
|
|
|
// Use calculated duration as authoritative source (in ms)
|
|
|
|
|
qint64 durationMs = static_cast<qint64>(mCalculatedDuration * 1000);
|
|
|
|
|
if (durationMs <= 0) durationMs = mDuration;
|
|
|
|
|
|
|
|
|
|
// Draw playback position line
|
|
|
|
|
if (durationMs > 0)
|
|
|
|
|
{
|
|
|
|
|
qint64 pos = mPlayer->position();
|
|
|
|
|
double progress = static_cast<double>(pos) / static_cast<double>(durationMs);
|
|
|
|
|
int xPos = static_cast<int>(progress * displayPixmap.width());
|
|
|
|
|
|
|
|
|
|
// Use lighter version of accent color for position line
|
|
|
|
|
painter.setPen(QPen(mAccentColor.lighter(140), 3));
|
|
|
|
|
painter.drawLine(xPos, 0, xPos, displayPixmap.height());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
painter.end();
|
|
|
|
|
|
|
|
|
|
// Scale to fill the label
|
|
|
|
|
int targetWidth = mWaveformLabel->width();
|
|
|
|
|
int targetHeight = mWaveformLabel->height();
|
|
|
|
|
if (targetWidth < 100) targetWidth = 400;
|
|
|
|
|
if (targetHeight < 50) targetHeight = 120;
|
|
|
|
|
mWaveformLabel->setPixmap(displayPixmap.scaled(targetWidth, targetHeight, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AudioPreviewWidget::setMetadata(const QVariantMap &metadata)
|
|
|
|
|
{
|
2026-01-11 12:08:59 -05:00
|
|
|
// Add custom metadata from parsed fields (caller provides only visible fields)
|
2026-01-08 00:36:00 -05:00
|
|
|
for (auto it = metadata.begin(); it != metadata.end(); ++it) {
|
|
|
|
|
auto *item = new QTreeWidgetItem(mMetadataTree);
|
|
|
|
|
item->setText(0, it.key());
|
|
|
|
|
|
|
|
|
|
QVariant val = it.value();
|
|
|
|
|
if (val.typeId() == QMetaType::QByteArray) {
|
|
|
|
|
QByteArray ba = val.toByteArray();
|
|
|
|
|
item->setText(1, QString("<%1 bytes>").arg(ba.size()));
|
|
|
|
|
} else {
|
|
|
|
|
item->setText(1, val.toString());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AudioPreviewWidget::onPlayPause()
|
|
|
|
|
{
|
|
|
|
|
if (mPlayer->playbackState() == QMediaPlayer::PlayingState) {
|
|
|
|
|
mPlayer->pause();
|
|
|
|
|
mPlayButton->setText("Play");
|
|
|
|
|
mPositionTimer->stop();
|
|
|
|
|
} else {
|
|
|
|
|
mPlayer->play();
|
|
|
|
|
mPlayButton->setText("Pause");
|
|
|
|
|
mPositionTimer->start();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AudioPreviewWidget::onStop()
|
|
|
|
|
{
|
|
|
|
|
mPlayer->stop();
|
|
|
|
|
mPositionTimer->stop();
|
|
|
|
|
mPlayButton->setText("Play");
|
|
|
|
|
updateWaveformPosition();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AudioPreviewWidget::onPositionChanged(qint64 position)
|
|
|
|
|
{
|
|
|
|
|
if (!mPositionSlider->isSliderDown()) {
|
|
|
|
|
mPositionSlider->setValue(static_cast<int>(position));
|
|
|
|
|
}
|
|
|
|
|
updateTimeLabel();
|
|
|
|
|
updateWaveformPosition();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AudioPreviewWidget::onDurationChanged(qint64 duration)
|
|
|
|
|
{
|
|
|
|
|
// Prefer calculated duration from WAV header as it's more reliable
|
|
|
|
|
if (mCalculatedDuration > 0) {
|
|
|
|
|
mDuration = static_cast<qint64>(mCalculatedDuration * 1000);
|
|
|
|
|
} else if (duration > 0) {
|
|
|
|
|
mDuration = duration;
|
|
|
|
|
}
|
|
|
|
|
mPositionSlider->setRange(0, static_cast<int>(mDuration));
|
|
|
|
|
updateTimeLabel();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AudioPreviewWidget::onSliderMoved(int position)
|
|
|
|
|
{
|
|
|
|
|
mPlayer->setPosition(position);
|
|
|
|
|
updateWaveformPosition();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AudioPreviewWidget::onMediaStatusChanged(QMediaPlayer::MediaStatus status)
|
|
|
|
|
{
|
|
|
|
|
if (status == QMediaPlayer::EndOfMedia) {
|
|
|
|
|
mPositionTimer->stop();
|
|
|
|
|
mPlayButton->setText("Play");
|
|
|
|
|
mPlayer->setPosition(0);
|
|
|
|
|
updateWaveformPosition();
|
|
|
|
|
} else if (status == QMediaPlayer::LoadedMedia) {
|
|
|
|
|
// Set duration from calculated value (authoritative)
|
|
|
|
|
if (mCalculatedDuration > 0) {
|
|
|
|
|
mDuration = static_cast<qint64>(mCalculatedDuration * 1000);
|
|
|
|
|
mPositionSlider->setRange(0, static_cast<int>(mDuration));
|
|
|
|
|
updateTimeLabel();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AudioPreviewWidget::onUpdatePosition()
|
|
|
|
|
{
|
|
|
|
|
// Always update when timer fires - let the timer start/stop control this
|
|
|
|
|
qint64 position = mPlayer->position();
|
|
|
|
|
|
|
|
|
|
// Update slider if not being dragged
|
|
|
|
|
if (!mPositionSlider->isSliderDown()) {
|
|
|
|
|
mPositionSlider->blockSignals(true);
|
|
|
|
|
mPositionSlider->setValue(static_cast<int>(position));
|
|
|
|
|
mPositionSlider->blockSignals(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateTimeLabel();
|
|
|
|
|
updateWaveformPosition();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AudioPreviewWidget::updateTimeLabel()
|
|
|
|
|
{
|
|
|
|
|
qint64 pos = mPlayer->position();
|
|
|
|
|
// Always use calculated duration as authoritative source
|
|
|
|
|
qint64 dur = static_cast<qint64>(mCalculatedDuration * 1000);
|
|
|
|
|
if (dur <= 0) dur = mDuration;
|
|
|
|
|
|
|
|
|
|
// Format: MM:SS.mmm
|
|
|
|
|
QString posStr = QString("%1:%2.%3")
|
|
|
|
|
.arg(pos / 60000, 2, 10, QChar('0'))
|
|
|
|
|
.arg((pos / 1000) % 60, 2, 10, QChar('0'))
|
|
|
|
|
.arg(pos % 1000, 3, 10, QChar('0'));
|
|
|
|
|
|
|
|
|
|
QString durStr = QString("%1:%2.%3")
|
|
|
|
|
.arg(dur / 60000, 2, 10, QChar('0'))
|
|
|
|
|
.arg((dur / 1000) % 60, 2, 10, QChar('0'))
|
|
|
|
|
.arg(dur % 1000, 3, 10, QChar('0'));
|
|
|
|
|
|
|
|
|
|
mTimeLabel->setText(posStr + " / " + durStr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AudioPreviewWidget::showEvent(QShowEvent *event)
|
|
|
|
|
{
|
|
|
|
|
QWidget::showEvent(event);
|
|
|
|
|
// Redraw waveform now that widget is properly sized
|
|
|
|
|
if (!mAudioData.isEmpty()) {
|
|
|
|
|
QTimer::singleShot(0, this, &AudioPreviewWidget::drawWaveform);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AudioPreviewWidget::resizeEvent(QResizeEvent *event)
|
|
|
|
|
{
|
|
|
|
|
QWidget::resizeEvent(event);
|
|
|
|
|
// Redraw waveform at new size
|
|
|
|
|
if (!mAudioData.isEmpty()) {
|
|
|
|
|
drawWaveform();
|
|
|
|
|
}
|
|
|
|
|
}
|