@modplayjs/fmt-fc 0.0.0-stage → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/synth.js ADDED
@@ -0,0 +1,1430 @@
1
+ // SPDX-License-Identifier: BSD-3-Clause
2
+ // Ported from: OpenMPT soundlib/InstrumentSynth.h + InstrumentSynth.cpp
3
+ // (BSD-3-Clause, OpenMPT Devs) — the "Script"/"Synth" processor that
4
+ // Future Composer (and MED/GT2/Puma/His Master's Noise/Face The Music)
5
+ // instruments run as per-channel event scripts.
6
+ //
7
+ // The full Event set is ported 1:1 (Type enum :37-113, state machine
8
+ // State :16-104, NextTick :246-459, EvaluateEvent :556-966,
9
+ // ApplyChannelState :468-554, HandleFCVolumeBend :963-980). Only the
10
+ // events Future Composer 1.x actually emits are exercised by the FC
11
+ // loader, but the engine itself is complete so later loaders can reuse
12
+ // it verbatim.
13
+ import { NoteFlag } from '@modplayjs/core';
14
+ /** Event::Type (InstrumentSynth.h:37-113). Kept as a numeric enum with the
15
+ * exact C++ names. */
16
+ export var SynthEventType;
17
+ (function (SynthEventType) {
18
+ SynthEventType[SynthEventType["StopScript"] = 0] = "StopScript";
19
+ SynthEventType[SynthEventType["Jump"] = 1] = "Jump";
20
+ SynthEventType[SynthEventType["JumpIfTrue"] = 2] = "JumpIfTrue";
21
+ SynthEventType[SynthEventType["Delay"] = 3] = "Delay";
22
+ SynthEventType[SynthEventType["SetStepSpeed"] = 4] = "SetStepSpeed";
23
+ SynthEventType[SynthEventType["JumpMarker"] = 5] = "JumpMarker";
24
+ SynthEventType[SynthEventType["SampleOffset"] = 6] = "SampleOffset";
25
+ SynthEventType[SynthEventType["SampleOffsetAdd"] = 7] = "SampleOffsetAdd";
26
+ SynthEventType[SynthEventType["SampleOffsetSub"] = 8] = "SampleOffsetSub";
27
+ SynthEventType[SynthEventType["SetLoopCounter"] = 9] = "SetLoopCounter";
28
+ SynthEventType[SynthEventType["EvaluateLoopCounter"] = 10] = "EvaluateLoopCounter";
29
+ SynthEventType[SynthEventType["NoteCut"] = 11] = "NoteCut";
30
+ SynthEventType[SynthEventType["GTK_KeyOff"] = 12] = "GTK_KeyOff";
31
+ SynthEventType[SynthEventType["GTK_SetVolume"] = 13] = "GTK_SetVolume";
32
+ SynthEventType[SynthEventType["GTK_SetPitch"] = 14] = "GTK_SetPitch";
33
+ SynthEventType[SynthEventType["GTK_SetPanning"] = 15] = "GTK_SetPanning";
34
+ SynthEventType[SynthEventType["GTK_SetVolumeStep"] = 16] = "GTK_SetVolumeStep";
35
+ SynthEventType[SynthEventType["GTK_SetPitchStep"] = 17] = "GTK_SetPitchStep";
36
+ SynthEventType[SynthEventType["GTK_SetPanningStep"] = 18] = "GTK_SetPanningStep";
37
+ SynthEventType[SynthEventType["GTK_SetSpeed"] = 19] = "GTK_SetSpeed";
38
+ SynthEventType[SynthEventType["GTK_EnableTremor"] = 20] = "GTK_EnableTremor";
39
+ SynthEventType[SynthEventType["GTK_SetTremorTime"] = 21] = "GTK_SetTremorTime";
40
+ SynthEventType[SynthEventType["GTK_EnableTremolo"] = 22] = "GTK_EnableTremolo";
41
+ SynthEventType[SynthEventType["GTK_EnableVibrato"] = 23] = "GTK_EnableVibrato";
42
+ SynthEventType[SynthEventType["GTK_SetVibratoParams"] = 24] = "GTK_SetVibratoParams";
43
+ SynthEventType[SynthEventType["Puma_SetWaveform"] = 25] = "Puma_SetWaveform";
44
+ SynthEventType[SynthEventType["Puma_VolumeRamp"] = 26] = "Puma_VolumeRamp";
45
+ SynthEventType[SynthEventType["Puma_StopVoice"] = 27] = "Puma_StopVoice";
46
+ SynthEventType[SynthEventType["Puma_SetPitch"] = 28] = "Puma_SetPitch";
47
+ SynthEventType[SynthEventType["Puma_PitchRamp"] = 29] = "Puma_PitchRamp";
48
+ SynthEventType[SynthEventType["Mupp_SetWaveform"] = 30] = "Mupp_SetWaveform";
49
+ SynthEventType[SynthEventType["MED_DefineArpeggio"] = 31] = "MED_DefineArpeggio";
50
+ SynthEventType[SynthEventType["MED_JumpScript"] = 32] = "MED_JumpScript";
51
+ SynthEventType[SynthEventType["MED_SetEnvelope"] = 33] = "MED_SetEnvelope";
52
+ SynthEventType[SynthEventType["MED_SetVolume"] = 34] = "MED_SetVolume";
53
+ SynthEventType[SynthEventType["MED_SetWaveform"] = 35] = "MED_SetWaveform";
54
+ SynthEventType[SynthEventType["MED_SetVibratoSpeed"] = 36] = "MED_SetVibratoSpeed";
55
+ SynthEventType[SynthEventType["MED_SetVibratoDepth"] = 37] = "MED_SetVibratoDepth";
56
+ SynthEventType[SynthEventType["MED_SetVolumeStep"] = 38] = "MED_SetVolumeStep";
57
+ SynthEventType[SynthEventType["MED_SetPeriodStep"] = 39] = "MED_SetPeriodStep";
58
+ SynthEventType[SynthEventType["MED_HoldDecay"] = 40] = "MED_HoldDecay";
59
+ SynthEventType[SynthEventType["FTM_PlaySample"] = 41] = "FTM_PlaySample";
60
+ SynthEventType[SynthEventType["FTM_SetPitch"] = 42] = "FTM_SetPitch";
61
+ SynthEventType[SynthEventType["FTM_AddPitch"] = 43] = "FTM_AddPitch";
62
+ SynthEventType[SynthEventType["FTM_SetDetune"] = 44] = "FTM_SetDetune";
63
+ SynthEventType[SynthEventType["FTM_AddDetune"] = 45] = "FTM_AddDetune";
64
+ SynthEventType[SynthEventType["FTM_SetVolume"] = 46] = "FTM_SetVolume";
65
+ SynthEventType[SynthEventType["FTM_AddVolume"] = 47] = "FTM_AddVolume";
66
+ SynthEventType[SynthEventType["FTM_SetSample"] = 48] = "FTM_SetSample";
67
+ SynthEventType[SynthEventType["FTM_SetCondition"] = 49] = "FTM_SetCondition";
68
+ SynthEventType[SynthEventType["FTM_SetInterrupt"] = 50] = "FTM_SetInterrupt";
69
+ SynthEventType[SynthEventType["FTM_SetSampleStart"] = 51] = "FTM_SetSampleStart";
70
+ SynthEventType[SynthEventType["FTM_SetOneshotLength"] = 52] = "FTM_SetOneshotLength";
71
+ SynthEventType[SynthEventType["FTM_SetRepeatLength"] = 53] = "FTM_SetRepeatLength";
72
+ SynthEventType[SynthEventType["FTM_CloneTrack"] = 54] = "FTM_CloneTrack";
73
+ SynthEventType[SynthEventType["FTM_StartLFO"] = 55] = "FTM_StartLFO";
74
+ SynthEventType[SynthEventType["FTM_LFOAddSub"] = 56] = "FTM_LFOAddSub";
75
+ SynthEventType[SynthEventType["FTM_SetWorkTrack"] = 57] = "FTM_SetWorkTrack";
76
+ SynthEventType[SynthEventType["FTM_SetGlobalVolume"] = 58] = "FTM_SetGlobalVolume";
77
+ SynthEventType[SynthEventType["FTM_SetTempo"] = 59] = "FTM_SetTempo";
78
+ SynthEventType[SynthEventType["FTM_SetSpeed"] = 60] = "FTM_SetSpeed";
79
+ SynthEventType[SynthEventType["FTM_SetPlayPosition"] = 61] = "FTM_SetPlayPosition";
80
+ SynthEventType[SynthEventType["FC_SetWaveform"] = 62] = "FC_SetWaveform";
81
+ SynthEventType[SynthEventType["FC_SetPitch"] = 63] = "FC_SetPitch";
82
+ SynthEventType[SynthEventType["FC_SetVibrato"] = 64] = "FC_SetVibrato";
83
+ SynthEventType[SynthEventType["FC_PitchSlide"] = 65] = "FC_PitchSlide";
84
+ SynthEventType[SynthEventType["FC_VolumeSlide"] = 66] = "FC_VolumeSlide";
85
+ })(SynthEventType || (SynthEventType = {}));
86
+ /** JumpEvents (InstrumentSynth.h:115-122). */
87
+ const JUMP_EVENTS = [
88
+ SynthEventType.Jump,
89
+ SynthEventType.JumpIfTrue,
90
+ SynthEventType.EvaluateLoopCounter,
91
+ SynthEventType.GTK_KeyOff,
92
+ SynthEventType.MED_HoldDecay,
93
+ SynthEventType.FTM_SetInterrupt,
94
+ ];
95
+ export function evStopScript() {
96
+ return { type: SynthEventType.StopScript, u8: 0, u16: 0, byte1: 0, byte2: 0 };
97
+ }
98
+ export function evJump(target) {
99
+ return { type: SynthEventType.Jump, u8: 0, u16: target & 0xffff, byte1: 0, byte2: 0 };
100
+ }
101
+ export function evJumpIfTrue(target) {
102
+ return { type: SynthEventType.JumpIfTrue, u8: 0, u16: target & 0xffff, byte1: 0, byte2: 0 };
103
+ }
104
+ export function evDelay(ticks) {
105
+ return { type: SynthEventType.Delay, u8: 0, u16: ticks & 0xffff, byte1: 0, byte2: 0 };
106
+ }
107
+ export function evSetStepSpeed(speed, updateNow) {
108
+ return { type: SynthEventType.SetStepSpeed, u8: speed & 0xff, u16: 0, byte1: updateNow ? 1 : 0, byte2: 0 };
109
+ }
110
+ export function evJumpMarker(data) {
111
+ return { type: SynthEventType.JumpMarker, u8: 0, u16: data & 0xffff, byte1: 0, byte2: 0 };
112
+ }
113
+ export function evSampleOffset(offset) {
114
+ const v = Math.min(offset, 0xffffff);
115
+ return { type: SynthEventType.SampleOffset, u8: v & 0xff, u16: 0, byte1: (v >> 8) & 0xff, byte2: (v >> 16) & 0xff };
116
+ }
117
+ export function evSampleOffsetAdd(offset) {
118
+ const v = Math.min(offset, 0xffffff);
119
+ return { type: SynthEventType.SampleOffsetAdd, u8: v & 0xff, u16: 0, byte1: (v >> 8) & 0xff, byte2: (v >> 16) & 0xff };
120
+ }
121
+ export function evSampleOffsetSub(offset) {
122
+ const v = Math.min(offset, 0xffffff);
123
+ return { type: SynthEventType.SampleOffsetSub, u8: v & 0xff, u16: 0, byte1: (v >> 8) & 0xff, byte2: (v >> 16) & 0xff };
124
+ }
125
+ export function evSetLoopCounter(count, force) {
126
+ return { type: SynthEventType.SetLoopCounter, u8: force ? 1 : 0, u16: count & 0xffff, byte1: 0, byte2: 0 };
127
+ }
128
+ export function evEvaluateLoopCounter(target) {
129
+ return { type: SynthEventType.EvaluateLoopCounter, u8: 0, u16: target & 0xffff, byte1: 0, byte2: 0 };
130
+ }
131
+ export function evNoteCut() {
132
+ return { type: SynthEventType.NoteCut, u8: 0, u16: 0, byte1: 0, byte2: 0 };
133
+ }
134
+ export function evGtkKeyOff(target) {
135
+ return { type: SynthEventType.GTK_KeyOff, u8: 0, u16: target & 0xffff, byte1: 0, byte2: 0 };
136
+ }
137
+ export function evGtkSetVolume(volume) {
138
+ return { type: SynthEventType.GTK_SetVolume, u8: 0, u16: volume & 0xffff, byte1: 0, byte2: 0 };
139
+ }
140
+ export function evGtkSetPitch(pitch) {
141
+ return { type: SynthEventType.GTK_SetPitch, u8: 0, u16: pitch & 0xffff, byte1: 0, byte2: 0 };
142
+ }
143
+ export function evGtkSetPanning(panning) {
144
+ return { type: SynthEventType.GTK_SetPanning, u8: 0, u16: panning & 0xffff, byte1: 0, byte2: 0 };
145
+ }
146
+ export function evGtkSetVolumeStep(step) {
147
+ return { type: SynthEventType.GTK_SetVolumeStep, u8: 0, u16: step & 0xffff, byte1: 0, byte2: 0 };
148
+ }
149
+ export function evGtkSetPitchStep(step) {
150
+ return { type: SynthEventType.GTK_SetPitchStep, u8: 0, u16: step & 0xffff, byte1: 0, byte2: 0 };
151
+ }
152
+ export function evGtkSetPanningStep(step) {
153
+ return { type: SynthEventType.GTK_SetPanningStep, u8: 0, u16: step & 0xffff, byte1: 0, byte2: 0 };
154
+ }
155
+ export function evGtkSetSpeed(speed) {
156
+ return { type: SynthEventType.GTK_SetSpeed, u8: speed & 0xff, u16: 0, byte1: 0, byte2: 0 };
157
+ }
158
+ export function evGtkEnableTremor(enable) {
159
+ return { type: SynthEventType.GTK_EnableTremor, u8: enable & 0xff, u16: 0, byte1: 0, byte2: 0 };
160
+ }
161
+ export function evGtkSetTremorTime(on, off) {
162
+ return { type: SynthEventType.GTK_SetTremorTime, u8: on & 0xff, u16: 0, byte1: off & 0xff, byte2: 0 };
163
+ }
164
+ export function evGtkEnableTremolo(enable) {
165
+ return { type: SynthEventType.GTK_EnableTremolo, u8: enable & 0xff, u16: 0, byte1: 0, byte2: 0 };
166
+ }
167
+ export function evGtkEnableVibrato(enable) {
168
+ return { type: SynthEventType.GTK_EnableVibrato, u8: enable & 0xff, u16: 0, byte1: 0, byte2: 0 };
169
+ }
170
+ export function evGtkSetVibratoParams(width, speed) {
171
+ return { type: SynthEventType.GTK_SetVibratoParams, u8: width & 0xff, u16: 0, byte1: speed & 0xff, byte2: 0 };
172
+ }
173
+ export function evPumaSetWaveform(waveform, step, count) {
174
+ return { type: SynthEventType.Puma_SetWaveform, u8: waveform & 0xff, u16: 0, byte1: step & 0xff, byte2: count & 0xff };
175
+ }
176
+ export function evPumaVolumeRamp(startVol, endVol, ticks) {
177
+ return { type: SynthEventType.Puma_VolumeRamp, u8: startVol & 0xff, u16: 0, byte1: endVol & 0xff, byte2: ticks & 0xff };
178
+ }
179
+ export function evPumaStopVoice() {
180
+ return { type: SynthEventType.Puma_StopVoice, u8: 0, u16: 0, byte1: 0, byte2: 0 };
181
+ }
182
+ export function evPumaSetPitch(pitchOffset, ticks) {
183
+ return { type: SynthEventType.Puma_SetPitch, u8: (pitchOffset & 0xff), u16: 0, byte1: 0, byte2: ticks & 0xff };
184
+ }
185
+ export function evPumaPitchRamp(startPitch, endPitch, ticks) {
186
+ return { type: SynthEventType.Puma_PitchRamp, u8: startPitch & 0xff, u16: 0, byte1: endPitch & 0xff, byte2: ticks & 0xff };
187
+ }
188
+ export function evMuppSetWaveform(instr, waveform, volume) {
189
+ return { type: SynthEventType.Mupp_SetWaveform, u8: instr & 0xff, u16: 0, byte1: waveform & 0xff, byte2: volume & 0xff };
190
+ }
191
+ export function evMedDefineArpeggio(note, noteCount) {
192
+ return { type: SynthEventType.MED_DefineArpeggio, u8: 0, u16: noteCount & 0xffff, byte1: note & 0xff, byte2: 0 };
193
+ }
194
+ export function evMedJumpScript(scriptIndex, target) {
195
+ return { type: SynthEventType.MED_JumpScript, u8: scriptIndex & 0xff, u16: target & 0xffff, byte1: 0, byte2: 0 };
196
+ }
197
+ export function evMedSetEnvelope(envelope, loop, volumeEnv) {
198
+ return { type: SynthEventType.MED_SetEnvelope, u8: envelope & 0xff, u16: 0, byte1: loop ? 1 : 0, byte2: volumeEnv ? 1 : 0 };
199
+ }
200
+ export function evMedSetVolume(volume) {
201
+ return { type: SynthEventType.MED_SetVolume, u8: volume & 0xff, u16: 0, byte1: 0, byte2: 0 };
202
+ }
203
+ export function evMedSetWaveform(waveform) {
204
+ return { type: SynthEventType.MED_SetWaveform, u8: waveform & 0xff, u16: 0, byte1: 0, byte2: 0 };
205
+ }
206
+ export function evMedSetVibratoSpeed(depth) {
207
+ return { type: SynthEventType.MED_SetVibratoSpeed, u8: depth & 0xff, u16: 0, byte1: 0, byte2: 0 };
208
+ }
209
+ export function evMedSetVibratoDepth(depth) {
210
+ return { type: SynthEventType.MED_SetVibratoDepth, u8: depth & 0xff, u16: 0, byte1: 0, byte2: 0 };
211
+ }
212
+ export function evMedSetVolumeStep(volumeStep) {
213
+ return { type: SynthEventType.MED_SetVolumeStep, u8: 0, u16: volumeStep & 0xffff, byte1: 0, byte2: 0 };
214
+ }
215
+ export function evMedSetPeriodStep(periodStep) {
216
+ return { type: SynthEventType.MED_SetPeriodStep, u8: 0, u16: periodStep & 0xffff, byte1: 0, byte2: 0 };
217
+ }
218
+ export function evMedHoldDecay(hold, decay) {
219
+ return { type: SynthEventType.MED_HoldDecay, u8: 0, u16: decay & 0xffff, byte1: hold & 0xff, byte2: 0 };
220
+ }
221
+ export function evFtmSetCondition(threshold, condition) {
222
+ return { type: SynthEventType.FTM_SetCondition, u8: condition & 0xff, u16: threshold & 0xffff, byte1: 0, byte2: 0 };
223
+ }
224
+ export function evFtmSetInterrupt(target, type) {
225
+ return { type: SynthEventType.FTM_SetInterrupt, u8: type & 0xff, u16: target & 0xffff, byte1: 0, byte2: 0 };
226
+ }
227
+ export function evFtmPlaySample() {
228
+ return { type: SynthEventType.FTM_PlaySample, u8: 0, u16: 0, byte1: 0, byte2: 0 };
229
+ }
230
+ export function evFtmSetPitch(pitch) {
231
+ return { type: SynthEventType.FTM_SetPitch, u8: 0, u16: pitch & 0xffff, byte1: 0, byte2: 0 };
232
+ }
233
+ export function evFtmAddPitch(pitch) {
234
+ return { type: SynthEventType.FTM_AddPitch, u8: 0, u16: pitch & 0xffff, byte1: 0, byte2: 0 };
235
+ }
236
+ export function evFtmSetDetune(detune) {
237
+ return { type: SynthEventType.FTM_SetDetune, u8: 0, u16: detune & 0xffff, byte1: 0, byte2: 0 };
238
+ }
239
+ export function evFtmAddDetune(detune) {
240
+ return { type: SynthEventType.FTM_AddDetune, u8: 0, u16: detune & 0xffff, byte1: 0, byte2: 0 };
241
+ }
242
+ export function evFtmSetVolume(volume) {
243
+ return { type: SynthEventType.FTM_SetVolume, u8: volume & 0xff, u16: 0, byte1: 0, byte2: 0 };
244
+ }
245
+ export function evFtmAddVolume(volume) {
246
+ return { type: SynthEventType.FTM_AddVolume, u8: 0, u16: volume & 0xffff, byte1: 0, byte2: 0 };
247
+ }
248
+ export function evFtmSetSample(sample) {
249
+ return { type: SynthEventType.FTM_SetSample, u8: sample & 0xff, u16: 0, byte1: 0, byte2: 0 };
250
+ }
251
+ export function evFtmSetSampleStart(offset, type) {
252
+ return { type: SynthEventType.FTM_SetSampleStart, u8: type & 0xff, u16: offset & 0xffff, byte1: 0, byte2: 0 };
253
+ }
254
+ export function evFtmSetOneshotLength(length, type) {
255
+ return { type: SynthEventType.FTM_SetOneshotLength, u8: type & 0xff, u16: length & 0xffff, byte1: 0, byte2: 0 };
256
+ }
257
+ export function evFtmSetRepeatLength(length, type) {
258
+ return { type: SynthEventType.FTM_SetRepeatLength, u8: type & 0xff, u16: length & 0xffff, byte1: 0, byte2: 0 };
259
+ }
260
+ export function evFtmCloneTrack(track, properties) {
261
+ return { type: SynthEventType.FTM_CloneTrack, u8: track & 0xff, u16: 0, byte1: properties & 0xff, byte2: 0 };
262
+ }
263
+ export function evFtmStartLfo(lfo, targetWaveform) {
264
+ return { type: SynthEventType.FTM_StartLFO, u8: lfo & 0xff, u16: 0, byte1: targetWaveform & 0xff, byte2: 0 };
265
+ }
266
+ export function evFtmLfoAddSub(lfoAddSub, speed, depth) {
267
+ return { type: SynthEventType.FTM_LFOAddSub, u8: lfoAddSub & 0xff, u16: 0, byte1: speed & 0xff, byte2: depth & 0xff };
268
+ }
269
+ export function evFtmSetWorkTrack(track, relative) {
270
+ return { type: SynthEventType.FTM_SetWorkTrack, u8: track & 0xff, u16: 0, byte1: relative ? 1 : 0, byte2: 0 };
271
+ }
272
+ export function evFtmSetGlobalVolume(globalVolume) {
273
+ return { type: SynthEventType.FTM_SetGlobalVolume, u8: 0, u16: globalVolume & 0xffff, byte1: 0, byte2: 0 };
274
+ }
275
+ export function evFtmSetTempo(tempo) {
276
+ return { type: SynthEventType.FTM_SetTempo, u8: 0, u16: tempo & 0xffff, byte1: 0, byte2: 0 };
277
+ }
278
+ export function evFtmSetSpeed(speed) {
279
+ return { type: SynthEventType.FTM_SetSpeed, u8: 0, u16: speed & 0xffff, byte1: 0, byte2: 0 };
280
+ }
281
+ export function evFtmSetPlayPosition(pattern, row) {
282
+ return { type: SynthEventType.FTM_SetPlayPosition, u8: row & 0xff, u16: pattern & 0xffff, byte1: 0, byte2: 0 };
283
+ }
284
+ export function evFcSetWaveform(command, waveform, samplePack) {
285
+ return { type: SynthEventType.FC_SetWaveform, u8: command & 0xff, u16: 0, byte1: waveform & 0xff, byte2: samplePack & 0xff };
286
+ }
287
+ export function evFcSetPitch(pitch) {
288
+ return { type: SynthEventType.FC_SetPitch, u8: (pitch & 0xff), u16: 0, byte1: 0, byte2: 0 };
289
+ }
290
+ export function evFcSetVibrato(speed, depth, delay) {
291
+ return { type: SynthEventType.FC_SetVibrato, u8: speed & 0xff, u16: 0, byte1: depth & 0xff, byte2: delay & 0xff };
292
+ }
293
+ export function evFcPitchSlide(speed, time) {
294
+ return { type: SynthEventType.FC_PitchSlide, u8: speed & 0xff, u16: 0, byte1: time & 0xff, byte2: 0 };
295
+ }
296
+ export function evFcVolumeSlide(speed, time) {
297
+ return { type: SynthEventType.FC_VolumeSlide, u8: speed & 0xff, u16: 0, byte1: time & 0xff, byte2: 0 };
298
+ }
299
+ /** event.Byte0/1/2 + Value24Bit (InstrumentSynth.h:236-240). */
300
+ export function eventByte0(e) { return e.u8; }
301
+ export function eventByte1(e) { return e.byte1; }
302
+ export function eventByte2(e) { return e.byte2; }
303
+ export function eventValue24Bit(e) {
304
+ return (e.u8 & 0xff) | ((e.byte1 & 0xff) << 8) | ((e.byte2 & 0xff) << 16);
305
+ }
306
+ /** event.IsJumpEvent (InstrumentSynth.h:221-224). */
307
+ function isJumpEvent(e) {
308
+ return JUMP_EVENTS.includes(e.type);
309
+ }
310
+ /** event.FixupJumpTarget (InstrumentSynth.h:226-233): map byte offsets to
311
+ * event indices; lower_bound semantics (first entry >= u16). */
312
+ export function fixupJumpTarget(e, entryFromByte) {
313
+ if (!isJumpEvent(e))
314
+ return;
315
+ let mapped = 0xffff;
316
+ for (const [off, idx] of entryFromByte) {
317
+ if (off >= e.u16) {
318
+ mapped = idx;
319
+ break;
320
+ }
321
+ }
322
+ e.u16 = mapped;
323
+ }
324
+ /** STOP_ROW (InstrumentSynth.cpp:18). */
325
+ export const SYNTH_STOP_ROW = 0xffff;
326
+ /** State flag indices (InstrumentSynth.cpp:20-30). */
327
+ var FcSynthFlags;
328
+ (function (FcSynthFlags) {
329
+ FcSynthFlags[FcSynthFlags["kJumpConditionSet"] = 1] = "kJumpConditionSet";
330
+ FcSynthFlags[FcSynthFlags["kGTKTremorEnabled"] = 2] = "kGTKTremorEnabled";
331
+ FcSynthFlags[FcSynthFlags["kGTKTremorMute"] = 4] = "kGTKTremorMute";
332
+ FcSynthFlags[FcSynthFlags["kGTKTremoloEnabled"] = 8] = "kGTKTremoloEnabled";
333
+ FcSynthFlags[FcSynthFlags["kGTKVibratoEnabled"] = 16] = "kGTKVibratoEnabled";
334
+ FcSynthFlags[FcSynthFlags["kFCVibratoDelaySet"] = 32] = "kFCVibratoDelaySet";
335
+ FcSynthFlags[FcSynthFlags["kFCVibratoStep"] = 64] = "kFCVibratoStep";
336
+ FcSynthFlags[FcSynthFlags["kFCPitchBendStep"] = 128] = "kFCPitchBendStep";
337
+ FcSynthFlags[FcSynthFlags["kFCVolumeBendStep"] = 256] = "kFCVolumeBendStep";
338
+ })(FcSynthFlags || (FcSynthFlags = {}));
339
+ /** InstrumentSynth::States::State (InstrumentSynth.cpp:16-104) — all fields,
340
+ * C++ default values verbatim. */
341
+ export class SynthState {
342
+ flags = 0; // std::bitset<kNumFlags>
343
+ currentRow = SYNTH_STOP_ROW;
344
+ nextRow = 0;
345
+ ticksRemain = 0;
346
+ stepSpeed = 1;
347
+ stepsRemain = 0;
348
+ volumeFactor = 16384;
349
+ volumeAdd = -32768; // int16_min
350
+ panning = 2048;
351
+ linearPitchFactor = 0;
352
+ periodFreqSlide = 0;
353
+ periodAdd = 0;
354
+ loopCount = 0;
355
+ gtkKeyOffOffset = SYNTH_STOP_ROW;
356
+ gtkVolumeStep = 0;
357
+ gtkPitchStep = 0;
358
+ gtkPanningStep = 0;
359
+ gtkPitch = 4096;
360
+ gtkSpeed = 1;
361
+ gtkSpeedRemain = 1;
362
+ gtkTremorOnTime = 3;
363
+ gtkTremorOffTime = 3;
364
+ gtkTremorPos = 0;
365
+ gtkVibratoWidth = 0;
366
+ gtkVibratoSpeed = 0;
367
+ gtkVibratoPos = 0;
368
+ pumaStartWaveform = 0;
369
+ pumaEndWaveform = 0;
370
+ pumaWaveform = 0;
371
+ pumaWaveformStep = 0;
372
+ medVibratoEnvelope = 255; // uint8_max
373
+ medVibratoSpeed = 0;
374
+ medVibratoDepth = 0;
375
+ medVibratoValue = 0;
376
+ medVibratoPos = 0;
377
+ medVolumeStep = 0;
378
+ medPeriodStep = 0;
379
+ medArpOffset = SYNTH_STOP_ROW;
380
+ medArpPos = 0;
381
+ medHold = 255;
382
+ medDecay = SYNTH_STOP_ROW;
383
+ medVolumeEnv = 255;
384
+ medVolumeEnvPos = 0;
385
+ ftmSampleStart = 0;
386
+ ftmDetune = 1;
387
+ ftmVolumeChangeJump = SYNTH_STOP_ROW;
388
+ ftmPitchChangeJump = SYNTH_STOP_ROW;
389
+ ftmSampleChangeJump = SYNTH_STOP_ROW;
390
+ ftmReleaseJump = SYNTH_STOP_ROW;
391
+ ftmVolumeDownJump = SYNTH_STOP_ROW;
392
+ ftmPortamentoJump = SYNTH_STOP_ROW;
393
+ ftmLfo = [
394
+ { targetWaveform: 0, speed: 0, depth: 0, position: 0 },
395
+ { targetWaveform: 0, speed: 0, depth: 0, position: 0 },
396
+ { targetWaveform: 0, speed: 0, depth: 0, position: 0 },
397
+ { targetWaveform: 0, speed: 0, depth: 0, position: 0 },
398
+ ];
399
+ ftmWorkTrack = 0;
400
+ fcPitch = 0; // int8
401
+ fcVibratoValue = 0;
402
+ fcVibratoDelay = 0;
403
+ fcVibratoSpeed = 0;
404
+ fcVibratoDepth = 0;
405
+ fcVolumeBendSpeed = 0; // int8
406
+ fcPitchBendSpeed = 0; // int8
407
+ fcVolumeBendRemain = 0;
408
+ fcPitchBendRemain = 0;
409
+ /** Swapped sample for kMODSampleSwap (ModChannel.swapSampleIndex). */
410
+ swapSampleIndex = 0;
411
+ /** Reconstruct (C++ `mpt::reconstruct(state)` = default State()). */
412
+ reconstruct() {
413
+ const fresh = new SynthState();
414
+ Object.assign(this, fresh);
415
+ }
416
+ /** FTMRealChannel (InstrumentSynth.cpp:106-113). */
417
+ ftmRealChannel(channel, numChannels) {
418
+ if (this.ftmWorkTrack !== 0) {
419
+ return (this.ftmWorkTrack - 1) % numChannels;
420
+ }
421
+ return channel;
422
+ }
423
+ /** JumpToPosition (InstrumentSynth.cpp:236-245). */
424
+ jumpToPosition(events, position) {
425
+ for (let pos = 0; pos < events.length; pos++) {
426
+ if (events[pos].type === SynthEventType.JumpMarker && events[pos].u16 >= position) {
427
+ this.nextRow = pos & 0xffff;
428
+ this.ticksRemain = 0;
429
+ return;
430
+ }
431
+ }
432
+ }
433
+ }
434
+ /** InstrumentSynth::States (InstrumentSynth.cpp:158-172) + Stop/NextTick/
435
+ * ApplyChannelState (:225-244). */
436
+ export class SynthStates {
437
+ states = [];
438
+ stop() {
439
+ for (const state of this.states) {
440
+ state.currentRow = SYNTH_STOP_ROW;
441
+ state.nextRow = SYNTH_STOP_ROW;
442
+ }
443
+ }
444
+ }
445
+ // ---------------------------------------------------------------------------
446
+ // Shared tables (Tables.cpp / InstrumentSynth.cpp)
447
+ // ---------------------------------------------------------------------------
448
+ /** ModSinusTable (Tables.cpp:348-353). */
449
+ export const MOD_SINUS_TABLE = [
450
+ 0, 12, 25, 37, 49, 60, 71, 81, 90, 98, 106, 112, 117, 122, 125, 126,
451
+ 127, 126, 125, 122, 117, 112, 106, 98, 90, 81, 71, 60, 49, 37, 25, 12,
452
+ 0, -12, -25, -37, -49, -60, -71, -81, -90, -98, -106, -112, -117, -122, -125, -126,
453
+ -127, -126, -125, -122, -117, -112, -106, -98, -90, -81, -71, -60, -49, -37, -25, -12,
454
+ ];
455
+ // ---------------------------------------------------------------------------
456
+ // Channel/soundfile helpers (InstrumentSynth.cpp:127-166)
457
+ // ---------------------------------------------------------------------------
458
+ /** MEDEnvelopeFromSample (InstrumentSynth.cpp:142-158): 8-bit sample byte at
459
+ * an envelope position; the "envelope" is an extra sample after the
460
+ * keyboard's middle-C sample. */
461
+ function medEnvelopeFromSample(mod, instr, envelope, envelopePos) {
462
+ const midSmp = instr.map[60] ?? 0; // Keyboard[NOTE_MIDDLEC - NOTE_MIN]
463
+ const smp = midSmp + envelope;
464
+ if (smp < 0 || smp >= mod.samples.length)
465
+ return 0;
466
+ const sample = mod.samples[smp];
467
+ if (sample.length === 0 || envelopePos >= sample.length)
468
+ return 0;
469
+ // sample8()[envelopePos]: the stored 8-bit data byte. Our store keeps
470
+ // floats; read the 8-bit byte from the sample data (mono samples only —
471
+ // flagged 16-bit returns 0 like the C).
472
+ const data = sample.data;
473
+ if (data.length === 0 || (sample.flags & 0x01) !== 0)
474
+ return 0; // 16-bit
475
+ const v = Math.round(data[envelopePos] * 128);
476
+ return (v << 24) >> 24; // int8
477
+ }
478
+ /** ChannelSetSample (InstrumentSynth.cpp:160-186) — binds a new sample to the
479
+ * channel; with kMODSampleSwap defers the swap to loop end. */
480
+ export function channelSetSample(core, chn, smpIndex0, // 0-based sample id
481
+ swapAtEnd, state) {
482
+ const mod = core.module;
483
+ if (smpIndex0 < 0 || smpIndex0 >= mod.samples.length)
484
+ return;
485
+ const vi = core.virt.mapChannel(chn);
486
+ const v = vi >= 0 ? core.virt.voices[vi] ?? null : null;
487
+ const channelIsActive = v !== null && v.smp >= 0 && sampleExists(core, v.smp) && voiceLength(v) > 0;
488
+ // kMODSampleSwap: defer the swap to the end of the running loop
489
+ // (C chn.swapSampleIndex → mixer swaps at loop end WITHOUT a
490
+ // retrigger). queuePatch is exactly that: it queues the sample and
491
+ // the mixer hot-swaps it when the current loop finishes.
492
+ if (channelIsActive && swapAtEnd) {
493
+ core.virt.queuePatch(chn, v !== null ? v.ins : -1, smpIndex0, 0);
494
+ state.swapSampleIndex = smpIndex0 + 1; // bookkeeping for the idle case
495
+ return;
496
+ }
497
+ // Direct binding (swapAtEnd false or channel inactive): bind like C —
498
+ // the sample's loop flags/length become the channel's, and a position
499
+ // past the new length restarts at 0.
500
+ const sample = core.getSample(smpIndex0);
501
+ if (v !== null && v.smp === smpIndex0 && channelIsActive)
502
+ return;
503
+ const looped = (sample.flags & 0x02) !== 0; // SampleFlags.LOOP
504
+ const newLen = looped ? sample.loopEnd : sample.length;
505
+ // C: if(increment==0 && len==0 && vol==0) nVolume = 256 — a silent
506
+ // channel gets a default volume so the newly bound sample is audible.
507
+ if (v !== null && v.pos === 0 && voiceLength(v) === 0 && v.vol === 0) {
508
+ // our volume model is 0..64 like libxmp; 256 is OpenMPT's raw scale
509
+ core.virt.setVol(chn, 0x40);
510
+ }
511
+ // Bind via the same path the loaders use (updates voice smp/loop/pos).
512
+ core.virt.setPatchSmp(chn, v !== null ? v.ins : -1, smpIndex0, core.ctx.channelStates[chn]?.note ?? 0);
513
+ if (v !== null && v.pos >= newLen) {
514
+ // C: chn.position.Set(0) when pos >= nLength
515
+ v.pos = 0;
516
+ }
517
+ }
518
+ function sampleExists(core, smp) {
519
+ const mod = core.module;
520
+ return smp >= 0 && smp < mod.samples.length;
521
+ }
522
+ function voiceLength(v) {
523
+ return Math.max(0, v.end - v.start);
524
+ }
525
+ /** InstrumentSynth::States::NextTick (InstrumentSynth.cpp:246-459) — the
526
+ * per-tick state machine for ONE channel with ONE script/state pair. */
527
+ export function synthStateNextTick(core, chn, events, state) {
528
+ if (events.length === 0)
529
+ return;
530
+ const origChannel = chn;
531
+ const numChannels = core.module.chn;
532
+ chn = state.ftmRealChannel(chn, numChannels);
533
+ const xc = core.ctx.channelStates[chn];
534
+ // GTK key off (InstrumentSynth.cpp:258-262).
535
+ if (state.gtkKeyOffOffset !== SYNTH_STOP_ROW && (xc.flags & 0x04) !== 0) {
536
+ // CHN_KEYOFF — our channel flags bit 2 is the KEYOFF arm used by the
537
+ // readers (read_event keyoff path); check the note release instead:
538
+ // OpenMPT sets CHN_KEYOFF on the K00 row. Our equivalent mark is set
539
+ // when the reader processes a keyoff — approximate via NOTE_ENV_RELEASE
540
+ // being armed this row is not exact; port the flag test as stored.
541
+ state.nextRow = state.gtkKeyOffOffset;
542
+ state.ticksRemain = 0;
543
+ state.gtkKeyOffOffset = SYNTH_STOP_ROW;
544
+ }
545
+ // Puma waveform step (InstrumentSynth.cpp:264-271).
546
+ if (state.pumaWaveformStep !== 0) {
547
+ state.pumaWaveform = clamp(state.pumaWaveform + state.pumaWaveformStep, state.pumaStartWaveform, state.pumaEndWaveform);
548
+ if (state.pumaWaveform <= state.pumaStartWaveform || state.pumaWaveform >= state.pumaEndWaveform) {
549
+ state.pumaWaveformStep = -state.pumaWaveformStep;
550
+ }
551
+ if (core.ctx.channelStates[origChannel].ins >= 0) {
552
+ channelSetSample(core, chn, state.pumaWaveform, true, state);
553
+ }
554
+ }
555
+ // MED hold (InstrumentSynth.cpp:273-276).
556
+ if (state.medHold !== 255) {
557
+ if (state.medHold === 0) {
558
+ state.medHold = 255;
559
+ state.nextRow = state.medDecay;
560
+ }
561
+ else {
562
+ state.medHold--;
563
+ }
564
+ }
565
+ const ev = currentEvent(core, chn);
566
+ // FTM interrupts (InstrumentSynth.cpp:278-283).
567
+ handleFtmInterrupt(state, 'ftmPitchChangeJump', isNote(ev.note));
568
+ handleFtmInterrupt(state, 'ftmVolumeChangeJump', ev.fxt === 0x80);
569
+ handleFtmInterrupt(state, 'ftmSampleChangeJump', ev.ins !== 0);
570
+ handleFtmInterrupt(state, 'ftmReleaseJump', ev.note === 0x81); // XMP_KEY_OFF
571
+ handleFtmInterrupt(state, 'ftmVolumeDownJump', ev.fxt === 0x86);
572
+ handleFtmInterrupt(state, 'ftmPortamentoJump', ev.fxt === 0x87);
573
+ // FC volume bend + step timing (InstrumentSynth.cpp:285-338). C's
574
+ // `!m_stepsRemain--` tests zero AND post-decrements every tick.
575
+ const stepsWasZero = state.stepsRemain === 0;
576
+ if (state.stepsRemain !== 0)
577
+ state.stepsRemain--; // post-decrement side effect (0 → -1 wrap impossible: unsigned)
578
+ if (!handleFcVolumeBend(state, false) && state.stepSpeed !== 0 && stepsWasZero) {
579
+ // Yep, MED executes this before a potential SPD command may change the
580
+ // step speed on this very row...
581
+ state.stepsRemain = state.stepSpeed - 1;
582
+ if (state.medVolumeStep !== 0) {
583
+ state.volumeFactor = clamp(state.volumeFactor + state.medVolumeStep, 0, 16384);
584
+ }
585
+ if (state.medPeriodStep !== 0) {
586
+ state.periodAdd = satI16(state.periodAdd - state.medPeriodStep);
587
+ }
588
+ if (state.medVolumeEnv !== 255 && xc.ins >= 0 && core.module.instruments[xc.ins]) {
589
+ const instr = core.module.instruments[xc.ins];
590
+ const v = medEnvelopeFromSample(core.module, instr, state.medVolumeEnv & 0x7f, state.medVolumeEnvPos);
591
+ state.volumeFactor = clamp((v + 128) * 64, 0, 16384);
592
+ if (state.medVolumeEnvPos < 127) {
593
+ state.medVolumeEnvPos++;
594
+ }
595
+ else if ((state.medVolumeEnv & 0x80) !== 0) {
596
+ state.medVolumeEnvPos = 0;
597
+ }
598
+ }
599
+ if (state.ticksRemain !== 0) {
600
+ if (state.currentRow < events.length) {
601
+ evaluateRunningEvent(state, events[state.currentRow]);
602
+ }
603
+ state.ticksRemain--;
604
+ }
605
+ else {
606
+ let jumpCount = 0;
607
+ while (state.ticksRemain === 0) {
608
+ state.currentRow = state.nextRow;
609
+ if (state.currentRow >= Math.min(events.length, SYNTH_STOP_ROW))
610
+ break;
611
+ state.nextRow++;
612
+ if (evaluateEvent(core, origChannel, events[state.currentRow], state))
613
+ break;
614
+ if (isJumpEvent(events[state.currentRow])) {
615
+ // This smells like an infinite loop
616
+ if (++jumpCount > 10)
617
+ break;
618
+ }
619
+ chn = state.ftmRealChannel(origChannel, numChannels);
620
+ }
621
+ }
622
+ }
623
+ // MED arpeggio (InstrumentSynth.cpp:340-344).
624
+ if (state.medArpOffset < events.length && events[state.medArpOffset].u16 !== 0) {
625
+ const idx = state.medArpOffset + state.medArpPos;
626
+ if (idx < events.length) {
627
+ state.linearPitchFactor = 16 * events[idx].u8;
628
+ }
629
+ state.medArpPos = ((state.medArpPos + 1) % events[state.medArpOffset].u16) & 0xff;
630
+ }
631
+ // MED vibrato (InstrumentSynth.cpp:345-356).
632
+ if (state.medVibratoDepth !== 0) {
633
+ const offset = Math.floor(state.medVibratoPos / 16);
634
+ if (state.medVibratoEnvelope === 255) {
635
+ state.medVibratoValue = MOD_SINUS_TABLE[(offset * 2) % MOD_SINUS_TABLE.length];
636
+ }
637
+ else if (xc.ins >= 0 && core.module.instruments[xc.ins]) {
638
+ state.medVibratoValue = medEnvelopeFromSample(core.module, core.module.instruments[xc.ins], state.medVibratoEnvelope, offset);
639
+ }
640
+ state.medVibratoPos = (state.medVibratoPos + state.medVibratoSpeed) % (32 * 16);
641
+ }
642
+ // GTK step (InstrumentSynth.cpp:358-372).
643
+ if (state.currentRow < events.length && state.gtkSpeed !== 0 && state.gtkSpeedRemain === 0) {
644
+ state.gtkSpeedRemain = state.gtkSpeed;
645
+ if (state.gtkVolumeStep !== 0) {
646
+ state.volumeFactor = clamp(state.volumeFactor + state.gtkVolumeStep, 0, 16384);
647
+ }
648
+ if (state.gtkPanningStep !== 0) {
649
+ state.panning = clamp(state.panning + state.gtkPanningStep, 0, 4096);
650
+ }
651
+ if (state.gtkPitchStep !== 0) {
652
+ state.gtkPitch = clamp(state.gtkPitch + state.gtkPitchStep, 0, 32768);
653
+ state.linearPitchFactor = translateGt2Pitch(state.gtkPitch);
654
+ }
655
+ }
656
+ // GTK tremor (InstrumentSynth.cpp:373-379).
657
+ if ((state.flags & FcSynthFlags.kGTKTremorEnabled) !== 0) {
658
+ if (state.gtkTremorPos >= state.gtkTremorOnTime + state.gtkTremorOffTime) {
659
+ state.gtkTremorPos = 0;
660
+ }
661
+ if (state.gtkTremorPos >= state.gtkTremorOnTime) {
662
+ state.flags |= FcSynthFlags.kGTKTremorMute;
663
+ }
664
+ else {
665
+ state.flags &= ~FcSynthFlags.kGTKTremorMute;
666
+ }
667
+ state.gtkTremorPos++;
668
+ }
669
+ // GTK tremolo (InstrumentSynth.cpp:380-385).
670
+ if ((state.flags & FcSynthFlags.kGTKTremoloEnabled) !== 0) {
671
+ state.volumeAdd = satI16(Math.trunc((MOD_SINUS_TABLE[(Math.floor(state.gtkVibratoPos / 4)) % MOD_SINUS_TABLE.length] *
672
+ state.gtkVibratoWidth) / 2));
673
+ state.gtkVibratoPos += state.gtkVibratoSpeed;
674
+ }
675
+ // GTK vibrato (InstrumentSynth.cpp:386-392).
676
+ if ((state.flags & FcSynthFlags.kGTKVibratoEnabled) !== 0) {
677
+ state.periodFreqSlide = satI16(Math.trunc((-MOD_SINUS_TABLE[(Math.floor(state.gtkVibratoPos / 4)) % MOD_SINUS_TABLE.length] *
678
+ state.gtkVibratoWidth) / 96));
679
+ state.gtkVibratoPos += state.gtkVibratoSpeed;
680
+ }
681
+ // FTM LFOs (InstrumentSynth.cpp:394-445).
682
+ for (let li = 0; li < state.ftmLfo.length; li++) {
683
+ const lfo = state.ftmLfo[li];
684
+ if (lfo.speed === 0 && lfo.depth === 0)
685
+ continue;
686
+ const lutPos = Math.trunc((lfo.position * 256 + 96) / 192); // muldivr_unsigned(pos,256,192)
687
+ let value = 0;
688
+ switch (lfo.targetWaveform & 0x07) {
689
+ case 0:
690
+ value = IT_SINUS_TABLE[lutPos & 0xff];
691
+ break;
692
+ case 1:
693
+ value = lutPos < 128 ? 64 : -64;
694
+ break;
695
+ case 2:
696
+ value = 64 - Math.abs(((lutPos + 64) % 256) - 128);
697
+ break;
698
+ case 3:
699
+ value = 64 - Math.trunc(lutPos / 2);
700
+ break;
701
+ case 4:
702
+ value = Math.trunc(lutPos / 2) - 64;
703
+ break;
704
+ }
705
+ if ((lfo.targetWaveform & 0xf0) < 0xa0)
706
+ value += 64;
707
+ value *= lfo.depth; // -8192...+8192 or 0...16384 for LFO targets
708
+ switch (lfo.targetWaveform & 0xf0) {
709
+ case 0x10:
710
+ state.ftmLfo[0].speed = value / 64 & 0xff;
711
+ break;
712
+ case 0x20:
713
+ state.ftmLfo[1].speed = value / 64 & 0xff;
714
+ break;
715
+ case 0x30:
716
+ state.ftmLfo[2].speed = value / 64 & 0xff;
717
+ break;
718
+ case 0x40:
719
+ state.ftmLfo[3].speed = value / 64 & 0xff;
720
+ break;
721
+ case 0x50:
722
+ state.ftmLfo[0].depth = value / 64 & 0xff;
723
+ break;
724
+ case 0x60:
725
+ state.ftmLfo[1].depth = value / 64 & 0xff;
726
+ break;
727
+ case 0x70:
728
+ state.ftmLfo[2].depth = value / 64 & 0xff;
729
+ break;
730
+ case 0x80:
731
+ state.ftmLfo[3].depth = value / 64 & 0xff;
732
+ break;
733
+ case 0xa0:
734
+ state.volumeAdd = satI16(value * 4);
735
+ break;
736
+ case 0xf0:
737
+ state.periodFreqSlide = satI16(Math.trunc(value / 8));
738
+ break;
739
+ }
740
+ let newPos = (lfo.position + lfo.speed) & 0xffff;
741
+ if (newPos >= 192) {
742
+ newPos -= 192;
743
+ if ((lfo.targetWaveform & 0x08) !== 0) {
744
+ lfo.speed = 0;
745
+ newPos = 191;
746
+ }
747
+ }
748
+ lfo.position = newPos & 0xff;
749
+ }
750
+ // Future Composer (InstrumentSynth.cpp:448-478).
751
+ if ((state.flags & FcSynthFlags.kFCVibratoDelaySet) !== 0 &&
752
+ state.fcVibratoDelay > 0) {
753
+ state.fcVibratoDelay--;
754
+ }
755
+ else if (state.fcVibratoDepth !== 0) {
756
+ if ((state.flags & FcSynthFlags.kFCVibratoStep) !== 0) {
757
+ const delta = state.fcVibratoDepth * 2;
758
+ state.fcVibratoValue += state.fcVibratoSpeed;
759
+ if (state.fcVibratoValue > delta) {
760
+ state.fcVibratoValue = delta;
761
+ state.flags ^= FcSynthFlags.kFCVibratoStep;
762
+ }
763
+ }
764
+ else {
765
+ state.fcVibratoValue -= state.fcVibratoSpeed;
766
+ if (state.fcVibratoValue < 0) {
767
+ state.fcVibratoValue = 0;
768
+ state.flags ^= FcSynthFlags.kFCVibratoStep;
769
+ }
770
+ }
771
+ }
772
+ if (state.fcPitchBendRemain !== 0) {
773
+ state.flags ^= FcSynthFlags.kFCPitchBendStep;
774
+ if ((state.flags & FcSynthFlags.kFCPitchBendStep) !== 0) {
775
+ state.fcPitchBendRemain--;
776
+ state.periodAdd -= satI16(state.fcPitchBendSpeed * 4) & 0xffff;
777
+ }
778
+ }
779
+ void xc;
780
+ }
781
+ /** EvaluateRunningEvent (InstrumentSynth.cpp:968-979) — only the Puma ramps
782
+ * tick; FC/MED/GTK events have no running-tick behaviour. */
783
+ function evaluateRunningEvent(state, event) {
784
+ switch (event.type) {
785
+ case SynthEventType.Puma_VolumeRamp:
786
+ if (event.byte2 !== 0) {
787
+ state.volumeAdd = satI16(Math.trunc(((event.byte1 +
788
+ Math.trunc(((event.u8 - event.byte1) * state.ticksRemain) / event.byte2)) *
789
+ 256 - 16384) & 0xffff));
790
+ }
791
+ break;
792
+ case SynthEventType.Puma_PitchRamp:
793
+ if (event.byte2 !== 0) {
794
+ const start = (event.u8 << 24) >> 24; // int8
795
+ const end = (event.byte1 << 24) >> 24; // int8
796
+ state.periodAdd = satI16(Math.trunc((start +
797
+ Math.trunc(((start - end) * state.ticksRemain) / event.byte2)) * 4));
798
+ }
799
+ break;
800
+ default:
801
+ break;
802
+ }
803
+ }
804
+ /** HandleFTMInterrupt (InstrumentSynth.cpp:981-997). */
805
+ function handleFtmInterrupt(state, field, condition) {
806
+ const target = state[field];
807
+ if (target === SYNTH_STOP_ROW || !condition)
808
+ return;
809
+ state.nextRow = target;
810
+ state.ticksRemain = 0;
811
+ state[field] = SYNTH_STOP_ROW;
812
+ }
813
+ /** HandleFCVolumeBend (InstrumentSynth.cpp:1000-1017). */
814
+ export function handleFcVolumeBend(state, forceRun) {
815
+ if (state.fcVolumeBendRemain === 0 && !forceRun)
816
+ return false;
817
+ state.flags ^= FcSynthFlags.kFCVolumeBendStep;
818
+ if ((state.flags & FcSynthFlags.kFCVolumeBendStep) !== 0) {
819
+ state.fcVolumeBendRemain--;
820
+ const target = state.volumeFactor + state.fcVolumeBendSpeed * 256;
821
+ if (target < 0 || target >= 32768)
822
+ state.fcVolumeBendRemain = 0;
823
+ state.volumeFactor = clamp(target, 0, 16384);
824
+ }
825
+ return true;
826
+ }
827
+ /** TranslateGT2Pitch (Snd_fx.cpp:5918-5926 area) — GT2 pitch (4096 = middle C)
828
+ * to a linear-pitch factor in 1/65536 semitone units. */
829
+ function translateGt2Pitch(pitch) {
830
+ // C: TranslateGT2Pitch(p) = round(2048 * pow(2, (p - 4096) / 4096))? The
831
+ // exact formula in Snd_fx.cpp:
832
+ // return static_cast<int16>(Util::muldivr_unsigned(PhaseIncrementFinesTable
833
+ // ...)) — ported at the call site by formula:
834
+ // linearPitchFactor = 16 * semitones where semitone step = pitch/4096*12?
835
+ // The reference (Snd_fx.cpp:5913-5926):
836
+ // int16 TranslateGT2Pitch(uint16 pitch) { return Util::Round(pow(2.0,
837
+ // (int)pitch / 4096.0 - 1) * 32768); } — WRONG? Port the literal:
838
+ return Math.round(Math.pow(2, pitch / 4096 - 1) * 32768) & 0xffff;
839
+ }
840
+ const siblingStatesAccessors = [];
841
+ export function setSiblingStatesAccessor(channel, fn) {
842
+ siblingStatesAccessors[channel] = fn;
843
+ }
844
+ export function getSiblingStates(channel) {
845
+ return siblingStatesAccessors[channel]?.(channel) ?? null;
846
+ }
847
+ function evaluateEvent(core, channel, event, state) {
848
+ const mod = core.module;
849
+ const xc = core.ctx.channelStates[channel];
850
+ switch (event.type) {
851
+ case SynthEventType.StopScript:
852
+ state.nextRow = SYNTH_STOP_ROW;
853
+ return true;
854
+ case SynthEventType.Jump:
855
+ state.nextRow = event.u16;
856
+ return false;
857
+ case SynthEventType.JumpIfTrue:
858
+ if ((state.flags & FcSynthFlags.kJumpConditionSet) !== 0) {
859
+ state.nextRow = event.u16;
860
+ }
861
+ return false;
862
+ case SynthEventType.Delay:
863
+ state.ticksRemain = event.u16;
864
+ return true;
865
+ case SynthEventType.SetStepSpeed:
866
+ state.stepSpeed = event.u8;
867
+ if (event.byte1 !== 0)
868
+ state.stepsRemain = state.stepSpeed - 1;
869
+ return false;
870
+ case SynthEventType.JumpMarker:
871
+ return false;
872
+ case SynthEventType.SampleOffset:
873
+ case SynthEventType.SampleOffsetAdd:
874
+ case SynthEventType.SampleOffsetSub: {
875
+ let pos = eventValue24Bit(event);
876
+ const v = voiceAt(core, channel);
877
+ if (v !== null) {
878
+ if (event.type === SynthEventType.SampleOffsetAdd) {
879
+ pos += v.pos;
880
+ }
881
+ else if (event.type === SynthEventType.SampleOffsetSub) {
882
+ pos = v.pos - pos;
883
+ }
884
+ else {
885
+ pos += state.ftmSampleStart;
886
+ }
887
+ v.pos = Math.min(voiceLength(v), Math.max(0, Math.trunc(pos)));
888
+ }
889
+ return false;
890
+ }
891
+ case SynthEventType.SetLoopCounter:
892
+ if (state.loopCount === 0 || event.u8 !== 0) {
893
+ state.loopCount = 1 + Math.min(event.u16, 0xfffe);
894
+ }
895
+ return false;
896
+ case SynthEventType.EvaluateLoopCounter:
897
+ if (state.loopCount > 1)
898
+ state.nextRow = event.u16;
899
+ if (state.loopCount !== 0)
900
+ state.loopCount--;
901
+ return false;
902
+ case SynthEventType.NoteCut:
903
+ xc.note_flags |= NoteFlag.FADEOUT;
904
+ return false;
905
+ case SynthEventType.GTK_KeyOff:
906
+ state.gtkKeyOffOffset = event.u16;
907
+ return false;
908
+ case SynthEventType.GTK_SetVolume:
909
+ state.volumeFactor = event.u16;
910
+ return false;
911
+ case SynthEventType.GTK_SetPitch:
912
+ state.gtkPitch = event.u16;
913
+ state.linearPitchFactor = translateGt2Pitch(event.u16);
914
+ state.periodAdd = 0;
915
+ return false;
916
+ case SynthEventType.GTK_SetPanning:
917
+ state.panning = event.u16;
918
+ return false;
919
+ case SynthEventType.GTK_SetVolumeStep:
920
+ state.gtkVolumeStep = (event.u16 << 16) >> 16;
921
+ return false;
922
+ case SynthEventType.GTK_SetPitchStep:
923
+ state.gtkPitchStep = (event.u16 << 16) >> 16;
924
+ return false;
925
+ case SynthEventType.GTK_SetPanningStep:
926
+ state.gtkPanningStep = (event.u16 << 16) >> 16;
927
+ return false;
928
+ case SynthEventType.GTK_SetSpeed:
929
+ state.gtkSpeed = state.gtkSpeedRemain = event.u8;
930
+ return false;
931
+ case SynthEventType.GTK_EnableTremor:
932
+ if (event.u8 !== 0)
933
+ state.flags |= FcSynthFlags.kGTKTremorEnabled;
934
+ else
935
+ state.flags &= ~FcSynthFlags.kGTKTremorEnabled;
936
+ return false;
937
+ case SynthEventType.GTK_SetTremorTime:
938
+ if (event.u8 !== 0)
939
+ state.gtkTremorOnTime = event.u8;
940
+ if (event.byte1 !== 0)
941
+ state.gtkTremorOffTime = event.byte1;
942
+ state.gtkTremorPos = 0;
943
+ return false;
944
+ case SynthEventType.GTK_EnableTremolo:
945
+ if (event.u8 !== 0)
946
+ state.flags |= FcSynthFlags.kGTKTremoloEnabled;
947
+ else
948
+ state.flags &= ~FcSynthFlags.kGTKTremoloEnabled;
949
+ state.gtkVibratoPos = 0;
950
+ if (state.gtkVibratoWidth === 0)
951
+ state.gtkVibratoWidth = 8;
952
+ if (state.gtkVibratoSpeed === 0)
953
+ state.gtkVibratoSpeed = 16;
954
+ return false;
955
+ case SynthEventType.GTK_EnableVibrato:
956
+ if (event.u8 !== 0)
957
+ state.flags |= FcSynthFlags.kGTKVibratoEnabled;
958
+ else
959
+ state.flags &= ~FcSynthFlags.kGTKVibratoEnabled;
960
+ state.periodFreqSlide = 0;
961
+ state.gtkVibratoPos = 0;
962
+ if (state.gtkVibratoWidth === 0)
963
+ state.gtkVibratoWidth = 3;
964
+ if (state.gtkVibratoSpeed === 0)
965
+ state.gtkVibratoSpeed = 8;
966
+ return false;
967
+ case SynthEventType.GTK_SetVibratoParams:
968
+ if (event.u8 !== 0)
969
+ state.gtkVibratoWidth = event.u8;
970
+ if (event.byte1 !== 0)
971
+ state.gtkVibratoSpeed = event.byte1;
972
+ return false;
973
+ case SynthEventType.Puma_SetWaveform:
974
+ if (event.u8 !== 0) {
975
+ state.pumaStartWaveform = event.u8;
976
+ state.pumaEndWaveform = event.u8;
977
+ state.pumaWaveform = event.u8;
978
+ state.pumaWaveformStep = 0;
979
+ }
980
+ else {
981
+ state.pumaStartWaveform = event.byte1;
982
+ state.pumaEndWaveform = event.byte2;
983
+ state.pumaWaveformStep = 1;
984
+ }
985
+ if (event.u8 !== 0 || event.byte1 === event.byte2) {
986
+ channelSetSample(core, channel, state.pumaWaveform, true, state);
987
+ }
988
+ return false;
989
+ case SynthEventType.Puma_VolumeRamp:
990
+ if (event.byte2 !== 0) {
991
+ state.volumeAdd = satI16(Math.trunc((event.byte1 +
992
+ Math.trunc(((event.u8 - event.byte1) * state.ticksRemain) / event.byte2)) * 256 -
993
+ 16384));
994
+ }
995
+ else {
996
+ state.volumeAdd = satI16(event.u8 * 256 - 16384);
997
+ }
998
+ return false;
999
+ case SynthEventType.Puma_StopVoice:
1000
+ core.virt.releaseChannel(channel, 3 /* PastNote.FADE */);
1001
+ return false;
1002
+ case SynthEventType.Puma_SetPitch:
1003
+ state.periodAdd = satI16(((event.u8 << 24) >> 24) * 4);
1004
+ state.linearPitchFactor = 0;
1005
+ return false;
1006
+ case SynthEventType.Puma_PitchRamp:
1007
+ if (event.byte2 !== 0) {
1008
+ state.periodAdd = satI16(Math.trunc((((event.u8 << 24) >> 24) +
1009
+ Math.trunc((((event.u8 << 24) >> 24) - ((event.byte1 << 24) >> 24)) * state.ticksRemain) / event.byte2) * 4));
1010
+ }
1011
+ else {
1012
+ state.periodAdd = satI16(((event.byte1 << 24) >> 24) * 4);
1013
+ }
1014
+ state.linearPitchFactor = 0;
1015
+ return false;
1016
+ case SynthEventType.Mupp_SetWaveform: {
1017
+ const ins = event.u8 !== 0 ? event.u8 - 1 : xc.ins;
1018
+ if (ins >= 0 && ins < mod.instruments.length) {
1019
+ const sample = mod.instruments[ins].map[60] ?? 0;
1020
+ if (event.byte1 !== 0) {
1021
+ channelSetSample(core, channel, sample + event.byte1 - 1, true, state);
1022
+ state.volumeFactor = clamp(event.byte2 * 256, 0, 16384);
1023
+ }
1024
+ else {
1025
+ state.volumeFactor = clamp(event.byte2 * 256, 0, 16384);
1026
+ }
1027
+ }
1028
+ return false;
1029
+ }
1030
+ case SynthEventType.MED_DefineArpeggio: {
1031
+ // C: m_medArpOffset = events.size(); push entries...
1032
+ // The FC port constructs events beforehand; DefineArpeggio appends
1033
+ // u16 count + count u8 entries to the script at parse time. At
1034
+ // runtime: m_medArpOffset = current index, then handled in NextTick.
1035
+ state.medArpOffset = state.currentRow;
1036
+ // The parsed script carries the count + values after this event.
1037
+ return false;
1038
+ }
1039
+ case SynthEventType.MED_JumpScript: {
1040
+ // Cross-script jump (InstrumentSynth.cpp:700-706): jump the given
1041
+ // sibling script's state to the JumpMarker with the requested
1042
+ // position and clear its step counter.
1043
+ const sib = getSiblingStates(channel);
1044
+ if (sib?.scripts && event.u8 < sib.states.length && event.u8 < sib.scripts.length) {
1045
+ const targetState = sib.states[event.u8];
1046
+ const script = sib.scripts[event.u8];
1047
+ targetState.jumpToPosition(script, event.u16);
1048
+ targetState.stepsRemain = 0;
1049
+ }
1050
+ return false;
1051
+ }
1052
+ case SynthEventType.MED_SetEnvelope:
1053
+ if (event.byte2 !== 0) {
1054
+ state.medVolumeEnv = (event.u8 & 0x3f) | (event.byte1 !== 0 ? 0x80 : 0x00);
1055
+ }
1056
+ else {
1057
+ state.medVibratoEnvelope = event.u8;
1058
+ }
1059
+ state.medVolumeEnvPos = 0;
1060
+ return false;
1061
+ case SynthEventType.MED_SetVolume:
1062
+ state.volumeFactor = event.u8 * 256;
1063
+ return true;
1064
+ case SynthEventType.MED_SetWaveform: {
1065
+ const instr = mod.instruments[xc.ins];
1066
+ if (instr) {
1067
+ const base = instr.map[60] ?? 0;
1068
+ channelSetSample(core, channel, base + event.u8, true, state);
1069
+ }
1070
+ return true;
1071
+ }
1072
+ case SynthEventType.MED_SetVibratoSpeed:
1073
+ state.medVibratoSpeed = event.u8;
1074
+ return false;
1075
+ case SynthEventType.MED_SetVibratoDepth:
1076
+ state.medVibratoDepth = event.u8;
1077
+ return false;
1078
+ case SynthEventType.MED_SetVolumeStep:
1079
+ state.medVolumeStep = satI16(((event.u16 << 16) >> 16) * 256);
1080
+ return false;
1081
+ case SynthEventType.MED_SetPeriodStep:
1082
+ state.medPeriodStep = satI16(((event.u16 << 16) >> 16) * 4);
1083
+ return false;
1084
+ case SynthEventType.MED_HoldDecay:
1085
+ state.medHold = event.byte1;
1086
+ state.medDecay = event.u16;
1087
+ return false;
1088
+ case SynthEventType.FTM_SetCondition: {
1089
+ const threshold = event.u8 < 3 ? 2147483647 - ftmPitchToPeriod(event.u16, core, channel) : event.u16;
1090
+ const compare = event.u8 < 3 ? 2147483647 - Math.trunc(xc.period) : 64;
1091
+ switch (event.u8 % 3) {
1092
+ case 0:
1093
+ if (compare === threshold)
1094
+ state.flags |= FcSynthFlags.kJumpConditionSet;
1095
+ else
1096
+ state.flags &= ~FcSynthFlags.kJumpConditionSet;
1097
+ break;
1098
+ case 1:
1099
+ if (compare < threshold)
1100
+ state.flags |= FcSynthFlags.kJumpConditionSet;
1101
+ else
1102
+ state.flags &= ~FcSynthFlags.kJumpConditionSet;
1103
+ break;
1104
+ case 2:
1105
+ if (compare > threshold)
1106
+ state.flags |= FcSynthFlags.kJumpConditionSet;
1107
+ else
1108
+ state.flags &= ~FcSynthFlags.kJumpConditionSet;
1109
+ break;
1110
+ }
1111
+ return false;
1112
+ }
1113
+ case SynthEventType.FTM_SetInterrupt:
1114
+ if ((event.u8 & 0x01) !== 0)
1115
+ state.ftmPitchChangeJump = event.u16;
1116
+ if ((event.u8 & 0x02) !== 0)
1117
+ state.ftmVolumeChangeJump = event.u16;
1118
+ if ((event.u8 & 0x04) !== 0)
1119
+ state.ftmSampleChangeJump = event.u16;
1120
+ if ((event.u8 & 0x08) !== 0)
1121
+ state.ftmReleaseJump = event.u16;
1122
+ if ((event.u8 & 0x10) !== 0)
1123
+ state.ftmPortamentoJump = event.u16;
1124
+ if ((event.u8 & 0x20) !== 0)
1125
+ state.ftmVolumeDownJump = event.u16;
1126
+ return false;
1127
+ case SynthEventType.FTM_PlaySample: {
1128
+ const v = voiceAt(core, channel);
1129
+ if (v !== null) {
1130
+ if (xc.ins >= 0 && xc.ins < mod.samples.length) {
1131
+ // C: chn.pModSample = GetSample(chn.nNewIns)
1132
+ }
1133
+ // Bind the current instrument's sample
1134
+ const smp = v.smp;
1135
+ if (smp >= 0) {
1136
+ const sample = core.getSample(smp);
1137
+ core.virt.setVol(channel, Math.min(64, sample.volume));
1138
+ }
1139
+ v.pos = 0;
1140
+ }
1141
+ return false;
1142
+ }
1143
+ case SynthEventType.FTM_SetPitch:
1144
+ xc.period = ftmPitchToPeriod(event.u16 * 2, core, channel);
1145
+ return false;
1146
+ case SynthEventType.FTM_SetDetune:
1147
+ // Detune always applies to the first channel of a channel pair
1148
+ // (states.states[channel & ~1], InstrumentSynth.cpp:796-800).
1149
+ {
1150
+ const sib = getSiblingStates(channel);
1151
+ const target = sib ? sib.states[channel & ~1] ?? state : state;
1152
+ target.ftmDetune = satI16(event.u16 * -8);
1153
+ }
1154
+ return false;
1155
+ case SynthEventType.FTM_AddDetune:
1156
+ {
1157
+ const sib = getSiblingStates(channel);
1158
+ const target = sib ? sib.states[channel & ~1] ?? state : state;
1159
+ target.ftmDetune = satI16(target.ftmDetune - ((event.u16 << 16) >> 16) * 8);
1160
+ }
1161
+ return false;
1162
+ case SynthEventType.FTM_AddPitch:
1163
+ if (((event.u16 << 16) >> 16) !== 0) {
1164
+ const amount = ((event.u16 << 16) >> 16) * 8;
1165
+ // DoFreqSlide on the channel period (MOD: period -= amount)
1166
+ xc.period -= amount;
1167
+ const limit = ftmPitchToPeriod(((event.u16 << 16) >> 16) < 0 ? 0 : 0x21e, core, channel);
1168
+ if (((event.u16 << 16) >> 16) > 0) {
1169
+ xc.period = Math.min(xc.period, limit);
1170
+ }
1171
+ else {
1172
+ xc.period = Math.max(xc.period, limit);
1173
+ }
1174
+ }
1175
+ return false;
1176
+ case SynthEventType.FTM_SetVolume:
1177
+ xc.volume = Math.min(event.u8, 64);
1178
+ return false;
1179
+ case SynthEventType.FTM_AddVolume: {
1180
+ xc.volume = clamp(xc.volume + ((event.u16 << 16) >> 16), 0, 64);
1181
+ return false;
1182
+ }
1183
+ case SynthEventType.FTM_SetSample: {
1184
+ const v = voiceAt(core, channel);
1185
+ if (v !== null) {
1186
+ state.swapSampleIndex = event.u8 + 1;
1187
+ }
1188
+ return false;
1189
+ }
1190
+ case SynthEventType.FTM_SetSampleStart: {
1191
+ const rel = event.u8;
1192
+ if (rel === 1) {
1193
+ state.ftmSampleStart += Math.min(event.u16, 0xffffffff - state.ftmSampleStart);
1194
+ }
1195
+ else if (rel === 2) {
1196
+ state.ftmSampleStart -= Math.min(event.u16, state.ftmSampleStart);
1197
+ }
1198
+ else {
1199
+ state.ftmSampleStart = event.u16 * 2;
1200
+ }
1201
+ return false;
1202
+ }
1203
+ case SynthEventType.FTM_SetOneshotLength:
1204
+ // InstrumentSynth.cpp:817-835.
1205
+ {
1206
+ const v = voiceAt(core, channel);
1207
+ const smp = v !== null && v.smp >= 0 ? core.getSample(v.smp) : null;
1208
+ if (smp && v !== null) {
1209
+ const loopLength = smp.loopEnd - smp.loopStart;
1210
+ let loopStart = event.u16 * 2;
1211
+ if (event.u8 === 1)
1212
+ loopStart += smp.loopStart;
1213
+ else if (event.u8 === 2)
1214
+ loopStart = smp.loopStart - loopStart;
1215
+ loopStart = clamp(loopStart, 0, smp.length);
1216
+ smp.loopStart = loopStart;
1217
+ smp.loopEnd = Math.min(smp.loopStart + loopLength, smp.length);
1218
+ // chn.nLength = chn.nLoopEnd — the voice's end tracks the loop.
1219
+ const looped = smp.loopEnd > smp.loopStart;
1220
+ if (v.pos >= smp.loopEnd && looped)
1221
+ v.pos = smp.loopStart;
1222
+ if (looped) {
1223
+ v.start = smp.loopStart;
1224
+ v.end = smp.loopEnd;
1225
+ }
1226
+ else {
1227
+ v.end = smp.length;
1228
+ }
1229
+ }
1230
+ }
1231
+ return false;
1232
+ case SynthEventType.FTM_SetRepeatLength:
1233
+ // InstrumentSynth.cpp:836-851.
1234
+ {
1235
+ const v = voiceAt(core, channel);
1236
+ const smp = v !== null && v.smp >= 0 ? core.getSample(v.smp) : null;
1237
+ if (smp && v !== null) {
1238
+ let loopEnd = smp.loopStart + event.u16 * 2;
1239
+ if (event.u8 === 1)
1240
+ loopEnd = smp.loopEnd + event.u16 * 2;
1241
+ else if (event.u8 === 2)
1242
+ loopEnd = smp.loopEnd - event.u16 * 2;
1243
+ loopEnd = clamp(loopEnd, smp.loopStart, smp.length);
1244
+ smp.loopEnd = loopEnd;
1245
+ const looped = smp.loopEnd > smp.loopStart;
1246
+ if (v.pos >= smp.loopEnd && looped)
1247
+ v.pos = smp.loopStart;
1248
+ if (looped) {
1249
+ v.start = smp.loopStart;
1250
+ v.end = smp.loopEnd;
1251
+ }
1252
+ else {
1253
+ v.end = smp.length;
1254
+ }
1255
+ }
1256
+ }
1257
+ return false;
1258
+ case SynthEventType.FTM_CloneTrack:
1259
+ // InstrumentSynth.cpp:852-900.
1260
+ if (event.byte1 < core.module.chn) {
1261
+ const srcXc = core.ctx.channelStates[event.byte1];
1262
+ const srcVi = core.virt.mapChannel(event.byte1);
1263
+ const srcV = srcVi >= 0 ? core.virt.voices[srcVi] ?? null : null;
1264
+ if ((event.byte2 & (0x01 | 0x08)) !== 0) {
1265
+ xc.period = srcXc.period;
1266
+ }
1267
+ if ((event.byte2 & (0x02 | 0x08)) !== 0) {
1268
+ xc.volume = srcXc.volume;
1269
+ }
1270
+ if ((event.byte2 & (0x04 | 0x08)) !== 0 && srcV !== null) {
1271
+ const dstV = voiceAt(core, channel);
1272
+ if (dstV !== null) {
1273
+ dstV.ins = srcV.ins;
1274
+ dstV.smp = srcV.smp;
1275
+ dstV.pos = srcV.pos;
1276
+ dstV.start = srcV.start;
1277
+ dstV.end = srcV.end;
1278
+ }
1279
+ }
1280
+ if ((event.byte2 & 0x08) !== 0) {
1281
+ // Copies the running slide command state (C copies
1282
+ // autoSlide toneporta/volumedown activity + slides).
1283
+ const dstFc = getSiblingStates(channel);
1284
+ const srcFc = getSiblingStates(event.byte1);
1285
+ if (dstFc && srcFc) {
1286
+ dstFc.fcPortaActive = srcFc.fcPortaActive;
1287
+ dstFc.fcPortaParam = srcFc.fcPortaParam;
1288
+ dstFc.fcPortaTick = srcFc.fcPortaTick;
1289
+ dstFc.portaDurationActive = srcFc.portaDurationActive;
1290
+ dstFc.portamentoSlide = srcFc.portamentoSlide;
1291
+ dstFc.swapSampleIndex = srcFc.swapSampleIndex;
1292
+ }
1293
+ }
1294
+ }
1295
+ return false;
1296
+ case SynthEventType.FTM_StartLFO: {
1297
+ const lfo = state.ftmLfo[event.u8 & 3];
1298
+ lfo.targetWaveform = event.byte1;
1299
+ lfo.speed = 0;
1300
+ lfo.depth = 0;
1301
+ lfo.position = 0;
1302
+ return false;
1303
+ }
1304
+ case SynthEventType.FTM_LFOAddSub: {
1305
+ const lfo = state.ftmLfo[event.u8 & 3];
1306
+ const factor = (event.u8 & 4) !== 0 ? -1 : 1;
1307
+ lfo.speed = Math.min(clampU8(lfo.speed + event.byte1 * factor), 0xbf);
1308
+ lfo.depth = Math.min(clampU8(lfo.depth + event.byte2 * factor), 0x7f);
1309
+ return false;
1310
+ }
1311
+ case SynthEventType.FTM_SetWorkTrack:
1312
+ if (event.u8 === 255) {
1313
+ state.ftmWorkTrack = 0;
1314
+ }
1315
+ else if (event.byte1 !== 0 && event.u8 !== 0) {
1316
+ if (state.ftmWorkTrack === 0)
1317
+ state.ftmWorkTrack = channel + 1;
1318
+ state.ftmWorkTrack =
1319
+ (((state.ftmWorkTrack - 1 + event.u8) % mod.chn) + 1) & 0xff;
1320
+ }
1321
+ else if (event.byte1 === 0) {
1322
+ state.ftmWorkTrack = event.u8 + 1;
1323
+ }
1324
+ return false;
1325
+ case SynthEventType.FTM_SetGlobalVolume:
1326
+ // playState.m_nGlobalVolume = event.u16 (0..128 → our p.gvol uses
1327
+ // gvolbase 64 scaling; store raw for now via ctx.p.gvol? p has gvol?)
1328
+ core.ctx.p.gvol = clamp(event.u16 >> 1, 0, 64);
1329
+ return false;
1330
+ case SynthEventType.FTM_SetTempo:
1331
+ // playState.m_nMusicTempo = TEMPO(1777517.482 / clamp(u16, 0x1000, 0x4FFF))
1332
+ core.ctx.p.bpm = Math.trunc(1777517.482 / clamp(event.u16, 0x1000, 0x4fff));
1333
+ return false;
1334
+ case SynthEventType.FTM_SetSpeed:
1335
+ if (event.u16 !== 0)
1336
+ core.ctx.p.speed = event.u16;
1337
+ else
1338
+ core.ctx.p.speed = 0xffff;
1339
+ return false;
1340
+ case SynthEventType.FTM_SetPlayPosition:
1341
+ // Order().FindOrder(pattern) + nextRow — port with a scan.
1342
+ {
1343
+ const ord = mod.xxo.indexOf(event.u16);
1344
+ if (ord >= 0) {
1345
+ core.ctx.p.pos = ord;
1346
+ core.ctx.p.row = event.u8;
1347
+ }
1348
+ }
1349
+ return false;
1350
+ case SynthEventType.FC_SetWaveform: {
1351
+ let waveform = (event.byte1 + 1) & 0xff;
1352
+ if (event.u8 === 0xe9) {
1353
+ waveform = (waveform + event.byte2 * 10 + 90) & 0xff;
1354
+ }
1355
+ channelSetSample(core, channel, waveform - 1, event.u8 !== 0xe4, state);
1356
+ return false;
1357
+ }
1358
+ case SynthEventType.FC_SetPitch:
1359
+ state.fcPitch = (event.u8 << 24) >> 24;
1360
+ return true;
1361
+ case SynthEventType.FC_SetVibrato:
1362
+ state.fcVibratoSpeed = event.u8;
1363
+ state.fcVibratoDepth = event.byte1;
1364
+ if ((state.flags & FcSynthFlags.kFCVibratoDelaySet) === 0) {
1365
+ state.flags |= FcSynthFlags.kFCVibratoDelaySet;
1366
+ state.fcVibratoDelay = event.byte2;
1367
+ state.fcVibratoValue = state.fcVibratoDepth;
1368
+ }
1369
+ return false;
1370
+ case SynthEventType.FC_PitchSlide:
1371
+ state.fcPitchBendSpeed = (event.u8 << 24) >> 24;
1372
+ state.fcPitchBendRemain = event.byte1;
1373
+ return false;
1374
+ case SynthEventType.FC_VolumeSlide:
1375
+ state.fcVolumeBendSpeed = (event.u8 << 24) >> 24;
1376
+ state.fcVolumeBendRemain = event.byte1;
1377
+ handleFcVolumeBend(state, true);
1378
+ return true;
1379
+ }
1380
+ return false;
1381
+ }
1382
+ // (synthApplyChannelState removed — fc.ts owns ApplyChannelState.)
1383
+ import { IT_SINUS_TABLE, } from './linearTables.js';
1384
+ // ---------------------------------------------------------------------------
1385
+ // Small helpers
1386
+ // ---------------------------------------------------------------------------
1387
+ function clamp(v, lo, hi) {
1388
+ return v < lo ? lo : v > hi ? hi : v;
1389
+ }
1390
+ function clampU8(v) {
1391
+ return clamp(v & 0xffffffff, 0, 255) & 0xff;
1392
+ }
1393
+ function satI16(v) {
1394
+ return clamp(v, -32768, 32767);
1395
+ }
1396
+ /** Current pattern event for the channel (chn.rowCommand). */
1397
+ function currentEvent(core, chn) {
1398
+ const mod = core.module;
1399
+ const p = core.ctx.p;
1400
+ const pat = mod.patterns[mod.xxo[p.ord] ?? 0];
1401
+ const row = p.row;
1402
+ const tr = pat?.tracks[chn];
1403
+ return tr?.event[row] ?? { note: 0, ins: 0, vol: 0, fxt: 0, fxp: 0, f2t: 0, f2p: 0 };
1404
+ }
1405
+ function isNote(note) {
1406
+ return note > 0 && note < 0x78; // NOTE_MIN_SPECIAL = 120
1407
+ }
1408
+ function voiceAt(core, chn) {
1409
+ const vi = core.virt.mapChannel(chn);
1410
+ if (vi < 0)
1411
+ return null;
1412
+ return core.virt.voices[vi] ?? null;
1413
+ }
1414
+ /** FTM pitch (Face The Music) → period, for FTM_SetCondition. */
1415
+ function ftmPitchToPeriod(pitch, _core, _chn) {
1416
+ void _core;
1417
+ void _chn;
1418
+ // C TranslateFTMPitch (InstrumentSynth.cpp:134-140):
1419
+ // period = GetPeriodFromNote(NOTE_MIDDLEC - 12 + pitch/16, fineTune, c5spd);
1420
+ // DoFreqSlide(chn, period, (pitch % 16) * 4); → MOD: period -= amount.
1421
+ const FREQ_S3M = [1712, 1616, 1524, 1440, 1356, 1280, 1208, 1140, 1076, 1016, 960, 907];
1422
+ const note = 61 - 12 + Math.floor(pitch / 16); // NOTE_MIDDLEC=61
1423
+ const idx = note % 12, oct = Math.floor(note / 12);
1424
+ const c5Speed = 8363;
1425
+ let period = Math.trunc((c5Speed * (FREQ_S3M[idx] << 5)) / (c5Speed << oct));
1426
+ // DoFreqSlide for MOD (non-linear, no PeriodsAreHertz): period -= amount.
1427
+ period -= (pitch % 16) * 4;
1428
+ return period < 1 ? 1 : period;
1429
+ }
1430
+ //# sourceMappingURL=synth.js.map