cf17cf828d
As playback is currently always performed for MasterScore, it would be logical to store the information about positions of playback cursor and loop markers in MasterScore rather than individual excerpt scores. This commit implements this approach and fixes the issue with inability to set loop markers from parts. Strictly speaking, only changing Seq::setLoopSelection is absolutely necessary to fix the issue, the approach with moving _pos to MasterScore allows to naturally avoid incorrect visual feedback due to markers positions information being not synchronized across scores and ScoreViews.
1764 lines
62 KiB
C++
1764 lines
62 KiB
C++
//=============================================================================
|
|
// MuseScore
|
|
// Linux Music Score Editor
|
|
//
|
|
// Copyright (C) 2002-2011 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"
|
|
#include "seq.h"
|
|
#include "musescore.h"
|
|
|
|
#include "synthesizer/msynthesizer.h"
|
|
#include "libmscore/rendermidi.h"
|
|
#include "libmscore/slur.h"
|
|
#include "libmscore/tie.h"
|
|
#include "libmscore/score.h"
|
|
#include "libmscore/segment.h"
|
|
#include "libmscore/note.h"
|
|
#include "libmscore/chord.h"
|
|
#include "libmscore/tempo.h"
|
|
#include "scoreview.h"
|
|
#include "playpanel.h"
|
|
#include "libmscore/staff.h"
|
|
#include "libmscore/measure.h"
|
|
#include "preferences.h"
|
|
#include "libmscore/part.h"
|
|
#include "libmscore/ottava.h"
|
|
#include "libmscore/utils.h"
|
|
#include "libmscore/repeatlist.h"
|
|
#include "libmscore/audio.h"
|
|
#include "synthcontrol.h"
|
|
#include "pianoroll.h"
|
|
#include "pianotools.h"
|
|
|
|
#include "click.h"
|
|
|
|
#define OV_EXCLUDE_STATIC_CALLBACKS
|
|
#include <vorbis/vorbisfile.h>
|
|
|
|
#ifdef USE_PORTMIDI
|
|
#if defined(Q_OS_MAC) || defined(Q_OS_WIN)
|
|
#include "portmidi/porttime/porttime.h"
|
|
#else
|
|
#include <porttime.h>
|
|
#endif
|
|
#endif
|
|
|
|
namespace Ms {
|
|
|
|
Seq* seq;
|
|
|
|
static const int guiRefresh = 10; // Hz
|
|
static const int peakHoldTime = 1400; // msec
|
|
static const int peakHold = (peakHoldTime * guiRefresh) / 1000;
|
|
static OggVorbis_File vf;
|
|
|
|
static constexpr int minUtickBufferSize = 480 * 4 * 10; // about 10 measures of 4/4 time signature
|
|
|
|
#if 0 // yet(?) unused
|
|
static const int AUDIO_BUFFER_SIZE = 1024 * 512; // 2 MB
|
|
#endif
|
|
|
|
//---------------------------------------------------------
|
|
// VorbisData
|
|
//---------------------------------------------------------
|
|
|
|
struct VorbisData {
|
|
int pos; // current position in audio->data()
|
|
QByteArray data;
|
|
};
|
|
|
|
static VorbisData vorbisData;
|
|
|
|
static size_t ovRead(void* ptr, size_t size, size_t nmemb, void* datasource);
|
|
static int ovSeek(void* datasource, ogg_int64_t offset, int whence);
|
|
static long ovTell(void* datasource);
|
|
|
|
static ov_callbacks ovCallbacks = {
|
|
ovRead, ovSeek, 0, ovTell
|
|
};
|
|
|
|
//---------------------------------------------------------
|
|
// ovRead
|
|
//---------------------------------------------------------
|
|
|
|
static size_t ovRead(void* ptr, size_t size, size_t nmemb, void* datasource)
|
|
{
|
|
VorbisData* vd = (VorbisData*)datasource;
|
|
size_t n = size * nmemb;
|
|
if (vd->data.size() < int(vd->pos + n))
|
|
n = vd->data.size() - vd->pos;
|
|
if (n) {
|
|
const char* src = vd->data.data() + vd->pos;
|
|
memcpy(ptr, src, n);
|
|
vd->pos += int(n);
|
|
}
|
|
return n;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// ovSeek
|
|
//---------------------------------------------------------
|
|
|
|
static int ovSeek(void* datasource, ogg_int64_t offset, int whence)
|
|
{
|
|
VorbisData* vd = (VorbisData*)datasource;
|
|
switch(whence) {
|
|
case SEEK_SET:
|
|
vd->pos = offset;
|
|
break;
|
|
case SEEK_CUR:
|
|
vd->pos += offset;
|
|
break;
|
|
case SEEK_END:
|
|
vd->pos = vd->data.size() - offset;
|
|
break;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// ovTell
|
|
//---------------------------------------------------------
|
|
|
|
static long ovTell(void* datasource)
|
|
{
|
|
VorbisData* vd = (VorbisData*)datasource;
|
|
return vd->pos;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// Seq
|
|
//---------------------------------------------------------
|
|
|
|
Seq::Seq()
|
|
: midi(nullptr)
|
|
{
|
|
running = false;
|
|
playlistChanged = false;
|
|
cs = 0;
|
|
cv = 0;
|
|
tackRemain = 0;
|
|
tickRemain = 0;
|
|
maxMidiOutPort = 0;
|
|
|
|
endUTick = 0;
|
|
state = Transport::STOP;
|
|
oggInit = false;
|
|
_driver = 0;
|
|
playPos = events.cbegin();
|
|
playFrame = 0;
|
|
metronomeVolume = 0.3;
|
|
useJackTransportSavedFlag = false;
|
|
|
|
inCountIn = false;
|
|
countInPlayPos = countInEvents.cbegin();
|
|
countInPlayFrame = 0;
|
|
|
|
meterValue[0] = 0.0;
|
|
meterValue[1] = 0.0;
|
|
meterPeakValue[0] = 0.0;
|
|
meterPeakValue[1] = 0.0;
|
|
peakTimer[0] = 0;
|
|
peakTimer[1] = 0;
|
|
|
|
heartBeatTimer = new QTimer(this);
|
|
connect(heartBeatTimer, SIGNAL(timeout()), this, SLOT(heartBeatTimeout()));
|
|
|
|
noteTimer = new QTimer(this);
|
|
noteTimer->setSingleShot(true);
|
|
connect(noteTimer, SIGNAL(timeout()), this, SLOT(stopNotes()));
|
|
noteTimer->stop();
|
|
|
|
connect(this, SIGNAL(toGui(int, int)), this, SLOT(seqMessage(int, int)), Qt::QueuedConnection);
|
|
|
|
prevTimeSig.setNumerator(0);
|
|
prevTempo = 0;
|
|
connect(this, SIGNAL(timeSigChanged()),this,SLOT(handleTimeSigTempoChanged()));
|
|
connect(this, SIGNAL(tempoChanged()),this,SLOT(handleTimeSigTempoChanged()));
|
|
|
|
initialMillisecondTimestampWithLatency = 0;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// Seq
|
|
//---------------------------------------------------------
|
|
|
|
Seq::~Seq()
|
|
{
|
|
delete _driver;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// setScoreView
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::setScoreView(ScoreView* v)
|
|
{
|
|
if (oggInit) {
|
|
ov_clear(&vf);
|
|
oggInit = false;
|
|
}
|
|
if (cv !=v && cs) {
|
|
unmarkNotes();
|
|
stopWait();
|
|
}
|
|
cv = v;
|
|
if (cs)
|
|
disconnect(cs, SIGNAL(playlistChanged()), this, SLOT(setPlaylistChanged()));
|
|
cs = cv ? cv->score()->masterScore() : 0;
|
|
midi = MidiRenderer(cs);
|
|
midi.setMinChunkSize(10);
|
|
|
|
if (!heartBeatTimer->isActive())
|
|
heartBeatTimer->start(20); // msec
|
|
|
|
playlistChanged = true;
|
|
_synti->reset();
|
|
if (cs) {
|
|
initInstruments();
|
|
connect(cs, SIGNAL(playlistChanged()), this, SLOT(setPlaylistChanged()));
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// init
|
|
// return false on error
|
|
//---------------------------------------------------------
|
|
|
|
bool Seq::init(bool hotPlug)
|
|
{
|
|
if (!_driver || !_driver->start(hotPlug)) {
|
|
qDebug("Cannot start I/O");
|
|
running = false;
|
|
return false;
|
|
}
|
|
running = true;
|
|
return true;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// exit
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::exit()
|
|
{
|
|
if (_driver) {
|
|
if (MScore::debugMode)
|
|
qDebug("Stop I/O");
|
|
stopWait();
|
|
delete _driver;
|
|
_driver = 0;
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// rewindStart
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::rewindStart()
|
|
{
|
|
seek(0);
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// loopStart
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::loopStart()
|
|
{
|
|
start();
|
|
// qDebug("LoopStart. playPos = %d", playPos);
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// canStart
|
|
// return true if sequencer can be started
|
|
//---------------------------------------------------------
|
|
|
|
bool Seq::canStart()
|
|
{
|
|
if (!_driver)
|
|
return false;
|
|
collectEvents(getPlayStartUtick());
|
|
return (!events.empty() && endUTick != 0);
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// start
|
|
// called from gui thread
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::start()
|
|
{
|
|
if (!_driver) {
|
|
qDebug("No driver!");
|
|
return;
|
|
}
|
|
|
|
allowBackgroundRendering = true;
|
|
collectEvents(getPlayStartUtick());
|
|
if (cs->playMode() == PlayMode::AUDIO) {
|
|
if (!oggInit) {
|
|
vorbisData.pos = 0;
|
|
vorbisData.data = cs->audio()->data();
|
|
int n = ov_open_callbacks(&vorbisData, &vf, 0, 0, ovCallbacks);
|
|
if (n < 0) {
|
|
qDebug("ogg open failed: %d", n);
|
|
}
|
|
oggInit = true;
|
|
}
|
|
}
|
|
|
|
if (!preferences.getBool(PREF_IO_JACK_USEJACKTRANSPORT) || (preferences.getBool(PREF_IO_JACK_USEJACKTRANSPORT) && state == Transport::STOP))
|
|
seek(getPlayStartUtick());
|
|
|
|
if (preferences.getBool(PREF_IO_JACK_USEJACKTRANSPORT) && mscore->countIn() && state == Transport::STOP) {
|
|
// Ready to start playing count in, switching to fake transport
|
|
// to prevent playing in other applications with our ticks simultaneously
|
|
useJackTransportSavedFlag = true;
|
|
preferences.setPreference(PREF_IO_JACK_USEJACKTRANSPORT, false);
|
|
}
|
|
_driver->startTransport();
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// stop
|
|
// called from gui thread
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::stop()
|
|
{
|
|
const bool seqStopped = (state == Transport::STOP);
|
|
const bool driverStopped = !_driver || _driver->getState() == Transport::STOP;
|
|
if (seqStopped && driverStopped)
|
|
return;
|
|
|
|
allowBackgroundRendering = false;
|
|
if (oggInit) {
|
|
ov_clear(&vf);
|
|
oggInit = false;
|
|
}
|
|
if (!_driver)
|
|
return;
|
|
if (!preferences.getBool(PREF_IO_JACK_USEJACKTRANSPORT) || (preferences.getBool(PREF_IO_JACK_USEJACKTRANSPORT) && _driver->getState() == Transport::PLAY))
|
|
_driver->stopTransport();
|
|
if (cv)
|
|
cv->setCursorOn(false);
|
|
if (midiRenderFuture.isRunning())
|
|
midiRenderFuture.waitForFinished();
|
|
if (cs) {
|
|
cs->setUpdateAll();
|
|
cs->update();
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// stopWait
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::stopWait()
|
|
{
|
|
stop();
|
|
QWaitCondition sleep;
|
|
int idx = 0;
|
|
while (state != Transport::STOP) {
|
|
qDebug("State %d", (int)state);
|
|
mutex.lock();
|
|
sleep.wait(&mutex, 100);
|
|
mutex.unlock();
|
|
++idx;
|
|
Q_ASSERT(idx <= 10);
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// seqStarted
|
|
//---------------------------------------------------------
|
|
|
|
void MuseScore::seqStarted()
|
|
{
|
|
if (cv)
|
|
cv->setCursorOn(true);
|
|
if (cs)
|
|
cs->update();
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// seqStopped
|
|
// JACK has stopped
|
|
// executed in gui environment
|
|
//---------------------------------------------------------
|
|
|
|
void MuseScore::seqStopped()
|
|
{
|
|
cv->setCursorOn(false);
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// unmarkNotes
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::unmarkNotes()
|
|
{
|
|
foreach(const Note* n, markedNotes) {
|
|
n->setMark(false);
|
|
cs->addRefresh(n->canvasBoundingRect());
|
|
}
|
|
markedNotes.clear();
|
|
PianoTools* piano = mscore->pianoTools();
|
|
if (piano && piano->isVisible())
|
|
piano->setPlaybackNotes(markedNotes);
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// guiStop
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::guiStop()
|
|
{
|
|
QAction* a = getAction("play");
|
|
a->setChecked(false);
|
|
|
|
unmarkNotes();
|
|
if (!cs)
|
|
return;
|
|
|
|
int tck = cs->repeatList().utick2tick(cs->utime2utick(qreal(playFrame) / qreal(MScore::sampleRate)));
|
|
cs->setPlayPos(Fraction::fromTicks(tck));
|
|
cs->update();
|
|
emit stopped();
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// seqSignal
|
|
// sequencer message to GUI
|
|
// execution environment: gui thread
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::seqMessage(int msg, int arg)
|
|
{
|
|
switch(msg) {
|
|
case '5': {
|
|
// Update the screen after seeking from the realtime thread
|
|
Segment* seg = cs->tick2segment(Fraction::fromTicks(arg));
|
|
if (seg)
|
|
mscore->currentScoreView()->moveCursor(seg->tick());
|
|
cs->setPlayPos(Fraction::fromTicks(arg));
|
|
cs->update();
|
|
break;
|
|
}
|
|
case '4': // Restart the playback at the end of the score
|
|
loopStart();
|
|
break;
|
|
case '3': // Loop restart while playing
|
|
seek(cs->repeatList().tick2utick(cs->loopInTick().ticks()));
|
|
break;
|
|
case '2':
|
|
guiStop();
|
|
// heartBeatTimer->stop();
|
|
if (_driver && mscore->getSynthControl()) {
|
|
meterValue[0] = .0f;
|
|
meterValue[1] = .0f;
|
|
meterPeakValue[0] = .0f;
|
|
meterPeakValue[1] = .0f;
|
|
peakTimer[0] = 0;
|
|
peakTimer[1] = 0;
|
|
mscore->getSynthControl()->setMeter(0.0, 0.0, 0.0, 0.0);
|
|
}
|
|
seek(0);
|
|
break;
|
|
case '0': // STOP
|
|
guiStop();
|
|
// heartBeatTimer->stop();
|
|
if (_driver && mscore->getSynthControl()) {
|
|
meterValue[0] = .0f;
|
|
meterValue[1] = .0f;
|
|
meterPeakValue[0] = .0f;
|
|
meterPeakValue[1] = .0f;
|
|
peakTimer[0] = 0;
|
|
peakTimer[1] = 0;
|
|
mscore->getSynthControl()->setMeter(0.0, 0.0, 0.0, 0.0);
|
|
}
|
|
break;
|
|
|
|
case '1': // PLAY
|
|
emit started();
|
|
// heartBeatTimer->start(1000/guiRefresh);
|
|
break;
|
|
|
|
default:
|
|
qDebug("MScore::Seq:: unknown seq msg %d", msg);
|
|
break;
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// playEvent
|
|
// send one event to the synthesizer
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::playEvent(const NPlayEvent& event, unsigned framePos)
|
|
{
|
|
int type = event.type();
|
|
if (type == ME_NOTEON) {
|
|
if (!event.isMuted()) {
|
|
if (event.discard()) { // ignore noteoff but restrike noteon
|
|
if (event.velo() > 0)
|
|
putEvent(NPlayEvent(ME_NOTEON, event.channel(), event.pitch(), 0) ,framePos);
|
|
else
|
|
return;
|
|
}
|
|
putEvent(event, framePos);
|
|
}
|
|
}
|
|
else if (type == ME_CONTROLLER || type == ME_PITCHBEND)
|
|
putEvent(event, framePos);
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// recomputeMaxMidiOutPort
|
|
// Computes the maximum number of midi out ports
|
|
// in all opened scores
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::recomputeMaxMidiOutPort()
|
|
{
|
|
if (!(preferences.getBool(PREF_IO_JACK_USEJACKMIDI) || preferences.getBool(PREF_IO_ALSA_USEALSAAUDIO)))
|
|
return;
|
|
int max = 0;
|
|
for (Score * s : MuseScoreCore::mscoreCore->scores()) {
|
|
if (s->masterScore()->midiPortCount() > max)
|
|
max = s->masterScore()->midiPortCount();
|
|
}
|
|
maxMidiOutPort = max;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// processMessages
|
|
// from gui to process thread
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::processMessages()
|
|
{
|
|
for (;;) {
|
|
if (toSeq.empty())
|
|
break;
|
|
SeqMsg msg = toSeq.dequeue();
|
|
switch(msg.id) {
|
|
case SeqMsgId::TEMPO_CHANGE:
|
|
{
|
|
if (!cs)
|
|
continue;
|
|
if (playFrame != 0) {
|
|
int utick = cs->utime2utick(qreal(playFrame) / qreal(MScore::sampleRate));
|
|
cs->tempomap()->setRelTempo(msg.realVal);
|
|
playFrame = cs->utick2utime(utick) * MScore::sampleRate;
|
|
if (preferences.getBool(PREF_IO_JACK_TIMEBASEMASTER) && preferences.getBool(PREF_IO_JACK_USEJACKTRANSPORT))
|
|
_driver->seekTransport(utick + 2 * cs->utime2utick(qreal((_driver->bufferSize()) + 1) / qreal(MScore::sampleRate)));
|
|
}
|
|
else
|
|
cs->tempomap()->setRelTempo(msg.realVal);
|
|
cs->masterScore()->updateRepeatListTempo();
|
|
prevTempo = curTempo();
|
|
emit tempoChanged();
|
|
}
|
|
break;
|
|
case SeqMsgId::PLAY:
|
|
putEvent(msg.event);
|
|
break;
|
|
case SeqMsgId::SEEK:
|
|
setPos(msg.intVal);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// metronome
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::metronome(unsigned n, float* p, bool force)
|
|
{
|
|
if (!mscore->metronome() && !force) {
|
|
tickRemain = 0;
|
|
tackRemain = 0;
|
|
return;
|
|
}
|
|
if (tickRemain) {
|
|
tackRemain = 0;
|
|
int idx = tickLength - tickRemain;
|
|
int nn = n < tickRemain ? n : tickRemain;
|
|
for (int i = 0; i < nn; ++i) {
|
|
qreal v = tick[idx] * tickVolume * metronomeVolume;
|
|
*p++ += v;
|
|
*p++ += v;
|
|
++idx;
|
|
}
|
|
tickRemain -= nn;
|
|
}
|
|
if (tackRemain) {
|
|
int idx = tackLength - tackRemain;
|
|
int nn = n < tackRemain ? n : tackRemain;
|
|
for (int i = 0; i < nn; ++i) {
|
|
qreal v = tack[idx] * tackVolume * metronomeVolume;
|
|
*p++ += v;
|
|
*p++ += v;
|
|
++idx;
|
|
}
|
|
tackRemain -= nn;
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// addCountInClicks
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::addCountInClicks()
|
|
{
|
|
const Fraction plPos = cs->playPos();
|
|
Measure* m = cs->tick2measure(plPos);
|
|
Fraction msrTick = m->tick();
|
|
qreal tempo = cs->tempomap()->tempo(msrTick.ticks());
|
|
TimeSigFrac timeSig = cs->sigmap()->timesig(m->tick()).nominal();
|
|
|
|
const int clickTicks = timeSig.isBeatedCompound(tempo) ? timeSig.beatTicks() : timeSig.dUnitTicks();
|
|
|
|
// add at least one full measure of just clicks.
|
|
Fraction endTick = Fraction::fromTicks(timeSig.ticksPerMeasure());
|
|
|
|
// add extra clicks if...
|
|
endTick += plPos - msrTick; // ...not starting playback at beginning of measure
|
|
|
|
if (m->isAnacrusis()) // ...measure is incomplete (anacrusis)
|
|
endTick += Fraction::fromTicks(timeSig.ticksPerMeasure()) - m->ticks();
|
|
|
|
for (int t = 0; t < endTick.ticks(); t += clickTicks) {
|
|
const int rtick = t % timeSig.ticksPerMeasure();
|
|
countInEvents.insert(std::pair<int,NPlayEvent>(t, NPlayEvent(timeSig.rtick2beatType(rtick))));
|
|
}
|
|
|
|
NPlayEvent event;
|
|
event.setType(ME_INVALID);
|
|
event.setPitch(0);
|
|
countInEvents.insert( std::pair<int,NPlayEvent>(endTick.ticks(), event));
|
|
// initialize play parameters to count-in events
|
|
countInPlayPos = countInEvents.cbegin();
|
|
countInPlayFrame = 0;
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
// process
|
|
// This function is called in a realtime context. This
|
|
// means that no blocking operations are allowed which
|
|
// includes memory allocation. The usual thread synchronisation
|
|
// methods like semaphores can also not be used.
|
|
//-------------------------------------------------------------------
|
|
|
|
void Seq::process(unsigned framesPerPeriod, float* buffer)
|
|
{
|
|
unsigned framesRemain = framesPerPeriod; // the number of frames remaining to be processed by this call to Seq::process
|
|
Transport driverState = _driver->getState();
|
|
// Checking for the reposition from JACK Transport
|
|
_driver->checkTransportSeek(playFrame, framesRemain, inCountIn);
|
|
|
|
if (driverState != state) {
|
|
// Got a message from JACK Transport panel: Play
|
|
if (state == Transport::STOP && driverState == Transport::PLAY) {
|
|
if ((preferences.getBool(PREF_IO_JACK_USEJACKMIDI) || preferences.getBool(PREF_IO_JACK_USEJACKAUDIO)) && !getAction("play")->isChecked()) {
|
|
// Do not play while editing elements
|
|
if (mscore->state() != STATE_NORMAL || !isRunning() || !canStart())
|
|
return;
|
|
getAction("play")->setChecked(true);
|
|
getAction("play")->triggered(true);
|
|
|
|
// If we just launch MuseScore and press "Play" on JACK Transport with time 0:00
|
|
// MuseScore doesn't seek to 0 and guiPos is uninitialized, so let's make it manually
|
|
if (preferences.getBool(PREF_IO_JACK_USEJACKTRANSPORT) && getCurTick() == 0)
|
|
seekRT(0);
|
|
|
|
// Switching to fake transport while playing count in
|
|
// to prevent playing in other applications with our ticks simultaneously
|
|
if (preferences.getBool(PREF_IO_JACK_USEJACKTRANSPORT) && mscore->countIn()) {
|
|
// Stopping real JACK Transport
|
|
_driver->stopTransport();
|
|
// Starting fake transport
|
|
useJackTransportSavedFlag = preferences.getBool(PREF_IO_JACK_USEJACKTRANSPORT);
|
|
preferences.setPreference(PREF_IO_JACK_USEJACKTRANSPORT, false);
|
|
_driver->startTransport();
|
|
}
|
|
}
|
|
// Initializing instruments every time we start playback.
|
|
// External synth can have wrong values, for example
|
|
// if we switch between scores
|
|
initInstruments(true);
|
|
// Need to change state after calling collectEvents()
|
|
state = Transport::PLAY;
|
|
if (mscore->countIn() && cs->playMode() == PlayMode::SYNTHESIZER) {
|
|
countInEvents.clear();
|
|
inCountIn = true;
|
|
}
|
|
emit toGui('1');
|
|
}
|
|
// Got a message from JACK Transport panel: Stop
|
|
else if (state == Transport::PLAY && driverState == Transport::STOP) {
|
|
state = Transport::STOP;
|
|
// Muting all notes
|
|
stopNotes(-1, true);
|
|
initInstruments(true);
|
|
if (playPos == eventsEnd) {
|
|
if (mscore->loop()) {
|
|
qDebug("Seq.cpp - Process - Loop whole score. playPos = %d, cs->pos() = %d", playPos->first, cs->pos().ticks());
|
|
emit toGui('4');
|
|
return;
|
|
}
|
|
else {
|
|
emit toGui('2');
|
|
}
|
|
}
|
|
else {
|
|
emit toGui('0');
|
|
}
|
|
}
|
|
else if (state != driverState)
|
|
qDebug("Seq: state transition %d -> %d ?",
|
|
(int)state, (int)driverState);
|
|
}
|
|
|
|
memset(buffer, 0, sizeof(float) * framesPerPeriod * 2); // assume two channels
|
|
float* p = buffer;
|
|
|
|
processMessages();
|
|
|
|
if (state == Transport::PLAY) {
|
|
if (!cs)
|
|
return;
|
|
|
|
// if currently in count-in, these pointers will reference data in the count-in
|
|
EventMap::const_iterator* pPlayPos = &playPos;
|
|
EventMap::const_iterator pEventsEnd = eventsEnd;
|
|
int* pPlayFrame = &playFrame;
|
|
if (inCountIn) {
|
|
if (countInEvents.size() == 0)
|
|
addCountInClicks();
|
|
pEventsEnd = countInEvents.cend();
|
|
pPlayPos = &countInPlayPos;
|
|
pPlayFrame = &countInPlayFrame;
|
|
}
|
|
|
|
//
|
|
// play events for one segment
|
|
//
|
|
unsigned framePos = 0; // frame currently being processed relative to the first frame of this call to Seq::process
|
|
int periodEndFrame = *pPlayFrame + framesPerPeriod; // the ending frame (relative to start of playback) of the period being processed by this call to Seq::process
|
|
int scoreEndUTick = cs->repeatList().tick2utick(cs->lastMeasure()->endTick().ticks());
|
|
while (*pPlayPos != pEventsEnd) {
|
|
int playPosUTick = (*pPlayPos)->first;
|
|
int n; // current frame (relative to start of playback) that is being synthesized
|
|
|
|
if (inCountIn) {
|
|
qreal beatsPerSecond = curTempo() * cs->tempomap()->relTempo(); // relTempo needed here to ensure that bps changes as we slide the tempo bar
|
|
qreal ticksPerSecond = beatsPerSecond * MScore::division;
|
|
qreal playPosSeconds = playPosUTick / ticksPerSecond;
|
|
int playPosFrame = playPosSeconds * MScore::sampleRate;
|
|
if (playPosFrame >= periodEndFrame)
|
|
break;
|
|
n = playPosFrame - *pPlayFrame;
|
|
if (n < 0) {
|
|
qDebug("Count-in: playPosUTick %d: n = %d - %d", playPosUTick, playPosFrame, *pPlayFrame);
|
|
n = 0;
|
|
}
|
|
}
|
|
else {
|
|
qreal playPosSeconds = cs->utick2utime(playPosUTick);
|
|
int playPosFrame = playPosSeconds * MScore::sampleRate;
|
|
if (playPosFrame >= periodEndFrame)
|
|
break;
|
|
n = playPosFrame - *pPlayFrame;
|
|
if (n < 0) {
|
|
qDebug("%d: %d - %d", playPosUTick, playPosFrame, *pPlayFrame);
|
|
n = 0;
|
|
}
|
|
if (mscore->loop()) {
|
|
int loopOutUTick = cs->repeatList().tick2utick(cs->loopOutTick().ticks());
|
|
if (loopOutUTick < scoreEndUTick) {
|
|
// Also make sure we are not "before" the loop
|
|
if (playPosUTick >= loopOutUTick || cs->repeatList().utick2tick(playPosUTick) < cs->loopInTick().ticks()) {
|
|
qDebug ("Process: playPosUTick = %d, cs->loopInTick().ticks() = %d, cs->loopOutTick().ticks() = %d, getCurTick() = %d, loopOutUTick = %d, playFrame = %d",
|
|
playPosUTick, cs->loopInTick().ticks(), cs->loopOutTick().ticks(), getCurTick(), loopOutUTick, *pPlayFrame);
|
|
if (preferences.getBool(PREF_IO_JACK_USEJACKTRANSPORT)) {
|
|
int loopInUTick = cs->repeatList().tick2utick(cs->loopInTick().ticks());
|
|
_driver->seekTransport(loopInUTick);
|
|
if (loopInUTick != 0) {
|
|
int seekto = loopInUTick - 2 * cs->utime2utick((qreal)_driver->bufferSize() / MScore::sampleRate);
|
|
seekRT((seekto > 0) ? seekto : 0 );
|
|
}
|
|
}
|
|
else {
|
|
emit toGui('3');
|
|
}
|
|
// Exit this function to avoid segmentation fault in Scoreview
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (n) {
|
|
if (cs->playMode() == PlayMode::SYNTHESIZER) {
|
|
metronome(n, p, inCountIn);
|
|
_synti->process(n, p);
|
|
p += n * 2;
|
|
*pPlayFrame += n;
|
|
framesRemain -= n;
|
|
framePos += n;
|
|
}
|
|
else {
|
|
while (n > 0) {
|
|
int section;
|
|
float** pcm;
|
|
long rn = ov_read_float(&vf, &pcm, n, §ion);
|
|
if (rn == 0)
|
|
break;
|
|
for (int i = 0; i < rn; ++i) {
|
|
*p++ = pcm[0][i];
|
|
*p++ = pcm[1][i];
|
|
}
|
|
*pPlayFrame += rn;
|
|
framesRemain -= rn;
|
|
framePos += rn;
|
|
n -= rn;
|
|
}
|
|
}
|
|
}
|
|
const NPlayEvent& event = (*pPlayPos)->second;
|
|
playEvent(event, framePos);
|
|
if (event.type() == ME_TICK1) {
|
|
tickRemain = tickLength;
|
|
tickVolume = event.velo() ? qreal(event.value()) / 127.0 : 1.0;
|
|
}
|
|
else if (event.type() == ME_TICK2) {
|
|
tackRemain = tackLength;
|
|
tackVolume = event.velo() ? qreal(event.value()) / 127.0 : 1.0;
|
|
}
|
|
mutex.lock();
|
|
++(*pPlayPos);
|
|
mutex.unlock();
|
|
}
|
|
if (framesRemain) {
|
|
if (cs->playMode() == PlayMode::SYNTHESIZER) {
|
|
metronome(framesRemain, p, inCountIn);
|
|
_synti->process(framesRemain, p);
|
|
*pPlayFrame += framesRemain;
|
|
}
|
|
else {
|
|
int n = framesRemain;
|
|
while (n > 0) {
|
|
int section;
|
|
float** pcm;
|
|
long rn = ov_read_float(&vf, &pcm, n, §ion);
|
|
if (rn == 0)
|
|
break;
|
|
for (int i = 0; i < rn; ++i) {
|
|
*p++ = pcm[0][i];
|
|
*p++ = pcm[1][i];
|
|
}
|
|
*pPlayFrame += rn;
|
|
framesRemain -= rn;
|
|
framePos += rn;
|
|
n -= rn;
|
|
}
|
|
}
|
|
}
|
|
if (*pPlayPos == pEventsEnd) {
|
|
if (inCountIn) {
|
|
inCountIn = false;
|
|
// Connecting to JACK Transport if MuseScore was temporarily disconnected from it
|
|
if (useJackTransportSavedFlag) {
|
|
// Stopping fake driver
|
|
_driver->stopTransport();
|
|
preferences.setPreference(PREF_IO_JACK_USEJACKTRANSPORT, true);
|
|
// Starting the real JACK Transport. All applications play in sync now
|
|
_driver->startTransport();
|
|
}
|
|
}
|
|
else
|
|
_driver->stopTransport();
|
|
}
|
|
}
|
|
else {
|
|
// Outside of playback mode
|
|
while (!liveEventQueue()->empty()) {
|
|
const NPlayEvent& event = liveEventQueue()->dequeue();
|
|
if (event.type() == ME_TICK1) {
|
|
tickRemain = tickLength;
|
|
tickVolume = event.velo() ? qreal(event.value()) / 127.0 : 1.0;
|
|
}
|
|
else if (event.type() == ME_TICK2) {
|
|
tackRemain = tackLength;
|
|
tackVolume = event.velo() ? qreal(event.value()) / 127.0 : 1.0;
|
|
}
|
|
}
|
|
if (framesRemain) {
|
|
metronome(framesRemain, p, true);
|
|
_synti->process(framesRemain, p);
|
|
}
|
|
}
|
|
//
|
|
// metering / master gain
|
|
//
|
|
qreal lv = 0.0f;
|
|
qreal rv = 0.0f;
|
|
p = buffer;
|
|
for (unsigned i = 0; i < framesRemain; ++i) {
|
|
qreal val = *p;
|
|
lv = qMax(lv, qAbs(val));
|
|
p++;
|
|
|
|
val = *p;
|
|
rv = qMax(rv, qAbs(val));
|
|
p++;
|
|
}
|
|
meterValue[0] = lv;
|
|
meterValue[1] = rv;
|
|
if (meterPeakValue[0] < lv) {
|
|
meterPeakValue[0] = lv;
|
|
peakTimer[0] = 0;
|
|
}
|
|
if (meterPeakValue[1] < rv) {
|
|
meterPeakValue[1] = rv;
|
|
peakTimer[1] = 0;
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// initInstruments
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::initInstruments(bool realTime)
|
|
{
|
|
// Add midi out ports if necessary
|
|
if (cs && (preferences.getBool(PREF_IO_JACK_USEJACKMIDI) || preferences.getBool(PREF_IO_ALSA_USEALSAAUDIO))) {
|
|
// Increase the maximum number of midi ports if user adds staves/instruments
|
|
int scoreMaxMidiPort = cs->masterScore()->midiPortCount();
|
|
if (maxMidiOutPort < scoreMaxMidiPort)
|
|
maxMidiOutPort = scoreMaxMidiPort;
|
|
// if maxMidiOutPort is equal to existing ports number, it will do nothing
|
|
if (_driver)
|
|
_driver->updateOutPortCount(maxMidiOutPort + 1);
|
|
}
|
|
|
|
for (const MidiMapping& mm : cs->midiMapping()) {
|
|
const Channel* channel = mm.articulation();
|
|
for (const MidiCoreEvent& e : channel->initList()) {
|
|
if (e.type() == ME_INVALID)
|
|
continue;
|
|
NPlayEvent event(e.type(), channel->channel(), e.dataA(), e.dataB());
|
|
if (realTime)
|
|
putEvent(event);
|
|
else
|
|
sendEvent(event);
|
|
}
|
|
// Setting pitch bend sensitivity to 12 semitones for external synthesizers
|
|
if ((preferences.getBool(PREF_IO_JACK_USEJACKMIDI) || preferences.getBool(PREF_IO_ALSA_USEALSAAUDIO)) && mm.channel() != 9) {
|
|
if (realTime) {
|
|
putEvent(NPlayEvent(ME_CONTROLLER, channel->channel(), CTRL_LRPN, 0));
|
|
putEvent(NPlayEvent(ME_CONTROLLER, channel->channel(), CTRL_HRPN, 0));
|
|
putEvent(NPlayEvent(ME_CONTROLLER, channel->channel(), CTRL_HDATA,12));
|
|
putEvent(NPlayEvent(ME_CONTROLLER, channel->channel(), CTRL_LRPN, 127));
|
|
putEvent(NPlayEvent(ME_CONTROLLER, channel->channel(), CTRL_HRPN, 127));
|
|
}
|
|
else {
|
|
sendEvent(NPlayEvent(ME_CONTROLLER, channel->channel(), CTRL_LRPN, 0));
|
|
sendEvent(NPlayEvent(ME_CONTROLLER, channel->channel(), CTRL_HRPN, 0));
|
|
sendEvent(NPlayEvent(ME_CONTROLLER, channel->channel(), CTRL_HDATA,12));
|
|
sendEvent(NPlayEvent(ME_CONTROLLER, channel->channel(), CTRL_LRPN, 127));
|
|
sendEvent(NPlayEvent(ME_CONTROLLER, channel->channel(), CTRL_HRPN, 127));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// renderChunk
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::renderChunk(const MidiRenderer::Chunk& ch, EventMap* eventMap)
|
|
{
|
|
midi.renderChunk(ch, eventMap, mscore->synthesizerState(), /* metronome */ true);
|
|
renderEventsStatus.setOccupied(ch.utick1(), ch.utick2());
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// updateEventsEnd
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::updateEventsEnd()
|
|
{
|
|
auto end = events.cend();
|
|
eventsEnd = end;
|
|
endUTick = events.empty() ? 0 : (--end)->first;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// collectEvents
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::collectEvents(int utick)
|
|
{
|
|
//do not collect even while playing
|
|
if (state == Transport::PLAY && playlistChanged)
|
|
return;
|
|
|
|
mutex.lock();
|
|
|
|
if (midiRenderFuture.isRunning())
|
|
midiRenderFuture.waitForFinished();
|
|
|
|
if (playlistChanged) {
|
|
midi.setScoreChanged();
|
|
events.clear();
|
|
renderEvents.clear();
|
|
renderEventsStatus.clear();
|
|
}
|
|
else if (!renderEvents.empty()) {
|
|
events.insert(renderEvents.begin(), renderEvents.end());
|
|
renderEvents.clear();
|
|
}
|
|
|
|
int unrenderedUtick = renderEventsStatus.occupiedRangeEnd(utick);
|
|
while (unrenderedUtick - utick < minUtickBufferSize) {
|
|
const MidiRenderer::Chunk chunk = midi.getChunkAt(unrenderedUtick);
|
|
if (!chunk)
|
|
break;
|
|
renderChunk(chunk, &events);
|
|
unrenderedUtick = renderEventsStatus.occupiedRangeEnd(utick);
|
|
}
|
|
|
|
updateEventsEnd();
|
|
playPos = events.cbegin();
|
|
playlistChanged = false;
|
|
mutex.unlock();
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// ensureBufferAsync
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::ensureBufferAsync(int utick)
|
|
{
|
|
if (mutex.tryLock()) { // sync with possible collectEvents calls
|
|
|
|
if (midiRenderFuture.isRunning() || !allowBackgroundRendering) {
|
|
mutex.unlock();
|
|
return;
|
|
}
|
|
|
|
if (!renderEvents.empty()) {
|
|
// TODO: use C++17 map::merge()?
|
|
events.insert(renderEvents.begin(), renderEvents.end());
|
|
updateEventsEnd();
|
|
renderEvents.clear();
|
|
}
|
|
|
|
const int unrenderedUtick = renderEventsStatus.occupiedRangeEnd(utick);
|
|
if (unrenderedUtick - utick < minUtickBufferSize) {
|
|
const MidiRenderer::Chunk chunk = midi.getChunkAt(unrenderedUtick);
|
|
if (chunk) {
|
|
midiRenderFuture = QtConcurrent::run([this, chunk]() {
|
|
renderChunk(chunk, &renderEvents);
|
|
});
|
|
}
|
|
}
|
|
mutex.unlock();
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// getCurTick
|
|
//---------------------------------------------------------
|
|
|
|
int Seq::getCurTick()
|
|
{
|
|
return cs->utime2utick(qreal(playFrame) / qreal(MScore::sampleRate));
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// setRelTempo
|
|
// relTempo = 1.0 = normal tempo
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::setRelTempo(double relTempo)
|
|
{
|
|
guiToSeq(SeqMsg(SeqMsgId::TEMPO_CHANGE, relTempo));
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// setPos
|
|
// seek
|
|
// realtime environment
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::setPos(int utick)
|
|
{
|
|
if (cs == 0)
|
|
return;
|
|
stopNotes(-1, true);
|
|
|
|
int ucur;
|
|
mutex.lock();
|
|
if (playPos != events.end())
|
|
ucur = cs->repeatList().utick2tick(playPos->first);
|
|
else
|
|
ucur = utick - 1;
|
|
if (utick != ucur)
|
|
updateSynthesizerState(ucur, utick);
|
|
|
|
playFrame = cs->utick2utime(utick) * MScore::sampleRate;
|
|
playPos = events.lower_bound(utick);
|
|
mutex.unlock();
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// getPlayStartUtick
|
|
//---------------------------------------------------------
|
|
|
|
int Seq::getPlayStartUtick()
|
|
{
|
|
if ((mscore->loop())) {
|
|
if (preferences.getBool(PREF_APP_PLAYBACK_LOOPTOSELECTIONONPLAY)) {
|
|
setLoopSelection();
|
|
}
|
|
return cs->repeatList().tick2utick(cs->loopInTick().ticks());
|
|
}
|
|
return cs->repeatList().tick2utick(cs->playPos().ticks());
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// seekCommon
|
|
// a common part of seek() and seekRT(), contains code
|
|
// that could be safely called from any thread.
|
|
// Do not use explicitly, use seek() or seekRT()
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::seekCommon(int utick)
|
|
{
|
|
if (cs == 0)
|
|
return;
|
|
|
|
collectEvents(utick);
|
|
|
|
if (cs->playMode() == PlayMode::AUDIO) {
|
|
ogg_int64_t sp = cs->utick2utime(utick) * MScore::sampleRate;
|
|
ov_pcm_seek(&vf, sp);
|
|
}
|
|
|
|
guiPos = events.lower_bound(utick);
|
|
mscore->setPos(Fraction::fromTicks(cs->repeatList().utick2tick(utick)));
|
|
unmarkNotes();
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// seek
|
|
// send seek message to sequencer
|
|
// gui thread
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::seek(int utick)
|
|
{
|
|
if (preferences.getBool(PREF_IO_JACK_USEJACKTRANSPORT)) {
|
|
if (utick > endUTick)
|
|
utick = 0;
|
|
_driver->seekTransport(utick);
|
|
if (utick != 0)
|
|
return;
|
|
}
|
|
seekCommon(utick);
|
|
|
|
int t = cs->repeatList().utick2tick(utick);
|
|
Segment* seg = cs->tick2segment(Fraction::fromTicks(t));
|
|
if (seg)
|
|
mscore->currentScoreView()->moveCursor(seg->tick());
|
|
cs->setPlayPos(Fraction::fromTicks(t));
|
|
cs->update();
|
|
guiToSeq(SeqMsg(SeqMsgId::SEEK, utick));
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// seekRT
|
|
// realtime thread
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::seekRT(int utick)
|
|
{
|
|
if (preferences.getBool(PREF_IO_JACK_USEJACKTRANSPORT) && utick > endUTick)
|
|
utick = 0;
|
|
seekCommon(utick);
|
|
setPos(utick);
|
|
// Update the screen in GUI thread
|
|
emit toGui('5', cs->repeatList().utick2tick(utick));
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// startNote
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::startNote(int channel, int pitch, int velo, double nt)
|
|
{
|
|
if (state != Transport::STOP && state != Transport::PLAY)
|
|
return;
|
|
NPlayEvent ev(ME_NOTEON, channel, pitch, velo);
|
|
ev.setTuning(nt);
|
|
sendEvent(ev);
|
|
}
|
|
|
|
void Seq::startNote(int channel, int pitch, int velo, int duration, double nt)
|
|
{
|
|
stopNotes();
|
|
startNote(channel, pitch, velo, nt);
|
|
startNoteTimer(duration);
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// playMetronomeBeat
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::playMetronomeBeat(BeatType type)
|
|
{
|
|
if (state != Transport::STOP)
|
|
return;
|
|
liveEventQueue()->enqueue(NPlayEvent(type));
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// startNoteTimer
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::startNoteTimer(int duration)
|
|
{
|
|
if (duration) {
|
|
noteTimer->setInterval(duration);
|
|
noteTimer->start();
|
|
}
|
|
}
|
|
//---------------------------------------------------------
|
|
// stopNoteTimer
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::stopNoteTimer()
|
|
{
|
|
if (noteTimer->isActive()) {
|
|
noteTimer->stop();
|
|
stopNotes();
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// stopNotes
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::stopNotes(int channel, bool realTime)
|
|
{
|
|
auto send = [this, realTime](const NPlayEvent& event) {
|
|
if (realTime)
|
|
putEvent(event);
|
|
else
|
|
sendEvent(event);
|
|
};
|
|
// Stop notes in all channels
|
|
if (channel == -1) {
|
|
for(int ch = 0; ch < int(cs->midiMapping().size()); ch++) {
|
|
send(NPlayEvent(ME_CONTROLLER, ch, CTRL_SUSTAIN, 0));
|
|
send(NPlayEvent(ME_CONTROLLER, ch, CTRL_ALL_NOTES_OFF, 0));
|
|
if (cs->midiChannel(ch) != 9)
|
|
send(NPlayEvent(ME_PITCHBEND, ch, 0, 64));
|
|
}
|
|
}
|
|
else {
|
|
send(NPlayEvent(ME_CONTROLLER, channel, CTRL_SUSTAIN, 0));
|
|
send(NPlayEvent(ME_CONTROLLER, channel, CTRL_ALL_NOTES_OFF, 0));
|
|
if (cs->midiChannel(channel) != 9)
|
|
send(NPlayEvent(ME_PITCHBEND, channel, 0, 64));
|
|
}
|
|
if (preferences.getBool(PREF_IO_ALSA_USEALSAAUDIO) || preferences.getBool(PREF_IO_JACK_USEJACKAUDIO) || preferences.getBool(PREF_IO_PULSEAUDIO_USEPULSEAUDIO) || preferences.getBool(PREF_IO_PORTAUDIO_USEPORTAUDIO))
|
|
_synti->allNotesOff(channel);
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// setController
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::setController(int channel, int ctrl, int data)
|
|
{
|
|
NPlayEvent event(ME_CONTROLLER, channel, ctrl, data);
|
|
sendEvent(event);
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// sendEvent
|
|
// called from GUI context to send a midi event to
|
|
// midi out or synthesizer
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::sendEvent(const NPlayEvent& ev)
|
|
{
|
|
guiToSeq(SeqMsg(SeqMsgId::PLAY, ev));
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// nextMeasure
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::nextMeasure()
|
|
{
|
|
Measure* m = cs->tick2measure(Fraction::fromTicks(guiPos->first));
|
|
if (m) {
|
|
if (m->nextMeasure())
|
|
m = m->nextMeasure();
|
|
seek(m->tick().ticks());
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// nextChord
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::nextChord()
|
|
{
|
|
int t = guiPos->first;
|
|
for (auto i = guiPos; i != eventsEnd; ++i) {
|
|
if (i->second.type() == ME_NOTEON && i->first > t && i->second.velo()) {
|
|
seek(i->first);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// prevMeasure
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::prevMeasure()
|
|
{
|
|
auto i = guiPos;
|
|
if (i == events.begin())
|
|
return;
|
|
--i;
|
|
Measure* m = cs->tick2measure(Fraction::fromTicks(i->first));
|
|
if (m) {
|
|
if ((i->first == m->tick().ticks()) && m->prevMeasure())
|
|
m = m->prevMeasure();
|
|
seek(m->tick().ticks());
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// prevChord
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::prevChord()
|
|
{
|
|
int t = playPos->first;
|
|
//find the chord just before playpos
|
|
EventMap::const_iterator i = events.upper_bound(cs->repeatList().tick2utick(t));
|
|
for (;;) {
|
|
if (i->second.type() == ME_NOTEON) {
|
|
const NPlayEvent& n = i->second;
|
|
if (i->first < t && n.velo()) {
|
|
t = i->first;
|
|
break;
|
|
}
|
|
}
|
|
if (i == events.cbegin())
|
|
break;
|
|
--i;
|
|
}
|
|
//go the previous chord
|
|
if (i != events.cbegin()) {
|
|
i = playPos;
|
|
for (;;) {
|
|
if (i->second.type() == ME_NOTEON) {
|
|
const NPlayEvent& n = i->second;
|
|
if (i->first < t && n.velo()) {
|
|
seek(i->first);
|
|
break;
|
|
}
|
|
}
|
|
if (i == events.cbegin())
|
|
break;
|
|
--i;
|
|
}
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// seekEnd
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::seekEnd()
|
|
{
|
|
qDebug("seek to end");
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// guiToSeq
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::guiToSeq(const SeqMsg& msg)
|
|
{
|
|
if (!_driver || !running)
|
|
return;
|
|
toSeq.enqueue(msg);
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// eventToGui
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::eventToGui(NPlayEvent e)
|
|
{
|
|
fromSeq.enqueue(SeqMsg(SeqMsgId::MIDI_INPUT_EVENT, e));
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// midiInputReady
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::midiInputReady()
|
|
{
|
|
if (_driver)
|
|
_driver->midiRead();
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// SeqMsgFifo
|
|
//---------------------------------------------------------
|
|
|
|
SeqMsgFifo::SeqMsgFifo()
|
|
{
|
|
maxCount = SEQ_MSG_FIFO_SIZE;
|
|
clear();
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// enqueue
|
|
//---------------------------------------------------------
|
|
|
|
void SeqMsgFifo::enqueue(const SeqMsg& msg)
|
|
{
|
|
int i = 0;
|
|
int n = 50;
|
|
|
|
QMutex mutex;
|
|
QWaitCondition qwc;
|
|
mutex.lock();
|
|
for (; i < n; ++i) {
|
|
if (!isFull())
|
|
break;
|
|
qwc.wait(&mutex,100);
|
|
}
|
|
mutex.unlock();
|
|
if (i == n) {
|
|
qDebug("===SeqMsgFifo: overflow");
|
|
return;
|
|
}
|
|
messages[widx] = msg;
|
|
push();
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// dequeue
|
|
//---------------------------------------------------------
|
|
|
|
SeqMsg SeqMsgFifo::dequeue()
|
|
{
|
|
SeqMsg msg = messages[ridx];
|
|
pop();
|
|
return msg;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// putEvent
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::putEvent(const NPlayEvent& event, unsigned framePos)
|
|
{
|
|
if (!cs)
|
|
return;
|
|
int channel = event.channel();
|
|
if (channel >= int(cs->midiMapping().size())) {
|
|
qDebug("bad channel value %d >= %d", channel, int(cs->midiMapping().size()));
|
|
return;
|
|
}
|
|
|
|
// audio
|
|
int syntiIdx= _synti->index(cs->midiMapping(channel)->articulation()->synti());
|
|
_synti->play(event, syntiIdx);
|
|
|
|
// midi
|
|
if (_driver != 0 && (preferences.getBool(PREF_IO_JACK_USEJACKMIDI) || preferences.getBool(PREF_IO_ALSA_USEALSAAUDIO) || preferences.getBool(PREF_IO_PORTAUDIO_USEPORTAUDIO)))
|
|
_driver->putEvent(event, framePos);
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// heartBeat
|
|
// update GUI
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::heartBeatTimeout()
|
|
{
|
|
SynthControl* sc = mscore->getSynthControl();
|
|
if (sc && _driver) {
|
|
if (++peakTimer[0] >= peakHold)
|
|
meterPeakValue[0] *= .7f;
|
|
if (++peakTimer[1] >= peakHold)
|
|
meterPeakValue[1] *= .7f;
|
|
sc->setMeter(meterValue[0], meterValue[1], meterPeakValue[0], meterPeakValue[1]);
|
|
}
|
|
|
|
while (!fromSeq.empty()) {
|
|
SeqMsg msg = fromSeq.dequeue();
|
|
if (msg.id == SeqMsgId::MIDI_INPUT_EVENT) {
|
|
int type = msg.event.type();
|
|
if (type == ME_NOTEON)
|
|
mscore->midiNoteReceived(msg.event.channel(), msg.event.pitch(), msg.event.velo());
|
|
else if (type == ME_NOTEOFF)
|
|
mscore->midiNoteReceived(msg.event.channel(), msg.event.pitch(), 0);
|
|
else if (type == ME_CONTROLLER)
|
|
mscore->midiCtrlReceived(msg.event.controller(), msg.event.value());
|
|
}
|
|
}
|
|
|
|
if (state != Transport::PLAY || inCountIn)
|
|
return;
|
|
|
|
int endFrame = playFrame;
|
|
|
|
mutex.lock();
|
|
auto ppos = playPos;
|
|
if (ppos != events.cbegin())
|
|
--ppos;
|
|
mutex.unlock();
|
|
|
|
ensureBufferAsync(ppos->first);
|
|
|
|
if (cs && cs->sigmap()->timesig(getCurTick()).nominal()!=prevTimeSig) {
|
|
prevTimeSig = cs->sigmap()->timesig(getCurTick()).nominal();
|
|
emit timeSigChanged();
|
|
}
|
|
if (cs && curTempo()!=prevTempo) {
|
|
prevTempo = curTempo();
|
|
emit tempoChanged();
|
|
}
|
|
|
|
QRectF r;
|
|
for (;guiPos != eventsEnd; ++guiPos) {
|
|
if (guiPos->first > ppos->first)
|
|
break;
|
|
if (mscore->loop())
|
|
if (guiPos->first >= cs->repeatList().tick2utick(cs->loopOutTick().ticks()))
|
|
break;
|
|
const NPlayEvent& n = guiPos->second;
|
|
if (n.type() == ME_NOTEON) {
|
|
const Note* note1 = n.note();
|
|
if (n.velo()) {
|
|
while (note1) {
|
|
for (ScoreElement* se : note1->linkList()) {
|
|
if (!se->isNote())
|
|
continue;
|
|
Note* currentNote = toNote(se);
|
|
currentNote->setMark(true);
|
|
markedNotes.append(currentNote);
|
|
r |= currentNote->canvasBoundingRect();
|
|
}
|
|
note1 = note1->tieFor() ? note1->tieFor()->endNote() : 0;
|
|
}
|
|
}
|
|
else {
|
|
while (note1) {
|
|
for (ScoreElement* se : note1->linkList()) {
|
|
if (!se->isNote())
|
|
continue;
|
|
Note* currentNote = toNote(se);
|
|
currentNote->setMark(false);
|
|
r |= currentNote->canvasBoundingRect();
|
|
markedNotes.removeOne(currentNote);
|
|
}
|
|
note1 = note1->tieFor() ? note1->tieFor()->endNote() : 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
int utick = ppos->first;
|
|
int t = cs->repeatList().utick2tick(utick);
|
|
mscore->currentScoreView()->moveCursor(Fraction::fromTicks(t));
|
|
mscore->setPos(Fraction::fromTicks(t));
|
|
|
|
emit(heartBeat(t, utick, endFrame));
|
|
|
|
PianorollEditor* pre = mscore->getPianorollEditor();
|
|
if (pre && pre->isVisible())
|
|
pre->heartBeat(this);
|
|
|
|
PianoTools* piano = mscore->pianoTools();
|
|
if (piano && piano->isVisible())
|
|
piano->setPlaybackNotes(markedNotes);
|
|
|
|
cv->update(cv->toPhysical(r));
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// updateSynthesizerState
|
|
// collect all controller events between tick1 and tick2
|
|
// and send them to the synthesizer
|
|
// Called from RT thread
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::updateSynthesizerState(int tick1, int tick2)
|
|
{
|
|
if (tick1 > tick2)
|
|
tick1 = 0;
|
|
// Making a local copy of events to avoid touching it
|
|
// from different threads at the same time
|
|
EventMap ev = events;
|
|
EventMap::const_iterator i1 = ev.lower_bound(tick1);
|
|
EventMap::const_iterator i2 = ev.upper_bound(tick2);
|
|
|
|
for (; i1 != i2; ++i1) {
|
|
if (i1->second.type() == ME_CONTROLLER)
|
|
playEvent(i1->second, 0);
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// curTempo
|
|
//---------------------------------------------------------
|
|
|
|
double Seq::curTempo() const
|
|
{
|
|
if (playPos != events.end())
|
|
return cs ? cs->tempomap()->tempo(playPos->first) : 0.0;
|
|
|
|
return 0.0;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// set Loop in position
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::setLoopIn()
|
|
{
|
|
Fraction t;
|
|
if (state == Transport::PLAY) { // If in playback mode, set the In position where note is being played
|
|
auto ppos = playPos;
|
|
if (ppos != events.cbegin())
|
|
--ppos; // We have to go back one pos to get the correct note that has just been played
|
|
t = Fraction::fromTicks(cs->repeatList().utick2tick(ppos->first));
|
|
}
|
|
else
|
|
t = cs->pos(); // Otherwise, use the selected note.
|
|
if (t >= cs->loopOutTick()) // If In pos >= Out pos, reset Out pos to end of score
|
|
cs->setPos(POS::RIGHT, cs->lastMeasure()->endTick());
|
|
cs->setPos(POS::LEFT, t);
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// set Loop Out position
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::setLoopOut()
|
|
{
|
|
Fraction t;
|
|
if (state == Transport::PLAY) { // If in playback mode, set the Out position where note is being played
|
|
t = Fraction::fromTicks(cs->repeatList().utick2tick(playPos->first));
|
|
}
|
|
else
|
|
t = cs->pos() + cs->inputState().ticks(); // Otherwise, use the selected note.
|
|
if (t <= cs->loopInTick()) // If Out pos <= In pos, reset In pos to beginning of score
|
|
cs->setPos(POS::LEFT, Fraction(0,1));
|
|
else
|
|
if (t > cs->lastMeasure()->endTick())
|
|
t = cs->lastMeasure()->endTick();
|
|
cs->setPos(POS::RIGHT, t);
|
|
if (state == Transport::PLAY)
|
|
guiToSeq(SeqMsg(SeqMsgId::SEEK, t.ticks()));
|
|
}
|
|
|
|
void Seq::setPos(POS, unsigned t)
|
|
{
|
|
qDebug("seq: setPos %d", t);
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// set Loop In/Out position based on the selection
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::setLoopSelection()
|
|
{
|
|
const Score* score = mscore->currentScore();
|
|
Q_ASSERT(!score || score->masterScore() == cs);
|
|
|
|
if (score && score->selection().isRange()) {
|
|
cs->setLoopInTick(score->selection().tickStart());
|
|
cs->setLoopOutTick(score->selection().tickEnd());
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// Called after tempo or time signature
|
|
// changed while playback
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::handleTimeSigTempoChanged()
|
|
{
|
|
_driver->handleTimeSigTempoChanged();
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// setInitialMillisecondTimestampWithLatency
|
|
// Called whenever seq->process() starts.
|
|
// Sets a starting reference time for which subsequent PortMidi events will be offset from.
|
|
// Time is relative to the start of PortMidi's initialization.
|
|
//---------------------------------------------------------
|
|
|
|
void Seq::setInitialMillisecondTimestampWithLatency()
|
|
{
|
|
#ifdef USE_PORTMIDI
|
|
initialMillisecondTimestampWithLatency = Pt_Time() + preferences.getInt(PREF_IO_PORTMIDI_OUTPUTLATENCYMILLISECONDS);
|
|
//qDebug("PortMidi initialMillisecondTimestampWithLatency: %d = %d + %d", initialMillisecondTimestampWithLatency, unsigned(Pt_Time()), preferences.getInt(PREF_IO_PORTMIDI_OUTPUTLATENCYMILLISECONDS));
|
|
#endif
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// getCurrentMillisecondTimestampWithLatency
|
|
// Called when midi messages are sent to PortMidi device.
|
|
// Returns the time in milliseconds of the current play cursor.
|
|
// Time is relative to the start of PortMidi's initialization.
|
|
//---------------------------------------------------------
|
|
|
|
unsigned Seq::getCurrentMillisecondTimestampWithLatency(unsigned framePos) const
|
|
{
|
|
#ifdef USE_PORTMIDI
|
|
unsigned playTimeMilliseconds = unsigned(framePos * 1000) / unsigned(MScore::sampleRate);
|
|
//qDebug("PortMidi timestamp = %d + %d", initialMillisecondTimestampWithLatency, playTimeMilliseconds);
|
|
return initialMillisecondTimestampWithLatency + playTimeMilliseconds;
|
|
#else
|
|
qDebug("Shouldn't be using this function if not using PortMidi");
|
|
return 0;
|
|
#endif
|
|
}
|
|
}
|