Add FLAC export and related options (#3731)
* Add FLAC export, based on WAV renderer * Depend on sndfile>=1.0.18 (previously >=1.0.11) * Add compression option to FLAC export, if available. The code related to compression is only generated if LMMS_HAVE_SF_COMPLEVEL is defined(libsndfile>=1.0.26). * Save into the correct file extension upon single-export. * Use unique_ptr in FLAC renderer and be more expressive about involved types. * Use unique_ptr and remove manual memory management in ExportProjectDialog * Add 'flac' format info to --help and manpage
This commit is contained in:
committed by
Hyunjin Song
parent
37f6032b4d
commit
e9a4063119
@@ -71,6 +71,7 @@ set(LMMS_SRCS
|
||||
core/audio/AudioFileDevice.cpp
|
||||
core/audio/AudioFileMP3.cpp
|
||||
core/audio/AudioFileOgg.cpp
|
||||
core/audio/AudioFileFlac.cpp
|
||||
core/audio/AudioFileWave.cpp
|
||||
core/audio/AudioJack.cpp
|
||||
core/audio/AudioOss.cpp
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "AudioFileWave.h"
|
||||
#include "AudioFileOgg.h"
|
||||
#include "AudioFileMP3.h"
|
||||
#include "AudioFileFlac.h"
|
||||
|
||||
#ifdef LMMS_HAVE_SCHED_H
|
||||
#include "sched.h"
|
||||
@@ -42,6 +43,11 @@ const ProjectRenderer::FileEncodeDevice ProjectRenderer::fileEncodeDevices[] =
|
||||
{ ProjectRenderer::WaveFile,
|
||||
QT_TRANSLATE_NOOP( "ProjectRenderer", "WAV-File (*.wav)" ),
|
||||
".wav", &AudioFileWave::getInst },
|
||||
{ ProjectRenderer::FlacFile,
|
||||
QT_TRANSLATE_NOOP("ProjectRenderer", "FLAC-File (*.flac)"),
|
||||
".flac",
|
||||
&AudioFileFlac::getInst
|
||||
},
|
||||
{ ProjectRenderer::OggFile,
|
||||
QT_TRANSLATE_NOOP( "ProjectRenderer", "Compressed OGG-File (*.ogg)" ),
|
||||
".ogg",
|
||||
@@ -176,8 +182,8 @@ void ProjectRenderer::run()
|
||||
|
||||
Engine::getSong()->startExport();
|
||||
Engine::getSong()->updateLength();
|
||||
//skip first empty buffer
|
||||
Engine::mixer()->nextBuffer();
|
||||
//skip first empty buffer
|
||||
Engine::mixer()->nextBuffer();
|
||||
|
||||
const Song::PlayPos & exportPos = Engine::getSong()->getPlayPos(
|
||||
Song::Mode_PlaySong );
|
||||
|
||||
119
src/core/audio/AudioFileFlac.cpp
Normal file
119
src/core/audio/AudioFileFlac.cpp
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* AudioFileFlac.cpp - Audio device which encodes a wave stream into a FLAC file (Implementation).
|
||||
*
|
||||
* Copyright (c) 2017 to present Levin Oehlmann <irrenhaus3/at/gmail[dot]com> et al.
|
||||
*
|
||||
* This file is part of LMMS - https://lmms.io
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public
|
||||
* License along with this program (see COPYING); if not, write to the
|
||||
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
||||
* Boston, MA 02110-1301 USA.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "AudioFileFlac.h"
|
||||
#include "endian_handling.h"
|
||||
#include "Mixer.h"
|
||||
|
||||
AudioFileFlac::AudioFileFlac(OutputSettings const& outputSettings, ch_cnt_t const channels, bool& successful, QString const& file, Mixer* mixer):
|
||||
AudioFileDevice(outputSettings,channels,file,mixer),
|
||||
m_sf(nullptr)
|
||||
{
|
||||
successful = outputFileOpened() && startEncoding();
|
||||
}
|
||||
|
||||
AudioFileFlac::~AudioFileFlac()
|
||||
{
|
||||
finishEncoding();
|
||||
}
|
||||
|
||||
bool AudioFileFlac::startEncoding()
|
||||
{
|
||||
m_sfinfo.samplerate=sampleRate();
|
||||
m_sfinfo.channels=channels();
|
||||
m_sfinfo.frames = mixer()->framesPerPeriod();
|
||||
m_sfinfo.sections=1;
|
||||
m_sfinfo.seekable=0;
|
||||
|
||||
m_sfinfo.format = SF_FORMAT_FLAC;
|
||||
|
||||
switch (getOutputSettings().getBitDepth())
|
||||
{
|
||||
case OutputSettings::Depth_24Bit:
|
||||
case OutputSettings::Depth_32Bit:
|
||||
// FLAC does not support 32bit sampling, so take it as 24.
|
||||
m_sfinfo.format |= SF_FORMAT_PCM_24;
|
||||
break;
|
||||
default:
|
||||
m_sfinfo.format |= SF_FORMAT_PCM_16;
|
||||
}
|
||||
|
||||
#ifdef LMMS_HAVE_SF_COMPLEVEL
|
||||
double compression = getOutputSettings().getCompressionLevel();
|
||||
sf_command(m_sf, SFC_SET_COMPRESSION_LEVEL, &compression, sizeof(double));
|
||||
#endif
|
||||
|
||||
m_sf = sf_open(
|
||||
#ifdef LMMS_BUILD_WIN32
|
||||
outputFile().toLocal8Bit().constData(),
|
||||
#else
|
||||
outputFile().toUtf8().constData(),
|
||||
#endif
|
||||
SFM_WRITE,
|
||||
&m_sfinfo
|
||||
);
|
||||
|
||||
sf_command(m_sf, SFC_SET_CLIPPING, nullptr, SF_TRUE);
|
||||
|
||||
sf_set_string(m_sf, SF_STR_SOFTWARE, "LMMS");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void AudioFileFlac::writeBuffer(surroundSampleFrame const* _ab, fpp_t const frames, float master_gain)
|
||||
{
|
||||
OutputSettings::BitDepth depth = getOutputSettings().getBitDepth();
|
||||
|
||||
if (depth == OutputSettings::Depth_24Bit || depth == OutputSettings::Depth_32Bit) // Float encoding
|
||||
{
|
||||
std::unique_ptr<sample_t[]> buf{ new sample_t[frames*channels()] };
|
||||
for(fpp_t frame = 0; frame < frames; ++frame)
|
||||
{
|
||||
for(ch_cnt_t channel=0; channel<channels(); ++channel)
|
||||
{
|
||||
buf[frame*channels() + channel] = _ab[frame][channel] * master_gain;
|
||||
}
|
||||
}
|
||||
sf_writef_float(m_sf,static_cast<float*>(buf.get()),frames);
|
||||
}
|
||||
else // integer PCM encoding
|
||||
{
|
||||
std::unique_ptr<int_sample_t[]> buf{ new int_sample_t[frames*channels()] };
|
||||
convertToS16(_ab, frames, master_gain, buf.get(), !isLittleEndian());
|
||||
sf_writef_short(m_sf, static_cast<short*>(buf.get()), frames);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
void AudioFileFlac::finishEncoding()
|
||||
{
|
||||
if (m_sf)
|
||||
{
|
||||
sf_write_sync(m_sf);
|
||||
sf_close(m_sf);
|
||||
}
|
||||
}
|
||||
@@ -166,7 +166,7 @@ void printHelp()
|
||||
"-c, --config <configfile> Get the configuration from <configfile>\n"
|
||||
"-d, --dump <in> Dump XML of compressed file <in>\n"
|
||||
"-f, --format <format> Specify format of render-output where\n"
|
||||
" Format is either 'wav', 'ogg' or 'mp3'.\n"
|
||||
" Format is either 'wav', 'flac', 'ogg' or 'mp3'.\n"
|
||||
" --geometry <geometry> Specify the size and position of the main window\n"
|
||||
" geometry is <xsizexysize+xoffset+yoffsety>.\n"
|
||||
"-h, --help Show this usage information and exit.\n"
|
||||
@@ -444,6 +444,10 @@ int main( int argc, char * * argv )
|
||||
eff = ProjectRenderer::MP3File;
|
||||
}
|
||||
#endif
|
||||
else if (ext == "flac")
|
||||
{
|
||||
eff = ProjectRenderer::FlacFile;
|
||||
}
|
||||
else
|
||||
{
|
||||
printf( "\nInvalid output format %s.\n\n"
|
||||
|
||||
@@ -41,7 +41,7 @@ ExportProjectDialog::ExportProjectDialog( const QString & _file_name,
|
||||
m_fileName( _file_name ),
|
||||
m_fileExtension(),
|
||||
m_multiExport( multi_export ),
|
||||
m_renderManager( NULL )
|
||||
m_renderManager( nullptr )
|
||||
{
|
||||
setupUi( this );
|
||||
setWindowTitle( tr( "Export project to %1" ).arg(
|
||||
@@ -79,30 +79,37 @@ ExportProjectDialog::ExportProjectDialog( const QString & _file_name,
|
||||
cbIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
int const MAX_LEVEL=8;
|
||||
for(int i=0; i<=MAX_LEVEL; ++i)
|
||||
{
|
||||
QString info="";
|
||||
if (i==0){ info = tr("(fastest)"); }
|
||||
else if (i==4){ info = tr("(default)"); }
|
||||
else if (i==MAX_LEVEL){ info = tr("(smallest)"); }
|
||||
|
||||
compLevelCB->addItem(
|
||||
QString::number(i)+" "+info,
|
||||
QVariant(i/static_cast<double>(MAX_LEVEL))
|
||||
);
|
||||
}
|
||||
compLevelCB->setCurrentIndex(MAX_LEVEL/2);
|
||||
#ifndef LMMS_HAVE_SF_COMPLEVEL
|
||||
//Disable this widget; the setting would be ignored by the renderer.
|
||||
compressionWidget->setVisible(false);
|
||||
#endif
|
||||
|
||||
connect( startButton, SIGNAL( clicked() ),
|
||||
this, SLOT( startBtnClicked() ) );
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
ExportProjectDialog::~ExportProjectDialog()
|
||||
{
|
||||
delete m_renderManager;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
void ExportProjectDialog::reject()
|
||||
{
|
||||
if( m_renderManager ) {
|
||||
m_renderManager->abortProcessing();
|
||||
}
|
||||
|
||||
delete m_renderManager;
|
||||
m_renderManager = NULL;
|
||||
m_renderManager.reset(nullptr);
|
||||
|
||||
QDialog::reject();
|
||||
}
|
||||
@@ -111,9 +118,7 @@ void ExportProjectDialog::reject()
|
||||
|
||||
void ExportProjectDialog::accept()
|
||||
{
|
||||
delete m_renderManager;
|
||||
m_renderManager = NULL;
|
||||
|
||||
m_renderManager.reset(nullptr);
|
||||
QDialog::accept();
|
||||
}
|
||||
|
||||
@@ -164,18 +169,31 @@ void ExportProjectDialog::startExport()
|
||||
static_cast<OutputSettings::BitDepth>( depthCB->currentIndex() ),
|
||||
mapToStereoMode(stereoModeComboBox->currentIndex()) );
|
||||
|
||||
m_renderManager = new RenderManager( qs, os, m_ft, m_fileName );
|
||||
if (compressionWidget->isVisible())
|
||||
{
|
||||
double level = compLevelCB->itemData(compLevelCB->currentIndex()).toDouble();
|
||||
os.setCompressionLevel(level);
|
||||
}
|
||||
|
||||
//Make sure we have the the correct file extension
|
||||
//so there's no confusion about the codec in use.
|
||||
auto output_name = m_fileName;
|
||||
if (!(m_multiExport || output_name.endsWith(m_fileExtension,Qt::CaseInsensitive)))
|
||||
{
|
||||
output_name+=m_fileExtension;
|
||||
}
|
||||
m_renderManager.reset(new RenderManager( qs, os, m_ft, output_name ));
|
||||
|
||||
Engine::getSong()->setExportLoop( exportLoopCB->isChecked() );
|
||||
Engine::getSong()->setRenderBetweenMarkers( renderMarkersCB->isChecked() );
|
||||
|
||||
connect( m_renderManager, SIGNAL( progressChanged( int ) ),
|
||||
connect( m_renderManager.get(), SIGNAL( progressChanged( int ) ),
|
||||
progressBar, SLOT( setValue( int ) ) );
|
||||
connect( m_renderManager, SIGNAL( progressChanged( int ) ),
|
||||
connect( m_renderManager.get(), SIGNAL( progressChanged( int ) ),
|
||||
this, SLOT( updateTitleBar( int ) )) ;
|
||||
connect( m_renderManager, SIGNAL( finished() ),
|
||||
connect( m_renderManager.get(), SIGNAL( finished() ),
|
||||
this, SLOT( accept() ) );
|
||||
connect( m_renderManager, SIGNAL( finished() ),
|
||||
connect( m_renderManager.get(), SIGNAL( finished() ),
|
||||
gui->mainWindow(), SLOT( resetWindowTitle() ) );
|
||||
|
||||
if ( m_multiExport )
|
||||
@@ -188,7 +206,6 @@ void ExportProjectDialog::startExport()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void ExportProjectDialog::onFileFormatChanged(int index)
|
||||
{
|
||||
// Extract the format tag from the currently selected item,
|
||||
@@ -200,17 +217,24 @@ void ExportProjectDialog::onFileFormatChanged(int index)
|
||||
);
|
||||
Q_ASSERT(successful_conversion);
|
||||
|
||||
bool stereoModeVisible = exportFormat == ProjectRenderer::MP3File;
|
||||
bool stereoModeVisible = (exportFormat == ProjectRenderer::MP3File);
|
||||
|
||||
bool sampleRateControlsVisible = exportFormat != ProjectRenderer::MP3File;
|
||||
bool sampleRateControlsVisible = (exportFormat != ProjectRenderer::MP3File);
|
||||
|
||||
bool bitRateControlsEnabled =
|
||||
(exportFormat == ProjectRenderer::OggFile ||
|
||||
exportFormat == ProjectRenderer::MP3File);
|
||||
|
||||
bool bitDepthControlEnabled = exportFormat == ProjectRenderer::WaveFile;
|
||||
bool bitDepthControlEnabled =
|
||||
(exportFormat == ProjectRenderer::WaveFile ||
|
||||
exportFormat == ProjectRenderer::FlacFile);
|
||||
|
||||
bool variableBitrateVisible = exportFormat != ProjectRenderer::MP3File;
|
||||
bool variableBitrateVisible = !(exportFormat == ProjectRenderer::MP3File || exportFormat == ProjectRenderer::FlacFile);
|
||||
|
||||
#ifdef LMMS_HAVE_SF_COMPLEVEL
|
||||
bool compressionLevelVisible = (exportFormat == ProjectRenderer::FlacFile);
|
||||
compressionWidget->setVisible(compressionLevelVisible);
|
||||
#endif
|
||||
|
||||
stereoModeWidget->setVisible(stereoModeVisible);
|
||||
sampleRateWidget->setVisible(sampleRateControlsVisible);
|
||||
@@ -228,14 +252,16 @@ void ExportProjectDialog::startBtnClicked()
|
||||
//Get file format from current menu selection.
|
||||
bool successful_conversion = false;
|
||||
QVariant tag = fileFormatCB->itemData(fileFormatCB->currentIndex());
|
||||
m_ft = static_cast<ProjectRenderer::ExportFileFormats>(tag.toInt(&successful_conversion));
|
||||
m_ft = static_cast<ProjectRenderer::ExportFileFormats>(
|
||||
tag.toInt(&successful_conversion)
|
||||
);
|
||||
|
||||
if( !successful_conversion )
|
||||
{
|
||||
QMessageBox::information( this, tr( "Error" ),
|
||||
tr( "Error while determining file-encoder device. "
|
||||
"Please try to choose a different output "
|
||||
"format." ) );
|
||||
"Please try to choose a different output "
|
||||
"format." ) );
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,16 @@
|
||||
<item>
|
||||
<widget class="QWidget" name="sampleRateWidget" native="true">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="margin">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
@@ -98,7 +107,16 @@
|
||||
</size>
|
||||
</property>
|
||||
<layout class="QVBoxLayout">
|
||||
<property name="margin">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
@@ -136,7 +154,16 @@
|
||||
<item>
|
||||
<widget class="QWidget" name="stereoModeWidget" native="true">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<property name="margin">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
@@ -171,13 +198,57 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="compressionWidget" native="true">
|
||||
<layout class="QVBoxLayout" name="_2">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelCompLevel">
|
||||
<property name="text">
|
||||
<string>Compression level:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="compLevelCB">
|
||||
<property name="currentIndex">
|
||||
<number>-1</number>
|
||||
</property>
|
||||
<property name="maxVisibleItems">
|
||||
<number>9</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="bitrateWidget" native="true">
|
||||
<layout class="QVBoxLayout">
|
||||
<property name="spacing">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#cmakedefine LMMS_HAVE_SDL2
|
||||
#cmakedefine LMMS_HAVE_STK
|
||||
#cmakedefine LMMS_HAVE_VST
|
||||
#cmakedefine LMMS_HAVE_SF_COMPLEVEL
|
||||
|
||||
#cmakedefine LMMS_DEBUG_FPE
|
||||
|
||||
|
||||
Reference in New Issue
Block a user