#include "audiopreviewwidget.h" #include #include #include #include #include #include #include #include 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(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(d + offset) : qFromLittleEndian(d + offset); }; auto readU32 = [d, this](int offset) -> quint32 { return mBigEndian ? qFromBigEndian(d + offset) : qFromLittleEndian(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(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(mDataSize) / (mSampleRate * mChannels * bytesPerSample); auto *durItem = new QTreeWidgetItem(mMetadataTree); durItem->setText(0, "Duration"); int mins = static_cast(mCalculatedDuration) / 60; int secs = static_cast(mCalculatedDuration) % 60; int ms = static_cast((mCalculatedDuration - static_cast(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(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(mCalculatedDuration * 1000); mPositionSlider->setRange(0, static_cast(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(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(mCalculatedDuration * 1000); if (durationMs <= 0) durationMs = mDuration; // Draw playback position line if (durationMs > 0) { qint64 pos = mPlayer->position(); double progress = static_cast(pos) / static_cast(durationMs); int xPos = static_cast(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) { // Add custom metadata from parsed fields for (auto it = metadata.begin(); it != metadata.end(); ++it) { if (it.key().startsWith("_")) continue; // Skip internal fields 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(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(mCalculatedDuration * 1000); } else if (duration > 0) { mDuration = duration; } mPositionSlider->setRange(0, static_cast(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(mCalculatedDuration * 1000); mPositionSlider->setRange(0, static_cast(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(position)); mPositionSlider->blockSignals(false); } updateTimeLabel(); updateWaveformPosition(); } void AudioPreviewWidget::updateTimeLabel() { qint64 pos = mPlayer->position(); // Always use calculated duration as authoritative source qint64 dur = static_cast(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(); } }