MuseScore/mscore/exportaudio.cpp

352 lines
12 KiB
C++

//=============================================================================
// MusE Score
// Linux Music Score Editor
//
// Copyright (C) 2009 Werner Schweer and others
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2.
//
// 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; if not, write to the Free Software
// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
//=============================================================================
#include "config.h"
#ifdef HAS_AUDIOFILE
#include <sndfile.h>
#endif
#include "libmscore/score.h"
#include "libmscore/note.h"
#include "libmscore/part.h"
#include "libmscore/mscore.h"
#include "synthesizer/msynthesizer.h"
#include "musescore.h"
#include "preferences.h"
namespace Ms {
///
/// \brief Function to synthesize audio and output it into a generic QIODevice
/// \param score The score to output
/// \param device The output device
/// \param updateProgress An optional callback function that will be notified with the progress in range [0, 1]
/// \return True on success, false otherwise.
///
/// If the callback function is non zero an returns false the export will be canceled.
///
bool MuseScore::saveAudio(Score* score, QIODevice *device, std::function<bool(float)> updateProgress)
{
if (!device) {
qDebug() << "Invalid device";
return false;
}
if (!device->open(QIODevice::WriteOnly)) {
qDebug() << "Could not write to device";
return false;
}
EventMap events;
// In non-GUI mode current synthesizer settings won't
// allow single note dynamics. See issue #289947.
const bool useCurrentSynthesizerState = !MScore::noGui;
if (useCurrentSynthesizerState) {
score->renderMidi(&events, synthesizerState());
if (events.empty())
return false;
}
MasterSynthesizer* synth = synthesizerFactory();
synth->init();
int sampleRate = preferences.getInt(PREF_EXPORT_AUDIO_SAMPLERATE);
synth->setSampleRate(sampleRate);
if (MScore::noGui) { // use score settings if possible
bool r = synth->setState(score->synthesizerState());
if (!r)
synth->init();
}
else { // use current synth settings
bool r = synth->setState(mscore->synthesizerState());
if (!r)
synth->init();
}
if (!useCurrentSynthesizerState) {
score->masterScore()->rebuildAndUpdateExpressive(synth->synthesizer("Fluid"));
score->renderMidi(&events, score->synthesizerState());
if (synti)
score->masterScore()->rebuildAndUpdateExpressive(synti->synthesizer("Fluid"));
if (events.empty())
return false;
}
int oldSampleRate = MScore::sampleRate;
MScore::sampleRate = sampleRate;
float peak = 0.0;
double gain = 1.0;
EventMap::const_iterator endPos = events.cend();
--endPos;
const int et = (score->utick2utime(endPos->first) + 1) * MScore::sampleRate;
const int maxEndTime = (score->utick2utime(endPos->first) + 3) * MScore::sampleRate;
bool cancelled = false;
int passes = preferences.getBool(PREF_EXPORT_AUDIO_NORMALIZE) ? 2 : 1;
for (int pass = 0; pass < passes; ++pass) {
EventMap::const_iterator playPos;
playPos = events.cbegin();
synth->allSoundsOff(-1);
//
// init instruments
//
for (Part* part : score->parts()) {
const InstrumentList* il = part->instruments();
for (auto i = il->begin(); i!= il->end(); i++) {
for (const Channel* instrChan : i->second->channel()) {
const Channel* a = score->masterScore()->playbackChannel(instrChan);
for (MidiCoreEvent e : a->initList()) {
if (e.type() == ME_INVALID)
continue;
e.setChannel(a->channel());
int syntiIdx = synth->index(score->masterScore()->midiMapping(a->channel())->articulation()->synti());
synth->play(e, syntiIdx);
}
}
}
}
static const unsigned FRAMES = 512;
float buffer[FRAMES * 2];
int playTime = 0;
for (;;) {
unsigned frames = FRAMES;
//
// collect events for one segment
//
float max = 0.0;
memset(buffer, 0, sizeof(float) * FRAMES * 2);
int endTime = playTime + frames;
float* p = buffer;
for (; playPos != events.cend(); ++playPos) {
int f = score->utick2utime(playPos->first) * MScore::sampleRate;
if (f >= endTime)
break;
int n = f - playTime;
if (n) {
synth->process(n, p);
p += 2 * n;
}
playTime += n;
frames -= n;
const NPlayEvent& e = playPos->second;
if (e.isChannelEvent()) {
int channelIdx = e.channel();
const Channel* c = score->masterScore()->midiMapping(channelIdx)->articulation();
if (!c->mute()) {
synth->play(e, synth->index(c->synti()));
}
}
}
if (frames) {
synth->process(frames, p);
playTime += frames;
}
if (pass == 1) {
for (unsigned i = 0; i < FRAMES * 2; ++i) {
max = qMax(max, qAbs(buffer[i]));
buffer[i] *= gain;
}
}
else {
for (unsigned i = 0; i < FRAMES * 2; ++i) {
max = qMax(max, qAbs(buffer[i]));
peak = qMax(peak, qAbs(buffer[i]));
}
}
if (pass == (passes - 1))
device->write(reinterpret_cast<const char*>(buffer), 2 * FRAMES * sizeof(float));
playTime = endTime;
if (updateProgress) {
// normalize to [0, 1] range
if (!updateProgress(float(pass * et + playTime) / passes / et)) {
cancelled = true;
break;
}
}
if (playTime >= et)
synth->allNotesOff(-1);
// create sound until the sound decays
if (playTime >= et && max*peak < 0.000001)
break;
// hard limit
if (playTime > maxEndTime)
break;
}
if (cancelled)
break;
if (pass == 0 && peak == 0.0) {
qDebug("song is empty");
break;
}
gain = 0.99 / peak;
}
MScore::sampleRate = oldSampleRate;
delete synth;
device->close();
return !cancelled;
}
#ifdef HAS_AUDIOFILE
//---------------------------------------------------------
// saveAudio
//---------------------------------------------------------
bool MuseScore::saveAudio(Score* score, const QString& name)
{
// QIODevice - SoundFile wrapper class
class SoundFileDevice : public QIODevice {
private:
SF_INFO info;
SNDFILE *sf = nullptr;
const QString filename;
public:
SoundFileDevice(int sampleRate, int format, const QString& name)
: filename(name) {
memset(&info, 0, sizeof(info));
info.channels = 2;
info.samplerate = sampleRate;
info.format = format;
}
~SoundFileDevice() {
if (sf) {
sf_close(sf);
sf = nullptr;
}
}
virtual qint64 readData(char *dta, qint64 maxlen) override final {
Q_UNUSED(dta);
qDebug() << "Error: No write supported!";
return maxlen;
}
virtual qint64 writeData(const char *dta, qint64 len) override final {
size_t trueFrames = len / sizeof(float) / 2;
sf_writef_float(sf, reinterpret_cast<const float*>(dta), trueFrames);
return trueFrames * 2 * sizeof(float);
}
bool open(QIODevice::OpenMode mode) {
if ((mode & QIODevice::WriteOnly) == 0) {
return false;
}
sf = sf_open(qPrintable(filename), SFM_WRITE, &info);
if (sf == nullptr) {
qDebug("open soundfile failed: %s", sf_strerror(sf));
return false;
}
return QIODevice::open(mode);
}
void close() {
if (sf && sf_close(sf)) {
qDebug("close soundfile failed");
}
sf = nullptr;
QIODevice::close();
}
};
int format;
if (name.endsWith(".wav"))
format = SF_FORMAT_WAV | SF_FORMAT_PCM_16;
else if (name.endsWith(".ogg"))
format = SF_FORMAT_OGG | SF_FORMAT_VORBIS;
else if (name.endsWith("flac"))
format = SF_FORMAT_FLAC | SF_FORMAT_PCM_16;
else {
qDebug("unknown audio file type <%s>", qPrintable(name));
return false;
}
EventMap events;
score->renderMidi(&events, synthesizerState());
if(events.size() == 0)
return false;
MasterSynthesizer* synth = synthesizerFactory();
synth->init();
int sampleRate = preferences.getInt(PREF_EXPORT_AUDIO_SAMPLERATE);
synth->setSampleRate(sampleRate);
bool r = synth->setState(score->synthesizerState());
if (!r)
synth->init();
int oldSampleRate = MScore::sampleRate;
MScore::sampleRate = sampleRate;
SoundFileDevice device(sampleRate, format, name);
// dummy callback function that will be used if there is no gui
std::function<bool(float)> progressCallback = [](float) {return true;};
QProgressDialog progress(this);
progress.setWindowFlags(Qt::WindowFlags(Qt::Dialog | Qt::FramelessWindowHint | Qt::WindowTitleHint));
progress.setWindowModality(Qt::ApplicationModal);
//progress.setCancelButton(0);
progress.setCancelButtonText(tr("Cancel"));
progress.setLabelText(tr("Exporting…"));
if (!MScore::noGui) {
// callback function that will update the progress bar
// it will return false and thus cancel the export if the user
// cancels the progress dialog.
progressCallback = [&progress](float v) -> bool {
if (progress.wasCanceled())
return false;
progress.setValue(v * 1000);
qApp->processEvents();
return true;
};
progress.show();
}
// The range is set arbitrarily to 1000 as steps.
// The callback will return float numbers between 0 and 1
// which will be scaled into integer 0 to 1000 numbers
// which allows a smooth transition.
progress.setRange(0, 1000);
// Save the audio to the SoundFile device
bool result = saveAudio(score, &device, progressCallback);
bool wasCanceled = progress.wasCanceled();
progress.close();
MScore::sampleRate = oldSampleRate;
delete synth;
if (wasCanceled)
QFile::remove(name);
return result;
}
#endif // HAS_AUDIOFILE
}