Add audio preview widget
- Waveform visualization with theme accent color - Playback controls (play/pause, stop, position slider) - Time display with milliseconds (MM:SS.mmm) - Position tracking line on waveform - Volume control 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
aa92fe014a
commit
885b4f6ad6
568
app/audiopreviewwidget.cpp
Normal file
568
app/audiopreviewwidget.cpp
Normal file
@ -0,0 +1,568 @@
|
||||
#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)
|
||||
{
|
||||
// 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<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();
|
||||
}
|
||||
}
|
||||
90
app/audiopreviewwidget.h
Normal file
90
app/audiopreviewwidget.h
Normal file
@ -0,0 +1,90 @@
|
||||
#ifndef AUDIOPREVIEWWIDGET_H
|
||||
#define AUDIOPREVIEWWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QSlider>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QMediaPlayer>
|
||||
#include <QAudioOutput>
|
||||
#include <QBuffer>
|
||||
#include <QTreeWidget>
|
||||
#include <QSplitter>
|
||||
#include <QTimer>
|
||||
#include <QColor>
|
||||
|
||||
#include "settings.h"
|
||||
|
||||
class AudioPreviewWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AudioPreviewWidget(QWidget *parent = nullptr);
|
||||
~AudioPreviewWidget();
|
||||
|
||||
bool loadFromData(const QByteArray &data, const QString &filename);
|
||||
void setMetadata(const QVariantMap &metadata);
|
||||
|
||||
private slots:
|
||||
void onPlayPause();
|
||||
void onStop();
|
||||
void onPositionChanged(qint64 position);
|
||||
void onDurationChanged(qint64 duration);
|
||||
void onSliderMoved(int position);
|
||||
void onMediaStatusChanged(QMediaPlayer::MediaStatus status);
|
||||
void onUpdatePosition();
|
||||
void applyTheme(const Theme &theme);
|
||||
|
||||
private:
|
||||
void updateTimeLabel();
|
||||
void parseWavHeader(const QByteArray &data);
|
||||
void drawWaveform();
|
||||
void updateWaveformPosition();
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent *event) override;
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
|
||||
private:
|
||||
QMediaPlayer *mPlayer;
|
||||
QAudioOutput *mAudioOutput;
|
||||
QBuffer *mAudioBuffer;
|
||||
QByteArray mAudioData;
|
||||
QTimer *mPositionTimer;
|
||||
|
||||
// UI elements
|
||||
QLabel *mFilenameLabel;
|
||||
QLabel *mWaveformLabel;
|
||||
QLabel *mTimeLabel;
|
||||
QPushButton *mPlayButton;
|
||||
QPushButton *mStopButton;
|
||||
QSlider *mPositionSlider;
|
||||
QSlider *mVolumeSlider;
|
||||
QTreeWidget *mMetadataTree;
|
||||
|
||||
qint64 mDuration;
|
||||
QString mFilename;
|
||||
QPixmap mWaveformPixmap; // Store base waveform for position overlay
|
||||
double mCalculatedDuration; // Duration in seconds from WAV header
|
||||
|
||||
// WAV info
|
||||
int mSampleRate;
|
||||
int mChannels;
|
||||
int mBitsPerSample;
|
||||
int mDataSize;
|
||||
int mAudioFormat; // 1=PCM, 2=ADPCM, etc.
|
||||
bool mBigEndian;
|
||||
|
||||
// Theme colors
|
||||
QColor mAccentColor;
|
||||
QColor mBgColor;
|
||||
QColor mPanelColor;
|
||||
QColor mTextColor;
|
||||
QColor mTextColorMuted;
|
||||
QColor mBorderColor;
|
||||
};
|
||||
|
||||
#endif // AUDIOPREVIEWWIDGET_H
|
||||
Loading…
x
Reference in New Issue
Block a user