Skip to content

Commit 2247b09

Browse files
Merge pull request hydrogen-music#2293 from theGreatWhiteShark/fix/midi-note-ordering-2290
Fix MIDI message ordering (hydrogen-music#2290)
2 parents 0d85d66 + e243cb0 commit 2247b09

8 files changed

Lines changed: 658 additions & 81 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,8 @@ All notable changes to this project will be documented in this file.
381381
- AppImage build folder is now removed on `build.sh r` (#2129).
382382
- Fix potential crash with JACK audio driver on startup, teardown, or
383383
song/drumkit loading.
384+
- In case the beginning of one note and the end of another coincide, Hydrogen
385+
now ensures to send `Note-Off` events before new `Note-On`s (#2290).
384386

385387
### Removed
386388

src/core/Midi/MidiMessage.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,8 @@ bool MidiMessage::operator==( const MidiMessage& other ) const {
247247
m_data1 != other.m_data1 ||
248248
m_data2 != other.m_data2 ||
249249
m_channel != other.m_channel ||
250-
m_sysexData.size() != other.m_sysexData.size() ) {
250+
m_sysexData.size() != other.m_sysexData.size() ||
251+
m_nFrameOffset != other.m_nFrameOffset ) {
251252
return false;
252253
}
253254

src/core/Sampler/Sampler.cpp

Lines changed: 109 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include <cassert>
2626
#include <cmath>
2727
#include <cstdlib>
28+
#include <list>
2829

2930
#include <core/AudioEngine/AudioEngine.h>
3031
#include <core/AudioEngine/TransportPosition.h>
@@ -172,26 +173,10 @@ void Sampler::process( uint32_t nFrames )
172173
}
173174

174175
// Only send Note-Off messages in case we already sent an Note-On.
175-
if ( pNote->getMidiNoteOnSentFrame() != -1 ) {
176-
// Ensure notes of custom length result in Note-On and Note-Off
177-
// messages corresponding to the user-defined length (regardless
178-
// of the underlying sample).
179-
if ( pNote->getLength() != LENGTH_ENTIRE_SAMPLE ) {
180-
const auto nPrevStart = pNote->getNoteStart();
181-
pNote->setMidiNoteOffFrame(
182-
pNote->getMidiNoteOnSentFrame() +
183-
TransportPosition::computeFrame(
184-
pNote->getLength(), Hydrogen::get_instance()
185-
->getAudioEngine()
186-
->getTransportPosition()
187-
->getTickSize()
188-
)
189-
);
190-
m_scheduledNoteOffQueue.push( pNote );
191-
}
192-
else {
193-
m_queuedNoteOffs.push_back( pNote );
194-
}
176+
// Notes of custom length will be handled using the scheduled queue.
177+
if ( pNote->getMidiNoteOnSentFrame() != -1 &&
178+
pNote->getLength() == LENGTH_ENTIRE_SAMPLE ) {
179+
m_queuedNoteOffs.push_back( pNote );
195180
}
196181
}
197182
else if ( pNote == nullptr ) {
@@ -252,13 +237,13 @@ void Sampler::process( uint32_t nFrames )
252237
nCurrentFrame
253238
);
254239
#if SAMPLER_DEBUG
255-
INFOLOG( QString( "nCurrentFrame: [%1], Sending "
240+
INFOLOG( QString( "nCurrentFrame: [%1], Queuing "
256241
"immediate Note-Off [%2] for [%3]" )
257242
.arg( nCurrentFrame )
258243
.arg( midiMessage.toQString() )
259244
.arg( pNote->toQString() ) );
260245
#endif
261-
pMidiDriver->enqueueOutputMessage( midiMessage );
246+
m_midiMessageQueue.push( std::move( midiMessage ) );
262247
}
263248
}
264249
else if ( pNote == nullptr ||
@@ -300,7 +285,7 @@ void Sampler::process( uint32_t nFrames )
300285

301286
if ( !sendNote( pNote ) ) {
302287
#if SAMPLER_DEBUG
303-
INFOLOG( QString( "nCurrentFrame: [%1], Dropping queued "
288+
INFOLOG( QString( "nCurrentFrame: [%1], Dropping scheduled "
304289
"Note-Off for [%2]" )
305290
.arg( nCurrentFrame )
306291
.arg( pNote->toQString() ) );
@@ -336,13 +321,13 @@ void Sampler::process( uint32_t nFrames )
336321
nCurrentFrame
337322
);
338323
#if SAMPLER_DEBUG
339-
INFOLOG( QString( "nCurrentFrame: [%1], Sending "
340-
"queued Note-Off [%2] for [%3]" )
324+
INFOLOG( QString( "nCurrentFrame: [%1], Queuing "
325+
"scheduled Note-Off [%2] for [%3]" )
341326
.arg( nCurrentFrame )
342327
.arg( midiMessage.toQString() )
343328
.arg( pNote->toQString() ) );
344329
#endif
345-
pMidiDriver->enqueueOutputMessage( midiMessage );
330+
m_midiMessageQueue.push( std::move( midiMessage ) );
346331
}
347332
}
348333
else {
@@ -352,6 +337,8 @@ void Sampler::process( uint32_t nFrames )
352337
}
353338
}
354339

340+
processMidiEvents();
341+
355342
processPlaybackTrack( nFrames );
356343
}
357344

@@ -838,23 +825,6 @@ bool Sampler::handleNote( std::shared_ptr<Note> pNote, unsigned nBufferSize )
838825
if ( !pNote->isPartiallyRendered() ) {
839826
long long nNoteStartInFrames = pNote->getNoteStart();
840827

841-
#if SAMPLER_DEBUG
842-
DEBUGLOG(
843-
QString( "nCurrentFrame: %1, note pos: %2, "
844-
"pAudioEngine->getTransportPosition()->getTickSize(): %3, "
845-
"pAudioEngine->getTransportPosition()->getTick(): %4, "
846-
"pAudioEngine->getTransportPosition()->getFrame(): %5, "
847-
"nNoteStartInFrames: %6 " )
848-
.arg( nCurrentFrame )
849-
.arg( pNote->getPosition() )
850-
.arg( pAudioEngine->getTransportPosition()->getTickSize() )
851-
.arg( pAudioEngine->getTransportPosition()->getTick() )
852-
.arg( pAudioEngine->getTransportPosition()->getFrame() )
853-
.arg( nNoteStartInFrames )
854-
.append( pNote->toQString( "", true ) )
855-
);
856-
#endif
857-
858828
if ( nNoteStartInFrames > nCurrentFrame ) {
859829
// The note doesn't start right at the beginning of the
860830
// buffer rendered in this cycle.
@@ -1052,7 +1022,7 @@ bool Sampler::handleNote( std::shared_ptr<Note> pNote, unsigned nBufferSize )
10521022
// We delay checking for the audio driver till here in order to allow
10531023
// usign Hydrogen in "MIDI-only" mode.
10541024
if ( pLayer == nullptr || pHydrogen->getAudioDriver() == nullptr ||
1055-
bIsMuted ) {
1025+
bIsMuted && pNote->getLength() == LENGTH_ENTIRE_SAMPLE ) {
10561026
// For note with neither custom length nor a backing sample, we will
10571027
// send a Note-Off immediately after its Note-On. But we have to
10581028
// watch out for notes associated with multi-component instruments
@@ -1089,48 +1059,55 @@ bool Sampler::handleNote( std::shared_ptr<Note> pNote, unsigned nBufferSize )
10891059
pNote->getLength() != LENGTH_ENTIRE_SAMPLE ) ) ) {
10901060
auto noteOffMessage = MidiMessage::from( noteOnMessage );
10911061
noteOffMessage.setType( MidiMessage::Type::NoteOff );
1092-
noteOffMessage.setFrameOffset( std::max(
1093-
nInitialBufferPos - 1, static_cast<long long>( 0 )
1094-
) );
1062+
noteOffMessage.setFrameOffset( nInitialBufferPos );
10951063

10961064
#if SAMPLER_DEBUG
1097-
INFOLOG( QString( "nCurrentFrame: [%1], Sending "
1065+
INFOLOG( QString( "nCurrentFrame: [%1], Queuing "
10981066
"auto-stop Note-Off [%2] for [%3]" )
10991067
.arg( nCurrentFrame )
11001068
.arg( noteOffMessage.toQString() )
11011069
.arg( pNote->toQString() ) );
11021070
#endif
11031071

1104-
pHydrogen->getMidiDriver()->enqueueOutputMessage( noteOffMessage
1105-
);
1106-
1107-
if ( nInitialBufferPos == 0 ) {
1108-
// In case the new note is located at the very beginning of
1109-
// the new buffer we deliberately delay it for one frame. As
1110-
// such the MIDI output is not as precise as it could be.
1111-
// But we need to avoid spurious reordering of messages send
1112-
// at the same time stamp. Else the Note-Off could be
1113-
// handled _after_ our Note-On.
1114-
noteOnMessage.setFrameOffset( 1 );
1115-
pNote->setMidiNoteOnSentFrame( nCurrentFrame + 1 );
1116-
}
1117-
else {
1118-
pNote->setMidiNoteOnSentFrame( nCurrentFrame );
1119-
}
1120-
}
1121-
else {
1122-
pNote->setMidiNoteOnSentFrame( nCurrentFrame );
1072+
m_midiMessageQueue.push( std::move( noteOffMessage ) );
11231073
}
11241074

1075+
pNote->setMidiNoteOnSentFrame( nCurrentFrame + nInitialBufferPos );
1076+
11251077
#if SAMPLER_DEBUG
1126-
INFOLOG( QString( "nCurrentFrame: [%1], Sending "
1078+
INFOLOG( QString( "nCurrentFrame: [%1], Queuing "
11271079
"Note-On [%2] for [%3]" )
11281080
.arg( nCurrentFrame )
11291081
.arg( noteOnMessage.toQString() )
11301082
.arg( pNote->toQString() ) );
11311083
#endif
11321084

1133-
pHydrogen->getMidiDriver()->enqueueOutputMessage( noteOnMessage );
1085+
m_midiMessageQueue.push( std::move( noteOnMessage ) );
1086+
1087+
// Ensure notes of custom length result in Note-On and Note-Off
1088+
// messages corresponding to the user-defined length (regardless
1089+
// of the underlying sample).
1090+
if ( pNote->getLength() != LENGTH_ENTIRE_SAMPLE ) {
1091+
const auto nPrevStart = pNote->getNoteStart();
1092+
pNote->setMidiNoteOffFrame(
1093+
nCurrentFrame + nInitialBufferPos +
1094+
TransportPosition::computeFrame(
1095+
pNote->getLength(), Hydrogen::get_instance()
1096+
->getAudioEngine()
1097+
->getTransportPosition()
1098+
->getTickSize()
1099+
)
1100+
);
1101+
1102+
#if SAMPLER_DEBUG
1103+
INFOLOG( QString( "nCurrentFrame: [%1], Scheduling "
1104+
"a Note-Off for [%2]" )
1105+
.arg( nCurrentFrame )
1106+
.arg( pNote->toQString() ) );
1107+
#endif
1108+
1109+
m_scheduledNoteOffQueue.push( pNote );
1110+
}
11341111
}
11351112
}
11361113

@@ -1340,6 +1317,65 @@ void resample(
13401317
}
13411318
}
13421319

1320+
void Sampler::processMidiEvents()
1321+
{
1322+
auto pMidiDriver = Hydrogen::get_instance()->getMidiDriver();
1323+
if ( pMidiDriver == nullptr ) {
1324+
return;
1325+
}
1326+
1327+
// We only need to deduplicate and reorder messages of the same frame
1328+
// offset. When arranging Note-Off and Note-On events, the particular order
1329+
// of the Note-Off/On events is not important. We only need to ensure to
1330+
// send Note-Offs _prior_ to Note-Ons.
1331+
auto sendMessages = [&]( std::list<MidiMessage>& messageList ) {
1332+
for ( ; !messageList.empty(); messageList.pop_front() ) {
1333+
pMidiDriver->enqueueOutputMessage( std::move( messageList.front() )
1334+
);
1335+
}
1336+
};
1337+
std::list<MidiMessage> messagesPerTick;
1338+
int nCurrentFrameOffset = -1;
1339+
for ( ; !m_midiMessageQueue.empty(); m_midiMessageQueue.pop() ) {
1340+
const auto message = std::move( m_midiMessageQueue.top() );
1341+
1342+
if ( message.getFrameOffset() != nCurrentFrameOffset ) {
1343+
nCurrentFrameOffset = message.getFrameOffset();
1344+
if ( !messagesPerTick.empty() ) {
1345+
sendMessages( messagesPerTick );
1346+
}
1347+
}
1348+
1349+
for ( const auto& qqueuedMessage : messagesPerTick ) {
1350+
if ( qqueuedMessage == message ) {
1351+
// Already present - deduplication. E.g. when a custom length
1352+
// tail touches a note head and auto-stop notes is enabled for
1353+
// that instrument.
1354+
1355+
#if SAMPLER_DEBUG
1356+
INFOLOG( QString( "Dropped during deduplication: [%1]" )
1357+
.arg( message.toQString() ) );
1358+
#endif
1359+
1360+
continue;
1361+
}
1362+
}
1363+
1364+
// We push all Note-Offs to the front and all Note-Ons to the back.
1365+
// This provides us with sufficient ordering when sending them.
1366+
if ( message.getType() == MidiMessage::Type::NoteOff ) {
1367+
messagesPerTick.push_front( std::move( message ) );
1368+
}
1369+
else {
1370+
messagesPerTick.push_back( std::move( message ) );
1371+
}
1372+
}
1373+
1374+
if ( !messagesPerTick.empty() ) {
1375+
sendMessages( messagesPerTick );
1376+
}
1377+
}
1378+
13431379
bool Sampler::processPlaybackTrack( int nBufferSize )
13441380
{
13451381
Hydrogen* pHydrogen = Hydrogen::get_instance();
@@ -1602,6 +1638,7 @@ bool Sampler::renderNote(
16021638
pSelectedLayerInfo->fSamplePosition ) /
16031639
fFrequencyRatio
16041640
);
1641+
const int nNoteOffFrame = std::min( nRemainingFrames, nBufferSize - 1 );
16051642

16061643
bool bRetValue = true; // the note is ended
16071644
int nAvail_bytes;
@@ -1824,11 +1861,12 @@ bool Sampler::renderNote(
18241861
// Since the last portion of the layers's sample is rendered in this
18251862
// processing cycle, we store the corresponding frame in order to send
18261863
// MIDI Note-Off notes as precisely as possible.
1827-
if ( pNote->getMidiNoteOffFrame() < nCurrentFrame + nRemainingFrames ) {
1864+
if ( pNote->getLength() == LENGTH_ENTIRE_SAMPLE &&
1865+
pNote->getMidiNoteOffFrame() < nCurrentFrame + nNoteOffFrame ) {
18281866
// For notes corresponding to instruments holding multiple
18291867
// components, we send a Note-Off after all of them have been
18301868
// rendered.
1831-
pNote->setMidiNoteOffFrame( nCurrentFrame + nRemainingFrames );
1869+
pNote->setMidiNoteOffFrame( nCurrentFrame + nNoteOffFrame );
18321870
}
18331871
}
18341872

src/core/Sampler/Sampler.h

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
#include <core/Basics/Note.h>
2727
#include <core/Globals.h>
28+
#include <core/Midi/MidiMessage.h>
2829
#include <core/Object.h>
2930
#include <core/Sampler/Interpolation.h>
3031

@@ -265,6 +266,7 @@ class Sampler : public H2Core::Object<Sampler> {
265266
*/
266267
float panLaw( float fPan, std::shared_ptr<Song> pSong );
267268

269+
void processMidiEvents();
268270
bool processPlaybackTrack( int nBufferSize );
269271

270272
/** @return false - the note is not ended, true - the note is ended */
@@ -296,9 +298,17 @@ class Sampler : public H2Core::Object<Sampler> {
296298
}
297299
};
298300

301+
struct compareQueuedMidiMessages {
302+
bool operator()( const MidiMessage& msg1, const MidiMessage& msg2 )
303+
{
304+
return msg1.getFrameOffset() >= msg2.getFrameOffset();
305+
}
306+
};
307+
299308
std::vector<std::shared_ptr<Note>> m_playingNotesQueue;
309+
300310
/** Notes for which a Note-Off message will be send at the end of the
301-
* next processing cycle. */
311+
* processing cycle. */
302312
std::vector<std::shared_ptr<Note>> m_queuedNoteOffs;
303313

304314
/** Notes - ordered by their start position - scheduled to become Note-Off
@@ -312,6 +322,16 @@ class Sampler : public H2Core::Object<Sampler> {
312322
compareMidiNoteOff>
313323
m_scheduledNoteOffQueue;
314324

325+
/** MIDI messages to be sent at the end of the processing cycle. It is used
326+
* as an intermediate cache to allow for both merging multiple Note-Off
327+
* events for the same instrument as well as ensuring ordering of Note-Off
328+
* and Note-On events. */
329+
std::priority_queue<
330+
MidiMessage,
331+
std::deque<MidiMessage>,
332+
compareQueuedMidiMessages>
333+
m_midiMessageQueue;
334+
315335
/// Instrument used for the playback track feature.
316336
std::shared_ptr<Instrument> m_pPlaybackTrackInstrument;
317337

0 commit comments

Comments
 (0)