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>
385 lines
13 KiB
C++
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);
|
|
}
|
|
}
|