| /*---------------------------------------------------------------------------- |
| * |
| * File: |
| * fmsynth.c |
| * |
| * Contents and purpose: |
| * Implements the high-level FM synthesizer functions. |
| * |
| * Copyright Sonic Network Inc. 2004 |
| |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| * |
| *---------------------------------------------------------------------------- |
| * Revision Control: |
| * $Revision: 795 $ |
| * $Date: 2007-08-01 00:14:45 -0700 (Wed, 01 Aug 2007) $ |
| *---------------------------------------------------------------------------- |
| */ |
| |
| // includes |
| #include "eas_host.h" |
| #include "eas_report.h" |
| |
| #include "eas_data.h" |
| #include "eas_synth_protos.h" |
| #include "eas_audioconst.h" |
| #include "eas_fmengine.h" |
| #include "eas_math.h" |
| |
| /* option sanity check */ |
| #ifdef _REVERB |
| #error "No reverb for FM synthesizer" |
| #endif |
| #ifdef _CHORUS |
| #error "No chorus for FM synthesizer" |
| #endif |
| |
| /* |
| * WARNING: These macros can cause unwanted side effects. Use them |
| * with care. For example, min(x++,y++) will cause either x or y to be |
| * incremented twice. |
| */ |
| #define min(a,b) ((a) < (b) ? (a) : (b)) |
| #define max(a,b) ((a) > (b) ? (a) : (b)) |
| |
| /* pivot point for keyboard scalars */ |
| #define EG_SCALE_PIVOT_POINT 64 |
| #define KEY_SCALE_PIVOT_POINT 36 |
| |
| /* This number is the negative of the frequency of the note (in cents) of |
| * the sine wave played at unity. The number can be calculated as follows: |
| * |
| * MAGIC_NUMBER = 1200 * log(base2) (SINE_TABLE_SIZE * 8.175798916 / SAMPLE_RATE) |
| * |
| * 8.17578 is a reference to the frequency of MIDI note 0 |
| */ |
| #if defined (_SAMPLE_RATE_8000) |
| #define MAGIC_NUMBER 1279 |
| #elif defined (_SAMPLE_RATE_16000) |
| #define MAGIC_NUMBER 79 |
| #elif defined (_SAMPLE_RATE_20000) |
| #define MAGIC_NUMBER -308 |
| #elif defined (_SAMPLE_RATE_22050) |
| #define MAGIC_NUMBER -477 |
| #elif defined (_SAMPLE_RATE_24000) |
| #define MAGIC_NUMBER -623 |
| #elif defined (_SAMPLE_RATE_32000) |
| #define MAGIC_NUMBER -1121 |
| #elif defined (_SAMPLE_RATE_44100) |
| #define MAGIC_NUMBER -1677 |
| #elif defined (_SAMPLE_RATE_48000) |
| #define MAGIC_NUMBER -1823 |
| #endif |
| |
| /* externs */ |
| extern const EAS_I16 fmControlTable[128]; |
| extern const EAS_U16 fmRateTable[256]; |
| extern const EAS_U16 fmAttackTable[16]; |
| extern const EAS_U8 fmDecayTable[16]; |
| extern const EAS_U8 fmReleaseTable[16]; |
| extern const EAS_U8 fmScaleTable[16]; |
| |
| /* local prototypes */ |
| /*lint -esym(715, pVoiceMgr) standard synthesizer interface - pVoiceMgr not used */ |
| static EAS_RESULT FM_Initialize (S_VOICE_MGR *pVoiceMgr) { return EAS_SUCCESS; } |
| static EAS_RESULT FM_StartVoice (S_VOICE_MGR *pVoiceMgr, S_SYNTH *pSynth, S_SYNTH_VOICE *pVoice, EAS_I32 voiceNum, EAS_U16 regionIndex); |
| static EAS_BOOL FM_UpdateVoice (S_VOICE_MGR *pVoiceMgr, S_SYNTH *pSynth, S_SYNTH_VOICE *pVoice, EAS_I32 voiceNum, EAS_I32 *pMixBuffer, EAS_I32 numSamples); |
| static void FM_ReleaseVoice (S_VOICE_MGR *pVoiceMgr, S_SYNTH *pSynth, S_SYNTH_VOICE *pVoice, EAS_I32 voiceNum); |
| static void FM_MuteVoice (S_VOICE_MGR *pVoiceMgr, S_SYNTH *pSynth, S_SYNTH_VOICE *pVoice, EAS_I32 voiceNum); |
| static void FM_SustainPedal (S_VOICE_MGR *pVoiceMgr, S_SYNTH *pSynth, S_SYNTH_VOICE *pVoice, S_SYNTH_CHANNEL *pChannel, EAS_I32 voiceNum); |
| static void FM_UpdateChannel (S_VOICE_MGR *pVoiceMgr, S_SYNTH *pSynth, EAS_U8 channel); |
| |
| |
| /*---------------------------------------------------------------------------- |
| * Synthesizer interface |
| *---------------------------------------------------------------------------- |
| */ |
| const S_SYNTH_INTERFACE fmSynth = |
| { |
| FM_Initialize, |
| FM_StartVoice, |
| FM_UpdateVoice, |
| FM_ReleaseVoice, |
| FM_MuteVoice, |
| FM_SustainPedal, |
| FM_UpdateChannel |
| }; |
| |
| #ifdef FM_OFFBOARD |
| const S_FRAME_INTERFACE fmFrameInterface = |
| { |
| FM_StartFrame, |
| FM_EndFrame |
| }; |
| #endif |
| |
| /*---------------------------------------------------------------------------- |
| * inline functions |
| *---------------------------------------------------------------------------- |
| */ |
| EAS_INLINE S_FM_VOICE *GetFMVoicePtr (S_VOICE_MGR *pVoiceMgr, EAS_INT voiceNum) |
| { |
| return &pVoiceMgr->fmVoices[voiceNum]; |
| } |
| EAS_INLINE S_SYNTH_CHANNEL *GetChannelPtr (S_SYNTH *pSynth, S_SYNTH_VOICE *pVoice) |
| { |
| return &pSynth->channels[pVoice->channel & 15]; |
| } |
| EAS_INLINE const S_FM_REGION *GetFMRegionPtr (S_SYNTH *pSynth, S_SYNTH_VOICE *pVoice) |
| { |
| #ifdef _SECONDARY_SYNTH |
| return &pSynth->pEAS->pFMRegions[pVoice->regionIndex & REGION_INDEX_MASK]; |
| #else |
| return &pSynth->pEAS->pFMRegions[pVoice->regionIndex]; |
| #endif |
| } |
| |
| /*---------------------------------------------------------------------------- |
| * FM_SynthIsOutputOperator |
| *---------------------------------------------------------------------------- |
| * Purpose: |
| * Returns true if the operator is a direct output and not muted |
| * |
| * Inputs: |
| * |
| * Outputs: |
| * Returns boolean |
| *---------------------------------------------------------------------------- |
| */ |
| static EAS_BOOL FM_SynthIsOutputOperator (const S_FM_REGION *pRegion, EAS_INT operIndex) |
| { |
| |
| /* see if voice is muted */ |
| if ((pRegion->oper[operIndex].gain & 0xfc) == 0) |
| return 0; |
| |
| /* check based on mode */ |
| switch (pRegion->region.keyGroupAndFlags & 7) |
| { |
| |
| /* mode 0 - all operators are external */ |
| case 0: |
| return EAS_TRUE; |
| |
| /* mode 1 - operators 1-3 are external */ |
| case 1: |
| if (operIndex != 0) |
| return EAS_TRUE; |
| break; |
| |
| /* mode 2 - operators 1 & 3 are external */ |
| case 2: |
| if ((operIndex == 1) || (operIndex == 3)) |
| return EAS_TRUE; |
| break; |
| |
| /* mode 2 - operators 1 & 2 are external */ |
| case 3: |
| if ((operIndex == 1) || (operIndex == 2)) |
| return EAS_TRUE; |
| break; |
| |
| /* modes 4 & 5 - operator 1 is external */ |
| case 4: |
| case 5: |
| if (operIndex == 1) |
| return EAS_TRUE; |
| break; |
| |
| default: |
| { /* dpp: EAS_ReportEx(_EAS_SEVERITY_FATAL,"Invalid voice mode: %d", |
| pRegion->region.keyGroupAndFlags & 7); */ } |
| } |
| |
| return EAS_FALSE; |
| } |
| |
| /*---------------------------------------------------------------------------- |
| * FM_CalcEGRate() |
| *---------------------------------------------------------------------------- |
| * Purpose: |
| * |
| * Inputs: |
| * nKeyNumber - MIDI note |
| * nLogRate - logarithmic scale rate from patch data |
| * nKeyScale - key scaling factor for this EG |
| * |
| * Outputs: |
| * 16-bit linear multiplier |
| *---------------------------------------------------------------------------- |
| */ |
| |
| static EAS_U16 FM_CalcEGRate (EAS_U8 nKeyNumber, EAS_U8 nLogRate, EAS_U8 nEGScale) |
| { |
| EAS_I32 temp; |
| |
| /* incorporate key scaling on release rate */ |
| temp = (EAS_I32) nLogRate << 7; |
| temp += ((EAS_I32) nKeyNumber - EG_SCALE_PIVOT_POINT) * (EAS_I32) nEGScale; |
| |
| /* saturate */ |
| temp = max(temp, 0); |
| temp = min(temp, 32767); |
| |
| /* look up in rate table */ |
| /*lint -e{704} use shift for performance */ |
| return fmRateTable[temp >> 8]; |
| } |
| |
| /*---------------------------------------------------------------------------- |
| * FM_ReleaseVoice() |
| *---------------------------------------------------------------------------- |
| * Purpose: |
| * The selected voice is being released. |
| * |
| * Inputs: |
| * psEASData - pointer to S_EAS_DATA |
| * pVoice - pointer to voice to release |
| * |
| * Outputs: |
| * None |
| *---------------------------------------------------------------------------- |
| */ |
| static void FM_ReleaseVoice (S_VOICE_MGR *pVoiceMgr, S_SYNTH *pSynth, S_SYNTH_VOICE *pVoice, EAS_I32 voiceNum) |
| { |
| EAS_INT operIndex; |
| const S_FM_REGION *pRegion; |
| S_FM_VOICE *pFMVoice; |
| |
| /* check to see if voice responds to NOTE-OFF's */ |
| pRegion = GetFMRegionPtr(pSynth, pVoice); |
| if (pRegion->region.keyGroupAndFlags & REGION_FLAG_ONE_SHOT) |
| return; |
| |
| /* set all envelopes to release state */ |
| pFMVoice = GetFMVoicePtr(pVoiceMgr, voiceNum); |
| for (operIndex = 0; operIndex < 4; operIndex++) |
| { |
| pFMVoice->oper[operIndex].envState = eFMEnvelopeStateRelease; |
| |
| /* incorporate key scaling on release rate */ |
| pFMVoice->oper[operIndex].envRate = FM_CalcEGRate( |
| pVoice->note, |
| fmReleaseTable[pRegion->oper[operIndex].velocityRelease & 0x0f], |
| fmScaleTable[pRegion->oper[operIndex].egKeyScale >> 4]); |
| } /* end for (operIndex = 0; operIndex < 4; operIndex++) */ |
| } |
| |
| /*---------------------------------------------------------------------------- |
| * FM_MuteVoice() |
| *---------------------------------------------------------------------------- |
| * Purpose: |
| * The selected voice is being muted. |
| * |
| * Inputs: |
| * pVoice - pointer to voice to release |
| * |
| * Outputs: |
| * None |
| *---------------------------------------------------------------------------- |
| */ |
| /*lint -esym(715, pSynth) standard interface, pVoiceMgr not used */ |
| static void FM_MuteVoice (S_VOICE_MGR *pVoiceMgr, S_SYNTH *pSynth, S_SYNTH_VOICE *pVoice, EAS_I32 voiceNum) |
| { |
| S_FM_VOICE *pFMVoice; |
| |
| /* clear deferred action flags */ |
| pVoice->voiceFlags &= |
| ~(VOICE_FLAG_DEFER_MIDI_NOTE_OFF | |
| VOICE_FLAG_SUSTAIN_PEDAL_DEFER_NOTE_OFF | |
| VOICE_FLAG_DEFER_MUTE); |
| |
| /* set all envelopes to muted state */ |
| pFMVoice = GetFMVoicePtr(pVoiceMgr, voiceNum); |
| pFMVoice->oper[0].envState = eFMEnvelopeStateMuted; |
| pFMVoice->oper[1].envState = eFMEnvelopeStateMuted; |
| pFMVoice->oper[2].envState = eFMEnvelopeStateMuted; |
| pFMVoice->oper[3].envState = eFMEnvelopeStateMuted; |
| } |
| |
| /*---------------------------------------------------------------------------- |
| * FM_SustainPedal() |
| *---------------------------------------------------------------------------- |
| * Purpose: |
| * The selected voice is held due to sustain pedal |
| * |
| * Inputs: |
| * pVoice - pointer to voice to sustain |
| * |
| * Outputs: |
| * None |
| *---------------------------------------------------------------------------- |
| */ |
| /*lint -esym(715, pChannel) standard interface, pVoiceMgr not used */ |
| static void FM_SustainPedal (S_VOICE_MGR *pVoiceMgr, S_SYNTH *pSynth, S_SYNTH_VOICE *pVoice, S_SYNTH_CHANNEL *pChannel, EAS_I32 voiceNum) |
| { |
| const S_FM_REGION *pRegion; |
| S_FM_VOICE *pFMVoice; |
| EAS_INT operIndex; |
| |
| pRegion = GetFMRegionPtr(pSynth, pVoice); |
| pFMVoice = GetFMVoicePtr(pVoiceMgr, voiceNum); |
| |
| /* check to see if any envelopes are above the sustain level */ |
| for (operIndex = 0; operIndex < 4; operIndex++) |
| { |
| |
| /* if level control or envelope gain is zero, skip this envelope */ |
| if (((pRegion->oper[operIndex].gain & 0xfc) == 0) || |
| (pFMVoice->oper[operIndex].envGain == 0)) |
| { |
| continue; |
| } |
| |
| /* if the envelope gain is above the sustain level, we need to catch this voice */ |
| if (pFMVoice->oper[operIndex].envGain >= ((EAS_U16) (pRegion->oper[operIndex].sustain & 0xfc) << 7)) |
| { |
| |
| /* reset envelope to decay state */ |
| pFMVoice->oper[operIndex].envState = eFMEnvelopeStateDecay; |
| |
| pFMVoice->oper[operIndex].envRate = FM_CalcEGRate( |
| pVoice->note, |
| fmDecayTable[pRegion->oper[operIndex].attackDecay & 0x0f], |
| fmScaleTable[pRegion->oper[operIndex].egKeyScale >> 4]); |
| |
| /* set voice to decay state */ |
| pVoice->voiceState = eVoiceStatePlay; |
| |
| /* set sustain flag */ |
| pVoice->voiceFlags |= VOICE_FLAG_SUSTAIN_PEDAL_DEFER_NOTE_OFF; |
| } |
| } /* end for (operIndex = 0; operIndex < 4; operIndex++) */ |
| } |
| |
| /*---------------------------------------------------------------------------- |
| * FM_StartVoice() |
| *---------------------------------------------------------------------------- |
| * Purpose: |
| * Assign the region for the given instrument using the midi key number |
| * and the RPN2 (coarse tuning) value. By using RPN2 as part of the |
| * region selection process, we reduce the amount a given sample has |
| * to be transposed by selecting the closest recorded root instead. |
| * |
| * This routine is the second half of SynthAssignRegion(). |
| * If the region was successfully found by SynthFindRegionIndex(), |
| * then assign the region's parameters to the voice. |
| * |
| * Setup and initialize the following voice parameters: |
| * m_nRegionIndex |
| * |
| * Inputs: |
| * pVoice - ptr to the voice we have assigned for this channel |
| * nRegionIndex - index of the region |
| * psEASData - pointer to overall EAS data structure |
| * |
| * Outputs: |
| * success - could find and assign the region for this voice's note otherwise |
| * failure - could not find nor assign the region for this voice's note |
| * |
| * Side Effects: |
| * psSynthObject->m_sVoice[].m_nRegionIndex is assigned |
| * psSynthObject->m_sVoice[] parameters are assigned |
| *---------------------------------------------------------------------------- |
| */ |
| static EAS_RESULT FM_StartVoice (S_VOICE_MGR *pVoiceMgr, S_SYNTH *pSynth, S_SYNTH_VOICE *pVoice, EAS_I32 voiceNum, EAS_U16 regionIndex) |
| { |
| S_FM_VOICE *pFMVoice; |
| S_SYNTH_CHANNEL *pChannel; |
| const S_FM_REGION *pRegion; |
| EAS_I32 temp; |
| EAS_INT operIndex; |
| |
| /* establish pointers to data */ |
| pVoice->regionIndex = regionIndex; |
| pFMVoice = GetFMVoicePtr(pVoiceMgr, voiceNum); |
| pChannel = GetChannelPtr(pSynth, pVoice); |
| pRegion = GetFMRegionPtr(pSynth, pVoice); |
| |
| /* update static channel parameters */ |
| if (pChannel->channelFlags & CHANNEL_FLAG_UPDATE_CHANNEL_PARAMETERS) |
| FM_UpdateChannel(pVoiceMgr, pSynth, pVoice->channel & 15); |
| |
| /* init the LFO */ |
| pFMVoice->lfoValue = 0; |
| pFMVoice->lfoPhase = 0; |
| pFMVoice->lfoDelay = (EAS_U16) (fmScaleTable[pRegion->lfoFreqDelay & 0x0f] >> 1); |
| |
| #if (NUM_OUTPUT_CHANNELS == 2) |
| /* calculate pan gain values only if stereo output */ |
| /* set up panning only at note start */ |
| temp = (EAS_I32) pChannel->pan - 64; |
| temp += (EAS_I32) pRegion->pan; |
| if (temp < -64) |
| temp = -64; |
| if (temp > 64) |
| temp = 64; |
| pFMVoice->pan = (EAS_I8) temp; |
| #endif /* #if (NUM_OUTPUT_CHANNELS == 2) */ |
| |
| /* no samples have been synthesized for this note yet */ |
| pVoice->voiceFlags = VOICE_FLAG_NO_SAMPLES_SYNTHESIZED_YET; |
| |
| /* initialize gain value for anti-zipper filter */ |
| pFMVoice->voiceGain = (EAS_I16) EAS_LogToLinear16(pChannel->staticGain); |
| pFMVoice->voiceGain = (EAS_I16) FMUL_15x15(pFMVoice->voiceGain, pSynth->masterVolume); |
| |
| /* initialize the operators */ |
| for (operIndex = 0; operIndex < 4; operIndex++) |
| { |
| |
| /* establish operator output gain level */ |
| /*lint -e{701} <use shift for performance> */ |
| pFMVoice->oper[operIndex].outputGain = EAS_LogToLinear16(((EAS_I16) (pRegion->oper[operIndex].gain & 0xfc) - 0xfc) << 7); |
| |
| /* check for linear velocity flag */ |
| /*lint -e{703} <use shift for performance> */ |
| if (pRegion->oper[operIndex].flags & FM_OPER_FLAG_LINEAR_VELOCITY) |
| temp = (EAS_I32) (pVoice->velocity - 127) << 5; |
| else |
| temp = (EAS_I32) fmControlTable[pVoice->velocity]; |
| |
| /* scale velocity */ |
| /*lint -e{704} use shift for performance */ |
| temp = (temp * (EAS_I32)(pRegion->oper[operIndex].velocityRelease & 0xf0)) >> 7; |
| |
| /* include key scalar */ |
| temp -= ((EAS_I32) pVoice->note - KEY_SCALE_PIVOT_POINT) * (EAS_I32) fmScaleTable[pRegion->oper[operIndex].egKeyScale & 0x0f]; |
| |
| /* saturate */ |
| temp = min(temp, 0); |
| temp = max(temp, -32768); |
| |
| /* save static gain parameters */ |
| pFMVoice->oper[operIndex].baseGain = (EAS_I16) EAS_LogToLinear16(temp); |
| |
| /* incorporate key scaling on decay rate */ |
| pFMVoice->oper[operIndex].envRate = FM_CalcEGRate( |
| pVoice->note, |
| fmDecayTable[pRegion->oper[operIndex].attackDecay & 0x0f], |
| fmScaleTable[pRegion->oper[operIndex].egKeyScale >> 4]); |
| |
| /* if zero attack time, max out envelope and jump to decay state */ |
| if ((pRegion->oper[operIndex].attackDecay & 0xf0) == 0xf0) |
| { |
| |
| /* start out envelope at max */ |
| pFMVoice->oper[operIndex].envGain = 0x7fff; |
| |
| /* set envelope to decay state */ |
| pFMVoice->oper[operIndex].envState = eFMEnvelopeStateDecay; |
| } |
| |
| /* start envelope at zero and start in attack state */ |
| else |
| { |
| pFMVoice->oper[operIndex].envGain = 0; |
| pFMVoice->oper[operIndex].envState = eFMEnvelopeStateAttack; |
| } |
| } |
| |
| return EAS_SUCCESS; |
| } |
| |
| /*---------------------------------------------------------------------------- |
| * FM_UpdateChannel() |
| *---------------------------------------------------------------------------- |
| * Purpose: |
| * Calculate and assign static channel parameters |
| * These values only need to be updated if one of the controller values |
| * for this channel changes. |
| * Called when CHANNEL_FLAG_UPDATE_CHANNEL_PARAMETERS flag is set. |
| * |
| * Inputs: |
| * nChannel - channel to update |
| * psEASData - pointer to overall EAS data structure |
| * |
| * Outputs: |
| * |
| * Side Effects: |
| * - the given channel's static gain and static pitch are updated |
| *---------------------------------------------------------------------------- |
| */ |
| /*lint -esym(715, pVoiceMgr) standard interface, pVoiceMgr not used */ |
| static void FM_UpdateChannel (S_VOICE_MGR *pVoiceMgr, S_SYNTH *pSynth, EAS_U8 channel) |
| { |
| S_SYNTH_CHANNEL *pChannel; |
| EAS_I32 temp; |
| |
| pChannel = &pSynth->channels[channel]; |
| |
| /* convert CC7 volume controller to log scale */ |
| temp = fmControlTable[pChannel->volume]; |
| |
| /* incorporate CC11 expression controller */ |
| temp += fmControlTable[pChannel->expression]; |
| |
| /* saturate */ |
| pChannel->staticGain = (EAS_I16) max(temp,-32768); |
| |
| /* calculate pitch bend */ |
| /*lint -e{703} <avoid multiply for performance>*/ |
| temp = (((EAS_I32)(pChannel->pitchBend) << 2) - 32768); |
| |
| temp = FMUL_15x15(temp, pChannel->pitchBendSensitivity); |
| |
| /* include "magic number" compensation for sample rate and lookup table size */ |
| temp += MAGIC_NUMBER; |
| |
| /* if this is not a drum channel, then add in the per-channel tuning */ |
| if (!(pChannel->channelFlags & CHANNEL_FLAG_RHYTHM_CHANNEL)) |
| temp += (pChannel->finePitch + (pChannel->coarsePitch * 100)); |
| |
| /* save static pitch */ |
| pChannel->staticPitch = temp; |
| |
| /* Calculate LFO modulation depth */ |
| /* mod wheel to LFO depth */ |
| temp = FMUL_15x15(DEFAULT_LFO_MOD_WHEEL_TO_PITCH_CENTS, |
| pChannel->modWheel << (NUM_EG1_FRAC_BITS -7)); |
| |
| /* channel pressure to LFO depth */ |
| pChannel->lfoAmt = (EAS_I16) (temp + |
| FMUL_15x15(DEFAULT_LFO_CHANNEL_PRESSURE_TO_PITCH_CENTS, |
| pChannel->channelPressure << (NUM_EG1_FRAC_BITS -7))); |
| |
| /* clear update flag */ |
| pChannel->channelFlags &= ~CHANNEL_FLAG_UPDATE_CHANNEL_PARAMETERS; |
| return; |
| } |
| |
| /*---------------------------------------------------------------------------- |
| * FM_UpdateLFO() |
| *---------------------------------------------------------------------------- |
| * Purpose: |
| * Calculate the LFO for the given voice |
| * |
| * Inputs: |
| * pVoice - ptr to the voice whose LFO we want to update |
| * psEASData - pointer to overall EAS data structure - used for debug only |
| * |
| * Outputs: |
| * |
| * Side Effects: |
| * - updates LFO values for the given voice |
| *---------------------------------------------------------------------------- |
| */ |
| static void FM_UpdateLFO (S_FM_VOICE *pFMVoice, const S_FM_REGION *pRegion) |
| { |
| |
| /* increment the LFO phase if the delay time has elapsed */ |
| if (!pFMVoice->lfoDelay) |
| { |
| /*lint -e{701} <use shift for performance> */ |
| pFMVoice->lfoPhase = pFMVoice->lfoPhase + (EAS_U16) (-fmControlTable[((15 - (pRegion->lfoFreqDelay >> 4)) << 3) + 4]); |
| |
| /* square wave LFO? */ |
| if (pRegion->region.keyGroupAndFlags & REGION_FLAG_SQUARE_WAVE) |
| pFMVoice->lfoValue = (EAS_I16)(pFMVoice->lfoPhase & 0x8000 ? -32767 : 32767); |
| |
| /* trick to get a triangle wave out of a sawtooth */ |
| else |
| { |
| pFMVoice->lfoValue = (EAS_I16) (pFMVoice->lfoPhase << 1); |
| /*lint -e{502} <shortcut to turn sawtooth into sine wave> */ |
| if ((pFMVoice->lfoPhase > 0x3fff) && (pFMVoice->lfoPhase < 0xC000)) |
| pFMVoice->lfoValue = ~pFMVoice->lfoValue; |
| } |
| } |
| |
| /* still in delay */ |
| else |
| pFMVoice->lfoDelay--; |
| |
| return; |
| } |
| |
| /*---------------------------------------------------------------------------- |
| * FM_UpdateEG() |
| *---------------------------------------------------------------------------- |
| * Purpose: |
| * Calculate the synthesis parameters for an operator to be used during |
| * the next buffer |
| * |
| * Inputs: |
| * pVoice - pointer to the voice being updated |
| * psEASData - pointer to overall EAS data structure |
| * |
| * Outputs: |
| * |
| * Side Effects: |
| * |
| *---------------------------------------------------------------------------- |
| */ |
| static EAS_BOOL FM_UpdateEG (S_SYNTH_VOICE *pVoice, S_OPERATOR *pOper, const S_FM_OPER *pOperData, EAS_BOOL oneShot) |
| { |
| EAS_U32 temp; |
| EAS_BOOL done; |
| |
| /* set flag assuming the envelope is not done */ |
| done = EAS_FALSE; |
| |
| /* take appropriate action based on state */ |
| switch (pOper->envState) |
| { |
| |
| case eFMEnvelopeStateAttack: |
| |
| /* the envelope is linear during the attack, so add the value */ |
| temp = pOper->envGain + fmAttackTable[pOperData->attackDecay >> 4]; |
| |
| /* check for end of attack */ |
| if (temp >= 0x7fff) |
| { |
| pOper->envGain = 0x7fff; |
| pOper->envState = eFMEnvelopeStateDecay; |
| } |
| else |
| pOper->envGain = (EAS_U16) temp; |
| break; |
| |
| case eFMEnvelopeStateDecay: |
| |
| /* decay is exponential, multiply by decay rate */ |
| pOper->envGain = (EAS_U16) FMUL_15x15(pOper->envGain, pOper->envRate); |
| |
| /* check for sustain level reached */ |
| temp = (EAS_U32) (pOperData->sustain & 0xfc) << 7; |
| if (pOper->envGain <= (EAS_U16) temp) |
| { |
| /* if this is a one-shot patch, go directly to release phase */ |
| if (oneShot) |
| { |
| pOper->envRate = FM_CalcEGRate( |
| pVoice->note, |
| fmReleaseTable[pOperData->velocityRelease & 0x0f], |
| fmScaleTable[pOperData->egKeyScale >> 4]); |
| pOper->envState = eFMEnvelopeStateRelease; |
| } |
| |
| /* normal sustaining type */ |
| else |
| { |
| pOper->envGain = (EAS_U16) temp; |
| pOper->envState = eFMEnvelopeStateSustain; |
| } |
| } |
| break; |
| |
| case eFMEnvelopeStateSustain: |
| pOper->envGain = (EAS_U16)((EAS_U16)(pOperData->sustain & 0xfc) << 7); |
| break; |
| |
| case eFMEnvelopeStateRelease: |
| |
| /* release is exponential, multiply by release rate */ |
| pOper->envGain = (EAS_U16) FMUL_15x15(pOper->envGain, pOper->envRate); |
| |
| /* fully released */ |
| if (pOper->envGain == 0) |
| { |
| pOper->envGain = 0; |
| pOper->envState = eFMEnvelopeStateMuted; |
| done = EAS_TRUE; |
| } |
| break; |
| |
| case eFMEnvelopeStateMuted: |
| pOper->envGain = 0; |
| done = EAS_TRUE; |
| break; |
| default: |
| { /* dpp: EAS_ReportEx(_EAS_SEVERITY_FATAL,"Invalid operator state: %d", pOper->envState); */ } |
| } /* end switch (pOper->m_eState) */ |
| |
| return done; |
| } |
| |
| /*---------------------------------------------------------------------------- |
| * FM_UpdateDynamic() |
| *---------------------------------------------------------------------------- |
| * Purpose: |
| * Calculate the synthesis parameters for this voice that will be used to |
| * synthesize the next buffer |
| * |
| * Inputs: |
| * pVoice - pointer to the voice being updated |
| * psEASData - pointer to overall EAS data structure |
| * |
| * Outputs: |
| * Returns EAS_TRUE if voice will be fully ramped to zero at the end of |
| * the next synthesized buffer. |
| * |
| * Side Effects: |
| * |
| *---------------------------------------------------------------------------- |
| */ |
| static EAS_BOOL FM_UpdateDynamic (S_SYNTH_VOICE *pVoice, S_FM_VOICE *pFMVoice, const S_FM_REGION *pRegion, S_SYNTH_CHANNEL *pChannel) |
| { |
| EAS_I32 temp; |
| EAS_I32 pitch; |
| EAS_I32 lfoPitch; |
| EAS_INT operIndex; |
| EAS_BOOL done; |
| |
| /* increment LFO phase */ |
| FM_UpdateLFO(pFMVoice, pRegion); |
| |
| /* base pitch in cents */ |
| pitch = pVoice->note * 100; |
| |
| /* LFO amount includes LFO depth from programming + channel dynamics */ |
| temp = (fmScaleTable[pRegion->vibTrem >> 4] >> 1) + pChannel->lfoAmt; |
| |
| /* multiply by LFO output to get final pitch modulation */ |
| lfoPitch = FMUL_15x15(pFMVoice->lfoValue, temp); |
| |
| /* flag to indicate this voice is done */ |
| done = EAS_TRUE; |
| |
| /* iterate through operators to establish parameters */ |
| for (operIndex = 0; operIndex < 4; operIndex++) |
| { |
| EAS_BOOL bTemp; |
| |
| /* set base phase increment for each operator */ |
| temp = pRegion->oper[operIndex].tuning + |
| pChannel->staticPitch; |
| |
| /* add vibrato effect unless it is disabled for this operator */ |
| if ((pRegion->oper[operIndex].flags & FM_OPER_FLAG_NO_VIBRATO) == 0) |
| temp += lfoPitch; |
| |
| /* if note is monotonic, bias to MIDI note 60 */ |
| if (pRegion->oper[operIndex].flags & FM_OPER_FLAG_MONOTONE) |
| temp += 6000; |
| else |
| temp += pitch; |
| pFMVoice->oper[operIndex].pitch = (EAS_I16) temp; |
| |
| /* calculate envelope, returns true if done */ |
| bTemp = FM_UpdateEG(pVoice, &pFMVoice->oper[operIndex], &pRegion->oper[operIndex], pRegion->region.keyGroupAndFlags & REGION_FLAG_ONE_SHOT); |
| |
| /* check if all output envelopes have completed */ |
| if (FM_SynthIsOutputOperator(pRegion, operIndex)) |
| done = done && bTemp; |
| } |
| |
| return done; |
| } |
| |
| /*---------------------------------------------------------------------------- |
| * FM_UpdateVoice() |
| *---------------------------------------------------------------------------- |
| * Purpose: |
| * Synthesize a block of samples for the given voice. |
| * |
| * Inputs: |
| * psEASData - pointer to overall EAS data structure |
| * |
| * Outputs: |
| * number of samples actually written to buffer |
| * |
| * Side Effects: |
| * - samples are added to the presently free buffer |
| * |
| *---------------------------------------------------------------------------- |
| */ |
| static EAS_BOOL FM_UpdateVoice (S_VOICE_MGR *pVoiceMgr, S_SYNTH *pSynth, S_SYNTH_VOICE *pVoice, EAS_I32 voiceNum, EAS_I32 *pMixBuffer, EAS_I32 numSamples) |
| { |
| S_SYNTH_CHANNEL *pChannel; |
| const S_FM_REGION *pRegion; |
| S_FM_VOICE *pFMVoice; |
| S_FM_VOICE_CONFIG vCfg; |
| S_FM_VOICE_FRAME vFrame; |
| EAS_I32 temp; |
| EAS_INT oper; |
| EAS_U16 voiceGainTarget; |
| EAS_BOOL done; |
| |
| /* setup some pointers */ |
| pChannel = GetChannelPtr(pSynth, pVoice); |
| pRegion = GetFMRegionPtr(pSynth, pVoice); |
| pFMVoice = GetFMVoicePtr(pVoiceMgr, voiceNum); |
| |
| /* if the voice is just starting, get the voice configuration data */ |
| if (pVoice->voiceFlags & VOICE_FLAG_NO_SAMPLES_SYNTHESIZED_YET) |
| { |
| |
| /* split architecture may limit the number of voice starts */ |
| #ifdef MAX_VOICE_STARTS |
| if (pVoiceMgr->numVoiceStarts == MAX_VOICE_STARTS) |
| return EAS_FALSE; |
| pVoiceMgr->numVoiceStarts++; |
| #endif |
| |
| /* get initial parameters */ |
| vCfg.feedback = pRegion->feedback; |
| vCfg.voiceGain = (EAS_U16) pFMVoice->voiceGain; |
| |
| #if (NUM_OUTPUT_CHANNELS == 2) |
| vCfg.pan = pFMVoice->pan; |
| #endif |
| |
| /* get voice mode */ |
| vCfg.flags = pRegion->region.keyGroupAndFlags & 7; |
| |
| /* get operator parameters */ |
| for (oper = 0; oper < 4; oper++) |
| { |
| /* calculate initial gain */ |
| vCfg.gain[oper] = (EAS_U16) FMUL_15x15(pFMVoice->oper[oper].baseGain, pFMVoice->oper[oper].envGain); |
| vCfg.outputGain[oper] = pFMVoice->oper[oper].outputGain; |
| |
| /* copy noise waveform flag */ |
| if (pRegion->oper[oper].flags & FM_OPER_FLAG_NOISE) |
| vCfg.flags |= (EAS_U8) (FLAG_FM_ENG_VOICE_OP1_NOISE << oper); |
| } |
| |
| #ifdef FM_OFFBOARD |
| FM_ConfigVoice(voiceNum, &vCfg, pVoiceMgr->pFrameBuffer); |
| #else |
| FM_ConfigVoice(voiceNum, &vCfg, NULL); |
| #endif |
| |
| /* clear startup flag */ |
| pVoice->voiceFlags &= ~VOICE_FLAG_NO_SAMPLES_SYNTHESIZED_YET; |
| } |
| |
| /* calculate new synthesis parameters */ |
| done = FM_UpdateDynamic(pVoice, pFMVoice, pRegion, pChannel); |
| |
| /* calculate LFO gain modulation */ |
| /*lint -e{702} <use shift for performance> */ |
| temp = ((fmScaleTable[pRegion->vibTrem & 0x0f] >> 1) * pFMVoice->lfoValue) >> FM_LFO_GAIN_SHIFT; |
| |
| /* include channel gain */ |
| temp += pChannel->staticGain; |
| |
| /* -32768 or lower is infinite attenuation */ |
| if (temp < -32767) |
| voiceGainTarget = 0; |
| |
| /* translate to linear gain multiplier */ |
| else |
| voiceGainTarget = EAS_LogToLinear16(temp); |
| |
| /* include synth master volume */ |
| voiceGainTarget = (EAS_U16) FMUL_15x15(voiceGainTarget, pSynth->masterVolume); |
| |
| /* save target values for this frame */ |
| vFrame.voiceGain = voiceGainTarget; |
| |
| /* assume voice output is zero */ |
| pVoice->gain = 0; |
| |
| /* save operator targets for this frame */ |
| for (oper = 0; oper < 4; oper++) |
| { |
| vFrame.gain[oper] = (EAS_U16) FMUL_15x15(pFMVoice->oper[oper].baseGain, pFMVoice->oper[oper].envGain); |
| vFrame.pitch[oper] = pFMVoice->oper[oper].pitch; |
| |
| /* use the highest output envelope level as the gain for the voice stealing algorithm */ |
| if (FM_SynthIsOutputOperator(pRegion, oper)) |
| pVoice->gain = max(pVoice->gain, (EAS_I16) vFrame.gain[oper]); |
| } |
| |
| /* consider voice gain multiplier in calculating gain for stealing algorithm */ |
| pVoice->gain = (EAS_I16) FMUL_15x15(voiceGainTarget, pVoice->gain); |
| |
| /* synthesize samples */ |
| #ifdef FM_OFFBOARD |
| FM_ProcessVoice(voiceNum, &vFrame, numSamples, pVoiceMgr->operMixBuffer, pVoiceMgr->voiceBuffer, pMixBuffer, pVoiceMgr->pFrameBuffer); |
| #else |
| FM_ProcessVoice(voiceNum, &vFrame, numSamples, pVoiceMgr->operMixBuffer, pVoiceMgr->voiceBuffer, pMixBuffer, NULL); |
| #endif |
| |
| return done; |
| } |
| |