diff --git a/include/FlacExporter.h b/include/FlacExporter.h new file mode 100644 index 00000000000..2ca2b2fe858 --- /dev/null +++ b/include/FlacExporter.h @@ -0,0 +1,53 @@ +/* + * FlacExporter.h - exports .flac files outside of AudioEngine + * + * Copyright (c) 2024 - 2025 szeli1 + * + * 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 LMMS_FLAC_EXPORTER_H +#define LMMS_FLAC_EXPORTER_H + +#include + +#include + +namespace lmms +{ + +class SampleFrame; + +class FlacExporter +{ +public: + FlacExporter(int sampleRate, int bitDepth, const QString& outputLocationAndName); + ~FlacExporter(); + + void writeThisBuffer(const SampleFrame* samples, size_t sampleCount); + bool getIsSuccesful() const; + +private: + bool m_isSuccesful; + SNDFILE* m_fileDescriptor; +}; + +} // namespace lmms + +#endif // LMMS_FLAC_EXPORTER_H diff --git a/include/SampleClip.h b/include/SampleClip.h index 3beca338bcd..b98d1450fe5 100644 --- a/include/SampleClip.h +++ b/include/SampleClip.h @@ -32,6 +32,7 @@ namespace lmms { +class ThreadedExportManager; class SampleBuffer; namespace gui @@ -90,10 +91,18 @@ public slots: private: + void exportSampleBuffer(const QString& fileName); + static void exportSampleBufferCallback(void* thisObject); + Sample m_sample; BoolModel m_recordModel; bool m_isPlaying; + //! used when a sample is exported and before it is reinported + QString m_exportedSampleName; + + static std::unique_ptr s_sampleExporter; + friend class gui::SampleClipView; diff --git a/include/SampleClipView.h b/include/SampleClipView.h index 4ff218fb0ef..69083286549 100644 --- a/include/SampleClipView.h +++ b/include/SampleClipView.h @@ -48,6 +48,7 @@ public slots: void updateSample(); void reverseSample(); void setAutomationGhost(); + void exportSampleBuffer(); diff --git a/include/ThreadedExportManager.h b/include/ThreadedExportManager.h new file mode 100644 index 00000000000..42fade28125 --- /dev/null +++ b/include/ThreadedExportManager.h @@ -0,0 +1,76 @@ +/* + * ThreadedExportManager.h - exports files in .flac format on an other thread + * + * Copyright (c) 2024 - 2025 szeli1 + * + * 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 LMMS_THREADED_EXPORT_MANAGER_H +#define LMMS_THREADED_EXPORT_MANAGER_H + +#include +#include +#include +#include +#include +#include + +#include + +#include + +namespace lmms +{ + +class SampleBuffer; + +typedef void (*callbackFn)(void*); + +class ThreadedExportManager +{ +public: + ThreadedExportManager(); + ~ThreadedExportManager(); + + //! outputLocationAndName: should include path and name, could include ".flac" + void startExporting(const QString& outputLocationAndName, std::shared_ptr buffer, callbackFn callbackFunction = nullptr, void* callbackObject = nullptr); + void stopExporting(); + + //void writeBuffer(const SampleFrame* _ab, fpp_t const frames); + +private: + static void threadedExportFunction(ThreadedExportManager* thisExporter, volatile std::atomic* abortExport); + + void stopThread(); + bool openFile(const QString& outputLocationAndName, std::shared_ptr buffer); + void exportBuffer(std::shared_ptr buffer); + void closeFile(); + + std::vector, callbackFn, void*>> m_buffers; + + volatile std::atomic m_abortExport; + bool m_isThreadRunning; + std::mutex m_readMutex; + std::thread* m_thread; +}; + +} // namespace lmms + +#endif // LMMS_THREADED_EXPORT_MANAGER_H diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 1e2c4f3cfdb..f41996ec4e3 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -77,6 +77,7 @@ set(LMMS_SRCS core/SerializingObject.cpp core/Song.cpp core/TempoSyncKnobModel.cpp + core/ThreadedExportManager.cpp core/ThreadPool.cpp core/Timeline.cpp core/TimePos.cpp @@ -106,6 +107,7 @@ set(LMMS_SRCS core/audio/AudioPulseAudio.cpp core/audio/AudioSampleRecorder.cpp core/audio/AudioSdl.cpp + core/audio/FlacExporter.cpp core/lv2/Lv2Basics.cpp core/lv2/Lv2ControlBase.cpp diff --git a/src/core/SampleClip.cpp b/src/core/SampleClip.cpp index 5ef001e20d1..0b496af754c 100644 --- a/src/core/SampleClip.cpp +++ b/src/core/SampleClip.cpp @@ -27,6 +27,7 @@ #include #include +#include "ThreadedExportManager.h" #include "PathUtil.h" #include "SampleBuffer.h" #include "SampleClipView.h" @@ -37,10 +38,14 @@ namespace lmms { -SampleClip::SampleClip(Track* _track, Sample sample, bool isPlaying) - : Clip(_track) - , m_sample(std::move(sample)) - , m_isPlaying(false) +std::unique_ptr SampleClip::s_sampleExporter = std::make_unique(); + + +SampleClip::SampleClip(Track* _track, Sample sample, bool isPlaying) : + Clip(_track), + m_sample(std::move(sample)), + m_isPlaying(false), + m_exportedSampleName("") { saveJournallingState( false ); setSampleFile( "" ); @@ -79,6 +84,7 @@ SampleClip::SampleClip(Track* _track, Sample sample, bool isPlaying) setAutoResize( false ); break; } + updateTrackClips(); } @@ -336,5 +342,19 @@ gui::ClipView * SampleClip::createView( gui::TrackView * _tv ) return new gui::SampleClipView( this, _tv ); } +void SampleClip::exportSampleBuffer(const QString& fileName) +{ + m_exportedSampleName = fileName; + s_sampleExporter->startExporting(fileName, m_sample.buffer(), &SampleClip::exportSampleBufferCallback, this); +} + +void SampleClip::exportSampleBufferCallback(void* thisObject) +{ + if (thisObject == nullptr) { return; } + SampleClip* castedThis = static_cast(thisObject); + castedThis->setSampleFile(castedThis->m_exportedSampleName); + castedThis->m_exportedSampleName.clear(); +} + } // namespace lmms diff --git a/src/core/ThreadedExportManager.cpp b/src/core/ThreadedExportManager.cpp new file mode 100644 index 00000000000..e1b89658c7d --- /dev/null +++ b/src/core/ThreadedExportManager.cpp @@ -0,0 +1,113 @@ +/* + * ThreadedExportManager.cpp - exports files in .flac format on an other thread + * + * Copyright (c) 2024 - 2025 szeli1 + * + * 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 "ThreadedExportManager.h" + +#include "FlacExporter.h" +#include "SampleBuffer.h" + +namespace lmms +{ + +ThreadedExportManager::ThreadedExportManager() : + m_abortExport(false), + m_isThreadRunning(false), + m_readMutex(), + m_thread(nullptr) +{} + +ThreadedExportManager::~ThreadedExportManager() +{ + stopExporting(); +} + + +void ThreadedExportManager::startExporting(const QString& outputLocationAndName, std::shared_ptr buffer, callbackFn callbackFunction, void* callbackObject) +{ + m_readMutex.lock(); + m_buffers.push_back(std::make_tuple(outputLocationAndName, buffer, callbackFunction, callbackObject)); + m_readMutex.unlock(); + + if (m_isThreadRunning == false) + { + stopExporting(); + m_isThreadRunning = true; + m_thread = new std::thread(&ThreadedExportManager::threadedExportFunction, this, &m_abortExport); + } +} + +void ThreadedExportManager::stopExporting() +{ + if (m_thread != nullptr) + { + if (m_isThreadRunning == true) + { + m_abortExport = true; + } + m_thread->join(); + delete m_thread; + m_thread = nullptr; + m_isThreadRunning = false; + m_abortExport = false; + } +} + + +void ThreadedExportManager::threadedExportFunction(ThreadedExportManager* thisExporter, volatile std::atomic* abortExport) +{ + thisExporter->m_isThreadRunning = true; + + while (*abortExport == false) + { + std::tuple, callbackFn, void*> curBuffer = std::make_tuple(QString(""), nullptr, nullptr, nullptr); + thisExporter->m_readMutex.lock(); + bool shouldExit = thisExporter->m_buffers.size() <= 0; + if (shouldExit == false) + { + curBuffer = thisExporter->m_buffers[thisExporter->m_buffers.size() - 1]; + thisExporter->m_buffers.pop_back(); + } + thisExporter->m_readMutex.unlock(); + if (shouldExit) { break; } + + // important new scope + { + FlacExporter flacExporter(std::get<1>(curBuffer)->sampleRate(), 24, std::get<0>(curBuffer)); + if (flacExporter.getIsSuccesful()) + { + flacExporter.writeThisBuffer(std::get<1>(curBuffer)->data(), std::get<1>(curBuffer)->size()); + } + } + + if (std::get<2>(curBuffer)) + { + // calling callback funcion + std::get<2>(curBuffer)(std::get<3>(curBuffer)); + } + } + + thisExporter->m_isThreadRunning = false; +} + +} // namespace lmms diff --git a/src/core/audio/FlacExporter.cpp b/src/core/audio/FlacExporter.cpp new file mode 100644 index 00000000000..9ea7fb2a99d --- /dev/null +++ b/src/core/audio/FlacExporter.cpp @@ -0,0 +1,110 @@ +/* + * FlacExporter.cpp - exports .flac files outside of AudioEngine + * + * Copyright (c) 2024 - 2025 szeli1 + * + * 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 + +#include "FlacExporter.h" + +#include "SampleFrame.h" + +namespace lmms +{ + +FlacExporter::FlacExporter(int sampleRate, int bitDepth, const QString& outputLocationAndName) : + m_isSuccesful(false), + m_fileDescriptor(NULL) +{ + constexpr int channelCount = 2; + SF_INFO exportInfo; + + memset(&exportInfo, 0, sizeof(exportInfo)); + exportInfo.samplerate = sampleRate; + exportInfo.channels = channelCount; + if (bitDepth == 16) + { + exportInfo.format = (SF_FORMAT_FLAC | SF_FORMAT_PCM_16); + } + else if (bitDepth == 24) + { + exportInfo.format = (SF_FORMAT_FLAC | SF_FORMAT_PCM_24); + } + else + { + // there is no 32 bit support + exportInfo.format = (SF_FORMAT_FLAC | SF_FORMAT_PCM_24); + } + + QByteArray characters = outputLocationAndName.toUtf8(); + m_fileDescriptor = sf_open(characters.data(), SFM_WRITE, &exportInfo); + + m_isSuccesful = m_fileDescriptor != NULL; + + if (m_isSuccesful == false) + { + printf("FlacExporter sf_open error\n"); + } +} + +FlacExporter::~FlacExporter() +{ + if (m_fileDescriptor == NULL) { return; } + sf_write_sync(m_fileDescriptor); + m_isSuccesful = sf_close(m_fileDescriptor) == 0; + if (m_isSuccesful == false) + { + printf("FlacExporter sf_close error\n"); + } +} + +void FlacExporter::writeThisBuffer(const SampleFrame* samples, size_t sampleCount) +{ + if (m_fileDescriptor == NULL) { m_isSuccesful = false; return; } + constexpr size_t channelCount = 2; + // multiply by 2 since there is 2 channels + std::vector outputBuffer(sampleCount * channelCount); + size_t i = 0; + for (size_t j = 0; j < sampleCount; j++) + { + outputBuffer[i] = static_cast((samples + j)->left()); + outputBuffer[i + 1] = static_cast((samples + j)->right()); + outputBuffer[i] = outputBuffer[i] > 1.0f ? 1.0f : outputBuffer[i] < -1.0f ? -1.0f : outputBuffer[i]; + outputBuffer[i + 1] = outputBuffer[i + 1] > 1.0f ? 1.0f : outputBuffer[i + 1] < -1.0f ? -1.0f : outputBuffer[i + 1]; + i = i + channelCount; + } + size_t count = sf_writef_float(m_fileDescriptor, outputBuffer.data(), static_cast(sampleCount)); + if (count != sampleCount) + { + m_isSuccesful = false; + printf("FlacExporter sf_writef_float error\n"); + } +} + +bool FlacExporter::getIsSuccesful() const +{ + return m_isSuccesful; +} + +} // namespace lmms diff --git a/src/gui/clips/SampleClipView.cpp b/src/gui/clips/SampleClipView.cpp index a7251be8de6..353069032af 100644 --- a/src/gui/clips/SampleClipView.cpp +++ b/src/gui/clips/SampleClipView.cpp @@ -31,6 +31,7 @@ #include "GuiApplication.h" #include "AutomationEditor.h" #include "embed.h" +#include "FileDialog.h" #include "PathUtil.h" #include "SampleClip.h" #include "SampleLoader.h" @@ -81,6 +82,13 @@ void SampleClipView::constructContextMenu(QMenu* cm) tr( "Set/clear record" ), m_clip, SLOT(toggleRecord()));*/ + cm->addAction( + embed::getIconPixmap("flip_x"), + tr("Export sample buffer"), + this, + SLOT(exportSampleBuffer()) + ); + cm->addAction( embed::getIconPixmap("flip_x"), tr("Reverse sample"), @@ -376,5 +384,15 @@ bool SampleClipView::splitClip( const TimePos pos ) else { return false; } } +void SampleClipView::exportSampleBuffer() +{ + const auto outputFilename = FileDialog::getSaveFileName(nullptr, tr("Export audio file"), QString(), tr("FLAC (*.flac)")); + + if (!outputFilename.isEmpty()) + { + m_clip->exportSampleBuffer(outputFilename); + } +} + } // namespace lmms::gui