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:
Levin Oehlmann
2017-08-04 00:19:39 +02:00
committed by Hyunjin Song
parent 37f6032b4d
commit e9a4063119
13 changed files with 361 additions and 46 deletions

View File

@@ -176,10 +176,14 @@ ELSE()
ENDIF()
# check for libsndfile
PKG_CHECK_MODULES(SNDFILE REQUIRED sndfile>=1.0.11)
PKG_CHECK_MODULES(SNDFILE REQUIRED sndfile>=1.0.18)
IF(NOT SNDFILE_FOUND)
MESSAGE(FATAL_ERROR "LMMS requires libsndfile1 and libsndfile1-dev >= 1.0.11 - please install, remove CMakeCache.txt and try again!")
ENDIF(NOT SNDFILE_FOUND)
MESSAGE(FATAL_ERROR "LMMS requires libsndfile1 and libsndfile1-dev >= 1.0.18 - please install, remove CMakeCache.txt and try again!")
ENDIF()
# check if we can use SF_SET_COMPRESSION_LEVEL
IF(NOT SNDFILE_VERSION VERSION_LESS 1.0.26)
SET(LMMS_HAVE_SF_COMPLEVEL TRUE)
ENDIF()
IF(WANT_CALF)
SET(LMMS_HAVE_CALF TRUE)
@@ -625,6 +629,7 @@ MESSAGE(
"Supported file formats for project export\n"
"-----------------------------------------\n"
"* WAVE : OK\n"
"* FLAC : OK\n"
"* OGG/VORBIS : ${STATUS_OGGVORBIS}\n"
"* MP3/Lame : ${STATUS_MP3LAME}\n"
)

View File

@@ -97,7 +97,7 @@ Get the configuration from \fIconfigfile\fP instead of ~/.lmmsrc.xml (default)
.IP "\fB\-d, --dump\fP \fIin\fP
Dump XML of compressed file \fIin\fP (i.e. MMPZ-file)
.IP "\fB\-f, --format\fP \fIformat\fP
Specify format of render-output where \fIformat\fP is either 'wav', 'ogg' or 'mp3'.
Specify format of render-output where \fIformat\fP is either 'wav', 'flac', 'ogg' or 'mp3'.
.IP "\fB\ --geometry\fP \fIgeometry\fP
Specify the prefered size and position of the main window
.br

74
include/AudioFileFlac.h Normal file
View File

@@ -0,0 +1,74 @@
/*
* AudioFileFlac.h - Audio device which encodes a wave stream into a FLAC file.
*
* 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.
*
*/
#ifndef AUDIO_FILE_FLAC_H
#define AUDIO_FILE_FLAC_H
#include "lmmsconfig.h"
#include "AudioFileDevice.h"
#include <sndfile.h>
class AudioFileFlac: public AudioFileDevice
{
public:
AudioFileFlac(OutputSettings const& outputSettings,
ch_cnt_t const channels,
bool& successful,
QString const& file,
Mixer* mixer
);
virtual ~AudioFileFlac();
static AudioFileDevice* getInst(QString const& outputFilename,
OutputSettings const& outputSettings,
ch_cnt_t const channels,
Mixer* mixer,
bool& successful)
{
return new AudioFileFlac(
outputSettings,
channels,
successful,
outputFilename,
mixer
);
}
private:
SF_INFO m_sfinfo;
SNDFILE* m_sf;
virtual void writeBuffer(surroundSampleFrame const* _ab,
fpp_t const frames,
float master_gain) override;
bool startEncoding();
void finishEncoding();
};
#endif //AUDIO_FILE_FLAC_H

View File

@@ -28,7 +28,7 @@
#define EXPORT_PROJECT_DIALOG_H
#include <QDialog>
#include <vector>
#include <memory>
#include "ui_export_project.h"
#include "ProjectRenderer.h"
@@ -39,8 +39,6 @@ class ExportProjectDialog : public QDialog, public Ui::ExportProjectDialog
Q_OBJECT
public:
ExportProjectDialog( const QString & _file_name, QWidget * _parent, bool multi_export );
virtual ~ExportProjectDialog();
protected:
virtual void reject( void );
@@ -62,7 +60,7 @@ private:
bool m_multiExport;
ProjectRenderer::ExportFileFormats m_ft;
RenderManager* m_renderManager;
std::unique_ptr<RenderManager> m_renderManager;
} ;
#endif

View File

@@ -72,7 +72,8 @@ public:
m_sampleRate(sampleRate),
m_bitRateSettings(bitRateSettings),
m_bitDepth(bitDepth),
m_stereoMode(stereoMode)
m_stereoMode(stereoMode),
m_compressionLevel(0.5)
{
}
@@ -95,11 +96,19 @@ public:
StereoMode getStereoMode() const { return m_stereoMode; }
void setStereoMode(StereoMode stereoMode) { m_stereoMode = stereoMode; }
double getCompressionLevel() const{ return m_compressionLevel; }
void setCompressionLevel(double level){
// legal range is 0.0 to 1.0.
m_compressionLevel = level;
}
private:
sample_rate_t m_sampleRate;
BitRateSettings m_bitRateSettings;
BitDepth m_bitDepth;
StereoMode m_stereoMode;
double m_compressionLevel;
};
#endif

View File

@@ -38,6 +38,7 @@ public:
enum ExportFileFormats: int
{
WaveFile,
FlacFile,
OggFile,
MP3File,
NumFileFormats

View File

@@ -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

View File

@@ -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 );

View 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);
}
}

View File

@@ -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"

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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