ref: e81e24ffbc9efdd4a146b38329cf85c11ca36386
dir: /src/ft2_replayer.c/
// for finding memory leaks in debug mode with Visual Studio #if defined _DEBUG && defined _MSC_VER #include <crtdbg.h> #endif #include <stdint.h> #include <stdio.h> #include <math.h> #include "ft2_header.h" #include "ft2_config.h" #include "ft2_gui.h" #include "ft2_video.h" #include "ft2_pattern_ed.h" #include "ft2_sample_ed.h" #include "ft2_inst_ed.h" #include "ft2_diskop.h" #include "ft2_midi.h" #include "scopes/ft2_scopes.h" #include "ft2_mouse.h" #include "ft2_sample_loader.h" #include "ft2_tables.h" #include "ft2_structs.h" #include "mixer/ft2_cubic_spline.h" #include "mixer/ft2_windowed_sinc.h" static double dLogTab[4*12*16], dExp2MulTab[32]; static bool bxxOverflow; static note_t nilPatternLine[MAX_CHANNELS]; typedef void (*volColumnEfxRoutine)(channel_t *ch); typedef void (*volColumnEfxRoutine2)(channel_t *ch, uint8_t *volColumnData); typedef void (*efxRoutine)(channel_t *ch, uint8_t param); // globally accessed int8_t playMode = 0; bool songPlaying = false, audioPaused = false, musicPaused = false; volatile bool replayerBusy = false; const uint16_t *note2Period = NULL; int16_t patternNumRows[MAX_PATTERNS]; channel_t channel[MAX_CHANNELS]; song_t song; instr_t *instr[128+4]; note_t *pattern[MAX_PATTERNS]; // ---------------------------------- void fixString(char *str, int32_t lastChrPos) // removes leading spaces and 0x1A chars { for (int32_t i = lastChrPos; i >= 0; i--) { if (str[i] == ' ' || str[i] == 0x1A) str[i] = '\0'; else if (str[i] != '\0') break; } str[lastChrPos+1] = '\0'; } void fixSongName(void) { fixString(song.name, 19); } void fixInstrAndSampleNames(int16_t insNum) { fixString(song.instrName[insNum], 21); if (instr[insNum] != NULL) { sample_t *s = instr[insNum]->smp; for (int32_t i = 0; i < MAX_SMP_PER_INST; i++, s++) fixString(s->name, 21); } } void resetReplayerState(void) { song.pattDelTime = song.pattDelTime2 = 0; song.posJumpFlag = false; song.pBreakPos = 0; song.pBreakFlag = false; channel_t *ch = channel; for (int32_t i = 0; i < song.numChannels; i++, ch++) { ch->patternLoopStartRow = 0; ch->patternLoopCounter = 0; } // reset global volume (if song was playing) if (songPlaying) { song.globalVolume = 64; ch = channel; for (int32_t i = 0; i < song.numChannels; i++, ch++) ch->status |= IS_Vol; } } void resetChannels(void) { const bool audioWasntLocked = !audio.locked; if (audioWasntLocked) lockAudio(); memset(channel, 0, sizeof (channel)); channel_t *ch = channel; for (int32_t i = 0; i < MAX_CHANNELS; i++, ch++) { ch->instrPtr = instr[0]; ch->status = IS_Vol; ch->oldPan = 128; ch->outPan = 128; ch->finalPan = 128; ch->channelOff = !editor.chnMode[i]; // set channel mute flag from global mute flag } if (audioWasntLocked) unlockAudio(); } void setSongModifiedFlag(void) { song.isModified = true; editor.updateWindowTitle = true; } void removeSongModifiedFlag(void) { song.isModified = false; editor.updateWindowTitle = true; } // used on external sample load and during sample loading (on some module formats) void tuneSample(sample_t *s, const int32_t midCFreq, bool linearPeriodsFlag) { #define MIN_PERIOD (0) #define MAX_PERIOD (((10*12*16)-1)-1) /* -1 (because of bugged amigaPeriods table values) */ double (*dGetHzFromPeriod)(int32_t) = linearPeriodsFlag ? dLinearPeriod2Hz : dAmigaPeriod2Hz; const uint16_t *periodTab = linearPeriodsFlag ? linearPeriods : amigaPeriods; if (midCFreq <= 0 || periodTab == NULL) { s->finetune = s->relativeNote = 0; return; } // handle frequency boundaries first... if (midCFreq <= (int32_t)dGetHzFromPeriod(periodTab[MIN_PERIOD])) { s->finetune = -128; s->relativeNote = -48; return; } if (midCFreq >= (int32_t)dGetHzFromPeriod(periodTab[MAX_PERIOD])) { s->finetune = 127; s->relativeNote = 71; return; } // check if midCFreq is matching any of the non-finetuned note frequencies (C-0..B-9) for (int8_t i = 0; i < 10*12; i++) { if (midCFreq == (int32_t)dGetHzFromPeriod(periodTab[16 + (i<<4)])) { s->finetune = 0; s->relativeNote = i - NOTE_C4; return; } } // find closest frequency in period table int32_t period = MAX_PERIOD; for (; period >= MIN_PERIOD; period--) { const int32_t curr = (int32_t)dGetHzFromPeriod(periodTab[period]); if (midCFreq == curr) break; if (midCFreq > curr) { const int32_t next = (int32_t)dGetHzFromPeriod(periodTab[period+1]); const int32_t errorCurr = ABS(curr-midCFreq); const int32_t errorNext = ABS(next-midCFreq); if (errorCurr <= errorNext) break; // current is the closest period++; break; // current+1 is the closest } } s->finetune = ((period & 31) - 16) << 3; s->relativeNote = (int8_t)(((period & ~31) >> 4) - NOTE_C4); } void setPatternLen(uint16_t pattNum, int16_t numRows) { assert(pattNum < MAX_PATTERNS); if ((numRows < 1 || numRows > MAX_PATT_LEN) || numRows == patternNumRows[pattNum]) return; const bool audioWasntLocked = !audio.locked; if (audioWasntLocked) lockAudio(); patternNumRows[pattNum] = numRows; if (pattern[pattNum] != NULL) killPatternIfUnused(pattNum); // non-FT2 security song.pattDelTime = 0; song.pattDelTime2 = 0; song.pBreakFlag = false; song.posJumpFlag = false; song.pBreakPos = 0; song.currNumRows = numRows; if (song.row >= song.currNumRows) { song.row = song.currNumRows - 1; editor.row = song.row; } checkMarkLimits(); if (audioWasntLocked) unlockAudio(); ui.updatePatternEditor = true; ui.updatePosSections = true; } int16_t getUsedSamples(int16_t smpNum) { if (instr[smpNum] == NULL) return 0; instr_t *ins = instr[smpNum]; int16_t i = 16 - 1; while (i >= 0 && ins->smp[i].dataPtr == NULL && ins->smp[i].name[0] == '\0') i--; /* Yes, 'i' can be -1 here, and will be set to at least 0 ** because of ins->ta values. Possibly an FT2 bug... */ for (int16_t j = 0; j < 96; j++) { if (ins->note2SampleLUT[j] > i) i = ins->note2SampleLUT[j]; } return i+1; } int16_t getRealUsedSamples(int16_t smpNum) { if (instr[smpNum] == NULL) return 0; int8_t i = 16 - 1; while (i >= 0 && instr[smpNum]->smp[i].dataPtr == NULL) i--; return i+1; } double dLinearPeriod2Hz(int32_t period) { period &= 0xFFFF; // just in case (actual period range is 0..65535) if (period == 0) return 0.0; // in FT2, a period of 0 results in 0Hz const uint32_t invPeriod = ((12 * 192 * 4) - period) & 0xFFFF; // mask needed for FT2 period overflow quirk const uint32_t quotient = invPeriod / (12 * 16 * 4); const uint32_t remainder = invPeriod % (12 * 16 * 4); return dLogTab[remainder] * dExp2MulTab[(14-quotient) & 31]; // x = y >> ((14-quotient) & 31); } double dAmigaPeriod2Hz(int32_t period) { period &= 0xFFFF; // just in case (actual period range is 0..65535) if (period == 0) return 0.0; // in FT2, a period of 0 results in 0Hz return (8363.0 * 1712.0) / period; } double dPeriod2Hz(int32_t period) { return audio.linearPeriodsFlag ? dLinearPeriod2Hz(period) : dAmigaPeriod2Hz(period); } // returns *exact* FT2 C-4 voice rate (depending on finetune, relativeNote and linear/Amiga period mode) double getSampleC4Rate(sample_t *s) { int32_t note = NOTE_C4 + s->relativeNote; if (note < 0) return -1; // shouldn't happen (just in case...) if (note >= (10*12)-1) return -1; // B-9 (after relativeNote calculation) = illegal! (won't play in replayer) const int32_t C4Period = (note << 4) + (((int8_t)s->finetune >> 3) + 16); const int32_t period = audio.linearPeriodsFlag ? linearPeriods[C4Period] : amigaPeriods[C4Period]; return dPeriod2Hz(period); } void setLinearPeriods(bool linearPeriodsFlag) { pauseAudio(); audio.linearPeriodsFlag = linearPeriodsFlag; if (audio.linearPeriodsFlag) note2Period = linearPeriods; else note2Period = amigaPeriods; resumeAudio(); if (ui.configScreenShown && editor.currConfigScreen == CONFIG_SCREEN_AUDIO) { // update "frequency slides" radiobutton, if it's shown setConfigAudioRadioButtonStates(); } // update mid-C freq. in instr. editor (it can slightly differ between Amiga/linear) if (ui.instEditorShown) drawC4Rate(); } static void resetVolumes(channel_t *ch) { ch->realVol = ch->oldVol; ch->outVol = ch->oldVol; ch->outPan = ch->oldPan; ch->status |= IS_Vol + IS_Pan + IS_QuickVol; } static void triggerInstrument(channel_t *ch) { if (!(ch->vibTremCtrl & 0x04)) ch->vibratoPos = 0; if (!(ch->vibTremCtrl & 0x40)) ch->tremoloPos = 0; ch->noteRetrigCounter = 0; ch->tremorPos = 0; ch->keyOff = false; instr_t *ins = ch->instrPtr; if (ins != NULL) // just in case { // reset volume envelope if (ins->volEnvFlags & ENV_ENABLED) { ch->volEnvTick = 65535; // will be increased to 0 on envelope handling ch->volEnvPos = 0; } // reset panning envelope if (ins->panEnvFlags & ENV_ENABLED) { ch->panEnvTick = 65535; // will be increased to 0 on envelope handling ch->panEnvPos = 0; } // reset fadeout ch->fadeoutSpeed = ins->fadeout; // Warning: FT2 doesn't check if fadeout is more than 4095! ch->fadeoutVol = 32768; // reset auto-vibrato if (ins->autoVibDepth > 0) { ch->autoVibPos = 0; if (ins->autoVibSweep > 0) { ch->autoVibAmp = 0; ch->autoVibSweep = (ins->autoVibDepth << 8) / ins->autoVibSweep; } else { ch->autoVibAmp = ins->autoVibDepth << 8; ch->autoVibSweep = 0; } } } } void keyOff(channel_t *ch) { ch->keyOff = true; instr_t *ins = ch->instrPtr; assert(ins != NULL); if (ins->volEnvFlags & ENV_ENABLED) { if (ch->volEnvTick >= (uint16_t)ins->volEnvPoints[ch->volEnvPos][0]) ch->volEnvTick = ins->volEnvPoints[ch->volEnvPos][0] - 1; } else { ch->realVol = 0; ch->outVol = 0; ch->status |= IS_Vol + IS_QuickVol; } if (!(ins->panEnvFlags & ENV_ENABLED)) // great, another FT2 logic bug { if (ch->panEnvTick >= (uint16_t)ins->panEnvPoints[ch->panEnvPos][0]) ch->panEnvTick = ins->panEnvPoints[ch->panEnvPos][0] - 1; } } void calcReplayerLogTab(void) // for linear period -> hz calculation { for (int32_t i = 0; i < 32; i++) dExp2MulTab[i] = 1.0 / exp2(i); // 1/(2^i) for (int32_t i = 0; i < 4*12*16; i++) dLogTab[i] = (8363.0 * 256.0) * exp2(i / (4.0 * 12.0 * 16.0)); } void calcReplayerVars(int32_t audioFreq) { assert(audioFreq > 0); if (audioFreq <= 0) return; audio.dHz2MixDeltaMul = (double)MIXER_FRAC_SCALE / audioFreq; audio.quickVolRampSamples = (uint32_t)round(audioFreq / (double)FT2_QUICKRAMP_SAMPLES); for (int32_t bpm = MIN_BPM; bpm <= MAX_BPM; bpm++) { const int32_t i = bpm - MIN_BPM; // index for tables const double dBpmHz = bpm / 2.5; const double dSamplesPerTick = audioFreq / dBpmHz; double dSamplesPerTickInt; double dSamplesPerTickFrac = modf(dSamplesPerTick, &dSamplesPerTickInt); audio.samplesPerTickIntTab[i] = (uint32_t)dSamplesPerTickInt; audio.samplesPerTickFracTab[i] = (uint64_t)((dSamplesPerTickFrac * BPM_FRAC_SCALE) + 0.5); // rounded // BPM Hz -> tick length for performance counter (syncing visuals to audio) double dTimeInt; double dTimeFrac = modf(editor.dPerfFreq / dBpmHz, &dTimeInt); audio.tickTimeIntTab[i] = (uint32_t)dTimeInt; audio.tickTimeFracTab[i] = (uint64_t)((dTimeFrac * TICK_TIME_FRAC_SCALE) + 0.5); // rounded } } // for piano in Instr. Ed. (values outside 0..95 can happen) int32_t getPianoKey(uint16_t period, int8_t finetune, int8_t relativeNote) { assert(note2Period != NULL); if (period > note2Period[0]) return -1; // outside left piano edge finetune = ((int8_t)finetune >> 3) + 16; // -128..127 -> 0..31 int32_t hiPeriod = 10*12*16; int32_t loPeriod = 0; for (int32_t i = 0; i < 7; i++) { const int32_t tmpPeriod = (((loPeriod + hiPeriod) >> 1) & ~15) + finetune; int32_t lookUp = tmpPeriod - 16; if (lookUp < 0) lookUp = 0; if (period >= note2Period[lookUp]) hiPeriod = (tmpPeriod - finetune) & ~15; else loPeriod = (tmpPeriod - finetune) & ~15; } return (loPeriod >> 4) - relativeNote; } static void triggerNote(uint8_t note, uint8_t efx, uint8_t efxData, channel_t *ch) { if (note == NOTE_OFF) { keyOff(ch); return; } // if we came from Rxy (retrig), we didn't check note (Ton) yet if (note == 0) { note = ch->noteNum; if (note == 0) return; // if still no note, exit from routine } ch->noteNum = note; assert(ch->instrNum <= 130); instr_t *ins = instr[ch->instrNum]; if (ins == NULL) ins = instr[0]; // empty instruments use this placeholder instrument ch->instrPtr = ins; ch->mute = ins->mute; if (note > 96) // non-FT2 sanity check note = 96; ch->smpNum = ins->note2SampleLUT[note-1] & 0xF; // FT2 doesn't mask it, but let's do it anyway sample_t *s = &ins->smp[ch->smpNum]; ch->smpPtr = s; ch->relativeNote = s->relativeNote; note += ch->relativeNote; if (note >= 10*12) // unsigned check (also handles note < 0) return; ch->oldVol = s->volume; ch->oldPan = s->panning; if (efx == 0xE && (efxData & 0xF0) == 0x50) ch->finetune = ((efxData & 0x0F) * 16) - 128; else ch->finetune = s->finetune; if (note != 0) { const uint16_t noteIndex = ((note-1) * 16) + (((int8_t)ch->finetune >> 3) + 16); // 0..1920 assert(note2Period != NULL); ch->outPeriod = ch->realPeriod = note2Period[noteIndex]; } ch->status |= IS_Period + IS_Vol + IS_Pan + IS_Trigger + IS_QuickVol; if (efx == 9) { if (efxData > 0) ch->sampleOffset = ch->efxData; ch->smpStartPos = ch->sampleOffset << 8; } else { ch->smpStartPos = 0; } } static void volSlide(channel_t *ch, uint8_t param); static void doVibrato(channel_t *ch); static void portamento(channel_t *ch, uint8_t param); static void dummy(channel_t *ch, uint8_t param) { (void)ch; (void)param; return; } static void finePitchSlideUp(channel_t *ch, uint8_t param) { if (param == 0) param = ch->fPitchSlideUpSpeed; ch->fPitchSlideUpSpeed = param; ch->realPeriod -= param * 4; if ((int16_t)ch->realPeriod < 1) ch->realPeriod = 1; ch->outPeriod = ch->realPeriod; ch->status |= IS_Period; } static void finePitchSlideDown(channel_t *ch, uint8_t param) { if (param == 0) param = ch->fPitchSlideDownSpeed; ch->fPitchSlideDownSpeed = param; ch->realPeriod += param * 4; if ((int16_t)ch->realPeriod >= 32000) // FT2 bug, should've been unsigned comparison ch->realPeriod = 32000-1; ch->outPeriod = ch->realPeriod; ch->status |= IS_Period; } static void setPortamentoCtrl(channel_t *ch, uint8_t param) { ch->portaSemitoneSlides = (param != 0); } static void setVibratoCtrl(channel_t *ch, uint8_t param) { ch->vibTremCtrl = (ch->vibTremCtrl & 0xF0) | param; } static void patternLoop(channel_t *ch, uint8_t param) { if (param == 0) { ch->patternLoopStartRow = song.row & 0xFF; } else if (ch->patternLoopCounter == 0) { ch->patternLoopCounter = param; song.pBreakPos = ch->patternLoopStartRow; song.pBreakFlag = true; } else if (--ch->patternLoopCounter > 0) { song.pBreakPos = ch->patternLoopStartRow; song.pBreakFlag = true; } } static void setTremoloCtrl(channel_t *ch, uint8_t param) { ch->vibTremCtrl = (param << 4) | (ch->vibTremCtrl & 0x0F); } static void fineVolSlideUp(channel_t *ch, uint8_t param) { if (param == 0) param = ch->fVolSlideUpSpeed; ch->fVolSlideUpSpeed = param; ch->realVol += param; if (ch->realVol > 64) ch->realVol = 64; ch->outVol = ch->realVol; ch->status |= IS_Vol; } static void fineVolFineDown(channel_t *ch, uint8_t param) { if (param == 0) param = ch->fVolSlideDownSpeed; ch->fVolSlideDownSpeed = param; ch->realVol -= param; if ((int8_t)ch->realVol < 0) ch->realVol = 0; ch->outVol = ch->realVol; ch->status |= IS_Vol; } static void noteCut0(channel_t *ch, uint8_t param) { if (param == 0) // only a parameter of zero is handled here { ch->realVol = 0; ch->outVol = 0; ch->status |= IS_Vol + IS_QuickVol; } } static void patternDelay(channel_t *ch, uint8_t param) { if (song.pattDelTime2 == 0) song.pattDelTime = param + 1; (void)ch; } static const efxRoutine EJumpTab_TickZero[16] = { dummy, // 0 finePitchSlideUp, // 1 finePitchSlideDown, // 2 setPortamentoCtrl, // 3 setVibratoCtrl, // 4 dummy, // 5 patternLoop, // 6 setTremoloCtrl, // 7 dummy, // 8 dummy, // 9 fineVolSlideUp, // A fineVolFineDown, // B noteCut0, // C dummy, // D patternDelay, // E dummy // F }; static void E_Effects_TickZero(channel_t *ch, uint8_t param) { const uint8_t efx = param >> 4; param &= 0x0F; if (ch->channelOff) // channel is muted, only handle some E effects { if (efx == 0x6) patternLoop(ch, param); else if (efx == 0xE) patternDelay(ch, param); return; } EJumpTab_TickZero[efx](ch, param); } static void positionJump(channel_t *ch, uint8_t param) { if (playMode != PLAYMODE_PATT && playMode != PLAYMODE_RECPATT) { const int16_t pos = (int16_t)param - 1; if (pos < 0 || pos >= song.songLength) bxxOverflow = true; // non-FT2 security fix... else song.songPos = pos; } song.pBreakPos = 0; song.posJumpFlag = true; (void)ch; } static void patternBreak(channel_t *ch, uint8_t param) { param = ((param >> 4) * 10) + (param & 0x0F); if (param <= 63) song.pBreakPos = param; else song.pBreakPos = 0; song.posJumpFlag = true; (void)ch; } static void setSpeed(channel_t *ch, uint8_t param) { if (param >= 32) { song.BPM = param; setMixerBPM(song.BPM); } else { song.tick = song.speed = param; } (void)ch; } static void setGlobalVolume(channel_t *ch, uint8_t param) { if (param > 64) param = 64; song.globalVolume = param; channel_t *c = channel; for (int32_t i = 0; i < song.numChannels; i++, c++) // update all voice volumes c->status |= IS_Vol; (void)ch; } static void setEnvelopePos(channel_t *ch, uint8_t param) { bool envUpdate; int8_t point; int32_t tick; instr_t *ins = ch->instrPtr; assert(ins != NULL); // envelope precision has been upgraded from .8fp to single-precision float // *** VOLUME ENVELOPE *** if (ins->volEnvFlags & ENV_ENABLED) { ch->volEnvTick = param-1; point = 0; envUpdate = true; tick = param; if (ins->volEnvLength > 1) { point++; for (int32_t i = 0; i < ins->volEnvLength-1; i++) { if (tick < ins->volEnvPoints[point][0]) { point--; tick -= ins->volEnvPoints[point][0]; if (tick == 0) // FT2 doesn't test for <= 0 here { envUpdate = false; break; } const int32_t xDiff = ins->volEnvPoints[point+1][0] - ins->volEnvPoints[point+0][0]; if (xDiff <= 0) { envUpdate = true; break; } const int32_t y0 = ins->volEnvPoints[point+0][1] & 0xFF; const int32_t y1 = ins->volEnvPoints[point+1][1] & 0xFF; const int32_t yDiff = y1 - y0; ch->fVolEnvDelta = (float)yDiff / (float)xDiff; ch->fVolEnvValue = (float)y0 + (ch->fVolEnvDelta * (tick-1)); point++; envUpdate = false; break; } point++; } if (envUpdate) point--; } if (envUpdate) { ch->fVolEnvDelta = 0.0f; ch->fVolEnvValue = (float)(int32_t)(ins->volEnvPoints[point][1] & 0xFF); } if (point >= ins->volEnvLength) { point = ins->volEnvLength-1; if (point < 0) point = 0; } ch->volEnvPos = point; } // *** PANNING ENVELOPE *** if (ins->volEnvFlags & ENV_SUSTAIN) // FT2 REPLAYER BUG: Should've been ins->panEnvFlags { ch->panEnvTick = param-1; point = 0; envUpdate = true; tick = param; if (ins->panEnvLength > 1) { point++; for (int32_t i = 0; i < ins->panEnvLength-1; i++) { if (tick < ins->panEnvPoints[point][0]) { point--; tick -= ins->panEnvPoints[point][0]; if (tick == 0) // FT2 doesn't test for <= 0 here { envUpdate = false; break; } const int32_t xDiff = ins->panEnvPoints[point+1][0] - ins->panEnvPoints[point+0][0]; if (xDiff <= 0) { envUpdate = true; break; } const int32_t y0 = ins->panEnvPoints[point+0][1] & 0xFF; const int32_t y1 = ins->panEnvPoints[point+1][1] & 0xFF; const int32_t yDiff = y1 - y0; ch->fPanEnvDelta = (float)yDiff / (float)xDiff; ch->fPanEnvValue = (float)y0 + (ch->fPanEnvDelta * (tick-1)); point++; envUpdate = false; break; } point++; } if (envUpdate) point--; } if (envUpdate) { ch->fPanEnvDelta = 0.0f; ch->fPanEnvValue = (float)(int32_t)(ins->panEnvPoints[point][1] & 0xFF); } if (point >= ins->panEnvLength) { point = ins->panEnvLength-1; if (point < 0) point = 0; } ch->panEnvPos = point; } } static const efxRoutine JumpTab_TickZero[36] = { dummy, // 0 dummy, // 1 dummy, // 2 dummy, // 3 dummy, // 4 dummy, // 5 dummy, // 6 dummy, // 7 dummy, // 8 dummy, // 9 dummy, // A positionJump, // B dummy, // C patternBreak, // D E_Effects_TickZero, // E setSpeed, // F setGlobalVolume, // G dummy, // H dummy, // I dummy, // J dummy, // K setEnvelopePos, // L dummy, // M dummy, // N dummy, // O dummy, // P dummy, // Q dummy, // R dummy, // S dummy, // T dummy, // U dummy, // V dummy, // W dummy, // X dummy, // Y dummy // Z }; static void handleMoreEffects_TickZero(channel_t *ch) // called even if channel is muted { if (ch->efx > 35) return; JumpTab_TickZero[ch->efx](ch, ch->efxData); } /* -- tick-zero volume column effects -- ** 2nd parameter is used for a volume column quirk with the Rxy command (multiNoteRetrig) */ static void v_SetVibSpeed(channel_t *ch, uint8_t *volColumnData) { *volColumnData = (ch->volColumnVol & 0x0F) * 4; if (*volColumnData != 0) ch->vibratoSpeed = *volColumnData; } static void v_SetVolume(channel_t *ch, uint8_t *volColumnData) { *volColumnData -= 16; if (*volColumnData > 64) // no idea why FT2 has this check... *volColumnData = 64; ch->outVol = ch->realVol = *volColumnData; ch->status |= IS_Vol + IS_QuickVol; } static void v_FineVolSlideDown(channel_t *ch, uint8_t *volColumnData) { *volColumnData = (uint8_t)(0 - (ch->volColumnVol & 0x0F)) + ch->realVol; if ((int8_t)*volColumnData < 0) *volColumnData = 0; ch->outVol = ch->realVol = *volColumnData; ch->status |= IS_Vol; } static void v_FineVolSlideUp(channel_t *ch, uint8_t *volColumnData) { *volColumnData = (ch->volColumnVol & 0x0F) + ch->realVol; if (*volColumnData > 64) *volColumnData = 64; ch->outVol = ch->realVol = *volColumnData; ch->status |= IS_Vol; } static void v_SetPan(channel_t *ch, uint8_t *volColumnData) { *volColumnData <<= 4; ch->outPan = *volColumnData; ch->status |= IS_Pan; } // -- non-tick-zero volume column effects -- static void v_VolSlideDown(channel_t *ch) { uint8_t newVol = (uint8_t)(0 - (ch->volColumnVol & 0x0F)) + ch->realVol; if ((int8_t)newVol < 0) newVol = 0; ch->outVol = ch->realVol = newVol; ch->status |= IS_Vol; } static void v_VolSlideUp(channel_t *ch) { uint8_t newVol = (ch->volColumnVol & 0x0F) + ch->realVol; if (newVol > 64) newVol = 64; ch->outVol = ch->realVol = newVol; ch->status |= IS_Vol; } static void v_Vibrato(channel_t *ch) { const uint8_t param = ch->volColumnVol & 0xF; if (param > 0) ch->vibratoDepth = param; doVibrato(ch); } static void v_PanSlideLeft(channel_t *ch) { uint16_t tmp16 = ch->outPan + (uint8_t)(0 - (ch->volColumnVol & 0x0F)); if (tmp16 < 256) // includes an FT2 bug: pan-slide-left of 0 = set pan to 0 tmp16 = 0; ch->outPan = (uint8_t)tmp16; ch->status |= IS_Pan; } static void v_PanSlideRight(channel_t *ch) { uint16_t tmp16 = ch->outPan + (ch->volColumnVol & 0x0F); if (tmp16 > 255) tmp16 = 255; ch->outPan = (uint8_t)tmp16; ch->status |= IS_Pan; } static void v_Portamento(channel_t *ch) { portamento(ch, 0); // the last parameter is actually not used in portamento() } static void v_dummy(channel_t *ch) { (void)ch; return; } static void v_dummy2(channel_t *ch, uint8_t *volColumnData) { (void)ch; (void)volColumnData; return; } static const volColumnEfxRoutine VJumpTab_TickNonZero[16] = { v_dummy, v_dummy, v_dummy, v_dummy, v_dummy, v_dummy, v_VolSlideDown, v_VolSlideUp, v_dummy, v_dummy, v_dummy, v_Vibrato, v_dummy, v_PanSlideLeft, v_PanSlideRight, v_Portamento }; static const volColumnEfxRoutine2 VJumpTab_TickZero[16] = { v_dummy2, v_SetVolume, v_SetVolume, v_SetVolume, v_SetVolume, v_SetVolume, v_dummy2, v_dummy2, v_FineVolSlideDown, v_FineVolSlideUp, v_SetVibSpeed, v_dummy2, v_SetPan, v_dummy2, v_dummy2, v_dummy2 }; static void setPan(channel_t *ch, uint8_t param) { ch->outPan = param; ch->status |= IS_Pan; } static void setVol(channel_t *ch, uint8_t param) { if (param > 64) param = 64; ch->outVol = ch->realVol = param; ch->status |= IS_Vol + IS_QuickVol; } static void extraFinePitchSlide(channel_t *ch, uint8_t param) { const uint8_t slideType = param >> 4; param &= 0x0F; if (slideType == 1) // slide up { if (param == 0) param = ch->efPitchSlideUpSpeed; ch->efPitchSlideUpSpeed = param; uint16_t newPeriod = ch->realPeriod; newPeriod -= param; if ((int16_t)newPeriod < 1) newPeriod = 1; ch->outPeriod = ch->realPeriod = newPeriod; ch->status |= IS_Period; } else if (slideType == 2) // slide down { if (param == 0) param = ch->efPitchSlideDownSpeed; ch->efPitchSlideDownSpeed = param; uint16_t newPeriod = ch->realPeriod; newPeriod += param; if ((int16_t)newPeriod >= 32000) // FT2 bug, should've been unsigned comparison newPeriod = 32000-1; ch->outPeriod = ch->realPeriod = newPeriod; ch->status |= IS_Period; } } // "param" is never used (needed for efx jumptable structure) static void doMultiNoteRetrig(channel_t *ch, uint8_t param) { uint8_t cnt = ch->noteRetrigCounter + 1; if (cnt < ch->noteRetrigSpeed) { ch->noteRetrigCounter = cnt; return; } ch->noteRetrigCounter = 0; int16_t vol = ch->realVol; switch (ch->noteRetrigVol) { case 0x1: vol -= 1; break; case 0x2: vol -= 2; break; case 0x3: vol -= 4; break; case 0x4: vol -= 8; break; case 0x5: vol -= 16; break; case 0x6: vol = (vol >> 1) + (vol >> 3) + (vol >> 4); break; case 0x7: vol >>= 1; break; case 0x8: break; // does not change the volume case 0x9: vol += 1; break; case 0xA: vol += 2; break; case 0xB: vol += 4; break; case 0xC: vol += 8; break; case 0xD: vol += 16; break; case 0xE: vol = (vol >> 1) + vol; break; case 0xF: vol += vol; break; default: break; } vol = CLAMP(vol, 0, 64); ch->realVol = (uint8_t)vol; ch->outVol = ch->realVol; if (ch->volColumnVol >= 0x10 && ch->volColumnVol <= 0x50) { ch->outVol = ch->volColumnVol - 0x10; ch->realVol = ch->outVol; } else if (ch->volColumnVol >= 0xC0 && ch->volColumnVol <= 0xCF) { ch->outPan = (ch->volColumnVol & 0x0F) << 4; } triggerNote(0, 0, 0, ch); (void)param; } static void multiNoteRetrig(channel_t *ch, uint8_t param, uint8_t volumeColumnData) { uint8_t tmpParam; tmpParam = param & 0x0F; if (tmpParam == 0) tmpParam = ch->noteRetrigSpeed; ch->noteRetrigSpeed = tmpParam; tmpParam = param >> 4; if (tmpParam == 0) tmpParam = ch->noteRetrigVol; ch->noteRetrigVol = tmpParam; if (volumeColumnData == 0) doMultiNoteRetrig(ch, 0); // the second parameter is never used (needed for efx jumptable structure) } static void handleEffects_TickZero(channel_t *ch) { // volume column effects uint8_t newVolCol = ch->volColumnVol; // manipulated by vol. column effects, then used for multiNoteRetrig check (FT2 quirk) VJumpTab_TickZero[ch->volColumnVol >> 4](ch, &newVolCol); // normal effects const uint8_t param = ch->efxData; if (ch->efx == 0 && param == 0) return; // no effect if (ch->efx == 8) setPan(ch, param); else if (ch->efx == 12) setVol(ch, param); else if (ch->efx == 27) multiNoteRetrig(ch, param, newVolCol); else if (ch->efx == 33) extraFinePitchSlide(ch, param); handleMoreEffects_TickZero(ch); } static void preparePortamento(channel_t *ch, const note_t *p, uint8_t inst) { if (p->note > 0) { if (p->note == NOTE_OFF) { keyOff(ch); } else { const uint16_t note = (((p->note-1) + ch->relativeNote) * 16) + (((int8_t)ch->finetune >> 3) + 16); if (note < MAX_NOTES) { assert(note2Period != NULL); ch->portamentoTargetPeriod = note2Period[note]; if (ch->portamentoTargetPeriod == ch->realPeriod) ch->portamentoDirection = 0; else if (ch->portamentoTargetPeriod > ch->realPeriod) ch->portamentoDirection = 1; else ch->portamentoDirection = 2; } } } if (inst > 0) { resetVolumes(ch); if (p->note != NOTE_OFF) triggerInstrument(ch); } } static void getNewNote(channel_t *ch, const note_t *p) { ch->volColumnVol = p->vol; if (ch->efx == 0) { if (ch->efxData > 0) // we have an arpeggio running, set period back { ch->outPeriod = ch->realPeriod; ch->status |= IS_Period; } } else { // if we have a vibrato on previous row (ch) that ends at current row (p), set period back if ((ch->efx == 4 || ch->efx == 6) && (p->efx != 4 && p->efx != 6)) { ch->outPeriod = ch->realPeriod; ch->status |= IS_Period; } } ch->efx = p->efx; ch->efxData = p->efxData; ch->copyOfInstrAndNote = (p->instr << 8) | p->note; if (ch->channelOff) // channel is muted, only handle some effects { handleMoreEffects_TickZero(ch); return; } // 'inst' var is used for later if checks... uint8_t inst = p->instr; if (inst > 0) { if (inst <= MAX_INST) ch->instrNum = inst; else inst = 0; } if (p->efx == 0x0E && p->efxData >= 0xD1 && p->efxData <= 0xDF) return; // we have a note delay (ED1..EDF) // only handly effects here when no E90 (retrigger note, parameter zero) if (p->efx != 0x0E || p->efxData != 0x90) { if ((ch->volColumnVol & 0xF0) == 0xF0) // gxx { const uint8_t param = ch->volColumnVol & 0x0F; if (param > 0) ch->portamentoSpeed = (param << 4) * 4; preparePortamento(ch, p, inst); handleEffects_TickZero(ch); return; } if (p->efx == 3 || p->efx == 5) // 3xx or 5xx { if (p->efx != 5 && p->efxData != 0) ch->portamentoSpeed = p->efxData * 4; preparePortamento(ch, p, inst); handleEffects_TickZero(ch); return; } if (p->efx == 0x14 && p->efxData == 0) // K00 (KeyOff - only handle tick 0 here) { keyOff(ch); if (inst) resetVolumes(ch); handleEffects_TickZero(ch); return; } if (p->note == 0) { if (inst > 0) { resetVolumes(ch); triggerInstrument(ch); } handleEffects_TickZero(ch); return; } } if (p->note == NOTE_OFF) keyOff(ch); else triggerNote(p->note, p->efx, p->efxData, ch); if (inst > 0) { resetVolumes(ch); if (p->note != NOTE_OFF) triggerInstrument(ch); } handleEffects_TickZero(ch); } static void updateVolPanAutoVib(channel_t *ch) { bool envInterpolateFlag, envDidInterpolate; uint8_t envPos; float fEnvVal, fVol; instr_t *ins = ch->instrPtr; assert(ins != NULL); // *** FADEOUT ON KEY OFF *** if (ch->keyOff) { if (ch->fadeoutSpeed > 0) // 0..4095 { ch->fadeoutVol -= ch->fadeoutSpeed; if (ch->fadeoutVol <= 0) { ch->fadeoutVol = 0; ch->fadeoutSpeed = 0; } } ch->status |= IS_Vol; // always update volume, even if fadeout has reached 0 } if (!ch->mute) { // envelope precision has been upgraded from .8fp to single-precision float // *** VOLUME ENVELOPE *** fEnvVal = 0.0f; if (ins->volEnvFlags & ENV_ENABLED) { envDidInterpolate = false; envPos = ch->volEnvPos; ch->volEnvTick++; if (ch->volEnvTick == ins->volEnvPoints[envPos][0]) { ch->fVolEnvValue = (float)(int32_t)(ins->volEnvPoints[envPos][1] & 0xFF); envPos++; if (ins->volEnvFlags & ENV_LOOP) { envPos--; if (envPos == ins->volEnvLoopEnd) { if (!(ins->volEnvFlags & ENV_SUSTAIN) || envPos != ins->volEnvSustain || !ch->keyOff) { envPos = ins->volEnvLoopStart; ch->volEnvTick = ins->volEnvPoints[envPos][0]; ch->fVolEnvValue = (float)(int32_t)(ins->volEnvPoints[envPos][1] & 0xFF); } } envPos++; } if (envPos < ins->volEnvLength) { envInterpolateFlag = true; if ((ins->volEnvFlags & ENV_SUSTAIN) && !ch->keyOff) { if (envPos-1 == ins->volEnvSustain) { envPos--; ch->fVolEnvDelta = 0.0f; envInterpolateFlag = false; } } if (envInterpolateFlag) { ch->volEnvPos = envPos; const int32_t x0 = ins->volEnvPoints[envPos-1][0]; const int32_t x1 = ins->volEnvPoints[envPos-0][0]; const int32_t xDiff = x1 - x0; if (xDiff > 0) { const int32_t y0 = ins->volEnvPoints[envPos-1][1] & 0xFF; const int32_t y1 = ins->volEnvPoints[envPos-0][1] & 0xFF; const int32_t yDiff = y1 - y0; ch->fVolEnvDelta = (float)yDiff / (float)xDiff; fEnvVal = ch->fVolEnvValue; envDidInterpolate = true; } else { ch->fVolEnvDelta = 0.0f; } } } else { ch->fVolEnvDelta = 0.0f; } } if (!envDidInterpolate) { ch->fVolEnvValue += ch->fVolEnvDelta; fEnvVal = ch->fVolEnvValue; if (fEnvVal < 0.0f || fEnvVal > 64.0f) { fEnvVal = CLAMP(fEnvVal, 0.0f, 64.0f); ch->fVolEnvDelta = 0.0f; } } const int32_t vol = song.globalVolume * ch->outVol * ch->fadeoutVol; fVol = vol * (1.0f / (64.0f * 64.0f * 32768.0f)); fVol *= fEnvVal * (1.0f / 64.0f); // volume envelope value ch->status |= IS_Vol; // update mixer vol every tick when vol envelope is enabled } else { const int32_t vol = song.globalVolume * ch->outVol * ch->fadeoutVol; fVol = vol * (1.0f / (64.0f * 64.0f * 32768.0f)); } // FT2 doesn't clamp the volume, but let's do it anyway ch->fFinalVol = CLAMP(fVol, 0.0f, 1.0f); } else { ch->fFinalVol = 0.0f; } // *** PANNING ENVELOPE *** fEnvVal = 0.0f; if (ins->panEnvFlags & ENV_ENABLED) { envDidInterpolate = false; envPos = ch->panEnvPos; ch->panEnvTick++; if (ch->panEnvTick == ins->panEnvPoints[envPos][0]) { ch->fPanEnvValue = (float)(int32_t)(ins->panEnvPoints[envPos][1] & 0xFF); envPos++; if (ins->panEnvFlags & ENV_LOOP) { envPos--; if (envPos == ins->panEnvLoopEnd) { if (!(ins->panEnvFlags & ENV_SUSTAIN) || envPos != ins->panEnvSustain || !ch->keyOff) { envPos = ins->panEnvLoopStart; ch->panEnvTick = ins->panEnvPoints[envPos][0]; ch->fPanEnvValue = (float)(int32_t)(ins->panEnvPoints[envPos][1] & 0xFF); } } envPos++; } if (envPos < ins->panEnvLength) { envInterpolateFlag = true; if ((ins->panEnvFlags & ENV_SUSTAIN) && !ch->keyOff) { if (envPos-1 == ins->panEnvSustain) { envPos--; ch->fPanEnvDelta = 0.0f; envInterpolateFlag = false; } } if (envInterpolateFlag) { ch->panEnvPos = envPos; const int32_t x0 = ins->panEnvPoints[envPos-1][0]; const int32_t x1 = ins->panEnvPoints[envPos-0][0]; const int32_t xDiff = x1 - x0; if (xDiff > 0) { const int32_t y0 = ins->panEnvPoints[envPos-1][1] & 0xFF; const int32_t y1 = ins->panEnvPoints[envPos-0][1] & 0xFF; const int32_t yDiff = y1 - y0; ch->fPanEnvDelta = (float)yDiff / (float)xDiff; fEnvVal = ch->fPanEnvValue; envDidInterpolate = true; } else { ch->fPanEnvDelta = 0.0f; } } } else { ch->fPanEnvDelta = 0.0f; } } if (!envDidInterpolate) { ch->fPanEnvValue += ch->fPanEnvDelta; fEnvVal = ch->fPanEnvValue; if (fEnvVal < 0.0f || fEnvVal > 64.0f) { fEnvVal = CLAMP(fEnvVal, 0.0f, 64.0f); ch->fPanEnvDelta = 0.0f; } } fEnvVal -= 32.0f; // center panning envelope value (0..64 -> -32..32) const int32_t pan = 128 - ABS(ch->outPan - 128); const float fPanAdd = (pan * fEnvVal) * (1.0f / 32.0f); const int32_t newPan = (int32_t)(ch->outPan + fPanAdd); // truncate here, do not round ch->finalPan = (uint8_t)CLAMP(newPan, 0, 255); // FT2 doesn't clamp the pan, but let's do it anyway ch->status |= IS_Pan; // update pan every tick because pan envelope is enabled } else { ch->finalPan = ch->outPan; } // *** AUTO VIBRATO *** #ifdef HAS_MIDI if (ch->midiVibDepth > 0 || ins->autoVibDepth > 0) #else if (ins->autoVibDepth > 0) #endif { uint16_t autoVibAmp; if (ch->autoVibSweep > 0) { autoVibAmp = ch->autoVibSweep; if (!ch->keyOff) { autoVibAmp += ch->autoVibAmp; if ((autoVibAmp >> 8) > ins->autoVibDepth) { autoVibAmp = ins->autoVibDepth << 8; ch->autoVibSweep = 0; } ch->autoVibAmp = autoVibAmp; } } else { autoVibAmp = ch->autoVibAmp; } #ifdef HAS_MIDI // non-FT2 hack to make modulation wheel work when auto vibrato rate is zero if (ch->midiVibDepth > 0 && ins->autoVibRate == 0) ins->autoVibRate = 0x20; autoVibAmp += ch->midiVibDepth; #endif ch->autoVibPos += ins->autoVibRate; int16_t autoVibVal; if (ins->autoVibType == 1) autoVibVal = (ch->autoVibPos > 127) ? 64 : -64; // square else if (ins->autoVibType == 2) autoVibVal = (((ch->autoVibPos >> 1) + 64) & 127) - 64; // ramp up else if (ins->autoVibType == 3) autoVibVal = ((-(ch->autoVibPos >> 1) + 64) & 127) - 64; // ramp down else autoVibVal = autoVibSineTab[ch->autoVibPos]; // sine autoVibVal = (autoVibVal * (int16_t)autoVibAmp) >> (6+8); uint16_t tmpPeriod = ch->outPeriod + autoVibVal; if (tmpPeriod >= 32000) // unsigned comparison tmpPeriod = 0; #ifdef HAS_MIDI if (midi.enable) tmpPeriod -= ch->midiPitch; #endif ch->finalPeriod = tmpPeriod; ch->status |= IS_Period; } else { ch->finalPeriod = ch->outPeriod; #ifdef HAS_MIDI if (midi.enable) { ch->finalPeriod -= ch->midiPitch; ch->status |= IS_Period; } #endif } } // for arpeggio and portamento (semitone-slide mode) static uint16_t adjustPeriodFromNote(uint16_t period, uint8_t arpNote, channel_t *ch) { int32_t tmpPeriod; const int32_t fineTune = ((int8_t)ch->finetune >> 3) + 16; /* FT2 bug, should've been 10*12*16. Notes above B-7 (95) will have issues. ** You can only achieve such high notes by having a high relative note setting. */ int32_t hiPeriod = 8*12*16; int32_t loPeriod = 0; for (int32_t i = 0; i < 8; i++) { tmpPeriod = (((loPeriod + hiPeriod) >> 1) & ~15) + fineTune; int32_t lookUp = tmpPeriod - 8; if (lookUp < 0) lookUp = 0; // safety fix (C-0 w/ f.tune <= -65). This seems to result in 0 in FT2 (TODO: verify) if (period >= note2Period[lookUp]) hiPeriod = (tmpPeriod - fineTune) & ~15; else loPeriod = (tmpPeriod - fineTune) & ~15; } tmpPeriod = loPeriod + fineTune + (arpNote << 4); if (tmpPeriod >= (8*12*16+15)-1) // FT2 bug, should've been 10*12*16+16 (also notice the +2 difference) tmpPeriod = (8*12*16+16)-1; return note2Period[tmpPeriod]; } static void doVibrato(channel_t *ch) { uint8_t tmpVib = (ch->vibratoPos >> 2) & 0x1F; switch (ch->vibTremCtrl & 3) { // 0: sine case 0: tmpVib = vibratoTab[tmpVib]; break; // 1: ramp case 1: { tmpVib <<= 3; if ((int8_t)ch->vibratoPos < 0) tmpVib = ~tmpVib; } break; // 2/3: square default: tmpVib = 255; break; } tmpVib = (tmpVib * ch->vibratoDepth) >> 5; // logical shift (unsigned calc.), not arithmetic shift if ((int8_t)ch->vibratoPos < 0) ch->outPeriod = ch->realPeriod - tmpVib; else ch->outPeriod = ch->realPeriod + tmpVib; ch->status |= IS_Period; ch->vibratoPos += ch->vibratoSpeed; } static void arpeggio(channel_t *ch, uint8_t param) { uint8_t note; const uint8_t tick = arpeggioTab[song.tick & 255]; if (tick == 0) { ch->outPeriod = ch->realPeriod; } else { if (tick == 1) note = param >> 4; else note = param & 0x0F; // tick 2 ch->outPeriod = adjustPeriodFromNote(ch->realPeriod, note, ch); } ch->status |= IS_Period; } static void pitchSlideUp(channel_t *ch, uint8_t param) { if (param == 0) param = ch->pitchSlideUpSpeed; ch->pitchSlideUpSpeed = param; ch->realPeriod -= param * 4; if ((int16_t)ch->realPeriod < 1) ch->realPeriod = 1; ch->outPeriod = ch->realPeriod; ch->status |= IS_Period; } static void pitchSlideDown(channel_t *ch, uint8_t param) { if (param == 0) param = ch->pitchSlideDownSpeed; ch->pitchSlideDownSpeed = param; ch->realPeriod += param * 4; if ((int16_t)ch->realPeriod >= 32000) // FT2 bug, should've been unsigned comparison ch->realPeriod = 32000-1; ch->outPeriod = ch->realPeriod; ch->status |= IS_Period; } static void portamento(channel_t *ch, uint8_t param) { if (ch->portamentoDirection == 0) return; if (ch->portamentoDirection > 1) { ch->realPeriod -= ch->portamentoSpeed; if ((int16_t)ch->realPeriod <= (int16_t)ch->portamentoTargetPeriod) { ch->portamentoDirection = 1; ch->realPeriod = ch->portamentoTargetPeriod; } } else { ch->realPeriod += ch->portamentoSpeed; if (ch->realPeriod >= ch->portamentoTargetPeriod) { ch->portamentoDirection = 1; ch->realPeriod = ch->portamentoTargetPeriod; } } if (ch->portaSemitoneSlides) ch->outPeriod = adjustPeriodFromNote(ch->realPeriod, 0, ch); else ch->outPeriod = ch->realPeriod; ch->status |= IS_Period; (void)param; } static void vibrato(channel_t *ch, uint8_t param) { if (param > 0) { const uint8_t vibratoDepth = param & 0x0F; if (vibratoDepth > 0) ch->vibratoDepth = vibratoDepth; const uint8_t vibratoSpeed = (param & 0xF0) >> 2; if (vibratoSpeed > 0) ch->vibratoSpeed = vibratoSpeed; } doVibrato(ch); } static void portamentoPlusVolSlide(channel_t *ch, uint8_t param) { portamento(ch, 0); // the last parameter is actually not used in portamento() volSlide(ch, param); (void)param; } static void vibratoPlusVolSlide(channel_t *ch, uint8_t param) { doVibrato(ch); volSlide(ch, param); (void)param; } static void tremolo(channel_t *ch, uint8_t param) { int16_t tremVol; if (param > 0) { uint8_t tremoloDepth = param & 0x0F; if (tremoloDepth > 0) ch->tremoloDepth = tremoloDepth; uint8_t tremoloSpeed = (param & 0xF0) >> 2; if (tremoloSpeed > 0) ch->tremoloSpeed = tremoloSpeed; } uint8_t tmpTrem = (ch->tremoloPos >> 2) & 0x1F; switch ((ch->vibTremCtrl >> 4) & 3) { // 0: sine case 0: tmpTrem = vibratoTab[tmpTrem]; break; // 1: ramp case 1: { tmpTrem <<= 3; if ((int8_t)ch->vibratoPos < 0) // FT2 bug, should've been ch->tremoloPos tmpTrem = ~tmpTrem; } break; // 2/3: square default: tmpTrem = 255; break; } tmpTrem = (tmpTrem * ch->tremoloDepth) >> 6; // logical shift (unsigned calc.), not arithmetic shift if ((int8_t)ch->tremoloPos < 0) { tremVol = ch->realVol - tmpTrem; if (tremVol < 0) tremVol = 0; } else { tremVol = ch->realVol + tmpTrem; if (tremVol > 64) tremVol = 64; } ch->outVol = (uint8_t)tremVol; ch->status |= IS_Vol; ch->tremoloPos += ch->tremoloSpeed; } static void volSlide(channel_t *ch, uint8_t param) { if (param == 0) param = ch->volSlideSpeed; ch->volSlideSpeed = param; uint8_t newVol = ch->realVol; if ((param & 0xF0) == 0) { newVol -= param; if ((int8_t)newVol < 0) newVol = 0; } else { param >>= 4; newVol += param; if (newVol > 64) newVol = 64; } ch->outVol = ch->realVol = newVol; ch->status |= IS_Vol; } static void globalVolSlide(channel_t *ch, uint8_t param) { if (param == 0) param = ch->globVolSlideSpeed; ch->globVolSlideSpeed = param; uint8_t newVol = (uint8_t)song.globalVolume; if ((param & 0xF0) == 0) { newVol -= param; if ((int8_t)newVol < 0) newVol = 0; } else { param >>= 4; newVol += param; if (newVol > 64) newVol = 64; } song.globalVolume = newVol; channel_t *c = channel; for (int32_t i = 0; i < song.numChannels; i++, c++) // update all voice volumes c->status |= IS_Vol; } static void keyOffCmd(channel_t *ch, uint8_t param) { if ((uint8_t)(song.speed-song.tick) == (param & 31)) keyOff(ch); } static void panningSlide(channel_t *ch, uint8_t param) { if (param == 0) param = ch->panningSlideSpeed; ch->panningSlideSpeed = param; int16_t newPan = (int16_t)ch->outPan; if ((param & 0xF0) == 0) { newPan -= param; if (newPan < 0) newPan = 0; } else { param >>= 4; newPan += param; if (newPan > 255) newPan = 255; } ch->outPan = (uint8_t)newPan; ch->status |= IS_Pan; } static void tremor(channel_t *ch, uint8_t param) { if (param == 0) param = ch->tremorParam; ch->tremorParam = param; uint8_t tremorSign = ch->tremorPos & 0x80; uint8_t tremorData = ch->tremorPos & 0x7F; tremorData--; if ((int8_t)tremorData < 0) { if (tremorSign == 0x80) { tremorSign = 0x00; tremorData = param & 0x0F; } else { tremorSign = 0x80; tremorData = param >> 4; } } ch->tremorPos = tremorSign | tremorData; ch->outVol = (tremorSign == 0x80) ? ch->realVol : 0; ch->status |= IS_Vol + IS_QuickVol; } static void retrigNote(channel_t *ch, uint8_t param) { if (param == 0) // E9x with a param of zero is handled in getNewNote() return; if ((song.speed-song.tick) % param == 0) { triggerNote(0, 0, 0, ch); triggerInstrument(ch); } } static void noteCut(channel_t *ch, uint8_t param) { if ((uint8_t)(song.speed-song.tick) == param) { ch->outVol = ch->realVol = 0; ch->status |= IS_Vol + IS_QuickVol; } } static void noteDelay(channel_t *ch, uint8_t param) { if ((uint8_t)(song.speed-song.tick) == param) { const uint8_t note = ch->copyOfInstrAndNote & 0x00FF; triggerNote(note, 0, 0, ch); const uint8_t instrument = ch->copyOfInstrAndNote >> 8; if (instrument > 0) resetVolumes(ch); triggerInstrument(ch); if (ch->volColumnVol >= 0x10 && ch->volColumnVol <= 0x50) { ch->outVol = ch->volColumnVol - 16; ch->realVol = ch->outVol; } else if (ch->volColumnVol >= 0xC0 && ch->volColumnVol <= 0xCF) { ch->outPan = (ch->volColumnVol & 0x0F) << 4; } } } static const efxRoutine EJumpTab_TickNonZero[16] = { dummy, // 0 dummy, // 1 dummy, // 2 dummy, // 3 dummy, // 4 dummy, // 5 dummy, // 6 dummy, // 7 dummy, // 8 retrigNote, // 9 dummy, // A dummy, // B noteCut, // C noteDelay, // D dummy, // E dummy // F }; static void E_Effects_TickNonZero(channel_t *ch, uint8_t param) { EJumpTab_TickNonZero[param >> 4](ch, param & 0xF); } static const efxRoutine JumpTab_TickNonZero[36] = { arpeggio, // 0 pitchSlideUp, // 1 pitchSlideDown, // 2 portamento, // 3 vibrato, // 4 portamentoPlusVolSlide, // 5 vibratoPlusVolSlide, // 6 tremolo, // 7 dummy, // 8 dummy, // 9 volSlide, // A dummy, // B dummy, // C dummy, // D E_Effects_TickNonZero, // E dummy, // F dummy, // G globalVolSlide, // H dummy, // I dummy, // J keyOffCmd, // K dummy, // L dummy, // M dummy, // N dummy, // O panningSlide, // P dummy, // Q doMultiNoteRetrig, // R dummy, // S tremor, // T dummy, // U dummy, // V dummy, // W dummy, // X dummy, // Y dummy // Z }; static void handleEffects_TickNonZero(channel_t *ch) { if (ch->channelOff) return; // muted // volume column effects VJumpTab_TickNonZero[ch->volColumnVol >> 4](ch); // normal effects if ((ch->efx == 0 && ch->efxData == 0) || ch->efx > 35) return; // no effect JumpTab_TickNonZero[ch->efx](ch, ch->efxData); } static void getNextPos(void) { if (song.tick != 1) return; song.row++; if (song.pattDelTime > 0) { song.pattDelTime2 = song.pattDelTime; song.pattDelTime = 0; } if (song.pattDelTime2 > 0) { song.pattDelTime2--; if (song.pattDelTime2 > 0) song.row--; } if (song.pBreakFlag) { song.pBreakFlag = false; song.row = song.pBreakPos; } if (song.row >= song.currNumRows || song.posJumpFlag) { song.row = song.pBreakPos; song.pBreakPos = 0; song.posJumpFlag = false; if (playMode != PLAYMODE_PATT && playMode != PLAYMODE_RECPATT) { if (bxxOverflow) { song.songPos = 0; bxxOverflow = false; } else if (++song.songPos >= song.songLength) { editor.wavReachedEndFlag = true; song.songPos = song.songLoopStart; } assert(song.songPos <= 255); song.pattNum = song.orders[song.songPos & 0xFF]; song.currNumRows = patternNumRows[song.pattNum & 0xFF]; } /* ** Because of a bug in FT2, pattern loop commands will manipulate ** the row the next pattern will begin at (should be 0). ** However, this can overflow the number of rows (length) for that ** pattern and cause out-of-bounds reads. Set to row 0 in this case. */ if (song.row >= song.currNumRows) song.row = 0; } } void pauseMusic(void) // stops reading pattern data { musicPaused = true; while (replayerBusy); } void resumeMusic(void) // starts reading pattern data { musicPaused = false; } void tickReplayer(void) // periodically called from audio callback { int32_t i; channel_t *ch; if (!songPlaying) { ch = channel; for (i = 0; i < song.numChannels; i++, ch++) updateVolPanAutoVib(ch); return; } // for song playback counter (hh:mm:ss) if (song.BPM >= MIN_BPM && song.BPM <= MAX_BPM) { song.playbackSecondsFrac += musicTimeTab52[song.BPM-MIN_BPM]; if (song.playbackSecondsFrac >= 1ULL << 52) { song.playbackSecondsFrac &= (1ULL << 52)-1; song.playbackSeconds++; } } bool tickZero = false; if (--song.tick == 0) { song.tick = song.speed; tickZero = true; } song.curReplayerTick = (uint8_t)song.tick; // for audio/video syncing (and recording) const bool readNewNote = tickZero && (song.pattDelTime2 == 0); if (readNewNote) { // set audio/video syncing variables song.curReplayerRow = (uint8_t)song.row; song.curReplayerPattNum = (uint8_t)song.pattNum; song.curReplayerSongPos = (uint8_t)song.songPos; // ---------------------------------------------- const note_t *p = nilPatternLine; if (pattern[song.pattNum] != NULL) p = &pattern[song.pattNum][song.row * MAX_CHANNELS]; ch = channel; for (i = 0; i < song.numChannels; i++, ch++, p++) { getNewNote(ch, p); updateVolPanAutoVib(ch); } } else { ch = channel; for (i = 0; i < song.numChannels; i++, ch++) { handleEffects_TickNonZero(ch); updateVolPanAutoVib(ch); } } getNextPos(); } void resetMusic(void) { const bool audioWasntLocked = !audio.locked; if (audioWasntLocked) lockAudio(); song.tick = 1; stopVoices(); if (audioWasntLocked) unlockAudio(); setPos(0, 0, false); if (!songPlaying) { setScrollBarEnd(SB_POS_ED, (song.songLength - 1) + 5); setScrollBarPos(SB_POS_ED, 0, false); } } void setPos(int16_t songPos, int16_t row, bool resetTimer) { const bool audioWasntLocked = !audio.locked; if (audioWasntLocked) lockAudio(); if (songPos > -1) { song.songPos = songPos; if (song.songLength > 0 && song.songPos >= song.songLength) song.songPos = song.songLength - 1; song.pattNum = song.orders[song.songPos]; assert(song.pattNum < MAX_PATTERNS); song.currNumRows = patternNumRows[song.pattNum]; checkMarkLimits(); // non-FT2 safety } if (row > -1) { song.row = row; if (song.row >= song.currNumRows) song.row = song.currNumRows-1; } // if not playing, update local position variables if (!songPlaying) { if (row > -1) { editor.row = (uint8_t)row; ui.updatePatternEditor = true; } if (songPos > -1) { editor.editPattern = (uint8_t)song.pattNum; editor.songPos = song.songPos; ui.updatePosSections = true; } } if (resetTimer) song.tick = 1; if (audioWasntLocked) unlockAudio(); } void delta2Samp(int8_t *p, int32_t length, uint8_t smpFlags) { bool sample16Bit = !!(smpFlags & SAMPLE_16BIT); bool stereo = !!(smpFlags & SAMPLE_STEREO); if (stereo) { length >>= 1; if (sample16Bit) { int16_t *p16L = (int16_t *)p; int16_t *p16R = (int16_t *)p + length; int16_t olds16L = 0; int16_t olds16R = 0; for (int32_t i = 0; i < length; i++) { const int16_t news16L = p16L[i] + olds16L; p16L[i] = news16L; olds16L = news16L; const int16_t news16R = p16R[i] + olds16R; p16R[i] = news16R; olds16R = news16R; p16L[i] = (int16_t)((olds16L + olds16R) >> 1); } } else // 8-bit { int8_t *p8L = (int8_t *)p; int8_t *p8R = (int8_t *)p + length; int8_t olds8L = 0; int8_t olds8R = 0; for (int32_t i = 0; i < length; i++) { const int8_t news8L = p8L[i] + olds8L; p8L[i] = news8L; olds8L = news8L; const int8_t news8R = p8R[i] + olds8R; p8R[i] = news8R; olds8R = news8R; p8L[i] = (int8_t)((olds8L + olds8R) >> 1); } } } else // mono (normal sample) { if (sample16Bit) { int16_t *p16 = (int16_t *)p; int16_t olds16L = 0; for (int32_t i = 0; i < length; i++) { const int16_t news16 = p16[i] + olds16L; p16[i] = news16; olds16L = news16; } } else // 8-bit { int8_t *p8 = (int8_t *)p; int8_t olds8L = 0; for (int32_t i = 0; i < length; i++) { const int8_t news8 = p8[i] + olds8L; p8[i] = news8; olds8L = news8; } } } } void samp2Delta(int8_t *p, int32_t length, uint8_t smpFlags) { if (smpFlags & SAMPLE_16BIT) { int16_t *p16 = (int16_t *)p; int16_t newS16 = 0; for (int32_t i = 0; i < length; i++) { const int16_t oldS16 = p16[i]; p16[i] -= newS16; newS16 = oldS16; } } else // 8-bit { int8_t *p8 = (int8_t *)p; int8_t newS8 = 0; for (int32_t i = 0; i < length; i++) { const int8_t oldS8 = p8[i]; p8[i] -= newS8; newS8 = oldS8; } } } bool allocateInstr(int16_t insNum) { if (instr[insNum] != NULL) return false; // already allocated instr_t *p = (instr_t *)malloc(sizeof (instr_t)); if (p == NULL) return false; memset(p, 0, sizeof (instr_t)); sample_t *s = p->smp; for (int32_t i = 0; i < MAX_SMP_PER_INST; i++, s++) { s->panning = 128; s->volume = 64; } setStdEnvelope(p, 0, 3); const bool audioWasntLocked = !audio.locked; if (audioWasntLocked) lockAudio(); instr[insNum] = p; if (audioWasntLocked) unlockAudio(); return true; } void freeInstr(int32_t insNum) { if (instr[insNum] == NULL) return; // not allocated pauseAudio(); // channel instrument pointers are now cleared sample_t *s = instr[insNum]->smp; for (int32_t i = 0; i < MAX_SMP_PER_INST; i++, s++) // free sample data freeSmpData(s); free(instr[insNum]); instr[insNum] = NULL; resumeAudio(); } void freeAllInstr(void) { pauseAudio(); // channel instrument pointers are now cleared for (int32_t i = 1; i <= MAX_INST; i++) { if (instr[i] != NULL) { sample_t *s = instr[i]->smp; for (int32_t j = 0; j < MAX_SMP_PER_INST; j++, s++) // free sample data freeSmpData(s); free(instr[i]); instr[i] = NULL; } } resumeAudio(); } void freeSample(int16_t insNum, int16_t smpNum) { if (instr[insNum] == NULL) return; // instrument not allocated pauseAudio(); // voice sample pointers are now cleared sample_t *s = &instr[insNum]->smp[smpNum]; freeSmpData(s); memset(s, 0, sizeof (sample_t)); s->panning = 128; s->volume = 64; resumeAudio(); } void freeAllPatterns(void) { pauseAudio(); for (int32_t i = 0; i < MAX_PATTERNS; i++) { if (pattern[i] != NULL) { free(pattern[i]); pattern[i] = NULL; } } resumeAudio(); } void setStdEnvelope(instr_t *ins, int16_t i, uint8_t type) { if (ins == NULL) return; pauseMusic(); if (type & 1) { memcpy(ins->volEnvPoints, config.stdEnvPoints[i][0], 2*2*12); ins->volEnvLength = (uint8_t)config.stdVolEnvLength[i]; ins->volEnvSustain = (uint8_t)config.stdVolEnvSustain[i]; ins->volEnvLoopStart = (uint8_t)config.stdVolEnvLoopStart[i]; ins->volEnvLoopEnd = (uint8_t)config.stdVolEnvLoopEnd[i]; ins->fadeout = config.stdFadeout[i]; ins->autoVibRate = (uint8_t)config.stdVibRate[i]; ins->autoVibDepth = (uint8_t)config.stdVibDepth[i]; ins->autoVibSweep = (uint8_t)config.stdVibSweep[i]; ins->autoVibType = (uint8_t)config.stdVibType[i]; ins->volEnvFlags = (uint8_t)config.stdVolEnvFlags[i]; } if (type & 2) { memcpy(ins->panEnvPoints, config.stdEnvPoints[i][1], 2*2*12); ins->panEnvLength = (uint8_t)config.stdPanEnvLength[0]; ins->panEnvSustain = (uint8_t)config.stdPanEnvSustain[0]; ins->panEnvLoopStart = (uint8_t)config.stdPanEnvLoopStart[0]; ins->panEnvLoopEnd = (uint8_t)config.stdPanEnvLoopEnd[0]; ins->panEnvFlags = (uint8_t)config.stdPanEnvFlags[0]; } resumeMusic(); } void setNoEnvelope(instr_t *ins) { if (ins == NULL) return; pauseMusic(); memcpy(ins->volEnvPoints, config.stdEnvPoints[0][0], 2*2*12); ins->volEnvLength = (uint8_t)config.stdVolEnvLength[0]; ins->volEnvSustain = (uint8_t)config.stdVolEnvSustain[0]; ins->volEnvLoopStart = (uint8_t)config.stdVolEnvLoopStart[0]; ins->volEnvLoopEnd = (uint8_t)config.stdVolEnvLoopEnd[0]; ins->volEnvFlags = 0; memcpy(ins->panEnvPoints, config.stdEnvPoints[0][1], 2*2*12); ins->panEnvLength = (uint8_t)config.stdPanEnvLength[0]; ins->panEnvSustain = (uint8_t)config.stdPanEnvSustain[0]; ins->panEnvLoopStart = (uint8_t)config.stdPanEnvLoopStart[0]; ins->panEnvLoopEnd = (uint8_t)config.stdPanEnvLoopEnd[0]; ins->panEnvFlags = 0; ins->fadeout = 0; ins->autoVibRate = 0; ins->autoVibDepth = 0; ins->autoVibSweep = 0; ins->autoVibType = 0; resumeMusic(); } bool patternEmpty(uint16_t pattNum) { if (pattern[pattNum] == NULL) return true; const uint8_t *scanPtr = (const uint8_t *)pattern[pattNum]; const uint32_t scanLen = patternNumRows[pattNum] * TRACK_WIDTH; for (uint32_t i = 0; i < scanLen; i++) { if (scanPtr[i] != 0) return false; } return true; } void updateChanNums(void) { assert(!(song.numChannels & 1)); const int32_t maxChannelsShown = getMaxVisibleChannels(); int32_t channelsShown = song.numChannels; if (channelsShown > maxChannelsShown) channelsShown = maxChannelsShown; ui.numChannelsShown = (uint8_t)channelsShown; ui.pattChanScrollShown = (song.numChannels > maxChannelsShown); if (ui.patternEditorShown) { if (ui.channelOffset > song.numChannels-ui.numChannelsShown) setScrollBarPos(SB_CHAN_SCROLL, song.numChannels - ui.numChannelsShown, true); } if (ui.pattChanScrollShown) { if (ui.patternEditorShown) { showScrollBar(SB_CHAN_SCROLL); showPushButton(PB_CHAN_SCROLL_LEFT); showPushButton(PB_CHAN_SCROLL_RIGHT); } setScrollBarEnd(SB_CHAN_SCROLL, song.numChannels); setScrollBarPageLength(SB_CHAN_SCROLL, ui.numChannelsShown); } else { hideScrollBar(SB_CHAN_SCROLL); hidePushButton(PB_CHAN_SCROLL_LEFT); hidePushButton(PB_CHAN_SCROLL_RIGHT); setScrollBarPos(SB_CHAN_SCROLL, 0, false); ui.channelOffset = 0; } if (cursor.ch >= ui.channelOffset+ui.numChannelsShown) cursor.ch = ui.channelOffset+ui.numChannelsShown - 1; } void conv8BitSample(int8_t *p, int32_t length, bool stereo) // changes sample sign { if (stereo) { length >>= 1; int8_t *p2 = &p[length]; for (int32_t i = 0; i < length; i++) { const int8_t l = p[i] ^ 0x80; const int8_t r = p2[i] ^ 0x80; p[i] = (int8_t)((l + r) >> 1); } } else { for (int32_t i = 0; i < length; i++) p[i] ^= 0x80; } } void conv16BitSample(int8_t *p, int32_t length, bool stereo) // changes sample sign { int16_t *p16_1 = (int16_t *)p; if (stereo) { length >>= 1; int16_t *p16_2 = (int16_t *)p + length; for (int32_t i = 0; i < length; i++) { const int16_t l = p16_1[i] ^ 0x8000; const int16_t r = p16_2[i] ^ 0x8000; p16_1[i] = (int16_t)((l + r) >> 1); } } else { for (int32_t i = 0; i < length; i++) p16_1[i] ^= 0x8000; } } void closeReplayer(void) { freeAllInstr(); freeAllPatterns(); // free reserved instruments if (instr[0] != NULL) { free(instr[0]); instr[0] = NULL; } if (instr[130] != NULL) { free(instr[130]); instr[130] = NULL; } if (instr[131] != NULL) { free(instr[131]); instr[131] = NULL; } freeCubicSplineTable(); freeWindowedSincTables(); } bool setupReplayer(void) { for (int32_t i = 0; i < MAX_PATTERNS; i++) patternNumRows[i] = 64; playMode = PLAYMODE_IDLE; songPlaying = false; // unmute all channels (must be done before resetChannels() call) for (int32_t i = 0; i < MAX_CHANNELS; i++) editor.chnMode[i] = 1; resetChannels(); song.songLength = 1; song.numChannels = 8; editor.BPM = song.BPM = 125; editor.speed = song.initialSpeed = song.speed = 6; editor.globalVolume = song.globalVolume = 64; audio.linearPeriodsFlag = true; note2Period = linearPeriods; calcPanningTable(); setPos(0, 0, true); // important! if (!allocateInstr(0)) { showErrorMsgBox("Not enough memory!"); return false; } instr[0]->smp[0].volume = 0; if (!allocateInstr(130)) { showErrorMsgBox("Not enough memory!"); return false; } memset(instr[130], 0, sizeof (instr_t)); if (!allocateInstr(131)) // Instr. Ed. display instrument for unallocated/empty instruments { showErrorMsgBox("Not enough memory!"); return false; } memset(instr[131], 0, sizeof (instr_t)); for (int32_t i = 0; i < 16; i++) instr[131]->smp[i].panning = 128; editor.tmpPattern = 65535; // pattern editor update/redraw kludge return true; } void startPlaying(int8_t mode, int16_t row) { lockMixerCallback(); assert(mode != PLAYMODE_IDLE && mode != PLAYMODE_EDIT); if (mode == PLAYMODE_PATT || mode == PLAYMODE_RECPATT) setPos(-1, row, true); else setPos(editor.songPos, row, true); playMode = mode; songPlaying = true; resetReplayerState(); resetPlaybackTime(); // non-FT2 fix: If song speed was 0, set it back to initial speed on play if (song.speed == 0) song.speed = song.initialSpeed; // zero tick sample counter so that it will instantly initiate a tick audio.tickSampleCounterFrac = audio.tickSampleCounter = 0; unlockMixerCallback(); ui.updatePosSections = true; ui.updatePatternEditor = true; } void stopPlaying(void) { bool songWasPlaying = songPlaying; playMode = PLAYMODE_IDLE; songPlaying = false; if (config.killNotesOnStopPlay) { // safely kills all voices lockMixerCallback(); unlockMixerCallback(); } else { for (uint8_t i = 0; i < MAX_CHANNELS; i++) playTone(i, 0, NOTE_OFF, -1, 0, 0); } // if song was playing, update local row (fixes certain glitches) if (songWasPlaying) editor.row = song.row; #ifdef HAS_MIDI midi.currMIDIVibDepth = 0; midi.currMIDIPitch = 0; #endif memset(editor.keyOnTab, 0, sizeof (editor.keyOnTab)); // kludge to prevent UI from being out of sync with replayer vars on stop song.songPos = editor.songPos; song.row = editor.row; song.pattNum = editor.editPattern; ui.updatePosSections = true; ui.updatePatternEditor = true; // certain non-FT2 fixes song.tick = editor.tick = 1; song.globalVolume = editor.globalVolume = 64; ui.drawGlobVolFlag = true; } // from keyboard/smp. ed. void playTone(uint8_t chNum, uint8_t insNum, uint8_t note, int8_t vol, uint16_t midiVibDepth, uint16_t midiPitch) { instr_t *ins = instr[insNum]; if (ins == NULL) return; assert(chNum < MAX_CHANNELS && insNum <= MAX_INST && note <= NOTE_OFF); channel_t *ch = &channel[chNum]; // FT2 bugfix: Don't play tone if certain requirements are not met if (note != NOTE_OFF) { if (note == 0 || note > 96) return; sample_t *s = &ins->smp[ins->note2SampleLUT[note-1] & 0xF]; int16_t finalNote = (int16_t)note + s->relativeNote; if (s->dataPtr == NULL || s->length == 0 || finalNote <= 0 || finalNote >= 12*10) return; } // ------------------- lockAudio(); if (insNum != 0 && note != NOTE_OFF) { ch->copyOfInstrAndNote = (insNum << 8) | (ch->copyOfInstrAndNote & 0xFF); ch->instrNum = insNum; } ch->copyOfInstrAndNote = (ch->copyOfInstrAndNote & 0xFF00) | note; ch->efx = 0; ch->efxData = 0; triggerNote(note, 0, 0, ch); if (note != NOTE_OFF) { resetVolumes(ch); triggerInstrument(ch); if (vol != -1) // if jamming note keys, vol -1 = use sample's volume { ch->realVol = vol; ch->outVol = vol; ch->oldVol = vol; } } ch->midiVibDepth = midiVibDepth; ch->midiPitch = midiPitch; updateVolPanAutoVib(ch); unlockAudio(); } // smp. ed. void playSample(uint8_t chNum, uint8_t insNum, uint8_t smpNum, uint8_t note, uint16_t midiVibDepth, uint16_t midiPitch) { if (instr[insNum] == NULL) return; // for sampling playback line in Smp. Ed. lastChInstr[chNum].instrNum = 255; lastChInstr[chNum].smpNum = 255; editor.curPlayInstr = 255; editor.curPlaySmp = 255; assert(chNum < MAX_CHANNELS && insNum <= MAX_INST && smpNum < MAX_SMP_PER_INST && note <= NOTE_OFF); channel_t *ch = &channel[chNum]; memcpy(&instr[130]->smp[0], &instr[insNum]->smp[smpNum], sizeof (sample_t)); uint8_t vol = instr[insNum]->smp[smpNum].volume; lockAudio(); ch->instrNum = 130; ch->copyOfInstrAndNote = (ch->instrNum << 8) | note; ch->efx = 0; triggerNote(note, 0, 0, ch); if (note != NOTE_OFF) { resetVolumes(ch); triggerInstrument(ch); ch->realVol = vol; ch->outVol = vol; ch->oldVol = vol; } ch->midiVibDepth = midiVibDepth; ch->midiPitch = midiPitch; updateVolPanAutoVib(ch); unlockAudio(); while (ch->status & IS_Trigger); // wait for sample to latch in mixer // for sampling playback line in Smp. Ed. editor.curPlayInstr = editor.curInstr; editor.curPlaySmp = editor.curSmp; } // smp. ed. void playRange(uint8_t chNum, uint8_t insNum, uint8_t smpNum, uint8_t note, uint16_t midiVibDepth, uint16_t midiPitch, int32_t smpOffset, int32_t length) { if (instr[insNum] == NULL) return; // for sampling playback line in Smp. Ed. lastChInstr[chNum].instrNum = 255; lastChInstr[chNum].smpNum = 255; editor.curPlayInstr = 255; editor.curPlaySmp = 255; assert(chNum < MAX_CHANNELS && insNum <= MAX_INST && smpNum < MAX_SMP_PER_INST && note <= NOTE_OFF); channel_t *ch = &channel[chNum]; sample_t *s = &instr[130]->smp[0]; memcpy(s, &instr[insNum]->smp[smpNum], sizeof (sample_t)); uint8_t vol = instr[insNum]->smp[smpNum].volume; lockAudio(); s->length = smpOffset + length; s->loopStart = 0; s->loopLength = 0; DISABLE_LOOP(s->flags); // disable loop on sample #129 (placeholder) int32_t samplePlayOffset = smpOffset; ch->instrNum = 130; ch->copyOfInstrAndNote = (ch->instrNum << 8) | note; ch->efx = 0; ch->efxData = 0; triggerNote(note, 0, 0, ch); ch->smpStartPos = samplePlayOffset; if (note != NOTE_OFF) { resetVolumes(ch); triggerInstrument(ch); ch->realVol = vol; ch->outVol = vol; ch->oldVol = vol; } ch->midiVibDepth = midiVibDepth; ch->midiPitch = midiPitch; updateVolPanAutoVib(ch); unlockAudio(); while (ch->status & IS_Trigger); // wait for sample to latch in mixer // for sampling playback line in Smp. Ed. editor.curPlayInstr = editor.curInstr; editor.curPlaySmp = editor.curSmp; } void stopVoices(void) { const bool audioWasntLocked = !audio.locked; if (audioWasntLocked) lockAudio(); channel_t *ch = channel; for (int32_t i = 0; i < MAX_CHANNELS; i++, ch++) { lastChInstr[i].smpNum = 255; lastChInstr[i].instrNum = 255; ch->copyOfInstrAndNote = 0; ch->relativeNote = 0; ch->smpNum = 0; ch->smpPtr = NULL; ch->instrNum = 0; ch->instrPtr = instr[0]; // important: set instrument pointer to instr 0 (placeholder instrument) ch->status = IS_Vol; ch->realVol = 0; ch->outVol = 0; ch->oldVol = 0; ch->fFinalVol = 0.0f; ch->oldPan = 128; ch->outPan = 128; ch->finalPan = 128; ch->vibratoDepth = 0; // clear it because it can be set from the volumn column ch->midiVibDepth = 0; ch->midiPitch = 0; ch->portamentoDirection = 0; // FT2 bugfix: weird 3xx behavior if not used with note stopVoice(i); } // for sampling playback line in Smp. Ed. editor.curPlayInstr = 255; editor.curPlaySmp = 255; stopAllScopes(); resetCachedMixerVars(); // wait for scope thread to finish, so that we know pointers aren't deprecated while (editor.scopeThreadBusy); if (audioWasntLocked) unlockAudio(); } void setNewSongPos(int32_t pos) { resetReplayerState(); // FT2 bugfix setPos((int16_t)pos, 0, true); // non-FT2 fix: If song speed was 0, set it back to initial speed if (song.speed == 0) song.speed = song.initialSpeed; } void decSongPos(void) { if (song.songPos == 0) return; const bool audioWasntLocked = !audio.locked; if (audioWasntLocked) lockAudio(); if (song.songPos > 0) setNewSongPos(song.songPos - 1); if (audioWasntLocked) unlockAudio(); } void incSongPos(void) { if (song.songPos == song.songLength-1) return; const bool audioWasntLocked = !audio.locked; if (audioWasntLocked) lockAudio(); if (song.songPos < song.songLength-1) setNewSongPos(song.songPos + 1); if (audioWasntLocked) unlockAudio(); } void decCurIns(void) { if (editor.curInstr <= 1) return; editor.curInstr--; if ((editor.curInstr > 0x40 && !editor.instrBankSwapped) || (editor.curInstr <= 0x40 && editor.instrBankSwapped)) pbSwapInstrBank(); editor.instrBankOffset = ((editor.curInstr - 1) / 8) * 8; updateTextBoxPointers(); updateNewInstrument(); if (ui.advEditShown) updateAdvEdit(); } void incCurIns(void) { if (editor.curInstr >= MAX_INST) return; editor.curInstr++; if ((editor.curInstr > 0x40 && !editor.instrBankSwapped) || (editor.curInstr <= 0x40 && editor.instrBankSwapped)) pbSwapInstrBank(); editor.instrBankOffset = ((editor.curInstr - 1) / 8) * 8; if (editor.instrBankOffset > MAX_INST-8) editor.instrBankOffset = MAX_INST-8; updateTextBoxPointers(); updateNewInstrument(); if (ui.advEditShown) updateAdvEdit(); } void decCurSmp(void) { if (editor.curSmp == 0) return; editor.curSmp--; editor.sampleBankOffset = (editor.curSmp / 5) * 5; setScrollBarPos(SB_SAMPLE_LIST, editor.sampleBankOffset, true); updateTextBoxPointers(); updateNewSample(); } void incCurSmp(void) { if (editor.curSmp >= MAX_SMP_PER_INST-1) return; editor.curSmp++; editor.sampleBankOffset = (editor.curSmp / 5) * 5; if (editor.sampleBankOffset > MAX_SMP_PER_INST-5) editor.sampleBankOffset = MAX_SMP_PER_INST-5; setScrollBarPos(SB_SAMPLE_LIST, editor.sampleBankOffset, true); updateTextBoxPointers(); updateNewSample(); } void pbPlaySong(void) { startPlaying(PLAYMODE_SONG, 0); } void pbPlayPtn(void) { startPlaying(PLAYMODE_PATT, 0); } void pbRecSng(void) { startPlaying(PLAYMODE_RECSONG, 0); } void pbRecPtn(void) { startPlaying(PLAYMODE_RECPATT, 0); } void setSyncedReplayerVars(void) { uint8_t scopeUpdateStatus[MAX_CHANNELS]; pattSyncEntry = NULL; chSyncEntry = NULL; memset(scopeUpdateStatus, 0, sizeof (scopeUpdateStatus)); // this is needed uint64_t frameTime64 = SDL_GetPerformanceCounter(); // handle channel sync queue while (chQueueClearing); while (chQueueReadSize() > 0) { if (frameTime64 < getChQueueTimestamp()) break; // we have no more stuff to render for now chSyncEntry = chQueuePeek(); if (chSyncEntry == NULL) break; for (int32_t i = 0; i < song.numChannels; i++) scopeUpdateStatus[i] |= chSyncEntry->channels[i].status; // yes, OR the status if (!chQueuePop()) break; } /* Extra validation because of possible issues when the buffer is full ** and positions are being reset, which is not entirely thread safe. */ if (chSyncEntry != NULL && chSyncEntry->timestamp == 0) chSyncEntry = NULL; // handle pattern sync queue while (pattQueueClearing); while (pattQueueReadSize() > 0) { if (frameTime64 < getPattQueueTimestamp()) break; // we have no more stuff to render for now pattSyncEntry = pattQueuePeek(); if (pattSyncEntry == NULL) break; if (!pattQueuePop()) break; } /* Extra validation because of possible issues when the buffer is full ** and positions are being reset, which is not entirely thread safe. */ if (pattSyncEntry != NULL && pattSyncEntry->timestamp == 0) pattSyncEntry = NULL; // do actual updates if (chSyncEntry != NULL) { handleScopesFromChQueue(chSyncEntry, scopeUpdateStatus); ui.drawReplayerPianoFlag = true; } if (!songPlaying || pattSyncEntry == NULL) return; // we have a new tick editor.tick = pattSyncEntry->tick; if (editor.BPM != pattSyncEntry->BPM) { editor.BPM = pattSyncEntry->BPM; ui.drawBPMFlag = true; } if (editor.speed != pattSyncEntry->speed) { editor.speed = pattSyncEntry->speed; ui.drawSpeedFlag = true; } if (editor.globalVolume != pattSyncEntry->globalVolume) { editor.globalVolume = pattSyncEntry->globalVolume; ui.drawGlobVolFlag = true; } if (editor.songPos != pattSyncEntry->songPos) { editor.songPos = pattSyncEntry->songPos; ui.drawPosEdFlag = true; } // somewhat of a kludge... if (editor.tmpPattern != pattSyncEntry->pattNum || editor.row != pattSyncEntry->row) { // set pattern number editor.editPattern = editor.tmpPattern = pattSyncEntry->pattNum; checkMarkLimits(); ui.drawPattNumLenFlag = true; // set row editor.row = (uint8_t)pattSyncEntry->row; ui.updatePatternEditor = true; } }