--- linuxsampler/trunk/src/engines/common/AbstractVoice.cpp 2017/04/21 13:33:03 3118 +++ linuxsampler/trunk/src/engines/common/AbstractVoice.cpp 2019/08/23 11:44:00 3561 @@ -5,7 +5,7 @@ * Copyright (C) 2003,2004 by Benno Senoner and Christian Schoenebeck * * Copyright (C) 2005-2008 Christian Schoenebeck * * Copyright (C) 2009-2012 Christian Schoenebeck and Grigor Iliev * - * Copyright (C) 2013-2016 Christian Schoenebeck and Andreas Persson * + * Copyright (C) 2013-2017 Christian Schoenebeck and Andreas Persson * * * * 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 * @@ -129,7 +129,7 @@ AboutToTrigger(); // calculate volume - const double velocityAttenuation = GetVelocityAttenuation(itNoteOnEvent->Param.Note.Velocity); + const double velocityAttenuation = GetVelocityAttenuation(MIDIVelocity()); float volume = CalculateVolume(velocityAttenuation) * pKeyInfo->Volume; if (volume <= 0) return -1; @@ -139,7 +139,7 @@ SYNTHESIS_MODE_SET_BITDEPTH24(SynthesisMode, SmplInfo.BitDepth == 24); // get starting crossfade volume level - float crossfadeVolume = CalculateCrossfadeVolume(itNoteOnEvent->Param.Note.Velocity); + float crossfadeVolume = CalculateCrossfadeVolume(MIDIVelocity()); VolumeLeft = volume * pKeyInfo->PanLeft; VolumeRight = volume * pKeyInfo->PanRight; @@ -147,11 +147,14 @@ // this rate is used for rather mellow volume fades const float subfragmentRate = GetEngine()->SampleRate / CONFIG_DEFAULT_SUBFRAGMENT_SIZE; // this rate is used for very fast volume fades - const float quickRampRate = RTMath::Min(subfragmentRate, GetEngine()->SampleRate * 0.001f /* 1ms */); + const float quickRampRate = RTMath::Min(subfragmentRate, GetEngine()->SampleRate * 0.001f /* approx. 13ms */); CrossfadeSmoother.trigger(crossfadeVolume, subfragmentRate); VolumeSmoother.trigger(pEngineChannel->MidiVolume, subfragmentRate); - NoteVolumeSmoother.trigger(pNote ? pNote->Override.Volume : 1.f, quickRampRate); + NoteVolume.setCurveOnly(pNote ? pNote->Override.VolumeCurve : DEFAULT_FADE_CURVE); + NoteVolume.setCurrentValue(pNote ? pNote->Override.Volume.Value : 1.f); + NoteVolume.setDefaultDuration(pNote ? pNote->Override.VolumeTime : DEFAULT_NOTE_VOLUME_TIME_S); + NoteVolume.setFinal(pNote ? pNote->Override.Volume.Final : false); // Check if the sample needs disk streaming or is too short for that long cachedsamples = GetSampleCacheSize() / SmplInfo.FrameSize; @@ -191,40 +194,51 @@ } Pitch = CalculatePitchInfo(PitchBend); - NotePitch = (pNote) ? pNote->Override.Pitch : 1.0f; - NoteCutoff = (pNote) ? pNote->Override.Cutoff : 1.0f; - NoteResonance = (pNote) ? pNote->Override.Resonance : 1.0f; + NotePitch.setCurveOnly(pNote ? pNote->Override.PitchCurve : DEFAULT_FADE_CURVE); + NotePitch.setCurrentValue(pNote ? pNote->Override.Pitch.Value : 1.0f); + NotePitch.setFinal(pNote ? pNote->Override.Pitch.Final : false); + NotePitch.setDefaultDuration(pNote ? pNote->Override.PitchTime : DEFAULT_NOTE_PITCH_TIME_S); + NoteCutoff.Value = (pNote) ? pNote->Override.Cutoff.Value : 1.0f; + NoteCutoff.Final = (pNote) ? pNote->Override.Cutoff.isFinal() : false; + NoteResonance.Value = (pNote) ? pNote->Override.Resonance.Value : 1.0f; + NoteResonance.Final = (pNote) ? pNote->Override.Resonance.Final : false; // the length of the decay and release curves are dependent on the velocity - const double velrelease = 1 / GetVelocityRelease(itNoteOnEvent->Param.Note.Velocity); + const double velrelease = 1 / GetVelocityRelease(MIDIVelocity()); if (pSignalUnitRack == NULL) { // setup EG 1 (VCA EG) // get current value of EG1 controller - double eg1controllervalue = GetEG1ControllerValue(itNoteOnEvent->Param.Note.Velocity); + double eg1controllervalue = GetEG1ControllerValue(MIDIVelocity()); // calculate influence of EG1 controller on EG1's parameters EGInfo egInfo = CalculateEG1ControllerInfluence(eg1controllervalue); if (pNote) { - egInfo.Attack *= pNote->Override.Attack; - egInfo.Decay *= pNote->Override.Decay; - egInfo.Release *= pNote->Override.Release; + pNote->Override.Attack.applyTo(egInfo.Attack); + pNote->Override.Decay.applyTo(egInfo.Decay); + pNote->Override.Release.applyTo(egInfo.Release); } - TriggerEG1(egInfo, velrelease, velocityAttenuation, GetEngine()->SampleRate, itNoteOnEvent->Param.Note.Velocity); + TriggerEG1(egInfo, velrelease, velocityAttenuation, GetEngine()->SampleRate, MIDIVelocity()); } else { pSignalUnitRack->Trigger(); } const uint8_t pan = (pSignalUnitRack) ? pSignalUnitRack->GetEndpointUnit()->CalculatePan(MIDIPan) : MIDIPan; - NotePanLeft = (pNote) ? AbstractEngine::PanCurveValueNorm(pNote->Override.Pan, 0 /*left*/ ) : 1.f; - NotePanRight = (pNote) ? AbstractEngine::PanCurveValueNorm(pNote->Override.Pan, 1 /*right*/) : 1.f; + for (int c = 0; c < 2; ++c) { + float value = (pNote) ? AbstractEngine::PanCurveValueNorm(pNote->Override.Pan.Value, c) : 1.f; + NotePan[c].setCurveOnly(pNote ? pNote->Override.PanCurve : DEFAULT_FADE_CURVE); + NotePan[c].setCurrentValue(value); + NotePan[c].setFinal(pNote ? pNote->Override.Pan.Final : false); + NotePan[c].setDefaultDuration(pNote ? pNote->Override.PanTime : DEFAULT_NOTE_PAN_TIME_S); + } + PanLeftSmoother.trigger( - AbstractEngine::PanCurve[128 - pan] * NotePanLeft, + AbstractEngine::PanCurve[128 - pan], quickRampRate //NOTE: maybe we should have 2 separate pan smoothers, one for MIDI CC10 (with slow rate) and one for instrument script change_pan() calls (with fast rate) ); PanRightSmoother.trigger( - AbstractEngine::PanCurve[pan] * NotePanRight, + AbstractEngine::PanCurve[pan], quickRampRate //NOTE: maybe we should have 2 separate pan smoothers, one for MIDI CC10 (with slow rate) and one for instrument script change_pan() calls (with fast rate) ); @@ -238,15 +252,23 @@ else #else { - float finalVolume; + float finalVolume = pEngineChannel->MidiVolume * crossfadeVolume; + float fModVolume; if (pSignalUnitRack == NULL) { - finalVolume = pEngineChannel->MidiVolume * crossfadeVolume * pEG1->getLevel(); + fModVolume = pEG1->getLevel(); } else { - finalVolume = pEngineChannel->MidiVolume * crossfadeVolume * pSignalUnitRack->GetEndpointUnit()->GetVolume(); + fModVolume = pSignalUnitRack->GetEndpointUnit()->GetVolume(); } + NoteVolume.applyTo(fModVolume); + finalVolume *= fModVolume; + + float panL = PanLeftSmoother.render(); + float panR = PanRightSmoother.render(); + NotePan[0].applyTo(panL); + NotePan[1].applyTo(panR); - finalSynthesisParameters.fFinalVolumeLeft = finalVolume * VolumeLeft * PanLeftSmoother.render(); - finalSynthesisParameters.fFinalVolumeRight = finalVolume * VolumeRight * PanRightSmoother.render(); + finalSynthesisParameters.fFinalVolumeLeft = finalVolume * VolumeLeft * panL; + finalSynthesisParameters.fFinalVolumeRight = finalVolume * VolumeRight * panR; } #endif #endif @@ -255,12 +277,18 @@ // setup EG 2 (VCF Cutoff EG) { // get current value of EG2 controller - double eg2controllervalue = GetEG2ControllerValue(itNoteOnEvent->Param.Note.Velocity); + double eg2controllervalue = GetEG2ControllerValue(MIDIVelocity()); // calculate influence of EG2 controller on EG2's parameters EGInfo egInfo = CalculateEG2ControllerInfluence(eg2controllervalue); - TriggerEG2(egInfo, velrelease, velocityAttenuation, GetEngine()->SampleRate, itNoteOnEvent->Param.Note.Velocity); + if (pNote) { + pNote->Override.CutoffAttack.applyTo(egInfo.Attack); + pNote->Override.CutoffDecay.applyTo(egInfo.Decay); + pNote->Override.CutoffRelease.applyTo(egInfo.Release); + } + + TriggerEG2(egInfo, velrelease, velocityAttenuation, GetEngine()->SampleRate, MIDIVelocity()); } @@ -319,7 +347,7 @@ VCFResonanceCtrl.value = pEngineChannel->ControllerTable[VCFResonanceCtrl.controller]; // calculate cutoff frequency - CutoffBase = CalculateCutoffBase(itNoteOnEvent->Param.Note.Velocity); + CutoffBase = CalculateCutoffBase(MIDIVelocity()); VCFCutoffCtrl.fvalue = CalculateFinalCutoff(CutoffBase); @@ -344,8 +372,19 @@ } void AbstractVoice::SetSampleStartOffset() { - finalSynthesisParameters.dPos = RgnInfo.SampleStartOffset; // offset where we should start playback of sample (0 - 2000 sample points) - Pos = RgnInfo.SampleStartOffset; + double pos = RgnInfo.SampleStartOffset; // offset where we should start playback of sample + + // if another sample playback start position was requested by instrument + // script (built-in script function play_note()) + if (pNote && pNote->Override.SampleOffset >= 0) { + double overridePos = + double(SmplInfo.SampleRate) * double(pNote->Override.SampleOffset) / 1000000.0; + if (overridePos < SmplInfo.TotalFrameCount) + pos = overridePos; + } + + finalSynthesisParameters.dPos = pos; + Pos = pos; } /** @@ -453,12 +492,12 @@ uint8_t pan = MIDIPan; if (pSignalUnitRack != NULL) pan = pSignalUnitRack->GetEndpointUnit()->CalculatePan(MIDIPan); - PanLeftSmoother.update(AbstractEngine::PanCurve[128 - pan] * NotePanLeft); - PanRightSmoother.update(AbstractEngine::PanCurve[pan] * NotePanRight); + PanLeftSmoother.update(AbstractEngine::PanCurve[128 - pan]); + PanRightSmoother.update(AbstractEngine::PanCurve[pan]); - finalSynthesisParameters.fFinalPitch = Pitch.PitchBase * Pitch.PitchBend * NotePitch; + finalSynthesisParameters.fFinalPitch = Pitch.PitchBase * Pitch.PitchBend; - float fFinalVolume = VolumeSmoother.render() * CrossfadeSmoother.render() * NoteVolumeSmoother.render(); + float fFinalVolume = VolumeSmoother.render() * CrossfadeSmoother.render(); #ifdef CONFIG_PROCESS_MUTED_CHANNELS if (pChannel->GetMute()) fFinalVolume = 0; #endif @@ -466,7 +505,10 @@ // process transition events (note on, note off & sustain pedal) processTransitionEvents(itNoteEvent, iSubFragmentEnd); processGroupEvents(itGroupEvent, iSubFragmentEnd); - + + float fModVolume = 1; + float fModPitch = 1; + if (pSignalUnitRack == NULL) { // if the voice was killed in this subfragment, or if the // filter EG is finished, switch EG1 to fade out stage @@ -480,16 +522,16 @@ // process envelope generators switch (pEG1->getSegmentType()) { case EG::segment_lin: - fFinalVolume *= pEG1->processLin(); + fModVolume *= pEG1->processLin(); break; case EG::segment_exp: - fFinalVolume *= pEG1->processExp(); + fModVolume *= pEG1->processExp(); break; case EG::segment_end: - fFinalVolume *= pEG1->getLevel(); + fModVolume *= pEG1->getLevel(); break; // noop case EG::segment_pow: - fFinalVolume *= pEG1->processPow(); + fModVolume *= pEG1->processPow(); break; } switch (pEG2->getSegmentType()) { @@ -506,12 +548,12 @@ fFinalCutoff *= pEG2->processPow(); break; } - if (EG3.active()) finalSynthesisParameters.fFinalPitch *= EG3.render(); + if (EG3.active()) fModPitch *= EG3.render(); // process low frequency oscillators - if (bLFO1Enabled) fFinalVolume *= (1.0f - pLFO1->render()); + if (bLFO1Enabled) fModVolume *= (1.0f - pLFO1->render()); if (bLFO2Enabled) fFinalCutoff *= (1.0f - pLFO2->render()); - if (bLFO3Enabled) finalSynthesisParameters.fFinalPitch *= RTMath::CentsToFreqRatio(pLFO3->render()); + if (bLFO3Enabled) fModPitch *= RTMath::CentsToFreqRatio(pLFO3->render()); } else { // if the voice was killed in this subfragment, enter fade out stage if (itKillEvent && killPos <= iSubFragmentEnd) { @@ -531,13 +573,17 @@ fFinalCutoff = pSignalUnitRack->GetEndpointUnit()->CalculateFilterCutoff(fFinalCutoff); fFinalResonance = pSignalUnitRack->GetEndpointUnit()->CalculateResonance(fFinalResonance); - finalSynthesisParameters.fFinalPitch = - pSignalUnitRack->GetEndpointUnit()->CalculatePitch(finalSynthesisParameters.fFinalPitch); - + fModPitch = pSignalUnitRack->GetEndpointUnit()->CalculatePitch(fModPitch); } - fFinalCutoff *= NoteCutoff; - fFinalResonance *= NoteResonance; + NoteVolume.applyTo(fModVolume); + NotePitch.applyTo(fModPitch); + NoteCutoff.applyTo(fFinalCutoff); + NoteResonance.applyTo(fFinalResonance); + + fFinalVolume *= fModVolume; + + finalSynthesisParameters.fFinalPitch *= fModPitch; // limit the pitch so we don't read outside the buffer finalSynthesisParameters.fFinalPitch = RTMath::Min(finalSynthesisParameters.fFinalPitch, float(1 << CONFIG_MAX_PITCH)); @@ -557,18 +603,24 @@ // prepare final synthesis parameters structure finalSynthesisParameters.uiToGo = iSubFragmentEnd - i; + + float panL = PanLeftSmoother.render(); + float panR = PanRightSmoother.render(); + NotePan[0].applyTo(panL); + NotePan[1].applyTo(panR); + #ifdef CONFIG_INTERPOLATE_VOLUME finalSynthesisParameters.fFinalVolumeDeltaLeft = - (fFinalVolume * VolumeLeft * PanLeftSmoother.render() - + (fFinalVolume * VolumeLeft * panL - finalSynthesisParameters.fFinalVolumeLeft) / finalSynthesisParameters.uiToGo; finalSynthesisParameters.fFinalVolumeDeltaRight = - (fFinalVolume * VolumeRight * PanRightSmoother.render() - + (fFinalVolume * VolumeRight * panR - finalSynthesisParameters.fFinalVolumeRight) / finalSynthesisParameters.uiToGo; #else finalSynthesisParameters.fFinalVolumeLeft = - fFinalVolume * VolumeLeft * PanLeftSmoother.render(); + fFinalVolume * VolumeLeft * panL; finalSynthesisParameters.fFinalVolumeRight = - fFinalVolume * VolumeRight * PanRightSmoother.render(); + fFinalVolume * VolumeRight * panR; #endif // render audio for one subfragment if (!delay) RunSynthesisFunction(SynthesisMode, &finalSynthesisParameters, &loop); @@ -729,35 +781,94 @@ { EnterReleaseStage(); } + // process kill-note events (caused by built-in instrument script function fade_out()) + if (itEvent->Type == Event::type_kill_note && pNote && + pEngineChannel->pEngine->NoteByID( itEvent->Param.Note.ID ) == pNote) + { + Kill(itEvent); + } // process synthesis parameter events (caused by built-in realt-time instrument script functions) if (itEvent->Type == Event::type_note_synth_param && pNote && pEngineChannel->pEngine->NoteByID( itEvent->Param.NoteSynthParam.NoteID ) == pNote) { switch (itEvent->Param.NoteSynthParam.Type) { case Event::synth_param_volume: - NoteVolumeSmoother.update(itEvent->Param.NoteSynthParam.AbsValue); + NoteVolume.fadeTo(itEvent->Param.NoteSynthParam.AbsValue, GetEngine()->SampleRate / CONFIG_DEFAULT_SUBFRAGMENT_SIZE); + NoteVolume.setFinal(itEvent->Param.NoteSynthParam.isFinal()); + break; + case Event::synth_param_volume_time: + NoteVolume.setDefaultDuration(itEvent->Param.NoteSynthParam.AbsValue); + break; + case Event::synth_param_volume_curve: + NoteVolume.setCurve((fade_curve_t)itEvent->Param.NoteSynthParam.AbsValue, GetEngine()->SampleRate / CONFIG_DEFAULT_SUBFRAGMENT_SIZE); break; case Event::synth_param_pitch: - NotePitch = itEvent->Param.NoteSynthParam.AbsValue; + NotePitch.fadeTo(itEvent->Param.NoteSynthParam.AbsValue, GetEngine()->SampleRate / CONFIG_DEFAULT_SUBFRAGMENT_SIZE); + NotePitch.setFinal(itEvent->Param.NoteSynthParam.isFinal()); + break; + case Event::synth_param_pitch_time: + NotePitch.setDefaultDuration(itEvent->Param.NoteSynthParam.AbsValue); + break; + case Event::synth_param_pitch_curve: + NotePitch.setCurve((fade_curve_t)itEvent->Param.NoteSynthParam.AbsValue, GetEngine()->SampleRate / CONFIG_DEFAULT_SUBFRAGMENT_SIZE); break; case Event::synth_param_pan: - NotePanLeft = AbstractEngine::PanCurveValueNorm(itEvent->Param.NoteSynthParam.AbsValue, 0 /*left*/); - NotePanRight = AbstractEngine::PanCurveValueNorm(itEvent->Param.NoteSynthParam.AbsValue, 1 /*right*/); + NotePan[0].fadeTo( + AbstractEngine::PanCurveValueNorm(itEvent->Param.NoteSynthParam.AbsValue, 0 /*left*/), + GetEngine()->SampleRate / CONFIG_DEFAULT_SUBFRAGMENT_SIZE + ); + NotePan[1].fadeTo( + AbstractEngine::PanCurveValueNorm(itEvent->Param.NoteSynthParam.AbsValue, 1 /*right*/), + GetEngine()->SampleRate / CONFIG_DEFAULT_SUBFRAGMENT_SIZE + ); + NotePan[0].setFinal(itEvent->Param.NoteSynthParam.isFinal()); + NotePan[1].setFinal(itEvent->Param.NoteSynthParam.isFinal()); + break; + case Event::synth_param_pan_time: + NotePan[0].setDefaultDuration(itEvent->Param.NoteSynthParam.AbsValue); + NotePan[1].setDefaultDuration(itEvent->Param.NoteSynthParam.AbsValue); + break; + case Event::synth_param_pan_curve: + NotePan[0].setCurve((fade_curve_t)itEvent->Param.NoteSynthParam.AbsValue, GetEngine()->SampleRate / CONFIG_DEFAULT_SUBFRAGMENT_SIZE); + NotePan[1].setCurve((fade_curve_t)itEvent->Param.NoteSynthParam.AbsValue, GetEngine()->SampleRate / CONFIG_DEFAULT_SUBFRAGMENT_SIZE); break; case Event::synth_param_cutoff: - NoteCutoff = itEvent->Param.NoteSynthParam.AbsValue; + NoteCutoff.Value = itEvent->Param.NoteSynthParam.AbsValue; + NoteCutoff.Final = itEvent->Param.NoteSynthParam.isFinal(); break; case Event::synth_param_resonance: - NoteResonance = itEvent->Param.NoteSynthParam.AbsValue; + NoteResonance.Value = itEvent->Param.NoteSynthParam.AbsValue; + NoteResonance.Final = itEvent->Param.NoteSynthParam.isFinal(); break; case Event::synth_param_amp_lfo_depth: - pLFO1->setScriptDepthFactor(itEvent->Param.NoteSynthParam.AbsValue); + pLFO1->setScriptDepthFactor( + itEvent->Param.NoteSynthParam.AbsValue, + itEvent->Param.NoteSynthParam.isFinal() + ); break; case Event::synth_param_amp_lfo_freq: - pLFO1->setScriptFrequencyFactor(itEvent->Param.NoteSynthParam.AbsValue, GetEngine()->SampleRate / CONFIG_DEFAULT_SUBFRAGMENT_SIZE); + if (itEvent->Param.NoteSynthParam.isFinal()) + pLFO1->setScriptFrequencyFinal(itEvent->Param.NoteSynthParam.AbsValue, GetEngine()->SampleRate / CONFIG_DEFAULT_SUBFRAGMENT_SIZE); + else + pLFO1->setScriptFrequencyFactor(itEvent->Param.NoteSynthParam.AbsValue, GetEngine()->SampleRate / CONFIG_DEFAULT_SUBFRAGMENT_SIZE); + break; + case Event::synth_param_cutoff_lfo_depth: + pLFO2->setScriptDepthFactor( + itEvent->Param.NoteSynthParam.AbsValue, + itEvent->Param.NoteSynthParam.isFinal() + ); + break; + case Event::synth_param_cutoff_lfo_freq: + if (itEvent->Param.NoteSynthParam.isFinal()) + pLFO2->setScriptFrequencyFinal(itEvent->Param.NoteSynthParam.AbsValue, GetEngine()->SampleRate / CONFIG_DEFAULT_SUBFRAGMENT_SIZE); + else + pLFO2->setScriptFrequencyFactor(itEvent->Param.NoteSynthParam.AbsValue, GetEngine()->SampleRate / CONFIG_DEFAULT_SUBFRAGMENT_SIZE); break; case Event::synth_param_pitch_lfo_depth: - pLFO3->setScriptDepthFactor(itEvent->Param.NoteSynthParam.AbsValue); + pLFO3->setScriptDepthFactor( + itEvent->Param.NoteSynthParam.AbsValue, + itEvent->Param.NoteSynthParam.isFinal() + ); break; case Event::synth_param_pitch_lfo_freq: pLFO3->setScriptFrequencyFactor(itEvent->Param.NoteSynthParam.AbsValue, GetEngine()->SampleRate / CONFIG_DEFAULT_SUBFRAGMENT_SIZE); @@ -765,6 +876,7 @@ case Event::synth_param_attack: case Event::synth_param_decay: + case Event::synth_param_sustain: case Event::synth_param_release: break; // noop } @@ -827,7 +939,12 @@ // GSt behaviour: maximum transpose up is 40 semitones. If // MIDI key is more than 40 semitones above unity note, // the transpose is not done. - if (!SmplInfo.Unpitched && (MIDIKey() - (int) RgnInfo.UnityNote) < 40) pitchbasecents += (MIDIKey() - (int) RgnInfo.UnityNote) * 100; + // + // Update: Removed this GSt misbehavior. I don't think that any stock + // gig sound requires it to resemble its original sound. + // -- Christian, 2017-07-09 + if (!SmplInfo.Unpitched /* && (MIDIKey() - (int) RgnInfo.UnityNote) < 40*/) + pitchbasecents += (MIDIKey() - (int) RgnInfo.UnityNote) * 100; pitch.PitchBase = RTMath::CentsToFreqRatioUnlimited(pitchbasecents) * (double(SmplInfo.SampleRate) / double(GetEngine()->SampleRate)); pitch.PitchBendRange = 1.0 / 8192.0 * 100.0 * InstrInfo.PitchbendRange; @@ -843,7 +960,12 @@ // GSt behaviour: maximum transpose up is 40 semitones. If // MIDI key is more than 40 semitones above unity note, // the transpose is not done. - if (!SmplInfo.Unpitched && (MIDIKey() - (int) RgnInfo.UnityNote) < 40) pitchbasecents += (MIDIKey() - (int) RgnInfo.UnityNote) * 100; + // + // Update: Removed this GSt misbehavior. I don't think that any stock + // gig sound requires it to resemble its original sound. + // -- Christian, 2017-07-09 + if (!SmplInfo.Unpitched /* && (MIDIKey() - (int) RgnInfo.UnityNote) < 40*/) + pitchbasecents += (MIDIKey() - (int) RgnInfo.UnityNote) * 100; pitch.PitchBase = RTMath::CentsToFreqRatioUnlimited(pitchbasecents) * (double(SmplInfo.SampleRate) / double(GetEngine()->SampleRate)); this->Pitch = pitch;