diff --git a/CMakeLists.txt b/CMakeLists.txt index e4f6782ef5e..771333b0025 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) @@ -609,6 +613,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" ) diff --git a/doc/lmms.1 b/doc/lmms.1 index 41f906ecf00..1d8fa867296 100644 --- a/doc/lmms.1 +++ b/doc/lmms.1 @@ -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 diff --git a/include/AudioFileFlac.h b/include/AudioFileFlac.h new file mode 100644 index 00000000000..b421654c2d7 --- /dev/null +++ b/include/AudioFileFlac.h @@ -0,0 +1,74 @@ +/* + * AudioFileFlac.h - Audio device which encodes a wave stream into a FLAC file. + * + * Copyright (c) 2017 to present Levin Oehlmann 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 + +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 diff --git a/include/ExportProjectDialog.h b/include/ExportProjectDialog.h index d7159780feb..0eedb9722ce 100644 --- a/include/ExportProjectDialog.h +++ b/include/ExportProjectDialog.h @@ -28,7 +28,7 @@ #define EXPORT_PROJECT_DIALOG_H #include -#include +#include #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 slots: bool m_multiExport; ProjectRenderer::ExportFileFormats m_ft; - RenderManager* m_renderManager; + std::unique_ptr m_renderManager; } ; #endif diff --git a/include/OutputSettings.h b/include/OutputSettings.h index 2b294e13c79..16afedaea52 100644 --- a/include/OutputSettings.h +++ b/include/OutputSettings.h @@ -72,7 +72,8 @@ class OutputSettings m_sampleRate(sampleRate), m_bitRateSettings(bitRateSettings), m_bitDepth(bitDepth), - m_stereoMode(stereoMode) + m_stereoMode(stereoMode), + m_compressionLevel(0.5) { } @@ -95,11 +96,19 @@ class OutputSettings 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 diff --git a/include/ProjectRenderer.h b/include/ProjectRenderer.h index 543eb43c393..2c895c2384b 100644 --- a/include/ProjectRenderer.h +++ b/include/ProjectRenderer.h @@ -38,6 +38,7 @@ class ProjectRenderer : public QThread enum ExportFileFormats: int { WaveFile, + FlacFile, OggFile, MP3File, NumFileFormats diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 5dcfa3c4209..604a63c040e 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -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 diff --git a/src/core/ProjectRenderer.cpp b/src/core/ProjectRenderer.cpp index 3c16bce968e..11a76841577 100644 --- a/src/core/ProjectRenderer.cpp +++ b/src/core/ProjectRenderer.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 ); diff --git a/src/core/audio/AudioFileFlac.cpp b/src/core/audio/AudioFileFlac.cpp new file mode 100644 index 00000000000..d9d91f54bea --- /dev/null +++ b/src/core/audio/AudioFileFlac.cpp @@ -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 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 + +#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 buf{ new sample_t[frames*channels()] }; + for(fpp_t frame = 0; frame < frames; ++frame) + { + for(ch_cnt_t channel=0; channel(buf.get()),frames); + } + else // integer PCM encoding + { + std::unique_ptr buf{ new int_sample_t[frames*channels()] }; + convertToS16(_ab, frames, master_gain, buf.get(), !isLittleEndian()); + sf_writef_short(m_sf, static_cast(buf.get()), frames); + } + +} + + +void AudioFileFlac::finishEncoding() +{ + if (m_sf) + { + sf_write_sync(m_sf); + sf_close(m_sf); + } +} diff --git a/src/core/main.cpp b/src/core/main.cpp index 50882221713..5a7a52c130e 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -166,7 +166,7 @@ void printHelp() "-c, --config Get the configuration from \n" "-d, --dump Dump XML of compressed file \n" "-f, --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 Specify the size and position of the main window\n" " geometry is .\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" diff --git a/src/gui/ExportProjectDialog.cpp b/src/gui/ExportProjectDialog.cpp index 0bd815010a1..ef070c13478 100644 --- a/src/gui/ExportProjectDialog.cpp +++ b/src/gui/ExportProjectDialog.cpp @@ -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(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( 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(tag.toInt(&successful_conversion)); + m_ft = static_cast( + 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; } diff --git a/src/gui/dialogs/export_project.ui b/src/gui/dialogs/export_project.ui index 838fbee1f21..43ad56f3541 100644 --- a/src/gui/dialogs/export_project.ui +++ b/src/gui/dialogs/export_project.ui @@ -47,7 +47,16 @@ - + + 0 + + + 0 + + + 0 + + 0 @@ -98,7 +107,16 @@ - + + 0 + + + 0 + + + 0 + + 0 @@ -136,7 +154,16 @@ - + + 0 + + + 0 + + + 0 + + 0 @@ -171,13 +198,57 @@ + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Compression level: + + + + + + + -1 + + + 9 + + + + + + 6 - + + 0 + + + 0 + + + 0 + + 0 diff --git a/src/lmmsconfig.h.in b/src/lmmsconfig.h.in index 0de64a29f3f..207fb6a03f7 100644 --- a/src/lmmsconfig.h.in +++ b/src/lmmsconfig.h.in @@ -21,6 +21,7 @@ #cmakedefine LMMS_HAVE_SDL #cmakedefine LMMS_HAVE_STK #cmakedefine LMMS_HAVE_VST +#cmakedefine LMMS_HAVE_SF_COMPLEVEL #cmakedefine LMMS_DEBUG_FPE