spessasynth_lib 3.13.1 → 3.14.0

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.
@@ -0,0 +1,104 @@
1
+ import { SpessaSynthProcessor } from './main_processor.js'
2
+ import { releaseVoice, renderVoice, voiceKilling } from './worklet_methods/voice_control.js'
3
+ import { handleMessage } from './message_protocol/handle_message.js'
4
+ import { callEvent, post, sendChannelProperties } from './message_protocol/message_sending.js'
5
+ import { systemExclusive } from './worklet_methods/system_exclusive.js'
6
+ import { noteOn } from './worklet_methods/note_on.js'
7
+ import { killNote, noteOff, stopAll, stopAllChannels } from './worklet_methods/note_off.js'
8
+ import {
9
+ channelPressure, pitchWheel,
10
+ polyPressure, setChannelTuning, setChannelTuningSemitones, setMasterTuning, setModulationDepth, setOctaveTuning,
11
+ transposeAllChannels,
12
+ transposeChannel,
13
+ } from './worklet_methods/tuning_control.js'
14
+ import {
15
+ controllerChange,
16
+ muteChannel,
17
+ setMasterGain,
18
+ setMasterPan,
19
+ setMIDIVolume,
20
+ } from './worklet_methods/controller_control.js'
21
+ import { disableAndLockVibrato, setVibrato } from './worklet_methods/vibrato_control.js'
22
+ import { dataEntryCoarse, dataEntryFine } from './worklet_methods/data_entry.js'
23
+ import { createWorkletChannel } from './worklet_utilities/worklet_processor_channel.js'
24
+ import { resetAllControllers, resetControllers, resetParameters } from './worklet_methods/reset_controllers.js'
25
+ import {
26
+ clearSoundFont,
27
+ getPreset,
28
+ programChange,
29
+ reloadSoundFont, sampleDump, sendPresetList,
30
+ setDrums,
31
+ setPreset,
32
+ } from './worklet_methods/program_control.js'
33
+ import { applySynthesizerSnapshot, sendSynthesizerSnapshot } from './worklet_methods/snapshot.js'
34
+
35
+ // include other methods
36
+ // voice related
37
+ SpessaSynthProcessor.prototype.renderVoice = renderVoice;
38
+ SpessaSynthProcessor.prototype.releaseVoice = releaseVoice;
39
+ SpessaSynthProcessor.prototype.voiceKilling = voiceKilling;
40
+
41
+ // message port related
42
+ SpessaSynthProcessor.prototype.handleMessage = handleMessage;
43
+ SpessaSynthProcessor.prototype.post = post;
44
+ SpessaSynthProcessor.prototype.sendChannelProperties = sendChannelProperties;
45
+ SpessaSynthProcessor.prototype.callEvent = callEvent;
46
+
47
+ // system exlcusive related
48
+ SpessaSynthProcessor.prototype.systemExclusive = systemExclusive;
49
+
50
+ // note messages related
51
+ SpessaSynthProcessor.prototype.noteOn = noteOn;
52
+ SpessaSynthProcessor.prototype.noteOff = noteOff;
53
+ SpessaSynthProcessor.prototype.polyPressure = polyPressure;
54
+ SpessaSynthProcessor.prototype.killNote = killNote;
55
+ SpessaSynthProcessor.prototype.stopAll = stopAll;
56
+ SpessaSynthProcessor.prototype.stopAllChannels = stopAllChannels;
57
+ SpessaSynthProcessor.prototype.muteChannel = muteChannel;
58
+
59
+ // custom vibrato related
60
+ SpessaSynthProcessor.prototype.setVibrato = setVibrato;
61
+ SpessaSynthProcessor.prototype.disableAndLockVibrato = disableAndLockVibrato;
62
+
63
+ // data entry related
64
+ SpessaSynthProcessor.prototype.dataEntryCoarse = dataEntryCoarse;
65
+ SpessaSynthProcessor.prototype.dataEntryFine = dataEntryFine;
66
+
67
+ // channel related
68
+ SpessaSynthProcessor.prototype.createWorkletChannel = createWorkletChannel;
69
+ SpessaSynthProcessor.prototype.controllerChange = controllerChange;
70
+ SpessaSynthProcessor.prototype.channelPressure = channelPressure;
71
+ SpessaSynthProcessor.prototype.resetAllControllers = resetAllControllers;
72
+ SpessaSynthProcessor.prototype.resetControllers = resetControllers;
73
+ SpessaSynthProcessor.prototype.resetParameters = resetParameters;
74
+
75
+ // master parameter related
76
+ SpessaSynthProcessor.prototype.setMasterGain = setMasterGain;
77
+ SpessaSynthProcessor.prototype.setMasterPan = setMasterPan;
78
+ SpessaSynthProcessor.prototype.setMIDIVolume = setMIDIVolume;
79
+
80
+ // tuning related
81
+ SpessaSynthProcessor.prototype.transposeAllChannels = transposeAllChannels;
82
+ SpessaSynthProcessor.prototype.transposeChannel = transposeChannel;
83
+ SpessaSynthProcessor.prototype.setChannelTuning = setChannelTuning;
84
+ SpessaSynthProcessor.prototype.setChannelTuningSemitones = setChannelTuningSemitones;
85
+ SpessaSynthProcessor.prototype.setMasterTuning = setMasterTuning;
86
+ SpessaSynthProcessor.prototype.setModulationDepth = setModulationDepth;
87
+ SpessaSynthProcessor.prototype.pitchWheel = pitchWheel;
88
+ SpessaSynthProcessor.prototype.setOctaveTuning = setOctaveTuning;
89
+
90
+ // program related
91
+ SpessaSynthProcessor.prototype.programChange = programChange;
92
+ SpessaSynthProcessor.prototype.getPreset = getPreset;
93
+ SpessaSynthProcessor.prototype.setPreset = setPreset;
94
+ SpessaSynthProcessor.prototype.setDrums = setDrums;
95
+ SpessaSynthProcessor.prototype.reloadSoundFont = reloadSoundFont;
96
+ SpessaSynthProcessor.prototype.clearSoundFont = clearSoundFont;
97
+ SpessaSynthProcessor.prototype.sampleDump = sampleDump;
98
+ SpessaSynthProcessor.prototype.sendPresetList = sendPresetList;
99
+
100
+ // snapshot related
101
+ SpessaSynthProcessor.prototype.sendSynthesizerSnapshot = sendSynthesizerSnapshot;
102
+ SpessaSynthProcessor.prototype.applySynthesizerSnapshot = applySynthesizerSnapshot;
103
+
104
+ export {SpessaSynthProcessor}
@@ -1,45 +1,12 @@
1
1
  import { DEFAULT_PERCUSSION, DEFAULT_SYNTH_MODE, VOICE_CAP } from '../synthetizer.js'
2
- import {
3
- createWorkletChannel,
4
- } from './worklet_utilities/worklet_processor_channel.js'
5
-
6
2
  import { SoundFont2 } from '../../soundfont/soundfont.js'
7
- import { handleMessage } from './message_protocol/handle_message.js'
8
- import { systemExclusive } from './worklet_methods/system_exclusive.js'
9
- import { noteOn } from './worklet_methods/note_on.js'
10
- import { dataEntryCoarse, dataEntryFine } from './worklet_methods/data_entry.js'
11
- import { killNote, noteOff, stopAll, stopAllChannels } from './worklet_methods/note_off.js'
12
- import {
13
- controllerChange, muteChannel, setMasterGain, setMasterPan, setMIDIVolume,
14
- } from './worklet_methods/controller_control.js'
15
- import { callEvent, post, sendChannelProperties } from './message_protocol/message_sending.js'
16
- import {
17
- channelPressure,
18
- pitchWheel, polyPressure,
19
- setChannelTuning, setChannelTuningSemitones,
20
- setMasterTuning, setModulationDepth,
21
- transposeAllChannels,
22
- transposeChannel,
23
- } from './worklet_methods/tuning_control.js'
24
- import {
25
- clearSoundFont, getPreset,
26
- programChange,
27
- reloadSoundFont,
28
- sampleDump,
29
- sendPresetList,
30
- setDrums,
31
- setPreset,
32
- } from './worklet_methods/program_control.js'
33
- import { disableAndLockVibrato, setVibrato } from './worklet_methods/vibrato_control.js'
34
3
  import { WorkletSequencer } from '../../sequencer/worklet_sequencer/worklet_sequencer.js'
35
4
  import { SpessaSynthInfo } from '../../utils/loggin.js'
36
- import { applySynthesizerSnapshot, sendSynthesizerSnapshot } from './worklet_methods/snapshot.js'
37
5
  import { consoleColors } from '../../utils/other.js'
38
- import { PAN_SMOOTHING_FACTOR, releaseVoice, renderVoice, voiceKilling } from './worklet_methods/voice_control.js'
39
- import { returnMessageType } from './message_protocol/worklet_message.js'
6
+ import { PAN_SMOOTHING_FACTOR } from './worklet_methods/voice_control.js'
7
+ import { ALL_CHANNELS_OR_DIFFERENT_ACTION, returnMessageType } from './message_protocol/worklet_message.js'
40
8
  import { stbvorbis } from '../../externals/stbvorbis_sync/stbvorbis_sync.min.js'
41
9
  import { VOLUME_ENVELOPE_SMOOTHING_FACTOR } from './worklet_utilities/volume_envelope.js'
42
- import { resetAllControllers, resetControllers, resetParameters } from './worklet_methods/reset_controllers.js'
43
10
 
44
11
 
45
12
  /**
@@ -51,7 +18,8 @@ export const MIN_NOTE_LENGTH = 0.07; // if the note is released faster than that
51
18
 
52
19
  export const SYNTHESIZER_GAIN = 1.0;
53
20
 
54
- class SpessaSynthProcessor extends AudioWorkletProcessor {
21
+ export class SpessaSynthProcessor extends AudioWorkletProcessor
22
+ {
55
23
  /**
56
24
  * Creates a new worklet synthesis system. contains all channels
57
25
  * @param options {{
@@ -66,13 +34,20 @@ class SpessaSynthProcessor extends AudioWorkletProcessor {
66
34
  * }
67
35
  * }}}
68
36
  */
69
- constructor(options) {
37
+ constructor(options)
38
+ {
70
39
  super();
71
40
  this.oneOutputMode = options.processorOptions?.startRenderingData?.oneOutput === true;
72
41
  this._outputsAmount = this.oneOutputMode ? 1 : options.processorOptions.midiChannels;
73
42
 
74
43
  this.enableEventSystem = options.processorOptions.enableEventSystem;
75
44
 
45
+ /**
46
+ * Synth's device id: -1 means all
47
+ * @type {number}
48
+ */
49
+ this.deviceID = ALL_CHANNELS_OR_DIFFERENT_ACTION;
50
+
76
51
  /**
77
52
  * @type {function}
78
53
  */
@@ -304,75 +279,4 @@ class SpessaSynthProcessor extends AudioWorkletProcessor {
304
279
  }
305
280
  return true;
306
281
  }
307
- }
308
-
309
- // include other methods
310
- // voice related
311
- SpessaSynthProcessor.prototype.renderVoice = renderVoice;
312
- SpessaSynthProcessor.prototype.releaseVoice = releaseVoice;
313
- SpessaSynthProcessor.prototype.voiceKilling = voiceKilling;
314
-
315
- // message port related
316
- SpessaSynthProcessor.prototype.handleMessage = handleMessage;
317
- SpessaSynthProcessor.prototype.post = post;
318
- SpessaSynthProcessor.prototype.sendChannelProperties = sendChannelProperties;
319
- SpessaSynthProcessor.prototype.callEvent = callEvent;
320
-
321
- // system exlcusive related
322
- SpessaSynthProcessor.prototype.systemExclusive = systemExclusive;
323
-
324
- // note messages related
325
- SpessaSynthProcessor.prototype.noteOn = noteOn;
326
- SpessaSynthProcessor.prototype.noteOff = noteOff;
327
- SpessaSynthProcessor.prototype.polyPressure = polyPressure;
328
- SpessaSynthProcessor.prototype.killNote = killNote;
329
- SpessaSynthProcessor.prototype.stopAll = stopAll;
330
- SpessaSynthProcessor.prototype.stopAllChannels = stopAllChannels;
331
- SpessaSynthProcessor.prototype.muteChannel = muteChannel;
332
-
333
- // custom vibrato related
334
- SpessaSynthProcessor.prototype.setVibrato = setVibrato;
335
- SpessaSynthProcessor.prototype.disableAndLockVibrato = disableAndLockVibrato;
336
-
337
- // data entry related
338
- SpessaSynthProcessor.prototype.dataEntryCoarse = dataEntryCoarse;
339
- SpessaSynthProcessor.prototype.dataEntryFine = dataEntryFine;
340
-
341
- // channel related
342
- SpessaSynthProcessor.prototype.createWorkletChannel = createWorkletChannel;
343
- SpessaSynthProcessor.prototype.controllerChange = controllerChange;
344
- SpessaSynthProcessor.prototype.channelPressure = channelPressure;
345
- SpessaSynthProcessor.prototype.resetAllControllers = resetAllControllers;
346
- SpessaSynthProcessor.prototype.resetControllers = resetControllers;
347
- SpessaSynthProcessor.prototype.resetParameters = resetParameters;
348
-
349
- // master parameter related
350
- SpessaSynthProcessor.prototype.setMasterGain = setMasterGain;
351
- SpessaSynthProcessor.prototype.setMasterPan = setMasterPan;
352
- SpessaSynthProcessor.prototype.setMIDIVolume = setMIDIVolume;
353
-
354
- // tuning related
355
- SpessaSynthProcessor.prototype.transposeAllChannels = transposeAllChannels;
356
- SpessaSynthProcessor.prototype.transposeChannel = transposeChannel;
357
- SpessaSynthProcessor.prototype.setChannelTuning = setChannelTuning;
358
- SpessaSynthProcessor.prototype.setChannelTuningSemitones = setChannelTuningSemitones;
359
- SpessaSynthProcessor.prototype.setMasterTuning = setMasterTuning;
360
- SpessaSynthProcessor.prototype.setModulationDepth = setModulationDepth;
361
- SpessaSynthProcessor.prototype.pitchWheel = pitchWheel;
362
-
363
- // program related
364
- SpessaSynthProcessor.prototype.programChange = programChange;
365
- SpessaSynthProcessor.prototype.getPreset = getPreset;
366
- SpessaSynthProcessor.prototype.setPreset = setPreset;
367
- SpessaSynthProcessor.prototype.setDrums = setDrums;
368
- SpessaSynthProcessor.prototype.reloadSoundFont = reloadSoundFont;
369
- SpessaSynthProcessor.prototype.clearSoundFont = clearSoundFont;
370
- SpessaSynthProcessor.prototype.sampleDump = sampleDump;
371
- SpessaSynthProcessor.prototype.sendPresetList = sendPresetList;
372
-
373
- // snapshot related
374
- SpessaSynthProcessor.prototype.sendSynthesizerSnapshot = sendSynthesizerSnapshot;
375
- SpessaSynthProcessor.prototype.applySynthesizerSnapshot = applySynthesizerSnapshot;
376
-
377
-
378
- export { SpessaSynthProcessor }
282
+ }
@@ -96,6 +96,7 @@ export function setPreset(channel, preset)
96
96
  {
97
97
  return;
98
98
  }
99
+ delete this.workletProcessorChannels[channel].preset;
99
100
  this.workletProcessorChannels[channel].preset = preset;
100
101
 
101
102
  // reset cached voices
@@ -235,6 +236,11 @@ export function reloadSoundFont(buffer, isOverride = false)
235
236
  });
236
237
  return;
237
238
  }
239
+ this.defaultPreset = this.getPreset(0, 0);
240
+ this.drumPreset = this.getPreset(128, 0);
241
+ this.workletProcessorChannels.forEach((c, cNum) => {
242
+ this.programChange(cNum, c.preset.program);
243
+ });
238
244
  this.post({messageType: returnMessageType.ready, messageData: undefined});
239
245
  this.sendPresetList();
240
246
  SpessaSynthInfo("%cSpessaSynth is ready!", consoleColors.recognized);
@@ -131,6 +131,8 @@ export function resetControllers(channel)
131
131
  }
132
132
  });
133
133
 
134
+ channelObject.channelOctaveTuning.fill(0);
135
+
134
136
  // reset the array
135
137
  channelObject.midiControllers.set(resetArray);
136
138
  channelObject.channelVibrato = {rate: 0, depth: 0, delay: 0};
@@ -19,6 +19,7 @@
19
19
  * @property {number} channelVibrato.rate - vibrato rate in Hz
20
20
  *
21
21
  * @property {number} channelTransposeKeyShift - key shift for the channel
22
+ * @property {Int8Array} channelOctaveTuning - the channel's octave tuning in cents
22
23
  * @property {boolean} isMuted - indicates whether the channel is muted
23
24
  * @property {boolean} drumChannel - indicates whether the channel is a drum channel
24
25
  */
@@ -60,6 +61,7 @@ export function sendSynthesizerSnapshot()
60
61
  lockVibrato: channel.lockVibrato,
61
62
 
62
63
  channelTransposeKeyShift: channel.channelTransposeKeyShift,
64
+ channelOctaveTuning: channel.channelOctaveTuning,
63
65
  isMuted: channel.isMuted,
64
66
  drumChannel: channel.drumChannel
65
67
  }
@@ -118,6 +120,7 @@ export function applySynthesizerSnapshot(snapshot)
118
120
  channelObject.channelVibrato = channelSnapshot.channelVibrato;
119
121
  channelObject.lockVibrato = channelSnapshot.lockVibrato;
120
122
  channelObject.channelTransposeKeyShift = channelSnapshot.channelTransposeKeyShift;
123
+ channelObject.channelOctaveTuning = channelSnapshot.channelOctaveTuning;
121
124
 
122
125
  // restore preset and lock
123
126
  channelObject.lockPreset = false;
@@ -1,16 +1,56 @@
1
1
  import { arrayToHexString, consoleColors } from '../../../utils/other.js'
2
2
  import { SpessaSynthInfo, SpessaSynthWarn } from '../../../utils/loggin.js'
3
3
  import { midiControllers } from '../../../midi_parser/midi_message.js'
4
+ import { ALL_CHANNELS_OR_DIFFERENT_ACTION } from '../message_protocol/worklet_message.js'
5
+
6
+
7
+ /**
8
+ * Calculates freqency for MIDI Tuning Standard
9
+ * @param byte1 {number}
10
+ * @param byte2 {number}
11
+ * @param byte3 {number}
12
+ * @return {number|null}
13
+ */
14
+ function calculateFrequency(byte1, byte2, byte3)
15
+ {
16
+ // handle special case of "no change"
17
+ if (byte1 === 0x7F && byte2 === 0x7F && byte3 === 0x7F)
18
+ {
19
+ return null;
20
+ }
21
+
22
+ // Frequency base for MIDI note number 0
23
+ const baseFrequency = 8.1758;
24
+
25
+ const semitone = byte1;
26
+
27
+ // combine byte2 and byte3
28
+ const fraction = (byte2 << 7) | byte3;
29
+
30
+ // get total cents
31
+ const totalCents = semitone * 100 + (fraction / 16384) * 100;
32
+
33
+ return baseFrequency * Math.pow(2, totalCents / 1200);
34
+ }
35
+
36
+
4
37
  /**
5
38
  * Executes a system exclusive
6
39
  * @param messageData {number[]|IndexedByteArray} - the message data without f0
7
40
  * @param channelOffset {number}
8
41
  * @this {SpessaSynthProcessor}
9
42
  */
10
-
11
43
  export function systemExclusive(messageData, channelOffset = 0)
12
44
  {
13
45
  const type = messageData[0];
46
+ if(this.deviceID !== ALL_CHANNELS_OR_DIFFERENT_ACTION && messageData[1] !== 0x7F)
47
+ {
48
+ if(this.deviceID !== messageData[1])
49
+ {
50
+ // not our device ID
51
+ return;
52
+ }
53
+ }
14
54
  switch (type)
15
55
  {
16
56
  default:
@@ -21,91 +61,188 @@ export function systemExclusive(messageData, channelOffset = 0)
21
61
 
22
62
  // non realtime
23
63
  case 0x7E:
24
- // gm system
25
- if(messageData[2] === 0x09)
26
- {
27
- if(messageData[3] === 0x01)
28
- {
29
- SpessaSynthInfo("%cGM system on", consoleColors.info);
30
- this.system = "gm";
31
- }
32
- else if(messageData[3] === 0x03)
33
- {
34
- SpessaSynthInfo("%cGM2 system on", consoleColors.info);
35
- this.system = "gm2";
36
- }
37
- else
38
- {
39
- SpessaSynthInfo("%cGM system off, defaulting to GS", consoleColors.info);
40
- this.system = "gs";
41
- }
42
- }
43
- break;
44
-
45
- // realtime
46
- // https://midi.org/midi-1-0-universal-system-exclusive-messages
47
64
  case 0x7F:
48
- if(messageData[2] === 0x04)
65
+ switch(messageData[2])
49
66
  {
50
- let cents
51
- // device control
52
- switch(messageData[3])
53
- {
54
- case 0x01:
55
- // main volume
56
- const vol = messageData[5] << 7 | messageData[4];
57
- this.setMIDIVolume(vol / 16384);
58
- SpessaSynthInfo(`%cMaster Volume. Volume: %c${vol}`,
59
- consoleColors.info,
60
- consoleColors.value);
61
- break;
62
-
63
- case 0x02:
64
- // main balance
65
- // midi spec page 62
66
- const balance = messageData[5] << 7 | messageData[4];
67
- const pan = (balance - 8192) / 8192;
68
- this.setMasterPan(pan);
69
- SpessaSynthInfo(`%cMaster Pan. Pan: %c${pan}`,
70
- consoleColors.info,
71
- consoleColors.value);
72
- break;
67
+ case 0x04:
68
+ let cents
69
+ // device control
70
+ switch(messageData[3])
71
+ {
72
+ case 0x01:
73
+ // main volume
74
+ const vol = messageData[5] << 7 | messageData[4];
75
+ this.setMIDIVolume(vol / 16384);
76
+ SpessaSynthInfo(`%cMaster Volume. Volume: %c${vol}`,
77
+ consoleColors.info,
78
+ consoleColors.value);
79
+ break;
80
+
81
+ case 0x02:
82
+ // main balance
83
+ // midi spec page 62
84
+ const balance = messageData[5] << 7 | messageData[4];
85
+ const pan = (balance - 8192) / 8192;
86
+ this.setMasterPan(pan);
87
+ SpessaSynthInfo(`%cMaster Pan. Pan: %c${pan}`,
88
+ consoleColors.info,
89
+ consoleColors.value);
90
+ break;
73
91
 
74
92
 
75
- case 0x03:
76
- // fine tuning
77
- const tuningValue = ((messageData[5] << 7) | messageData[6]) - 8192;
78
- cents = Math.floor(tuningValue / 81.92); // [-100;+99] cents range
79
- this.setMasterTuning(cents);
80
- SpessaSynthInfo(`%cMaster Fine Tuning. Cents: %c${cents}`,
81
- consoleColors.info,
82
- consoleColors.value);
83
- break;
84
-
85
- case 0x04:
86
- // coarse tuning
87
- // lsb is ignored
88
- const semitones = messageData[5] - 64;
89
- cents = semitones * 100;
90
- this.setMasterTuning(cents);
91
- SpessaSynthInfo(`%cMaster Coarse Tuning. Cents: %c${cents}`,
92
- consoleColors.info,
93
- consoleColors.value)
94
- break;
95
-
96
- default:
97
- SpessaSynthWarn(
98
- `%cUnrecognized MIDI Device Control Real-time message: %c${arrayToHexString(messageData)}`,
99
- consoleColors.warn,
100
- consoleColors.unrecognized);
101
- }
102
- }
103
- else
104
- {
105
- SpessaSynthWarn(
106
- `%cUnrecognized MIDI Real-time message: %c${arrayToHexString(messageData)}`,
107
- consoleColors.warn,
108
- consoleColors.unrecognized);
93
+ case 0x03:
94
+ // fine tuning
95
+ const tuningValue = ((messageData[5] << 7) | messageData[6]) - 8192;
96
+ cents = Math.floor(tuningValue / 81.92); // [-100;+99] cents range
97
+ this.setMasterTuning(cents);
98
+ SpessaSynthInfo(`%cMaster Fine Tuning. Cents: %c${cents}`,
99
+ consoleColors.info,
100
+ consoleColors.value);
101
+ break;
102
+
103
+ case 0x04:
104
+ // coarse tuning
105
+ // lsb is ignored
106
+ const semitones = messageData[5] - 64;
107
+ cents = semitones * 100;
108
+ this.setMasterTuning(cents);
109
+ SpessaSynthInfo(`%cMaster Coarse Tuning. Cents: %c${cents}`,
110
+ consoleColors.info,
111
+ consoleColors.value)
112
+ break;
113
+
114
+ default:
115
+ SpessaSynthWarn(
116
+ `%cUnrecognized MIDI Device Control Real-time message: %c${arrayToHexString(messageData)}`,
117
+ consoleColors.warn,
118
+ consoleColors.unrecognized);
119
+ }
120
+ break;
121
+
122
+ case 0x09:
123
+ // gm system related
124
+ if(messageData[3] === 0x01)
125
+ {
126
+ SpessaSynthInfo("%cGM system on", consoleColors.info);
127
+ this.system = "gm";
128
+ }
129
+ else if(messageData[3] === 0x03)
130
+ {
131
+ SpessaSynthInfo("%cGM2 system on", consoleColors.info);
132
+ this.system = "gm2";
133
+ }
134
+ else
135
+ {
136
+ SpessaSynthInfo("%cGM system off, defaulting to GS", consoleColors.info);
137
+ this.system = "gs";
138
+ }
139
+ break;
140
+
141
+ // MIDI Tuning standard
142
+ // https://midi.org/midi-tuning-updated-specification
143
+ case 0x08:
144
+ switch(messageData[3])
145
+ {
146
+ // single note change
147
+ case 0x02:
148
+ const tuningProgram = messageData[4];
149
+ const numberOfChanges = messageData[5];
150
+ const keys = [];
151
+ let currentMessageIndex = 6
152
+ for (let i = 0; i < numberOfChanges; i++)
153
+ {
154
+ keys.push(messageData[currentMessageIndex]);
155
+ currentMessageIndex++;
156
+ }
157
+ const frequencies = [];
158
+ for (let i = 0; i < numberOfChanges; i++)
159
+ {
160
+ frequencies.push(calculateFrequency(
161
+ messageData[currentMessageIndex++],
162
+ messageData[currentMessageIndex++],
163
+ messageData[currentMessageIndex++],
164
+ ));
165
+ }
166
+ console.log(tuningProgram, numberOfChanges, keys, frequencies)
167
+ break;
168
+
169
+ // octave tuning (1 byte)
170
+ // and octave tuning (2 bytes)
171
+ case 0x09:
172
+ case 0x08:
173
+ // get tuning:
174
+ const newOctaveTuning = new Int8Array(12);
175
+ // start from bit 7
176
+ if(messageData[3] === 0x08)
177
+ {
178
+ // 1 byte tuning: 0 is -64 cents, 64 is 0, 127 is +63
179
+ for (let i = 0; i < 12; i++)
180
+ {
181
+ newOctaveTuning[i] = messageData[7 + i] - 64;
182
+ }
183
+ }
184
+ else
185
+ {
186
+ // 2 byte tuning. Like fine tune: 0 is -100 cents, 8192 is 0 cents, 16383 is +100 cents
187
+ for (let i = 0; i < 24; i += 2)
188
+ {
189
+ const tuning = ((messageData[7 + i] << 7) | messageData[8 + i]) - 8192;
190
+ newOctaveTuning[i / 2] = Math.floor(tuning / 81.92); // map to [-100;+99] cents
191
+ }
192
+ }
193
+ // apply to channels (ordered from 0)
194
+ // bit 1: 14 and 15
195
+ if((messageData[4] & 1) === 1)
196
+ {
197
+ this.setOctaveTuning(14 + channelOffset, newOctaveTuning);
198
+ }
199
+ if(((messageData[4] >> 1) & 1) === 1)
200
+ {
201
+ this.setOctaveTuning(15 + channelOffset, newOctaveTuning);
202
+ }
203
+
204
+ // bit 2: channels 7 to 13
205
+ for (let i = 0; i < 7; i++)
206
+ {
207
+ const bit = (messageData[5] >> i) & 1;
208
+ if(bit === 1)
209
+ {
210
+ this.setOctaveTuning(7 + i + channelOffset, newOctaveTuning);
211
+ }
212
+ }
213
+
214
+ // bit 3: channels 0 to 16
215
+ for (let i = 0; i < 7; i++)
216
+ {
217
+ const bit = (messageData[6] >> i) & 1;
218
+ if(bit === 1)
219
+ {
220
+ this.setOctaveTuning(i + channelOffset, newOctaveTuning);
221
+ }
222
+ }
223
+
224
+ SpessaSynthInfo(`%cMIDI Octave Scale ${
225
+ messageData[3] === 0x08 ? "(1 byte)" : "(2 bytes)"
226
+ } tuning via Tuning: %c${newOctaveTuning.join(" ")}`,
227
+ consoleColors.info,
228
+ consoleColors.value);
229
+ break;
230
+
231
+ default:
232
+ SpessaSynthWarn(
233
+ `%cUnrecognized MIDI Tuning standard message: %c${arrayToHexString(messageData)}`,
234
+ consoleColors.warn,
235
+ consoleColors.unrecognized)
236
+ break;
237
+ }
238
+ break;
239
+
240
+ default:
241
+ SpessaSynthWarn(
242
+ `%cUnrecognized MIDI Realtime/non realtime message: %c${arrayToHexString(messageData)}`,
243
+ consoleColors.warn,
244
+ consoleColors.unrecognized)
245
+
109
246
  }
110
247
  break;
111
248
 
@@ -192,4 +192,19 @@ export function polyPressure(channel, midiNote, pressure)
192
192
  midiNote: midiNote,
193
193
  pressure: pressure
194
194
  });
195
+ }
196
+
197
+ /**
198
+ * Sets the octave tuning for a given channel
199
+ * @this {SpessaSynthProcessor}
200
+ * @param channel {number} usually 0-15: the channel to use
201
+ * @param tuning {Int8Array} LENGTH of 12!!! relative cent tuning. min -128 max 127.
202
+ */
203
+ export function setOctaveTuning(channel, tuning)
204
+ {
205
+ if(tuning.length !== 12)
206
+ {
207
+ throw new Error("Tuning is not the length of 12.");
208
+ }
209
+ this.workletProcessorChannels[channel].channelOctaveTuning = tuning;
195
210
  }
@@ -60,7 +60,8 @@ export function renderVoice(
60
60
  let cents = voice.modulatedGenerators[generatorTypes.fineTune]
61
61
  + channel.customControllers[customControllers.channelTuning]
62
62
  + channel.customControllers[customControllers.channelTransposeFine]
63
- + channel.customControllers[customControllers.masterTuning];
63
+ + channel.customControllers[customControllers.masterTuning]
64
+ + channel.channelOctaveTuning[voice.midiNote % 12];
64
65
  let semitones = voice.modulatedGenerators[generatorTypes.coarseTune]
65
66
  + channel.customControllers[customControllers.channelTuningSemitones];
66
67