XPlor/app/audioexportdialog.cpp
njohnson d7488c5fa9 Add comprehensive export system with format-specific dialogs
Implement a unified export system for extracting data from parsed files:

ExportManager (singleton):
- Centralized export handling for all content types
- Content type detection (image, audio, video, text, binary)
- Batch export support with progress tracking

Format-specific export dialogs:
- ImageExportDialog: PNG, JPEG, BMP, TGA with quality options
- AudioExportDialog: WAV, MP3, OGG with FFmpeg integration
- BinaryExportDialog: Raw data export with optional decompression
- BatchExportDialog: Recursive export with filtering options
- Base ExportDialog class for common functionality

Settings additions:
- FFmpeg path configuration with auto-detection
- Search common install locations and PATH

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 20:54:38 -05:00

385 lines
13 KiB
C++

#include "audioexportdialog.h"
#include "settings.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QSlider>
#include <QSpinBox>
#include <QComboBox>
#include <QStackedWidget>
#include <QGroupBox>
#include <QPainter>
#include <QtEndian>
AudioExportDialog::AudioExportDialog(QWidget *parent)
: ExportDialog(ContentType::Audio, parent)
, mWaveformLabel(nullptr)
, mDurationLabel(nullptr)
, mSampleRateLabel(nullptr)
, mChannelsLabel(nullptr)
, mBitDepthLabel(nullptr)
, mSampleRate(44100)
, mChannels(2)
, mBitsPerSample(16)
, mDuration(0)
, mOptionsStack(nullptr)
{
// Populate format combo
for (const QString& fmt : supportedFormats()) {
formatCombo()->addItem(fmt.toUpper());
}
// Set default format from settings
QString defaultFormat = Settings::instance().defaultAudioExportFormat().toUpper();
int index = formatCombo()->findText(defaultFormat);
if (index >= 0) {
formatCombo()->setCurrentIndex(index);
}
setupPreview();
}
void AudioExportDialog::setupPreview()
{
// Create waveform label inside preview container
QVBoxLayout* previewLayout = new QVBoxLayout(previewContainer());
previewLayout->setContentsMargins(0, 0, 0, 0);
mWaveformLabel = new QLabel(previewContainer());
mWaveformLabel->setAlignment(Qt::AlignCenter);
mWaveformLabel->setMinimumSize(280, 100);
mWaveformLabel->setText("No audio loaded");
mWaveformLabel->setStyleSheet("color: #808080;");
previewLayout->addWidget(mWaveformLabel);
// Audio info labels
QHBoxLayout* infoLayout = new QHBoxLayout();
mDurationLabel = new QLabel("Duration: --", this);
mSampleRateLabel = new QLabel("Sample Rate: --", this);
mChannelsLabel = new QLabel("Channels: --", this);
infoLayout->addWidget(mDurationLabel);
infoLayout->addWidget(new QLabel("|", this));
infoLayout->addWidget(mSampleRateLabel);
infoLayout->addWidget(new QLabel("|", this));
infoLayout->addWidget(mChannelsLabel);
infoLayout->addStretch();
previewLayout->addLayout(infoLayout);
// Create stacked widget for format-specific options
mOptionsStack = new QStackedWidget(this);
// WAV options (none)
mWavOptionsWidget = new QWidget(this);
QVBoxLayout* wavLayout = new QVBoxLayout(mWavOptionsWidget);
wavLayout->setContentsMargins(0, 0, 0, 0);
QLabel* wavLabel = new QLabel("WAV: Uncompressed PCM audio.", mWavOptionsWidget);
wavLabel->setStyleSheet("color: #808080;");
wavLayout->addWidget(wavLabel);
wavLayout->addStretch();
// MP3 options
mMp3OptionsWidget = new QWidget(this);
QVBoxLayout* mp3Layout = new QVBoxLayout(mMp3OptionsWidget);
mp3Layout->setContentsMargins(0, 0, 0, 0);
QLabel* bitrateLabel = new QLabel("Bitrate:", mMp3OptionsWidget);
mBitrateCombo = new QComboBox(mMp3OptionsWidget);
mBitrateCombo->addItem("128 kbps", 128);
mBitrateCombo->addItem("192 kbps", 192);
mBitrateCombo->addItem("256 kbps", 256);
mBitrateCombo->addItem("320 kbps", 320);
// Set default bitrate from settings
int defaultBitrate = Settings::instance().audioMp3Bitrate();
for (int i = 0; i < mBitrateCombo->count(); ++i) {
if (mBitrateCombo->itemData(i).toInt() == defaultBitrate) {
mBitrateCombo->setCurrentIndex(i);
break;
}
}
mp3Layout->addWidget(bitrateLabel);
mp3Layout->addWidget(mBitrateCombo);
// FFmpeg warning
if (!Settings::instance().ffmpegPath().isEmpty()) {
QLabel* ffmpegOk = new QLabel("FFmpeg available", mMp3OptionsWidget);
ffmpegOk->setStyleSheet("color: #4CAF50;");
mp3Layout->addWidget(ffmpegOk);
} else {
QLabel* ffmpegWarn = new QLabel("FFmpeg required for MP3 export", mMp3OptionsWidget);
ffmpegWarn->setStyleSheet("color: #FF9800;");
mp3Layout->addWidget(ffmpegWarn);
}
mp3Layout->addStretch();
connect(mBitrateCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &AudioExportDialog::onBitrateChanged);
// OGG options
mOggOptionsWidget = new QWidget(this);
QVBoxLayout* oggLayout = new QVBoxLayout(mOggOptionsWidget);
oggLayout->setContentsMargins(0, 0, 0, 0);
QLabel* oggQualityLabel = new QLabel("Quality (-1=lowest, 10=highest):", mOggOptionsWidget);
QHBoxLayout* oggSliderLayout = new QHBoxLayout();
mOggQualitySlider = new QSlider(Qt::Horizontal, mOggOptionsWidget);
mOggQualitySlider->setRange(-1, 10);
mOggQualitySlider->setValue(Settings::instance().audioOggQuality());
mOggQualitySpinBox = new QSpinBox(mOggOptionsWidget);
mOggQualitySpinBox->setRange(-1, 10);
mOggQualitySpinBox->setValue(Settings::instance().audioOggQuality());
oggSliderLayout->addWidget(mOggQualitySlider);
oggSliderLayout->addWidget(mOggQualitySpinBox);
oggLayout->addWidget(oggQualityLabel);
oggLayout->addLayout(oggSliderLayout);
if (Settings::instance().ffmpegPath().isEmpty()) {
QLabel* ffmpegWarn = new QLabel("FFmpeg required for OGG export", mOggOptionsWidget);
ffmpegWarn->setStyleSheet("color: #FF9800;");
oggLayout->addWidget(ffmpegWarn);
}
oggLayout->addStretch();
connect(mOggQualitySlider, &QSlider::valueChanged, this, &AudioExportDialog::onOggQualityChanged);
connect(mOggQualitySpinBox, QOverload<int>::of(&QSpinBox::valueChanged),
mOggQualitySlider, &QSlider::setValue);
// FLAC options
mFlacOptionsWidget = new QWidget(this);
QVBoxLayout* flacLayout = new QVBoxLayout(mFlacOptionsWidget);
flacLayout->setContentsMargins(0, 0, 0, 0);
QLabel* flacCompressionLabel = new QLabel("Compression (0=fast, 8=best):", mFlacOptionsWidget);
QHBoxLayout* flacSliderLayout = new QHBoxLayout();
mFlacCompressionSlider = new QSlider(Qt::Horizontal, mFlacOptionsWidget);
mFlacCompressionSlider->setRange(0, 8);
mFlacCompressionSlider->setValue(Settings::instance().audioFlacCompression());
mFlacCompressionSpinBox = new QSpinBox(mFlacOptionsWidget);
mFlacCompressionSpinBox->setRange(0, 8);
mFlacCompressionSpinBox->setValue(Settings::instance().audioFlacCompression());
flacSliderLayout->addWidget(mFlacCompressionSlider);
flacSliderLayout->addWidget(mFlacCompressionSpinBox);
flacLayout->addWidget(flacCompressionLabel);
flacLayout->addLayout(flacSliderLayout);
if (Settings::instance().ffmpegPath().isEmpty()) {
QLabel* ffmpegWarn = new QLabel("FFmpeg required for FLAC export", mFlacOptionsWidget);
ffmpegWarn->setStyleSheet("color: #FF9800;");
flacLayout->addWidget(ffmpegWarn);
}
flacLayout->addStretch();
connect(mFlacCompressionSlider, &QSlider::valueChanged, this, &AudioExportDialog::onFlacCompressionChanged);
connect(mFlacCompressionSpinBox, QOverload<int>::of(&QSpinBox::valueChanged),
mFlacCompressionSlider, &QSlider::setValue);
// Add to stacked widget
mOptionsStack->addWidget(mWavOptionsWidget); // Index 0
mOptionsStack->addWidget(mMp3OptionsWidget); // Index 1
mOptionsStack->addWidget(mOggOptionsWidget); // Index 2
mOptionsStack->addWidget(mFlacOptionsWidget); // Index 3
// Add stacked widget to options container
QVBoxLayout* optionsLayout = qobject_cast<QVBoxLayout*>(optionsContainer()->layout());
if (optionsLayout) {
optionsLayout->addWidget(mOptionsStack);
}
// Show options for current format
showOptionsForFormat(formatCombo()->currentText());
}
void AudioExportDialog::updatePreview()
{
if (mData.isEmpty()) {
mWaveformLabel->setText("No audio loaded");
return;
}
parseWavInfo();
drawWaveform();
}
QStringList AudioExportDialog::supportedFormats() const
{
return {"wav", "mp3", "ogg", "flac"};
}
void AudioExportDialog::parseWavInfo()
{
if (mData.size() < 44) return;
const char* data = mData.constData();
// Check for RIFF header
bool isRiff = (data[0] == 'R' && data[1] == 'I' && data[2] == 'F' && data[3] == 'F');
bool isRifx = (data[0] == 'R' && data[1] == 'I' && data[2] == 'F' && data[3] == 'X');
if (!isRiff && !isRifx) return;
bool bigEndian = isRifx;
// Parse format chunk
if (bigEndian) {
mChannels = qFromBigEndian<quint16>(reinterpret_cast<const uchar*>(data + 22));
mSampleRate = qFromBigEndian<quint32>(reinterpret_cast<const uchar*>(data + 24));
mBitsPerSample = qFromBigEndian<quint16>(reinterpret_cast<const uchar*>(data + 34));
} else {
mChannels = qFromLittleEndian<quint16>(reinterpret_cast<const uchar*>(data + 22));
mSampleRate = qFromLittleEndian<quint32>(reinterpret_cast<const uchar*>(data + 24));
mBitsPerSample = qFromLittleEndian<quint16>(reinterpret_cast<const uchar*>(data + 34));
}
// Calculate duration
int dataSize = mData.size() - 44;
int bytesPerSample = mBitsPerSample / 8;
if (bytesPerSample > 0 && mChannels > 0 && mSampleRate > 0) {
int numSamples = dataSize / (bytesPerSample * mChannels);
mDuration = static_cast<double>(numSamples) / mSampleRate;
}
// Update labels
int minutes = static_cast<int>(mDuration) / 60;
double seconds = mDuration - (minutes * 60);
mDurationLabel->setText(QString("Duration: %1:%2")
.arg(minutes)
.arg(seconds, 6, 'f', 3, '0'));
mSampleRateLabel->setText(QString("%1 Hz").arg(mSampleRate));
mChannelsLabel->setText(mChannels == 1 ? "Mono" : "Stereo");
}
void AudioExportDialog::drawWaveform()
{
if (mData.size() < 44) return;
int labelWidth = mWaveformLabel->width();
int labelHeight = mWaveformLabel->height();
if (labelWidth < 10 || labelHeight < 10) {
labelWidth = 280;
labelHeight = 100;
}
// Get theme colors
Theme theme = Settings::instance().theme();
QColor bgColor(theme.panelColor);
QColor waveColor(theme.accentColor);
QColor centerLineColor(theme.borderColor);
// Create pixmap
mWaveformPixmap = QPixmap(labelWidth, labelHeight);
mWaveformPixmap.fill(bgColor);
QPainter painter(&mWaveformPixmap);
painter.setRenderHint(QPainter::Antialiasing);
// Draw center line
int centerY = labelHeight / 2;
painter.setPen(centerLineColor);
painter.drawLine(0, centerY, labelWidth, centerY);
// Get audio data (skip 44-byte header)
const qint16* samples = reinterpret_cast<const qint16*>(mData.constData() + 44);
int numSamples = (mData.size() - 44) / (mBitsPerSample / 8);
if (mChannels > 1) {
numSamples /= mChannels;
}
if (numSamples < 2) {
mWaveformLabel->setPixmap(mWaveformPixmap);
return;
}
// Draw waveform
painter.setPen(waveColor);
int samplesPerPixel = numSamples / labelWidth;
if (samplesPerPixel < 1) samplesPerPixel = 1;
int amplitude = labelHeight / 4; // Max amplitude (25% above/below center)
for (int x = 0; x < labelWidth; ++x) {
int startSample = x * samplesPerPixel;
int endSample = qMin(startSample + samplesPerPixel, numSamples);
qint16 minVal = 0, maxVal = 0;
for (int i = startSample; i < endSample; ++i) {
int idx = i * mChannels; // Use first channel
if (idx < (mData.size() - 44) / 2) {
qint16 sample = samples[idx];
if (sample < minVal) minVal = sample;
if (sample > maxVal) maxVal = sample;
}
}
// Scale to display
int yMin = centerY - (maxVal * amplitude / 32768);
int yMax = centerY - (minVal * amplitude / 32768);
painter.drawLine(x, yMin, x, yMax);
}
mWaveformLabel->setPixmap(mWaveformPixmap);
}
void AudioExportDialog::onFormatChanged(const QString& format)
{
ExportDialog::onFormatChanged(format);
showOptionsForFormat(format);
}
void AudioExportDialog::showOptionsForFormat(const QString& format)
{
QString fmt = format.toLower();
if (fmt == "wav") {
mOptionsStack->setCurrentWidget(mWavOptionsWidget);
} else if (fmt == "mp3") {
mOptionsStack->setCurrentWidget(mMp3OptionsWidget);
} else if (fmt == "ogg") {
mOptionsStack->setCurrentWidget(mOggOptionsWidget);
} else if (fmt == "flac") {
mOptionsStack->setCurrentWidget(mFlacOptionsWidget);
}
}
int AudioExportDialog::mp3Bitrate() const
{
return mBitrateCombo ? mBitrateCombo->currentData().toInt() : 256;
}
int AudioExportDialog::oggQuality() const
{
return mOggQualitySlider ? mOggQualitySlider->value() : 5;
}
int AudioExportDialog::flacCompression() const
{
return mFlacCompressionSlider ? mFlacCompressionSlider->value() : 5;
}
void AudioExportDialog::onBitrateChanged(int index)
{
Q_UNUSED(index);
// Could update size estimate here
}
void AudioExportDialog::onOggQualityChanged(int value)
{
if (mOggQualitySpinBox) {
mOggQualitySpinBox->blockSignals(true);
mOggQualitySpinBox->setValue(value);
mOggQualitySpinBox->blockSignals(false);
}
}
void AudioExportDialog::onFlacCompressionChanged(int value)
{
if (mFlacCompressionSpinBox) {
mFlacCompressionSpinBox->blockSignals(true);
mFlacCompressionSpinBox->setValue(value);
mFlacCompressionSpinBox->blockSignals(false);
}
}