diff --git a/.travis/linux..install.sh b/.travis/linux..install.sh index 41ee9d58b55..8abf83c24cb 100755 --- a/.travis/linux..install.sh +++ b/.travis/linux..install.sh @@ -2,7 +2,7 @@ set -e -PACKAGES="cmake libsndfile-dev fftw3-dev libvorbis-dev libogg-dev +PACKAGES="cmake libsndfile-dev fftw3-dev libvorbis-dev libogg-dev libmp3lame-dev libasound2-dev libjack-dev libsdl-dev libsamplerate0-dev libstk0-dev libfluidsynth-dev portaudio19-dev wine-dev g++-multilib libfltk1.3-dev libgig-dev libsoundio-dev" diff --git a/.travis/linux.win32.install.sh b/.travis/linux.win32.install.sh index cf60630c8ca..4c35555e3c2 100755 --- a/.travis/linux.win32.install.sh +++ b/.travis/linux.win32.install.sh @@ -12,7 +12,7 @@ MINGW_PACKAGES="mingw32-x-sdl mingw32-x-libvorbis mingw32-x-fluidsynth mingw32-x mingw32-x-glib2 mingw32-x-portaudio mingw32-x-libsndfile mingw32-x-fftw mingw32-x-flac mingw32-x-fltk mingw32-x-libsamplerate mingw32-x-pkgconfig mingw32-x-binutils mingw32-x-gcc mingw32-x-runtime - mingw32-x-libgig mingw32-x-libsoundio $MINGW_PACKAGES" + mingw32-x-libgig mingw32-x-libsoundio mingw32-x-lame $MINGW_PACKAGES" export MINGW_PACKAGES diff --git a/.travis/linux.win64.install.sh b/.travis/linux.win64.install.sh index 66ca0d16f28..2d03e032036 100755 --- a/.travis/linux.win64.install.sh +++ b/.travis/linux.win64.install.sh @@ -15,7 +15,7 @@ MINGW_PACKAGES="mingw64-x-sdl mingw64-x-libvorbis mingw64-x-fluidsynth mingw64-x mingw64-x-glib2 mingw64-x-portaudio mingw64-x-libsndfile mingw64-x-fftw mingw64-x-flac mingw64-x-fltk mingw64-x-libsamplerate mingw64-x-pkgconfig mingw64-x-binutils mingw64-x-gcc mingw64-x-runtime - mingw64-x-libgig mingw64-x-libsoundio $MINGW_PACKAGES" + mingw64-x-libgig mingw64-x-libsoundio mingw64-x-lame $MINGW_PACKAGES" export MINGW_PACKAGES diff --git a/.travis/osx..install.sh b/.travis/osx..install.sh index 74e89c03c1a..e344ffcd9a8 100755 --- a/.travis/osx..install.sh +++ b/.travis/osx..install.sh @@ -2,7 +2,7 @@ set -e -PACKAGES="cmake pkg-config fftw libogg libvorbis libsndfile libsamplerate jack sdl stk portaudio node fltk" +PACKAGES="cmake pkg-config fftw libogg libvorbis lame libsndfile libsamplerate jack sdl libgig libsoundio stk portaudio node fltk" if [ "$QT5" ]; then PACKAGES="$PACKAGES qt@5.5" diff --git a/CMakeLists.txt b/CMakeLists.txt index 37d6f25e9b3..98200f101c4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,6 +51,7 @@ OPTION(WANT_CAPS "Include C* Audio Plugin Suite (LADSPA plugins)" ON) OPTION(WANT_CARLA "Include Carla plugin" ON) OPTION(WANT_CMT "Include Computer Music Toolkit LADSPA plugins" ON) OPTION(WANT_JACK "Include JACK (Jack Audio Connection Kit) support" ON) +OPTION(WANT_MP3LAME "Include MP3/Lame support" ON) OPTION(WANT_OGGVORBIS "Include OGG/Vorbis support" ON) OPTION(WANT_PULSEAUDIO "Include PulseAudio support" ON) OPTION(WANT_PORTAUDIO "Include PortAudio support" ON) @@ -306,6 +307,21 @@ IF(NOT LMMS_HAVE_PULSEAUDIO) ENDIF(NOT LMMS_HAVE_PULSEAUDIO) +# check for MP3/Lame-libraries +IF(WANT_MP3LAME) + FIND_PACKAGE(Lame) + IF(LAME_FOUND) + SET(LMMS_HAVE_MP3LAME TRUE) + SET(STATUS_MP3LAME "OK") + ELSE(LAME_FOUND) + SET(STATUS_MP3LAME "not found, please install libmp3lame-dev (or similar)") + SET(LAME_LIBRARIES "") + SET(LAME_INCLUDE_DIRS "") + ENDIF(LAME_FOUND) +ELSE(WANT_MP3LAME) + SET(STATUS_MP3LAME "Disabled for build") +ENDIF(WANT_MP3LAME) + # check for OGG/Vorbis-libraries IF(WANT_OGGVORBIS) FIND_PACKAGE(OggVorbis) @@ -582,6 +598,7 @@ MESSAGE( "-----------------------------------------\n" "* WAVE : OK\n" "* OGG/VORBIS : ${STATUS_OGGVORBIS}\n" +"* MP3/Lame : ${STATUS_MP3LAME}\n" ) MESSAGE( diff --git a/cmake/modules/FindLame.cmake b/cmake/modules/FindLame.cmake new file mode 100644 index 00000000000..e5dc93bd5a7 --- /dev/null +++ b/cmake/modules/FindLame.cmake @@ -0,0 +1,16 @@ +# - Try to find LAME +# Once done this will define +# +# LAME_FOUND - system has liblame +# LAME_INCLUDE_DIRS - the liblame include directory +# LAME_LIBRARIES - The liblame libraries + +find_path(LAME_INCLUDE_DIRS lame/lame.h) +find_library(LAME_LIBRARIES mp3lame) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Lame DEFAULT_MSG LAME_INCLUDE_DIRS LAME_LIBRARIES) + +list(APPEND LAME_DEFINITIONS -DHAVE_LIBMP3LAME=1) + +mark_as_advanced(LAME_INCLUDE_DIRS LAME_LIBRARIES LAME_DEFINITIONS) diff --git a/include/AudioFileMP3.h b/include/AudioFileMP3.h new file mode 100644 index 00000000000..497208e20e6 --- /dev/null +++ b/include/AudioFileMP3.h @@ -0,0 +1,74 @@ +/* + * AudioFileMP3.h - Audio-device which encodes a wave stream into + * an MP3 file. This is used for song export. + * + * Copyright (c) 2017 to present Michael Gregorius + * + * 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_MP3_H +#define AUDIO_FILE_MP3_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_MP3LAME + +#include "AudioFileDevice.h" + +#include "lame/lame.h" + + +class AudioFileMP3 : public AudioFileDevice +{ +public: + AudioFileMP3( OutputSettings const & outputSettings, + const ch_cnt_t _channels, + bool & successful, + const QString & _file, + Mixer* mixer ); + virtual ~AudioFileMP3(); + + static AudioFileDevice * getInst( const QString & outputFilename, + OutputSettings const & outputSettings, + const ch_cnt_t channels, + Mixer* mixer, + bool & successful ) + { + return new AudioFileMP3( outputSettings, channels, successful, + outputFilename, mixer ); + } + +protected: + virtual void writeBuffer( const surroundSampleFrame * /* _buf*/, + const fpp_t /*_frames*/, + const float /*_master_gain*/ ); + +private: + void flushRemainingBuffers(); + bool initEncoder(); + void tearDownEncoder(); + +private: + lame_t m_lame; +}; + +#endif + +#endif diff --git a/include/OutputSettings.h b/include/OutputSettings.h index e4dccfd3a26..2b294e13c79 100644 --- a/include/OutputSettings.h +++ b/include/OutputSettings.h @@ -38,6 +38,13 @@ class OutputSettings NumDepths }; + enum StereoMode + { + StereoMode_Stereo, + StereoMode_JointStereo, + StereoMode_Mono + }; + class BitRateSettings { public: @@ -60,10 +67,19 @@ class OutputSettings public: OutputSettings( sample_rate_t sampleRate, BitRateSettings const & bitRateSettings, - BitDepth bitDepth ) : + BitDepth bitDepth, + StereoMode stereoMode ) : m_sampleRate(sampleRate), m_bitRateSettings(bitRateSettings), - m_bitDepth(bitDepth) + m_bitDepth(bitDepth), + m_stereoMode(stereoMode) + { + } + + OutputSettings( sample_rate_t sampleRate, + BitRateSettings const & bitRateSettings, + BitDepth bitDepth ) : + OutputSettings(sampleRate, bitRateSettings, bitDepth, StereoMode_Stereo ) { } @@ -76,10 +92,14 @@ class OutputSettings BitDepth getBitDepth() const { return m_bitDepth; } void setBitDepth(BitDepth bitDepth) { m_bitDepth = bitDepth; } + StereoMode getStereoMode() const { return m_stereoMode; } + void setStereoMode(StereoMode stereoMode) { m_stereoMode = stereoMode; } + private: sample_rate_t m_sampleRate; BitRateSettings m_bitRateSettings; BitDepth m_bitDepth; + StereoMode m_stereoMode; }; #endif diff --git a/include/ProjectRenderer.h b/include/ProjectRenderer.h index 20ccbc0de6a..15d043e51b9 100644 --- a/include/ProjectRenderer.h +++ b/include/ProjectRenderer.h @@ -39,11 +39,14 @@ class ProjectRenderer : public QThread { WaveFile, OggFile, + MP3File, NumFileFormats } ; struct FileEncodeDevice { + bool isAvailable() const { return m_getDevInst != nullptr; } + ExportFileFormats m_fileFormat; const char * m_description; const char * m_extension; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 19106d917c5..3d9093c4b72 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -75,6 +75,10 @@ IF(NOT ("${OGGVORBIS_INCLUDE_DIR}" STREQUAL "")) INCLUDE_DIRECTORIES("${OGGVORBIS_INCLUDE_DIR}") ENDIF() +IF(NOT ("${LAME_INCLUDE_DIRS}" STREQUAL "")) + INCLUDE_DIRECTORIES("${LAME_INCLUDE_DIRS}") +ENDIF() + # Use libraries in non-standard directories (e.g., another version of Qt) IF(LMMS_BUILD_LINUX) LINK_LIBRARIES(-Wl,--enable-new-dtags) @@ -136,6 +140,7 @@ SET(LMMS_REQUIRED_LIBS ${PULSEAUDIO_LIBRARIES} ${JACK_LIBRARIES} ${OGGVORBIS_LIBRARIES} + ${LAME_LIBRARIES} ${SAMPLERATE_LIBRARIES} ${SNDFILE_LIBRARIES} ${EXTRA_LIBRARIES} @@ -191,6 +196,7 @@ IF(LMMS_BUILD_WIN32) "${MINGW_PREFIX}/bin/libvorbisfile-3.dll" "${MINGW_PREFIX}/bin/libjpeg-9.dll" "${MINGW_PREFIX}/bin/libogg-0.dll" + "${MINGW_PREFIX}/bin/libmp3lame-0.dll" "${MINGW_PREFIX}/bin/libfftw3f-3.dll" "${MINGW_PREFIX}/bin/libFLAC-8.dll" "${MINGW_PREFIX}/bin/libpng16-16.dll" diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index c0f0e76e1e8..5dcfa3c4209 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -69,6 +69,7 @@ set(LMMS_SRCS core/audio/AudioAlsa.cpp core/audio/AudioDevice.cpp core/audio/AudioFileDevice.cpp + core/audio/AudioFileMP3.cpp core/audio/AudioFileOgg.cpp core/audio/AudioFileWave.cpp core/audio/AudioJack.cpp diff --git a/src/core/ProjectRenderer.cpp b/src/core/ProjectRenderer.cpp index 07dccc6ce15..10fb9eb0e65 100644 --- a/src/core/ProjectRenderer.cpp +++ b/src/core/ProjectRenderer.cpp @@ -30,6 +30,7 @@ #include "AudioFileWave.h" #include "AudioFileOgg.h" +#include "AudioFileMP3.h" #ifdef LMMS_HAVE_SCHED_H #include "sched.h" @@ -48,6 +49,15 @@ const ProjectRenderer::FileEncodeDevice ProjectRenderer::fileEncodeDevices[] = &AudioFileOgg::getInst #else NULL +#endif + }, + { ProjectRenderer::MP3File, + QT_TRANSLATE_NOOP( "ProjectRenderer", "Compressed MP3-File (*.mp3)" ), + ".mp3", +#ifdef LMMS_HAVE_MP3LAME + &AudioFileMP3::getInst +#else + NULL #endif }, // ... insert your own file-encoder-infos here... may be one day the diff --git a/src/core/Song.cpp b/src/core/Song.cpp index b776b531fb3..0732fd1d36e 100644 --- a/src/core/Song.cpp +++ b/src/core/Song.cpp @@ -1316,7 +1316,8 @@ void Song::exportProject( bool multiExport ) efd.setFileMode( FileDialog::AnyFile ); int idx = 0; QStringList types; - while( ProjectRenderer::fileEncodeDevices[idx].m_fileFormat != ProjectRenderer::NumFileFormats ) + while( ProjectRenderer::fileEncodeDevices[idx].m_fileFormat != ProjectRenderer::NumFileFormats && + ProjectRenderer::fileEncodeDevices[idx].isAvailable()) { types << tr( ProjectRenderer::fileEncodeDevices[idx].m_description ); ++idx; diff --git a/src/core/audio/AudioFileMP3.cpp b/src/core/audio/AudioFileMP3.cpp new file mode 100644 index 00000000000..cce7ec8e4ce --- /dev/null +++ b/src/core/audio/AudioFileMP3.cpp @@ -0,0 +1,133 @@ +/* + * AudioFileMP3.cpp - Audio-device which encodes a wave stream into + * an MP3 file. This is used for song export. + * + * Copyright (c) 2017 to present Michael Gregorius + * + * 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 "AudioFileMP3.h" + +#ifdef LMMS_HAVE_MP3LAME + +#include "Mixer.h" + +#include + + +AudioFileMP3::AudioFileMP3( OutputSettings const & outputSettings, + const ch_cnt_t channels, + bool & successful, + const QString & file, + Mixer* mixer ) : + AudioFileDevice( outputSettings, channels, file, mixer ) +{ + successful = true; + // For now only accept stereo sources + successful &= channels == 2; + successful &= initEncoder(); + successful &= outputFileOpened(); +} + +AudioFileMP3::~AudioFileMP3() +{ + flushRemainingBuffers(); + tearDownEncoder(); +} + +void AudioFileMP3::writeBuffer( const surroundSampleFrame * _buf, + const fpp_t _frames, + const float _master_gain ) +{ + if (_frames < 1) + { + return; + } + + // TODO Why isn't the gain applied by the driver but inside the device? + std::vector interleavedDataBuffer(_frames * 2); + for (fpp_t i = 0; i < _frames; ++i) + { + interleavedDataBuffer[2*i] = _buf[i][0] * _master_gain; + interleavedDataBuffer[2*i + 1] = _buf[i][1] * _master_gain; + } + + size_t minimumBufferSize = 1.25 * _frames + 7200; + std::vector encodingBuffer(minimumBufferSize); + + int bytesWritten = lame_encode_buffer_interleaved_ieee_float(m_lame, &interleavedDataBuffer[0], _frames, &encodingBuffer[0], static_cast(encodingBuffer.size())); + assert (bytesWritten >= 0); + + writeData(&encodingBuffer[0], bytesWritten); +} + +void AudioFileMP3::flushRemainingBuffers() +{ + // The documentation states that flush should have at least 7200 bytes. So let's be generous. + std::vector encodingBuffer(7200 * 4); + + int bytesWritten = lame_encode_flush(m_lame, &encodingBuffer[0], static_cast(encodingBuffer.size())); + assert (bytesWritten >= 0); + + writeData(&encodingBuffer[0], bytesWritten); +} + +MPEG_mode mapToMPEG_mode(OutputSettings::StereoMode stereoMode) +{ + switch (stereoMode) + { + case OutputSettings::StereoMode_Stereo: + return STEREO; + case OutputSettings::StereoMode_JointStereo: + return JOINT_STEREO; + case OutputSettings::StereoMode_Mono: + return MONO; + default: + return NOT_SET; + } +} + +bool AudioFileMP3::initEncoder() +{ + m_lame = lame_init(); + + // Handle stereo/joint/mono settings + OutputSettings::StereoMode stereoMode = getOutputSettings().getStereoMode(); + lame_set_mode(m_lame, mapToMPEG_mode(stereoMode)); + + // Handle bit rate settings + OutputSettings::BitRateSettings bitRateSettings = getOutputSettings().getBitRateSettings(); + int bitRate = static_cast(bitRateSettings.getBitRate()); + + lame_set_VBR(m_lame, vbr_off); + lame_set_brate(m_lame, bitRate); + + // Add a comment + id3tag_set_comment(m_lame, "Created with LMMS"); + + return lame_init_params(m_lame) != -1; +} + +void AudioFileMP3::tearDownEncoder() +{ + lame_close(m_lame); +} + +#endif diff --git a/src/gui/ExportProjectDialog.cpp b/src/gui/ExportProjectDialog.cpp index c4f7696b821..83c7883cc73 100644 --- a/src/gui/ExportProjectDialog.cpp +++ b/src/gui/ExportProjectDialog.cpp @@ -58,7 +58,7 @@ ExportProjectDialog::ExportProjectDialog( const QString & _file_name, int cbIndex = 0; for( int i = 0; i < ProjectRenderer::NumFileFormats; ++i ) { - if( ProjectRenderer::fileEncodeDevices[i].m_getDevInst != NULL ) + if( ProjectRenderer::fileEncodeDevices[i].isAvailable() ) { // get the extension of this format QString renderExt = ProjectRenderer::fileEncodeDevices[i].m_extension; @@ -128,7 +128,20 @@ void ExportProjectDialog::closeEvent( QCloseEvent * _ce ) } - +OutputSettings::StereoMode mapToStereoMode(int index) +{ + switch (index) + { + case 0: + return OutputSettings::StereoMode_Stereo; + case 1: + return OutputSettings::StereoMode_JointStereo; + case 2: + return OutputSettings::StereoMode_Mono; + default: + return OutputSettings::StereoMode_Stereo; + } +} void ExportProjectDialog::startExport() { @@ -146,7 +159,8 @@ void ExportProjectDialog::startExport() OutputSettings os = OutputSettings( samplerates[ samplerateCB->currentIndex() ], bitRateSettings, - static_cast( depthCB->currentIndex() ) ); + static_cast( depthCB->currentIndex() ), + mapToStereoMode(stereoModeComboBox->currentIndex()) ); m_renderManager = new RenderManager( qs, os, m_ft, m_fileName ); @@ -181,6 +195,8 @@ ProjectRenderer::ExportFileFormats convertIndexToExportFileFormat(int index) return ProjectRenderer::WaveFile; case 1: return ProjectRenderer::OggFile; + case 2: + return ProjectRenderer::MP3File; default: Q_ASSERT(false); break; @@ -195,10 +211,23 @@ void ExportProjectDialog::onFileFormatChanged(int index) ProjectRenderer::ExportFileFormats exportFormat = convertIndexToExportFileFormat(index); - bool bitRateControlsEnabled = exportFormat == ProjectRenderer::OggFile; + bool stereoModeVisible = exportFormat == ProjectRenderer::MP3File; + + bool sampleRateControlsVisible = exportFormat != ProjectRenderer::MP3File; + + bool bitRateControlsEnabled = + (exportFormat == ProjectRenderer::OggFile || + exportFormat == ProjectRenderer::MP3File); + bool bitDepthControlEnabled = exportFormat == ProjectRenderer::WaveFile; + bool variableBitrateVisible = exportFormat != ProjectRenderer::MP3File; + + stereoModeWidget->setVisible(stereoModeVisible); + sampleRateWidget->setVisible(sampleRateControlsVisible); + bitrateWidget->setVisible(bitRateControlsEnabled); + checkBoxVariableBitRate->setVisible(variableBitrateVisible); depthWidget->setVisible(bitDepthControlEnabled); } diff --git a/src/gui/dialogs/export_project.ui b/src/gui/dialogs/export_project.ui index 2cd466ee356..838fbee1f21 100644 --- a/src/gui/dialogs/export_project.ui +++ b/src/gui/dialogs/export_project.ui @@ -7,7 +7,7 @@ 0 0 715 - 447 + 491 @@ -45,39 +45,48 @@ - - - Samplerate: - - - - - - - - 44100 Hz - - - - - 48000 Hz - - - - - 88200 Hz - - - - - 96000 Hz - - - - - 192000 Hz + + + + 0 - + + + + Samplerate: + + + + + + + + 44100 Hz + + + + + 48000 Hz + + + + + 88200 Hz + + + + + 96000 Hz + + + + + 192000 Hz + + + + + @@ -124,6 +133,44 @@ + + + + + 0 + + + + + Stereo mode: + + + + + + + 1 + + + + Stereo + + + + + Joint Stereo + + + + + Mono + + + + + + + diff --git a/src/lmmsconfig.h.in b/src/lmmsconfig.h.in index 0948d8529e8..421d0cc999c 100644 --- a/src/lmmsconfig.h.in +++ b/src/lmmsconfig.h.in @@ -11,6 +11,7 @@ #cmakedefine LMMS_HAVE_ALSA #cmakedefine LMMS_HAVE_FLUIDSYNTH #cmakedefine LMMS_HAVE_JACK +#cmakedefine LMMS_HAVE_MP3LAME #cmakedefine LMMS_HAVE_OGGVORBIS #cmakedefine LMMS_HAVE_OSS #cmakedefine LMMS_HAVE_SNDIO