diff --git a/data/rc/rosegardenmainwindow.rc b/data/rc/rosegardenmainwindow.rc index d83c82932..39cc1e315 100644 --- a/data/rc/rosegardenmainwindow.rc +++ b/data/rc/rosegardenmainwindow.rc @@ -38,6 +38,8 @@ + + @@ -535,6 +537,7 @@ + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index dc44f789f..d548e7ae4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -579,6 +579,7 @@ set(rg_CPPS sound/Resampler.cpp sound/ExternalController.cpp sound/KorgNanoKontrol2.cpp + sound/WAVExporter.cpp commands/notation/ResetDisplacementsCommand.cpp commands/notation/RemoveNotationQuantizeCommand.cpp commands/notation/AddMarkCommand.cpp diff --git a/src/gui/application/RosegardenMainWindow.cpp b/src/gui/application/RosegardenMainWindow.cpp index 847d871b8..20eab7953 100644 --- a/src/gui/application/RosegardenMainWindow.cpp +++ b/src/gui/application/RosegardenMainWindow.cpp @@ -271,9 +271,6 @@ RosegardenMainWindow::RosegardenMainWindow(bool enableSound, m_recentFiles(), m_sequencerThread(nullptr), m_sequencerCheckedIn(false), -#ifdef HAVE_LIBJACK - m_jackProcess(nullptr), -#endif m_cpuBar(nullptr), m_zoomSlider(nullptr), m_zoomLabel(nullptr), @@ -747,6 +744,7 @@ RosegardenMainWindow::setupActions() createAction("file_export_midi", SLOT(slotExportMIDI())); createAction("file_export_lilypond", SLOT(slotExportLilyPond())); createAction("file_export_musicxml", SLOT(slotExportMusicXml())); + createAction("file_export_wav", SLOT(slotExportWAV())); createAction("file_export_csound", SLOT(slotExportCsound())); createAction("file_export_mup", SLOT(slotExportMup())); createAction("file_print_lilypond", SLOT(slotPrintLilyPond())); @@ -5080,70 +5078,6 @@ RosegardenMainWindow::slotTestStartupTester() return ; } -/* QStringList missingFeatures; - QStringList allMissing; - - QStringList missing; - -#ifdef HAVE_LIBJACK - if (m_seqManager && (m_seqManager->getSoundDriverStatus() & AUDIO_OK)) { - - m_haveAudioImporter = m_startupTester->haveAudioFileImporter(&missing); - - if (!m_haveAudioImporter) { - missingFeatures.push_back(tr("General audio file import and conversion")); - if (missing.count() == 0) { - allMissing.push_back(tr("The Rosegarden Audio File Importer helper script")); - } else { - for (int i = 0; i < missing.count(); ++i) { - if (missingFeatures.count() > 1) { - allMissing.push_back(tr("%1 - for audio file import").arg(missing[i])); - } else { - allMissing.push_back(missing[i]); - } - } - } - } - } -#endif - - if (missingFeatures.count() > 0) { - QString message = tr("

Helper programs not found

Rosegarden could not find one or more helper programs which it needs to provide some features. The following features will not be available:

"); - message += tr("
    "); - for (int i = 0; i < missingFeatures.count(); ++i) { - message += tr("
  • %1
  • ").arg(missingFeatures[i]); - } - message += tr("
"); - message += tr("

To fix this, you should install the following additional programs:

"); - message += tr("
    "); - for (int i = 0; i < allMissing.count(); ++i) { - message += tr("
  • %1
  • ").arg(allMissing[i]); - } - message += tr("
"); - - awaitDialogClearance(); - - QString shortMessage = tr("Helper programs not found"); - -// QMessageBox info(m_view); -// info.setText(shortMessage); -// info.setInformativeText(message); -// info.setStandardButtons(QMessageBox::Ok); -// info.setDefaultButton(QMessageBox::Ok); -// info.setIcon(QMessageBox::Warning); -// -// if (!DialogSuppressor::shouldSuppress -// (&info, "startuphelpersmissing")) { -// info.exec(); -// } - - // Looks like Thorn will have to keep the startup test for - // audiofile-importer around indefinitely, and so we need to move that - // irritating @#@^@#^ dialog into the warning widget, and get it out of - // my face before I punch it right in the nose. - m_warningWidget->queueMessage(shortMessage, message); - }*/ - m_startupTester->wait(); delete m_startupTester; m_startupTester = nullptr; @@ -5535,6 +5469,49 @@ RosegardenMainWindow::slotExportMusicXml() exportMusicXmlFile(fileName); } +void +RosegardenMainWindow::slotExportWAV() +{ + RG_DEBUG << "slotExportWAV()"; + + if (!m_seqManager) + return; + + if (!(m_seqManager->getSoundDriverStatus() & AUDIO_OK)) { + QMessageBox::information( + this, // parent + tr("Rosegarden"), // title + tr("Unable to export WAV without JACK running.")); // text + return; + } + + QString fileName = FileDialog::getSaveFileName( + this, // parent + tr("Rosegarden"), // caption + "", // dir + "", // defaultName + tr("WAV files") + " (*.wav)"); // filter + + if (fileName.isEmpty()) + return; + + if (fileName.right(4).toLower() != ".wav") + fileName += ".wav"; + + QString msg = tr( + "Press play to start exporting to\n" + "%1\n" + "Press stop to stop export.\n" + "Only audio and synth plugin tracks will be exported").arg(fileName); + + QMessageBox::information( + this, // parent + tr("Rosegarden"), // title + msg); // text + + m_seqManager->setExportWavFile(fileName); +} + void RosegardenMainWindow::exportMusicXmlFile(QString file) { diff --git a/src/gui/application/RosegardenMainWindow.h b/src/gui/application/RosegardenMainWindow.h index b6c872170..99a78f964 100644 --- a/src/gui/application/RosegardenMainWindow.h +++ b/src/gui/application/RosegardenMainWindow.h @@ -657,6 +657,11 @@ public slots: */ void slotExportMusicXml(); + /** + * Export (render) file to audio (only audio and synth plugins) + */ + void slotExportWAV(); + /** * closes all open windows by calling close() on each memberList * item until the list is empty, then quits the application. If @@ -1567,10 +1572,6 @@ protected slots: SequencerThread *m_sequencerThread; bool m_sequencerCheckedIn; -#ifdef HAVE_LIBJACK - QProcess *m_jackProcess; -#endif // HAVE_LIBJACK - /// CPU meter in the main window status bar. /** * This is NOT a general-purpose progress indicator. You want to use diff --git a/src/gui/dialogs/SelectBankDialog.cpp b/src/gui/dialogs/SelectBankDialog.cpp index ebebfcc82..555be3dee 100644 --- a/src/gui/dialogs/SelectBankDialog.cpp +++ b/src/gui/dialogs/SelectBankDialog.cpp @@ -16,7 +16,7 @@ */ #define RG_MODULE_STRING "[SelectBankDialog]" -//#define RG_NO_DEBUG_PRINT +#define RG_NO_DEBUG_PRINT #include "SelectBankDialog.h" diff --git a/src/gui/seqmanager/SequenceManager.cpp b/src/gui/seqmanager/SequenceManager.cpp index 3fae5ad93..7bf9134d7 100644 --- a/src/gui/seqmanager/SequenceManager.cpp +++ b/src/gui/seqmanager/SequenceManager.cpp @@ -55,6 +55,7 @@ #include "sound/MappedEventList.h" #include "sound/MappedEvent.h" #include "sound/MappedInstrument.h" +#include "sound/WAVExporter.h" #include "misc/Preferences.h" #include "rosegarden-version.h" // for VERSION @@ -95,8 +96,13 @@ SequenceManager::SequenceManager() : m_recordTime(new QElapsedTimer()), m_lastTransportStartPosition(0), m_sampleRate(0), - m_tempo(0) + m_tempo(0), + m_wavExporter(nullptr), + m_exportTimer(new QTimer(this)) { + // The timer is started when required + connect(m_exportTimer, &QTimer::timeout, + this, &SequenceManager::slotExportUpdate); } SequenceManager::~SequenceManager() @@ -105,6 +111,10 @@ SequenceManager::~SequenceManager() if (m_doc) m_doc->getComposition().removeObserver(this); + + if (m_wavExporter) { + delete m_wavExporter; + } } void @@ -1080,6 +1090,19 @@ void SequenceManager::slotLoopChanged() } } +void SequenceManager::slotExportUpdate() +{ + // The timer is only run when the m_compositionExportManager is set + m_wavExporter->update(); + if (m_wavExporter->isComplete()) { + RG_DEBUG << "deleting completed export manager"; + delete m_wavExporter; + m_wavExporter = nullptr; + // timer no longer needed + m_exportTimer->stop(); + } +} + bool SequenceManager::inCountIn(const RealTime &time) const { if (m_transportStatus == RECORDING || @@ -1866,6 +1889,25 @@ SequenceManager::getSampleRate() const return m_sampleRate; } +void +SequenceManager::setExportWavFile(const QString& fileName) +{ + RG_DEBUG << "setExportWavFile" << fileName; + if (m_wavExporter) { + RG_DEBUG << "replacing previous export manager"; + delete m_wavExporter; + } + m_wavExporter = new WAVExporter(fileName); + // If creation of the WAVExporter has failed, bail. + if (!m_wavExporter->isOK()) + return; + + // and install in the driver + RosegardenSequencer::getInstance()->installExporter(m_wavExporter); + // and start the timer + m_exportTimer->start(50); +} + bool SequenceManager::shouldWarnForImpreciseTimer() { diff --git a/src/gui/seqmanager/SequenceManager.h b/src/gui/seqmanager/SequenceManager.h index 4180dfa77..4773db35d 100644 --- a/src/gui/seqmanager/SequenceManager.h +++ b/src/gui/seqmanager/SequenceManager.h @@ -51,6 +51,7 @@ class CountdownDialog; class CompositionMapper; class AudioManagerDialog; class MappedBufMetaIterator; +class WAVExporter; /** @@ -261,6 +262,9 @@ class ROSEGARDENPRIVATE_EXPORT SequenceManager : /// Get sample rate from RosegardenSequencer. int getSampleRate() const; + /// set file for export of composition at next play + void setExportWavFile(const QString& fileName); + public slots: /** @@ -281,6 +285,8 @@ public slots: void slotLoopChanged(); + void slotExportUpdate(); + signals: /// A program change was received. /** @@ -482,6 +488,8 @@ private slots: /// Used by setTempo() to detect tempo changes. tempoT m_tempo; + WAVExporter* m_wavExporter; + QTimer *m_exportTimer; }; diff --git a/src/sequencer/RosegardenSequencer.cpp b/src/sequencer/RosegardenSequencer.cpp index 5aa900099..450c55234 100644 --- a/src/sequencer/RosegardenSequencer.cpp +++ b/src/sequencer/RosegardenSequencer.cpp @@ -1523,6 +1523,12 @@ RosegardenSequencer::initialiseStudio() m_studio->clear(); } +void +RosegardenSequencer::installExporter(WAVExporter* wavExporter) +{ + m_driver->installExporter(wavExporter); +} + void RosegardenSequencer::checkForNewClients() { diff --git a/src/sequencer/RosegardenSequencer.h b/src/sequencer/RosegardenSequencer.h index 1e7e2daa3..37b5432a7 100644 --- a/src/sequencer/RosegardenSequencer.h +++ b/src/sequencer/RosegardenSequencer.h @@ -38,6 +38,7 @@ namespace Rosegarden { class MappedInstrument; class SoundDriver; +class WAVExporter; /// MIDI and Audio recording and playback @@ -435,6 +436,8 @@ class RosegardenSequencer : public QObject /// Initialise the virtual studio at this end of the link. void initialiseStudio(); + void installExporter(WAVExporter* wavExporter); + // --------- Transport Interface -------- // diff --git a/src/sound/AlsaDriver.cpp b/src/sound/AlsaDriver.cpp index 71834d515..7e0ee6e82 100644 --- a/src/sound/AlsaDriver.cpp +++ b/src/sound/AlsaDriver.cpp @@ -5489,6 +5489,16 @@ AlsaDriver::scavengePlugins() m_pluginScavenger.scavenge(); } +void +AlsaDriver::installExporter(WAVExporter* wavExporter) +{ +#ifdef HAVE_LIBJACK + if (m_jackDriver) { + m_jackDriver->installExporter(wavExporter); + } +#endif +} + QString AlsaDriver::getStatusLog() { diff --git a/src/sound/AlsaDriver.h b/src/sound/AlsaDriver.h index 82e6527af..237ab82b2 100644 --- a/src/sound/AlsaDriver.h +++ b/src/sound/AlsaDriver.h @@ -350,6 +350,8 @@ class AlsaDriver : public SoundDriver void claimUnwantedPlugin(void *plugin) override; void scavengePlugins() override; + void installExporter(WAVExporter* WAVExporter) override; + /// Update Ports and Connections /** * Updates m_alsaPorts and m_devicePortMap to match the ports and diff --git a/src/sound/JackDriver.cpp b/src/sound/JackDriver.cpp index 5164b3130..3cebfd2e4 100644 --- a/src/sound/JackDriver.cpp +++ b/src/sound/JackDriver.cpp @@ -26,6 +26,7 @@ #include "Audit.h" #include "PluginFactory.h" #include "SequencerDataBlock.h" +#include "sound/WAVExporter.h" #include "misc/ConfigGroups.h" #include "misc/Debug.h" @@ -73,7 +74,9 @@ JackDriver::JackDriver(AlsaDriver *alsaDriver) : m_haveAsyncAudioEvent(false), m_kickedOutAt(0), m_framesProcessed(0), - m_ok(false) + m_ok(false), + m_playing(false), + m_exportManager(nullptr) { initialise(); } @@ -854,6 +857,22 @@ JackDriver::jackProcess(jack_nframes_t nframes) // receiving midi input so always process async audio bool asyncAudio = m_haveAsyncAudioEvent || (synthCount > 0); + if (m_exportManager) { + // Transitioning to play. + if (playing && !m_playing) { + RG_DEBUG << "export start playing"; + m_exportManager->start(); + } + // Transitioning to stop. + if (!playing && m_playing) { + RG_DEBUG << "export stop playing"; + m_exportManager->stop(); + // finished with the exportManager - it is deleted elsewhere + m_exportManager = nullptr; + } + m_playing = playing; + } + #ifdef DEBUG_JACK_PROCESS Profiler profiler("jackProcess", true); #else @@ -1289,6 +1308,11 @@ JackDriver::jackProcess(jack_nframes_t nframes) } } + + if (m_exportManager && playing) { + m_exportManager->addSamples(master[0], master[1], m_bufferSize); + } + if (playing) { if (!lowLatencyMode) { if (m_bussMixer->getBussCount() == 0) { @@ -2536,6 +2560,12 @@ JackDriver::reportFailure(MappedEvent::FailureCode code) m_alsaDriver->reportFailure(code); } +void +JackDriver::installExporter(WAVExporter* wavExporter) +{ + m_exportManager = wavExporter; +} + } diff --git a/src/sound/JackDriver.h b/src/sound/JackDriver.h index fea6591eb..fd633028f 100644 --- a/src/sound/JackDriver.h +++ b/src/sound/JackDriver.h @@ -36,6 +36,7 @@ class AudioBussMixer; class AudioInstrumentMixer; class AudioFileReader; class AudioFileWriter; +class WAVExporter; class JackDriver { @@ -195,6 +196,8 @@ class JackDriver // void reportFailure(MappedEvent::FailureCode code); + void installExporter(WAVExporter* wavExporter); + protected: // static methods for JACK process thread: @@ -290,8 +293,12 @@ class JackDriver bool m_ok; bool m_checkLoad; -}; + private: + /// Previous play state for detecting state transition for export. + bool m_playing; + WAVExporter* m_exportManager; +}; } diff --git a/src/sound/SoundDriver.h b/src/sound/SoundDriver.h index db6f6747c..c5b494901 100644 --- a/src/sound/SoundDriver.h +++ b/src/sound/SoundDriver.h @@ -39,6 +39,7 @@ namespace Rosegarden { class PlayableData; +class WAVExporter; // Current recording status - whether we're monitoring anything // or recording. @@ -327,6 +328,9 @@ class SoundDriver unsigned int getDevices(); */ + // install the manager for rendering the composition to an audio file + virtual void installExporter(WAVExporter*) { } + protected: // *** General *** diff --git a/src/sound/WAVExporter.cpp b/src/sound/WAVExporter.cpp new file mode 100644 index 000000000..c0206e9f8 --- /dev/null +++ b/src/sound/WAVExporter.cpp @@ -0,0 +1,172 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Rosegarden + A MIDI and audio sequencer and musical notation editor. + Copyright 2000-2024 the Rosegarden development team. + + Other copyrights also apply to some parts of this work. Please + see the AUTHORS file and individual file headers for details. + + 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. See the file + COPYING included with this distribution for more information. +*/ + +#define RG_MODULE_STRING "[WAVExporter]" +#define RG_NO_DEBUG_PRINT 1 + +#include "WAVExporter.h" + +#include "misc/Debug.h" +#include "sound/audiostream/AudioWriteStream.h" +#include "sound/audiostream/AudioWriteStreamFactory.h" +#include "gui/application/RosegardenMainWindow.h" +#include "sequencer/RosegardenSequencer.h" + +#include + + +namespace Rosegarden +{ + + +WAVExporter::WAVExporter(const QString& fileName) +{ + RG_DEBUG << "ctor" << fileName; + + const unsigned sampleRate = + RosegardenSequencer::getInstance()->getSampleRate(); + + // Create the output file. + m_audioWriteStream.reset(AudioWriteStreamFactory::createWriteStream( + fileName, + 2, // channelCount + sampleRate)); + if (!m_audioWriteStream) { + QMessageBox::information( + RosegardenMainWindow::self(), // parent + QObject::tr("Rosegarden"), // title + QObject::tr( + "

WAV Export

" + "

Unable to create WAV file.

")); // text + + return; + } + + // create the ring buffers + m_leftChannelBuffer.reset(new RingBuffer(sampleRate/2)); + m_rightChannelBuffer.reset(new RingBuffer(sampleRate/2)); +} + +void WAVExporter::start() +{ + if (!m_audioWriteStream) + return; + + RG_DEBUG << "start"; + + m_running = true; +} + +void WAVExporter::stop() +{ + RG_DEBUG << "stop"; + m_stopRequested = true; +} + +void WAVExporter::addSamples(sample_t *left, + sample_t *right, + size_t numSamples) +{ + if (!m_audioWriteStream) + return; + if (!m_leftChannelBuffer) + return; + if (!m_rightChannelBuffer) + return; + + RG_DEBUG << "addSamples" << left << right << numSamples; + if (! m_running) { + RG_DEBUG << "addSamples not running"; + return; + } + size_t spacel = m_leftChannelBuffer->getWriteSpace(); + size_t spacer = m_rightChannelBuffer->getWriteSpace(); + if (spacel < numSamples || spacer < numSamples) { + RG_WARNING << "export to audio buffer overflow"; + return; + } + m_leftChannelBuffer->write(left, numSamples); + m_rightChannelBuffer->write(right, numSamples); +} + +void WAVExporter::update() +{ + if (!m_audioWriteStream) + return; + if (!m_leftChannelBuffer) + return; + if (!m_rightChannelBuffer) + return; + + if (m_running) { + size_t spacel = m_leftChannelBuffer->getReadSpace(); + size_t spacer = m_rightChannelBuffer->getReadSpace(); + size_t toRead = spacel; + if (spacer < spacel) toRead = spacer; + if (toRead > 0) { + sample_t lbuf[toRead]; + sample_t rbuf[toRead]; + sample_t ileaveBuf[2 * toRead]; + RG_DEBUG << "update read" << toRead; + m_leftChannelBuffer->read(lbuf, toRead); + m_rightChannelBuffer->read(rbuf, toRead); +#ifndef NDEBUG + // Gather samples squared for debugging. + double ssq = 0.0; +#endif + // For each interleaved sample... + for (size_t is=0; isputInterleavedFrames(toRead, ileaveBuf); + } + if (m_stopRequested) { + RG_DEBUG << "stop requested - deleting write stream"; + + m_running = false; + + // Free all the memory since we are done. + m_audioWriteStream = nullptr; + m_leftChannelBuffer = nullptr; + m_rightChannelBuffer = nullptr; + } + } +} + +bool WAVExporter::isComplete() const +{ + // File creation failed? We're done. + if (!m_audioWriteStream) + return true; + + return (m_stopRequested && ! m_running); +} + + +} diff --git a/src/sound/WAVExporter.h b/src/sound/WAVExporter.h new file mode 100644 index 000000000..85802cdf1 --- /dev/null +++ b/src/sound/WAVExporter.h @@ -0,0 +1,83 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Rosegarden + A MIDI and audio sequencer and musical notation editor. + Copyright 2000-2024 the Rosegarden development team. + + Other copyrights also apply to some parts of this work. Please + see the AUTHORS file and individual file headers for details. + + 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. See the file + COPYING included with this distribution for more information. +*/ + +#ifndef RG_WAVEXPORTER_H +#define RG_WAVEXPORTER_H + +typedef float sample_t; +#include "RingBuffer.h" + +#include + +class QString; + + +namespace Rosegarden +{ + + +class AudioWriteStream; + + +/// Export playback to wav file +class WAVExporter +{ +public: + explicit WAVExporter(const QString& fileName); + /* + * Call this after the ctor to determine whether the file was + * successfully created. + */ + bool isOK() const { return static_cast(m_audioWriteStream); } + + /// called by the audio thread on start playback + void start(); + + /// called by the audio thread on stop playback + void stop(); + + /// called by the audio thread to provide channel data + void addSamples(sample_t *left, sample_t *right, size_t numSamples); + + /// called by the gui thread to update the file data + void update(); + + /// Export is complete, or this object is not OK. + /** + * Called by the gui thread to request completion status. + */ + bool isComplete() const; + +private: + + // Output File + std::shared_ptr m_audioWriteStream; + + // Processing state. + bool m_running{false}; + bool m_stopRequested{false}; + + // Lock-free buffers written by the audio thread and read by the GUI thread. + std::unique_ptr> m_leftChannelBuffer; + std::unique_ptr> m_rightChannelBuffer; + +}; + + +} + +#endif /* ifndef RG_WAVEXPORTER_H */ diff --git a/src/sound/audiostream/AudioWriteStreamFactory.cpp b/src/sound/audiostream/AudioWriteStreamFactory.cpp index dd9a5ef47..2f0b0b8b2 100644 --- a/src/sound/audiostream/AudioWriteStreamFactory.cpp +++ b/src/sound/audiostream/AudioWriteStreamFactory.cpp @@ -14,11 +14,14 @@ COPYING included with this distribution for more information. */ +#define RG_MODULE_STRING "[AudioWriteStreamFactory]" +#define RG_NO_DEBUG_PRINT 1 #include "AudioWriteStreamFactory.h" #include "AudioWriteStream.h" #include "base/ThingFactory.h" +#include "misc/Debug.h" #include @@ -49,7 +52,19 @@ AudioWriteStreamFactory::createWriteStream(QString audioFileName, } catch (...) { } - if (s && s->isOK() && s->getError() == "") { + if (!s) { + RG_WARNING << "createWriteStream(): createFor() returned nullptr"; + return nullptr; + } + + if (!s->isOK()) { + RG_WARNING << "createWriteStream(): AudioWriteStream is not OK"; + } + if (s->getError() != "") { + RG_WARNING << "createWriteStream(): AudioWriteStream error: " << s->getError(); + } + + if (s->isOK() && s->getError() == "") { return s; }