From ff0c9beaddb9adc4ba1d8f1369d8ab9d6d9ee1ec Mon Sep 17 00:00:00 2001 From: Vesa Date: Sun, 6 Jul 2014 16:11:35 +0300 Subject: [PATCH] SF2: Make timing more accurate, also some fixes I'm not saying sample-accurate, because it turns out, Fluidsynth has an internal buffer size and thus timing granularity of 64 frames. So 64 frames is the max. accuracy attainable for SF2. But it's better than nothing. Big thanks to David Henningsson of the Fluidsynth dev team, who very helpfully answered questions. A great guy. In addition, there are some fixes to earlier commits here, which I ran into while working on the SF2 timing. --- plugins/monstro/Monstro.cpp | 2 +- plugins/sf2_player/sf2_player.cpp | 255 ++++++++++++++++++------------ plugins/sf2_player/sf2_player.h | 6 + src/core/NotePlayHandle.cpp | 9 +- 4 files changed, 172 insertions(+), 100 deletions(-) diff --git a/plugins/monstro/Monstro.cpp b/plugins/monstro/Monstro.cpp index c1af10cd4..905269854 100644 --- a/plugins/monstro/Monstro.cpp +++ b/plugins/monstro/Monstro.cpp @@ -1246,7 +1246,7 @@ void MonstroInstrument::playNote( NotePlayHandle * _n, sampleFrame * _working_buffer ) { const fpp_t frames = _n->framesLeftForCurrentPeriod(); - const f_cnt_t offset = _n->offset(); + const f_cnt_t offset = _n->noteOffset(); if ( _n->totalFramesPlayed() == 0 || _n->m_pluginData == NULL ) { diff --git a/plugins/sf2_player/sf2_player.cpp b/plugins/sf2_player/sf2_player.cpp index 0d82f63db..eaf3d0a86 100644 --- a/plugins/sf2_player/sf2_player.cpp +++ b/plugins/sf2_player/sf2_player.cpp @@ -69,6 +69,9 @@ struct SF2PluginData int lastPanning; float lastVelocity; fluid_voice_t * fluidVoice; + bool isNew; + f_cnt_t offset; + bool noteOffSent; } ; @@ -526,112 +529,114 @@ void sf2Instrument::updateSampleRate() void sf2Instrument::playNote( NotePlayHandle * _n, sampleFrame * ) { - const float LOG440 = 2.643452676f; - const f_cnt_t tfp = _n->totalFramesPlayed(); - int midiNote = (int)floor( 12.0 * ( log2( _n->unpitchedFrequency() ) - LOG440 ) - 4.0 ); - - // out of range? - if( midiNote <= 0 || midiNote >= 128 ) - { - return; - } - if( tfp == 0 ) { + const float LOG440 = 2.643452676f; + + int midiNote = (int)floor( 12.0 * ( log2( _n->unpitchedFrequency() ) - LOG440 ) - 4.0 ); + + // out of range? + if( midiNote <= 0 || midiNote >= 128 ) + { + return; + } + const int baseVelocity = instrumentTrack()->midiPort()->baseVelocity(); + SF2PluginData * pluginData = new SF2PluginData; pluginData->midiNote = midiNote; pluginData->lastPanning = 0; - pluginData->lastVelocity = 127; + pluginData->lastVelocity = _n->midiVelocity( baseVelocity ); pluginData->fluidVoice = NULL; + pluginData->isNew = true; + pluginData->offset = _n->offset(); + pluginData->noteOffSent = false; _n->m_pluginData = pluginData; - - m_synthMutex.lock(); - - // get list of current voice IDs so we can easily spot the new - // voice after the fluid_synth_noteon() call - const int poly = fluid_synth_get_polyphony( m_synth ); - fluid_voice_t * voices[poly]; - unsigned int id[poly]; - fluid_synth_get_voicelist( m_synth, voices, poly, -1 ); - for( int i = 0; i < poly; ++i ) - { - id[i] = 0; - } - for( int i = 0; i < poly && voices[i]; ++i ) - { - id[i] = fluid_voice_get_id( voices[i] ); - } - - const int baseVelocity = instrumentTrack()->midiPort()->baseVelocity(); - - fluid_synth_noteon( m_synth, m_channel, midiNote, _n->midiVelocity( baseVelocity ) ); - - // get new voice and save it - fluid_synth_get_voicelist( m_synth, voices, poly, -1 ); - for( int i = 0; i < poly && voices[i]; ++i ) - { - const unsigned int newID = fluid_voice_get_id( voices[i] ); - if( id[i] != newID || newID == 0 ) - { - pluginData->fluidVoice = voices[i]; - break; - } - } - - m_synthMutex.unlock(); - - m_notesRunningMutex.lock(); - ++m_notesRunning[midiNote]; - m_notesRunningMutex.unlock(); + + // insert the nph to the playing notes vector + m_playingNotesMutex.lock(); + m_playingNotes.append( _n ); + m_playingNotesMutex.unlock(); } - -/* SF2PluginData * pluginData = static_cast( - _n->m_pluginData ); -#ifdef SOMEONE_FIXED_PER_NOTE_PANNING - if( pluginData->fluidVoice && - pluginData->lastPanning != _n->getPanning() ) + else if( _n->isReleased() ) // note is released during this period { - const float pan = -500 + - ( (float)( _n->getPanning() - PanningLeft ) ) / - ( (float)( PanningRight - PanningLeft ) ) * 1000; + SF2PluginData * pluginData = static_cast( _n->m_pluginData ); + pluginData->offset = _n->framesBeforeRelease(); + pluginData->isNew = false; + + m_playingNotesMutex.lock(); + m_playingNotes.append( _n ); + m_playingNotesMutex.unlock(); - m_synthMutex.lock(); - fluid_voice_gen_set( pluginData->fluidVoice, GEN_PAN, pan ); - fluid_voice_update_param( pluginData->fluidVoice, GEN_PAN ); - m_synthMutex.unlock(); - - pluginData->lastPanning = _n->getPanning(); } -#endif - - const float currentVelocity = _n->volumeLevel( tfp ) * instrumentTrack()->midiPort()->baseVelocity(); - if( pluginData->fluidVoice && - pluginData->lastVelocity != currentVelocity ) - { - m_synthMutex.lock(); - fluid_voice_gen_set( pluginData->fluidVoice, GEN_VELOCITY, currentVelocity ); - fluid_voice_update_param( pluginData->fluidVoice, GEN_VELOCITY ); - // make sure, FluidSynth modulates our changed GEN_VELOCITY via internal - // attenuation modulator, so changes take effect (7=Volume CC) - fluid_synth_cc( m_synth, m_channel, 7, 127 ); - m_synthMutex.unlock(); - - pluginData->lastVelocity = currentVelocity; - }*/ } +void sf2Instrument::noteOn( SF2PluginData * n ) +{ + m_synthMutex.lock(); + + // get list of current voice IDs so we can easily spot the new + // voice after the fluid_synth_noteon() call + const int poly = fluid_synth_get_polyphony( m_synth ); + fluid_voice_t * voices[poly]; + unsigned int id[poly]; + fluid_synth_get_voicelist( m_synth, voices, poly, -1 ); + for( int i = 0; i < poly; ++i ) + { + id[i] = 0; + } + for( int i = 0; i < poly && voices[i]; ++i ) + { + id[i] = fluid_voice_get_id( voices[i] ); + } + + fluid_synth_noteon( m_synth, m_channel, n->midiNote, n->lastVelocity ); + + // get new voice and save it + fluid_synth_get_voicelist( m_synth, voices, poly, -1 ); + for( int i = 0; i < poly && voices[i]; ++i ) + { + const unsigned int newID = fluid_voice_get_id( voices[i] ); + if( id[i] != newID || newID == 0 ) + { + n->fluidVoice = voices[i]; + break; + } + } + + m_synthMutex.unlock(); + + m_notesRunningMutex.lock(); + ++m_notesRunning[ n->midiNote ]; + m_notesRunningMutex.unlock(); +} + + +void sf2Instrument::noteOff( SF2PluginData * n ) +{ + n->noteOffSent = true; + m_notesRunningMutex.lock(); + const int notes = --m_notesRunning[n->midiNote]; + m_notesRunningMutex.unlock(); + + if( notes <= 0 ) + { + m_synthMutex.lock(); + fluid_synth_noteoff( m_synth, m_channel, n->midiNote ); + m_synthMutex.unlock(); + } + +} void sf2Instrument::play( sampleFrame * _working_buffer ) { const fpp_t frames = engine::mixer()->framesPerPeriod(); - m_synthMutex.lock(); - + // set midi pitch for this period const int currentMidiPitch = instrumentTrack()->midiPitch(); if( m_lastMidiPitch != currentMidiPitch ) { @@ -645,7 +650,71 @@ void sf2Instrument::play( sampleFrame * _working_buffer ) m_lastMidiPitchRange = currentMidiPitchRange; fluid_synth_pitch_wheel_sens( m_synth, m_channel, m_lastMidiPitchRange ); } + // if we have no new noteons/noteoffs, just render a period and call it a day + if( m_playingNotes.isEmpty() ) + { + renderFrames( frames, _working_buffer ); + instrumentTrack()->processAudioBuffer( _working_buffer, frames, NULL ); + return; + } + // processing loop + // go through noteplayhandles in processing order + f_cnt_t currentFrame = 0; + + while( ! m_playingNotes.isEmpty() ) + { + // find the note with lowest offset + NotePlayHandle * currentNote = m_playingNotes[0]; + for( int i = 1; i < m_playingNotes.size(); ++i ) + { + SF2PluginData * currentData = static_cast( currentNote->m_pluginData ); + SF2PluginData * iData = static_cast( m_playingNotes[i]->m_pluginData ); + if( currentData->offset > iData->offset ) + { + currentNote = m_playingNotes[i]; + } + } + + // process the current note: + // first see if we're synced in frame count + SF2PluginData * currentData = static_cast( currentNote->m_pluginData ); + if( currentData->offset > currentFrame ) + { + renderFrames( currentData->offset - currentFrame, _working_buffer + currentFrame ); + currentFrame = currentData->offset; + } + if( currentData->isNew ) + { + noteOn( currentData ); + if( currentNote->isReleased() ) // if the note is released during the same period, we have to process it again for noteoff + { + currentData->isNew = false; + currentData->offset = currentNote->framesBeforeRelease(); + } + else // otherwise remove the handle + { + m_playingNotes.remove( m_playingNotes.indexOf( currentNote ) ); + } + } + else + { + noteOff( currentData ); + m_playingNotes.remove( m_playingNotes.indexOf( currentNote ) ); + } + } + + if( currentFrame < frames ) + { + renderFrames( frames - currentFrame, _working_buffer + currentFrame ); + } + instrumentTrack()->processAudioBuffer( _working_buffer, frames, NULL ); +} + + +void sf2Instrument::renderFrames( f_cnt_t frames, sampleFrame * buf ) +{ + m_synthMutex.lock(); if( m_internalSampleRate < engine::mixer()->processingSampleRate() && m_srcState != NULL ) { @@ -658,8 +727,8 @@ void sf2Instrument::play( sampleFrame * _working_buffer ) fluid_synth_write_float( m_synth, f, tmp, 0, 2, tmp, 1, 2 ); SRC_DATA src_data; - src_data.data_in = tmp[0]; - src_data.data_out = _working_buffer[0]; + src_data.data_in = (float *)tmp; + src_data.data_out = (float *)buf; src_data.input_frames = f; src_data.output_frames = frames; src_data.src_ratio = (double) frames / f; @@ -679,11 +748,9 @@ void sf2Instrument::play( sampleFrame * _working_buffer ) } else { - fluid_synth_write_float( m_synth, frames, _working_buffer, 0, 2, _working_buffer, 1, 2 ); + fluid_synth_write_float( m_synth, frames, buf, 0, 2, buf, 1, 2 ); } m_synthMutex.unlock(); - - instrumentTrack()->processAudioBuffer( _working_buffer, frames, NULL ); } @@ -692,17 +759,11 @@ void sf2Instrument::play( sampleFrame * _working_buffer ) void sf2Instrument::deleteNotePluginData( NotePlayHandle * _n ) { SF2PluginData * pluginData = static_cast( _n->m_pluginData ); - m_notesRunningMutex.lock(); - const int n = --m_notesRunning[pluginData->midiNote]; - m_notesRunningMutex.unlock(); - - if( n <= 0 ) + if( ! pluginData->noteOffSent ) // if we for some reason haven't noteoffed the note before it gets deleted, + // do it here { - m_synthMutex.lock(); - fluid_synth_noteoff( m_synth, m_channel, pluginData->midiNote ); - m_synthMutex.unlock(); + noteOff( pluginData ); } - delete pluginData; } diff --git a/plugins/sf2_player/sf2_player.h b/plugins/sf2_player/sf2_player.h index 728a717cc..a50231c37 100644 --- a/plugins/sf2_player/sf2_player.h +++ b/plugins/sf2_player/sf2_player.h @@ -45,6 +45,7 @@ class NotePlayHandle; class patchesDialog; class QLabel; +struct SF2PluginData; class sf2Instrument : public Instrument { @@ -149,9 +150,14 @@ private: FloatModel m_chorusSpeed; FloatModel m_chorusDepth; + QVector m_playingNotes; + QMutex m_playingNotesMutex; private: void freeFont(); + void noteOn( SF2PluginData * n ); + void noteOff( SF2PluginData * n ); + void renderFrames( f_cnt_t frames, sampleFrame * buf ); friend class sf2InstrumentView; diff --git a/src/core/NotePlayHandle.cpp b/src/core/NotePlayHandle.cpp index ab3bfc3fb..934ebc129 100644 --- a/src/core/NotePlayHandle.cpp +++ b/src/core/NotePlayHandle.cpp @@ -29,6 +29,7 @@ #include "DetuningHelper.h" #include "InstrumentSoundShaping.h" #include "InstrumentTrack.h" +#include "Instrument.h" #include "MidiEvent.h" #include "MidiPort.h" #include "song.h" @@ -197,7 +198,9 @@ void NotePlayHandle::play( sampleFrame * _working_buffer ) instrumentTrack()->isSustainPedalPressed() == false && m_totalFramesPlayed + framesThisPeriod > m_frames ) { - noteOff( m_frames - m_totalFramesPlayed ); + noteOff( m_totalFramesPlayed == 0 + ? ( m_frames + offset() ) // if we have noteon and noteoff during the same period, take offset in account for release frame + : ( m_frames - m_totalFramesPlayed ) ); // otherwise, the offset is already negated and can be ignored } // under some circumstances we're called even if there's nothing to play @@ -206,7 +209,9 @@ void NotePlayHandle::play( sampleFrame * _working_buffer ) if( framesLeft() > 0 ) { // clear offset frames if we're at the first period - if( m_totalFramesPlayed == 0 ) + // skip for single-streamed instruments, because in their case NPH::play() could be called from an IPH without a buffer argument + // ... also, they don't actually render the sound in NPH's, which is an even better reason to skip... + if( m_totalFramesPlayed == 0 && ! ( m_instrumentTrack->instrument()->flags() & Instrument::IsSingleStreamed ) ) { memset( _working_buffer, 0, sizeof( sampleFrame ) * offset() ); }