spessasynth_lib 0.0.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/.idea/modules.xml +8 -0
- package/.idea/spessasynth_lib.iml +12 -0
- package/.idea/vcs.xml +6 -0
- package/copy_version.sh +38 -0
- package/index.js +73 -0
- package/package/@types/externals/stbvorbis_sync/stbvorbis_sync.min.d.ts +1 -0
- package/package/@types/index.d.ts +34 -0
- package/package/@types/midi_handler/midi_handler.d.ts +39 -0
- package/package/@types/midi_handler/web_midi_link.d.ts +12 -0
- package/package/@types/midi_parser/midi_data.d.ts +95 -0
- package/package/@types/midi_parser/midi_editor.d.ts +45 -0
- package/package/@types/midi_parser/midi_loader.d.ts +100 -0
- package/package/@types/midi_parser/midi_message.d.ts +154 -0
- package/package/@types/midi_parser/midi_writer.d.ts +6 -0
- package/package/@types/midi_parser/rmidi_writer.d.ts +9 -0
- package/package/@types/midi_parser/used_keys_loaded.d.ts +7 -0
- package/package/@types/sequencer/sequencer.d.ts +180 -0
- package/package/@types/sequencer/worklet_sequencer/sequencer_message.d.ts +28 -0
- package/package/@types/soundfont/read/generators.d.ts +98 -0
- package/package/@types/soundfont/read/instruments.d.ts +50 -0
- package/package/@types/soundfont/read/modulators.d.ts +73 -0
- package/package/@types/soundfont/read/presets.d.ts +87 -0
- package/package/@types/soundfont/read/riff_chunk.d.ts +31 -0
- package/package/@types/soundfont/read/samples.d.ts +134 -0
- package/package/@types/soundfont/read/zones.d.ts +141 -0
- package/package/@types/soundfont/soundfont.d.ts +76 -0
- package/package/@types/soundfont/write/ibag.d.ts +6 -0
- package/package/@types/soundfont/write/igen.d.ts +6 -0
- package/package/@types/soundfont/write/imod.d.ts +6 -0
- package/package/@types/soundfont/write/inst.d.ts +6 -0
- package/package/@types/soundfont/write/pbag.d.ts +6 -0
- package/package/@types/soundfont/write/pgen.d.ts +6 -0
- package/package/@types/soundfont/write/phdr.d.ts +6 -0
- package/package/@types/soundfont/write/pmod.d.ts +6 -0
- package/package/@types/soundfont/write/sdta.d.ts +11 -0
- package/package/@types/soundfont/write/shdr.d.ts +8 -0
- package/package/@types/soundfont/write/soundfont_trimmer.d.ts +6 -0
- package/package/@types/soundfont/write/write.d.ts +21 -0
- package/package/@types/synthetizer/audio_effects/effects_config.d.ts +29 -0
- package/package/@types/synthetizer/audio_effects/fancy_chorus.d.ts +93 -0
- package/package/@types/synthetizer/audio_effects/reverb.d.ts +7 -0
- package/package/@types/synthetizer/synth_event_handler.d.ts +161 -0
- package/package/@types/synthetizer/synthetizer.d.ts +294 -0
- package/package/@types/synthetizer/worklet_system/message_protocol/worklet_message.d.ts +89 -0
- package/package/@types/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.d.ts +134 -0
- package/package/@types/synthetizer/worklet_url.d.ts +5 -0
- package/package/@types/utils/buffer_to_wav.d.ts +8 -0
- package/package/@types/utils/byte_functions/big_endian.d.ts +13 -0
- package/package/@types/utils/byte_functions/little_endian.d.ts +35 -0
- package/package/@types/utils/byte_functions/string.d.ts +22 -0
- package/package/@types/utils/byte_functions/variable_length_quantity.d.ts +12 -0
- package/package/@types/utils/indexed_array.d.ts +21 -0
- package/package/@types/utils/loggin.d.ts +26 -0
- package/package/@types/utils/other.d.ts +32 -0
- package/package/LICENSE +26 -0
- package/package/README.md +84 -0
- package/package/externals/NOTICE +9 -0
- package/package/externals/libvorbis/@types/OggVorbisEncoder.d.ts +34 -0
- package/package/externals/libvorbis/OggVorbisEncoder.min.js +1 -0
- package/package/externals/stbvorbis_sync/@types/stbvorbis_sync.d.ts +12 -0
- package/package/externals/stbvorbis_sync/LICENSE +202 -0
- package/package/externals/stbvorbis_sync/stbvorbis_sync.min.js +1 -0
- package/package/index.js +73 -0
- package/package/midi_handler/README.md +3 -0
- package/package/midi_handler/midi_handler.js +118 -0
- package/package/midi_handler/web_midi_link.js +41 -0
- package/package/midi_parser/README.md +3 -0
- package/package/midi_parser/midi_data.js +121 -0
- package/package/midi_parser/midi_editor.js +557 -0
- package/package/midi_parser/midi_loader.js +502 -0
- package/package/midi_parser/midi_message.js +234 -0
- package/package/midi_parser/midi_writer.js +95 -0
- package/package/midi_parser/rmidi_writer.js +271 -0
- package/package/midi_parser/used_keys_loaded.js +172 -0
- package/package/package.json +43 -0
- package/package/sequencer/README.md +23 -0
- package/package/sequencer/sequencer.js +439 -0
- package/package/sequencer/worklet_sequencer/events.js +92 -0
- package/package/sequencer/worklet_sequencer/play.js +309 -0
- package/package/sequencer/worklet_sequencer/process_event.js +167 -0
- package/package/sequencer/worklet_sequencer/process_tick.js +85 -0
- package/package/sequencer/worklet_sequencer/sequencer_message.js +39 -0
- package/package/sequencer/worklet_sequencer/song_control.js +193 -0
- package/package/sequencer/worklet_sequencer/worklet_sequencer.js +218 -0
- package/package/soundfont/README.md +8 -0
- package/package/soundfont/read/generators.js +212 -0
- package/package/soundfont/read/instruments.js +125 -0
- package/package/soundfont/read/modulators.js +249 -0
- package/package/soundfont/read/presets.js +300 -0
- package/package/soundfont/read/riff_chunk.js +81 -0
- package/package/soundfont/read/samples.js +398 -0
- package/package/soundfont/read/zones.js +310 -0
- package/package/soundfont/soundfont.js +357 -0
- package/package/soundfont/write/ibag.js +39 -0
- package/package/soundfont/write/igen.js +75 -0
- package/package/soundfont/write/imod.js +46 -0
- package/package/soundfont/write/inst.js +34 -0
- package/package/soundfont/write/pbag.js +39 -0
- package/package/soundfont/write/pgen.js +77 -0
- package/package/soundfont/write/phdr.js +42 -0
- package/package/soundfont/write/pmod.js +46 -0
- package/package/soundfont/write/sdta.js +72 -0
- package/package/soundfont/write/shdr.js +54 -0
- package/package/soundfont/write/soundfont_trimmer.js +169 -0
- package/package/soundfont/write/write.js +180 -0
- package/package/synthetizer/README.md +6 -0
- package/package/synthetizer/audio_effects/effects_config.js +21 -0
- package/package/synthetizer/audio_effects/fancy_chorus.js +120 -0
- package/package/synthetizer/audio_effects/impulse_response_2.flac +0 -0
- package/package/synthetizer/audio_effects/reverb.js +24 -0
- package/package/synthetizer/synth_event_handler.js +156 -0
- package/package/synthetizer/synthetizer.js +766 -0
- package/package/synthetizer/worklet_processor.min.js +13 -0
- package/package/synthetizer/worklet_system/README.md +6 -0
- package/package/synthetizer/worklet_system/main_processor.js +363 -0
- package/package/synthetizer/worklet_system/message_protocol/handle_message.js +197 -0
- package/package/synthetizer/worklet_system/message_protocol/message_sending.js +74 -0
- package/package/synthetizer/worklet_system/message_protocol/worklet_message.js +121 -0
- package/package/synthetizer/worklet_system/minify_processor.sh +4 -0
- package/package/synthetizer/worklet_system/worklet_methods/controller_control.js +230 -0
- package/package/synthetizer/worklet_system/worklet_methods/data_entry.js +277 -0
- package/package/synthetizer/worklet_system/worklet_methods/note_off.js +109 -0
- package/package/synthetizer/worklet_system/worklet_methods/note_on.js +91 -0
- package/package/synthetizer/worklet_system/worklet_methods/program_control.js +183 -0
- package/package/synthetizer/worklet_system/worklet_methods/reset_controllers.js +177 -0
- package/package/synthetizer/worklet_system/worklet_methods/snapshot.js +129 -0
- package/package/synthetizer/worklet_system/worklet_methods/system_exclusive.js +272 -0
- package/package/synthetizer/worklet_system/worklet_methods/tuning_control.js +195 -0
- package/package/synthetizer/worklet_system/worklet_methods/vibrato_control.js +29 -0
- package/package/synthetizer/worklet_system/worklet_methods/voice_control.js +233 -0
- package/package/synthetizer/worklet_system/worklet_processor.js +9 -0
- package/package/synthetizer/worklet_system/worklet_utilities/lfo.js +23 -0
- package/package/synthetizer/worklet_system/worklet_utilities/lowpass_filter.js +130 -0
- package/package/synthetizer/worklet_system/worklet_utilities/modulation_envelope.js +73 -0
- package/package/synthetizer/worklet_system/worklet_utilities/modulator_curves.js +86 -0
- package/package/synthetizer/worklet_system/worklet_utilities/stereo_panner.js +81 -0
- package/package/synthetizer/worklet_system/worklet_utilities/unit_converter.js +66 -0
- package/package/synthetizer/worklet_system/worklet_utilities/volume_envelope.js +265 -0
- package/package/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js +83 -0
- package/package/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js +234 -0
- package/package/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js +116 -0
- package/package/synthetizer/worklet_system/worklet_utilities/worklet_voice.js +272 -0
- package/package/synthetizer/worklet_url.js +5 -0
- package/package/utils/README.md +4 -0
- package/package/utils/buffer_to_wav.js +101 -0
- package/package/utils/byte_functions/big_endian.js +28 -0
- package/package/utils/byte_functions/little_endian.js +74 -0
- package/package/utils/byte_functions/string.js +97 -0
- package/package/utils/byte_functions/variable_length_quantity.js +37 -0
- package/package/utils/encode_vorbis.js +30 -0
- package/package/utils/indexed_array.js +41 -0
- package/package/utils/loggin.js +79 -0
- package/package/utils/other.js +54 -0
- package/package.json +43 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { WorkletSequencerReturnMessageType } from './sequencer_message.js'
|
|
2
|
+
import { consoleColors, formatTime } from '../../utils/other.js'
|
|
3
|
+
import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo, SpessaSynthWarn } from '../../utils/loggin.js'
|
|
4
|
+
import { ticksToSeconds } from './play.js'
|
|
5
|
+
import { MidiData } from '../../midi_parser/midi_data.js'
|
|
6
|
+
import { MIDI } from '../../midi_parser/midi_loader.js'
|
|
7
|
+
import { getUsedProgramsAndKeys } from '../../midi_parser/used_keys_loaded.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param trackNum {number}
|
|
11
|
+
* @param port {number}
|
|
12
|
+
* @this {WorkletSequencer}
|
|
13
|
+
*/
|
|
14
|
+
export function assignMIDIPort(trackNum, port)
|
|
15
|
+
{
|
|
16
|
+
// assign new 16 channels if the port is not occupied yet
|
|
17
|
+
if(this.midiPortChannelOffset === 0)
|
|
18
|
+
{
|
|
19
|
+
this.midiPortChannelOffset += 16;
|
|
20
|
+
this.midiPortChannelOffsets[port] = 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if(this.midiPortChannelOffsets[port] === undefined)
|
|
24
|
+
{
|
|
25
|
+
if(this.synth.workletProcessorChannels.length < this.midiPortChannelOffset + 15)
|
|
26
|
+
{
|
|
27
|
+
this._addNewMidiPort();
|
|
28
|
+
}
|
|
29
|
+
this.midiPortChannelOffsets[port] = this.midiPortChannelOffset;
|
|
30
|
+
this.midiPortChannelOffset += 16;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
this.midiPorts[trackNum] = port;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Loads a new sequence
|
|
38
|
+
* @param parsedMidi {MIDI}
|
|
39
|
+
* @this {WorkletSequencer}
|
|
40
|
+
*/
|
|
41
|
+
export function loadNewSequence(parsedMidi)
|
|
42
|
+
{
|
|
43
|
+
this.stop();
|
|
44
|
+
if (!parsedMidi.tracks)
|
|
45
|
+
{
|
|
46
|
+
throw "No tracks supplied!";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.oneTickToSeconds = 60 / (120 * parsedMidi.timeDivision)
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @type {MIDI}
|
|
53
|
+
*/
|
|
54
|
+
this.midiData = parsedMidi;
|
|
55
|
+
|
|
56
|
+
// check for embedded soundfont
|
|
57
|
+
if(this.midiData.embeddedSoundFont !== undefined)
|
|
58
|
+
{
|
|
59
|
+
this.synth.reloadSoundFont(this.midiData.embeddedSoundFont);
|
|
60
|
+
// preload all samples
|
|
61
|
+
this.synth.soundfont.samples.forEach(s => s.getAudioData());
|
|
62
|
+
}
|
|
63
|
+
else
|
|
64
|
+
{
|
|
65
|
+
SpessaSynthGroupCollapsed("%cPreloading samples...", consoleColors.info);
|
|
66
|
+
// smart preloading: load only samples used in the midi!
|
|
67
|
+
const used = getUsedProgramsAndKeys(this.midiData, this.synth.soundfont);
|
|
68
|
+
for (const [programBank, combos] of Object.entries(used))
|
|
69
|
+
{
|
|
70
|
+
const bank = parseInt(programBank.split(":")[0]);
|
|
71
|
+
const program = parseInt(programBank.split(":")[1]);
|
|
72
|
+
const preset = this.synth.soundfont.getPreset(bank, program);
|
|
73
|
+
SpessaSynthInfo(`%cPreloading used samples on %c${preset.presetName}%c...`,
|
|
74
|
+
consoleColors.info,
|
|
75
|
+
consoleColors.recognized,
|
|
76
|
+
consoleColors.info)
|
|
77
|
+
for (const combo of combos) {
|
|
78
|
+
const split = combo.split("-");
|
|
79
|
+
preset.preloadSpecific(parseInt(split[0]), parseInt(split[1]));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
SpessaSynthGroupEnd();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* the midi track data
|
|
87
|
+
* @type {MidiMessage[][]}
|
|
88
|
+
*/
|
|
89
|
+
this.tracks = this.midiData.tracks;
|
|
90
|
+
|
|
91
|
+
// clear last port data
|
|
92
|
+
this.midiPortChannelOffset = 0;
|
|
93
|
+
this.midiPortChannelOffsets = {};
|
|
94
|
+
// copy over the port data
|
|
95
|
+
this.midiPorts = this.midiData.midiPorts;
|
|
96
|
+
|
|
97
|
+
// assign port offsets
|
|
98
|
+
this.midiData.midiPorts.forEach((port, trackIndex) => {
|
|
99
|
+
this.assignMIDIPort(trackIndex, port);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Same as Audio.duration (seconds)
|
|
104
|
+
* @type {number}
|
|
105
|
+
*/
|
|
106
|
+
this.duration = this.midiData.duration;
|
|
107
|
+
this.firstNoteTime = ticksToSeconds(this.midiData.tempoChanges, this.midiData.firstNoteOn, this.midiData.timeDivision);
|
|
108
|
+
SpessaSynthInfo(`%cTotal song time: ${formatTime(Math.ceil(this.duration)).time}`, consoleColors.recognized);
|
|
109
|
+
|
|
110
|
+
this.post(WorkletSequencerReturnMessageType.songChange, [new MidiData(this.midiData), this.songIndex]);
|
|
111
|
+
|
|
112
|
+
this.synth.resetAllControllers();
|
|
113
|
+
if(this.duration <= 1)
|
|
114
|
+
{
|
|
115
|
+
SpessaSynthWarn(`%cVery short song: (${formatTime(Math.round(this.duration)).time}). Disabling loop!`,
|
|
116
|
+
consoleColors.warn);
|
|
117
|
+
this.loop = false;
|
|
118
|
+
}
|
|
119
|
+
this.play(true);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @param midiBuffers {MIDIFile[]}
|
|
124
|
+
* @this {WorkletSequencer}
|
|
125
|
+
*/
|
|
126
|
+
export function loadNewSongList(midiBuffers)
|
|
127
|
+
{
|
|
128
|
+
/**
|
|
129
|
+
* parse the MIDIs (only the array buffers, MIDI is unchanged)
|
|
130
|
+
* @type {MIDI[]}
|
|
131
|
+
*/
|
|
132
|
+
this.songs = midiBuffers.reduce((mids, b) => {
|
|
133
|
+
if(b.duration)
|
|
134
|
+
{
|
|
135
|
+
mids.push(b);
|
|
136
|
+
return mids;
|
|
137
|
+
}
|
|
138
|
+
try
|
|
139
|
+
{
|
|
140
|
+
mids.push(new MIDI(b.binary, b.altName || ""));
|
|
141
|
+
}
|
|
142
|
+
catch (e)
|
|
143
|
+
{
|
|
144
|
+
this.post(WorkletSequencerReturnMessageType.midiError, e.message);
|
|
145
|
+
return mids;
|
|
146
|
+
}
|
|
147
|
+
return mids;
|
|
148
|
+
}, []);
|
|
149
|
+
if(this.songs.length < 1)
|
|
150
|
+
{
|
|
151
|
+
console.log("no valid songs!")
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
this.songIndex = 0;
|
|
155
|
+
if(this.songs.length > 1)
|
|
156
|
+
{
|
|
157
|
+
this.loop = false;
|
|
158
|
+
}
|
|
159
|
+
this.loadNewSequence(this.songs[this.songIndex]);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @this {WorkletSequencer}
|
|
164
|
+
*/
|
|
165
|
+
export function nextSong()
|
|
166
|
+
{
|
|
167
|
+
if(this.songs.length === 1)
|
|
168
|
+
{
|
|
169
|
+
this.currentTime = 0;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
this.songIndex++;
|
|
173
|
+
this.songIndex %= this.songs.length;
|
|
174
|
+
this.loadNewSequence(this.songs[this.songIndex]);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @this {WorkletSequencer}
|
|
179
|
+
*/
|
|
180
|
+
export function previousSong()
|
|
181
|
+
{
|
|
182
|
+
if(this.songs.length === 1)
|
|
183
|
+
{
|
|
184
|
+
this.currentTime = 0;
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
this.songIndex--;
|
|
188
|
+
if(this.songIndex < 0)
|
|
189
|
+
{
|
|
190
|
+
this.songIndex = this.songs.length - 1;
|
|
191
|
+
}
|
|
192
|
+
this.loadNewSequence(this.songs[this.songIndex]);
|
|
193
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { WorkletSequencerReturnMessageType } from './sequencer_message.js'
|
|
2
|
+
import { _addNewMidiPort, _processEvent } from './process_event.js'
|
|
3
|
+
import { _findFirstEventIndex, _processTick } from './process_tick.js'
|
|
4
|
+
import { assignMIDIPort, loadNewSequence, loadNewSongList, nextSong, previousSong } from './song_control.js'
|
|
5
|
+
import { _playTo, _recalculateStartTime, play, setTimeTicks } from './play.js'
|
|
6
|
+
import { messageTypes, midiControllers } from '../../midi_parser/midi_message.js'
|
|
7
|
+
import { post, processMessage, sendMIDIMessage } from './events.js'
|
|
8
|
+
import { SpessaSynthWarn } from '../../utils/loggin.js'
|
|
9
|
+
import { MIDI_CHANNEL_COUNT } from '../../synthetizer/synthetizer.js'
|
|
10
|
+
|
|
11
|
+
class WorkletSequencer
|
|
12
|
+
{
|
|
13
|
+
/**
|
|
14
|
+
* @param spessasynthProcessor {SpessaSynthProcessor}
|
|
15
|
+
*/
|
|
16
|
+
constructor(spessasynthProcessor)
|
|
17
|
+
{
|
|
18
|
+
this.synth = spessasynthProcessor;
|
|
19
|
+
this.ignoreEvents = false;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* If the event should instead be sent back to the main thread instead of synth
|
|
23
|
+
* @type {boolean}
|
|
24
|
+
*/
|
|
25
|
+
this.sendMIDIMessages = false;
|
|
26
|
+
|
|
27
|
+
// event's number in this.events
|
|
28
|
+
/**
|
|
29
|
+
* @type {number[]}
|
|
30
|
+
*/
|
|
31
|
+
this.eventIndex = [];
|
|
32
|
+
this.songIndex = 0;
|
|
33
|
+
|
|
34
|
+
// tracks the time that we have already played
|
|
35
|
+
/**
|
|
36
|
+
* @type {number}
|
|
37
|
+
*/
|
|
38
|
+
this.playedTime = 0;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The (relative) time when the sequencer was paused. If it's not paused then it's undefined.
|
|
42
|
+
* @type {number}
|
|
43
|
+
*/
|
|
44
|
+
this.pausedTime = undefined;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Absolute playback startTime, bases on the synth's time
|
|
48
|
+
* @type {number}
|
|
49
|
+
*/
|
|
50
|
+
this.absoluteStartTime = currentTime;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Controls the playback's rate
|
|
54
|
+
* @type {number}
|
|
55
|
+
*/
|
|
56
|
+
this._playbackRate = 1;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Currently playing notes (for pausing and resuming)
|
|
60
|
+
* @type {{
|
|
61
|
+
* midiNote: number,
|
|
62
|
+
* channel: number,
|
|
63
|
+
* velocity: number,
|
|
64
|
+
* startTime: number
|
|
65
|
+
* }[]}
|
|
66
|
+
*/
|
|
67
|
+
this.playingNotes = [];
|
|
68
|
+
|
|
69
|
+
// controls if the sequencer loops (defaults to true)
|
|
70
|
+
this.loop = true;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* the current track data
|
|
74
|
+
* @type {MIDI}
|
|
75
|
+
*/
|
|
76
|
+
this.midiData = undefined;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* midi port number for the corresponding track
|
|
80
|
+
* @type {number[]}
|
|
81
|
+
*/
|
|
82
|
+
this.midiPorts = [];
|
|
83
|
+
|
|
84
|
+
this.midiPortChannelOffset = 0;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* midi port: channel offset
|
|
88
|
+
* @type {Object<number, number>}
|
|
89
|
+
*/
|
|
90
|
+
this.midiPortChannelOffsets = {};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param value {number}
|
|
95
|
+
*/
|
|
96
|
+
set playbackRate(value)
|
|
97
|
+
{
|
|
98
|
+
const time = this.currentTime;
|
|
99
|
+
this._playbackRate = value;
|
|
100
|
+
this.currentTime = time;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get currentTime()
|
|
104
|
+
{
|
|
105
|
+
// return the paused time if it's set to something other than undefined
|
|
106
|
+
if(this.pausedTime)
|
|
107
|
+
{
|
|
108
|
+
return this.pausedTime;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return (currentTime - this.absoluteStartTime) * this._playbackRate;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
set currentTime(time)
|
|
115
|
+
{
|
|
116
|
+
if(time < this.firstNoteTime || time > this.duration)
|
|
117
|
+
{
|
|
118
|
+
// time is 0
|
|
119
|
+
this.setTimeTicks(this.midiData.firstNoteOn - 1);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
this.stop();
|
|
123
|
+
this.playingNotes = [];
|
|
124
|
+
this.pausedTime = undefined;
|
|
125
|
+
this.post(WorkletSequencerReturnMessageType.timeChange, currentTime - time);
|
|
126
|
+
const isNotFinished = this._playTo(time);
|
|
127
|
+
this._recalculateStartTime(time);
|
|
128
|
+
if(!isNotFinished)
|
|
129
|
+
{
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.play();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Pauses the playback
|
|
137
|
+
* @param isFinished {boolean}
|
|
138
|
+
*/
|
|
139
|
+
pause(isFinished = false)
|
|
140
|
+
{
|
|
141
|
+
if(this.paused)
|
|
142
|
+
{
|
|
143
|
+
SpessaSynthWarn("Already paused");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
this.pausedTime = this.currentTime;
|
|
147
|
+
this.stop();
|
|
148
|
+
this.post(WorkletSequencerReturnMessageType.pause, isFinished);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Stops the playback
|
|
153
|
+
*/
|
|
154
|
+
stop()
|
|
155
|
+
{
|
|
156
|
+
this.clearProcessHandler()
|
|
157
|
+
// disable sustain
|
|
158
|
+
for (let i = 0; i < 16; i++) {
|
|
159
|
+
this.synth.controllerChange(i, midiControllers.sustainPedal, 0);
|
|
160
|
+
}
|
|
161
|
+
this.synth.stopAllChannels();
|
|
162
|
+
if(this.sendMIDIMessages)
|
|
163
|
+
{
|
|
164
|
+
for (let c = 0; c < MIDI_CHANNEL_COUNT; c++)
|
|
165
|
+
{
|
|
166
|
+
this.sendMIDIMessage([messageTypes.controllerChange | c, 120, 0]); // all notes off
|
|
167
|
+
this.sendMIDIMessage([messageTypes.controllerChange | c, 123, 0]); // all sound off
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
_resetTimers()
|
|
173
|
+
{
|
|
174
|
+
this.playedTime = 0
|
|
175
|
+
this.eventIndex = Array(this.tracks.length).fill(0);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* true if paused, false if playing or stopped
|
|
180
|
+
* @returns {boolean}
|
|
181
|
+
*/
|
|
182
|
+
get paused()
|
|
183
|
+
{
|
|
184
|
+
return this.pausedTime !== undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
setProcessHandler()
|
|
188
|
+
{
|
|
189
|
+
this.synth.processTickCallback = this._processTick.bind(this);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
clearProcessHandler()
|
|
193
|
+
{
|
|
194
|
+
this.synth.processTickCallback = undefined;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
WorkletSequencer.prototype.post = post;
|
|
199
|
+
WorkletSequencer.prototype.sendMIDIMessage = sendMIDIMessage;
|
|
200
|
+
WorkletSequencer.prototype.assignMIDIPort = assignMIDIPort;
|
|
201
|
+
WorkletSequencer.prototype.processMessage = processMessage;
|
|
202
|
+
|
|
203
|
+
WorkletSequencer.prototype._processEvent = _processEvent;
|
|
204
|
+
WorkletSequencer.prototype._addNewMidiPort = _addNewMidiPort;
|
|
205
|
+
WorkletSequencer.prototype._processTick = _processTick;
|
|
206
|
+
WorkletSequencer.prototype._findFirstEventIndex = _findFirstEventIndex;
|
|
207
|
+
|
|
208
|
+
WorkletSequencer.prototype.loadNewSequence = loadNewSequence;
|
|
209
|
+
WorkletSequencer.prototype.loadNewSongList = loadNewSongList;
|
|
210
|
+
WorkletSequencer.prototype.nextSong = nextSong;
|
|
211
|
+
WorkletSequencer.prototype.previousSong = previousSong;
|
|
212
|
+
|
|
213
|
+
WorkletSequencer.prototype.play = play;
|
|
214
|
+
WorkletSequencer.prototype._playTo = _playTo;
|
|
215
|
+
WorkletSequencer.prototype.setTimeTicks = setTimeTicks;
|
|
216
|
+
WorkletSequencer.prototype._recalculateStartTime = _recalculateStartTime;
|
|
217
|
+
|
|
218
|
+
export { WorkletSequencer }
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
## This is the SoundFont2 parsing library.
|
|
2
|
+
The code here is responsible for parsing the SoundFont2 file and
|
|
3
|
+
providing an easy way to get the data out.
|
|
4
|
+
Default modulators are also stored here (in `modulators.js`)
|
|
5
|
+
|
|
6
|
+
`read` folder contains the classes that represent the soundfont file
|
|
7
|
+
|
|
8
|
+
`write` folder contains the code for writing out an `.sf2` file.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { IndexedByteArray } from '../../utils/indexed_array.js'
|
|
2
|
+
import { RiffChunk } from './riff_chunk.js'
|
|
3
|
+
import { signedInt16 } from '../../utils/byte_functions/little_endian.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* generators.js
|
|
7
|
+
* purpose: contains enums for generators and their limis parses reads soundfont generators, sums them and applies limits
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @enum {number}
|
|
12
|
+
*/
|
|
13
|
+
export const generatorTypes = {
|
|
14
|
+
startAddrsOffset: 0, // sample control - moves sample start point
|
|
15
|
+
endAddrOffset: 1, // sample control - moves sample end point
|
|
16
|
+
startloopAddrsOffset: 2, // loop control - moves loop start point
|
|
17
|
+
endloopAddrsOffset: 3, // loop control - moves loop end point
|
|
18
|
+
startAddrsCoarseOffset: 4, // sample control - moves sample start point in 32767 increments
|
|
19
|
+
modLfoToPitch: 5, // pitch modulation - modulation lfo pitch modulation in cents
|
|
20
|
+
vibLfoToPitch: 6, // pitch modulation - vibrato lfo pitch modulation in cents
|
|
21
|
+
modEnvToPitch: 7, // pitch modulation - modulation envelope pitch modulation in cents
|
|
22
|
+
initialFilterFc: 8, // filter - lowpass filter cutoff in cents
|
|
23
|
+
initialFilterQ: 9, // filter - lowpass filter resonance
|
|
24
|
+
modLfoToFilterFc: 10, // filter modulation - modulation lfo lowpass filter cutoff in cents
|
|
25
|
+
modEnvToFilterFc: 11, // filter modulation - modulation envelope lowpass filter cutoff in cents
|
|
26
|
+
endAddrsCoarseOffset: 12, // ample control - moves sample end point in 32767 increments
|
|
27
|
+
modLfoToVolume: 13, // modulation lfo - volume (tremolo), where 100 = 10dB
|
|
28
|
+
unused1: 14,
|
|
29
|
+
chorusEffectsSend: 15, // effect send - how much is sent to chorus 0 - 1000
|
|
30
|
+
reverbEffectsSend: 16, // effect send - how much is sent to reverb 0 - 1000
|
|
31
|
+
pan: 17, // panning - where -500 = left, 0 = center, 500 = right
|
|
32
|
+
unused2: 18,
|
|
33
|
+
unused3: 19,
|
|
34
|
+
unused4: 20,
|
|
35
|
+
delayModLFO: 21, // mod lfo - delay for mod lfo to start from zero (weird scale)
|
|
36
|
+
freqModLFO: 22, // mod lfo - frequency of mod lfo, 0 = 8.176Hz, unit: f => 1200log2(f/8.176)
|
|
37
|
+
delayVibLFO: 23, // vib lfo - delay for vibrato lfo to start from zero (weird scale)
|
|
38
|
+
freqVibLFO: 24, // vib lfo - frequency of vibrato lfo, 0 = 8.176Hz, unit: f => 1200log2(f/8.176)
|
|
39
|
+
delayModEnv: 25, // mod env - 0 = 1s declay till mod env starts
|
|
40
|
+
attackModEnv: 26, // mod env - attack of mod env
|
|
41
|
+
holdModEnv: 27, // mod env - hold of mod env
|
|
42
|
+
decayModEnv: 28, // mod env - decay of mod env
|
|
43
|
+
sustainModEnv: 29, // mod env - sustain of mod env
|
|
44
|
+
releaseModEnv: 30, // mod env - release of mod env
|
|
45
|
+
keyNumToModEnvHold: 31, // mod env - also modulating mod envelope hold with key number
|
|
46
|
+
keyNumToModEnvDecay: 32, // mod env - also modulating mod envelope decay with key number
|
|
47
|
+
delayVolEnv: 33, // vol env - delay of envelope from zero (weird scale)
|
|
48
|
+
attackVolEnv: 34, // vol env - attack of envelope
|
|
49
|
+
holdVolEnv: 35, // vol env - hold of envelope
|
|
50
|
+
decayVolEnv: 36, // vol env - decay of envelope
|
|
51
|
+
sustainVolEnv: 37, // vol env - sustain of envelope
|
|
52
|
+
releaseVolEnv: 38, // vol env - release of envelope
|
|
53
|
+
keyNumToVolEnvHold: 39, // vol env - key number to volume envelope hold
|
|
54
|
+
keyNumToVolEnvDecay: 40, // vol env - key number to volume envelope decay
|
|
55
|
+
instrument: 41, // zone - instrument index to use for preset zone
|
|
56
|
+
reserved1: 42,
|
|
57
|
+
keyRange: 43, // zone - key range for which preset / instrument zone is active
|
|
58
|
+
velRange: 44, // zone - velocity range for which preset / instrument zone is active
|
|
59
|
+
startloopAddrsCoarseOffset: 45, // ample control - moves sample loop start point in 32767 increments
|
|
60
|
+
keyNum: 46, // zone - instrument only: always use this midi number (ignore what's pressed)
|
|
61
|
+
velocity: 47, // zone - instrument only: always use this velocity (ignore what's pressed)
|
|
62
|
+
initialAttenuation: 48, // zone - allows turning down the volume, 10 = -1dB
|
|
63
|
+
reserved2: 49,
|
|
64
|
+
endloopAddrsCoarseOffset: 50, // ample control - moves sample loop end point in 32767 increments
|
|
65
|
+
coarseTune: 51, // tune - pitch offset in semitones
|
|
66
|
+
fineTune: 52, // tune - pitch offset in cents
|
|
67
|
+
sampleID: 53, // sample - instrument zone only: which sample to use
|
|
68
|
+
sampleModes: 54, // sample - 0 = no loop, 1 = loop, 2 = reserved, 3 = loop and play till end in release phase
|
|
69
|
+
reserved3: 55,
|
|
70
|
+
scaleTuning: 56, // sample - the degree to which MIDI key number influences pitch, 100 = default
|
|
71
|
+
exclusiveClass: 57, // sample - = cut = choke group
|
|
72
|
+
overridingRootKey: 58, // sample - can override the sample's original pitch
|
|
73
|
+
unused5: 59,
|
|
74
|
+
endOper: 60 // end marker
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @type {{min: number, max: number, def: number}[]}
|
|
79
|
+
*/
|
|
80
|
+
export const generatorLimits = [];
|
|
81
|
+
// offsets
|
|
82
|
+
generatorLimits[generatorTypes.startAddrsOffset] = {min: 0, max: 32768, def: 0};
|
|
83
|
+
generatorLimits[generatorTypes.endAddrOffset] = {min: -32768, max: 32768, def: 0};
|
|
84
|
+
generatorLimits[generatorTypes.startloopAddrsOffset] = {min: -32768, max: 32768, def: 0};
|
|
85
|
+
generatorLimits[generatorTypes.endloopAddrsOffset] = {min: -32768, max: 32768, def: 0};
|
|
86
|
+
generatorLimits[generatorTypes.startAddrsCoarseOffset] = {min: 0, max: 32768, def: 0};
|
|
87
|
+
|
|
88
|
+
// pitch influence
|
|
89
|
+
generatorLimits[generatorTypes.modLfoToPitch] = {min: -12000, max: 12000, def: 0};
|
|
90
|
+
generatorLimits[generatorTypes.vibLfoToPitch] = {min: -12000, max: 12000, def: 0};
|
|
91
|
+
generatorLimits[generatorTypes.modEnvToPitch] = {min: -12000, max: 12000, def: 0};
|
|
92
|
+
|
|
93
|
+
// lowpass
|
|
94
|
+
generatorLimits[generatorTypes.initialFilterFc] = {min: 1500, max: 13500, def: 13500};
|
|
95
|
+
generatorLimits[generatorTypes.initialFilterQ] = {min: 0, max: 960, def: 0};
|
|
96
|
+
generatorLimits[generatorTypes.modLfoToFilterFc] = {min: -12000, max: 12000, def: 0};
|
|
97
|
+
generatorLimits[generatorTypes.modEnvToFilterFc] = {min: -12000, max: 12000, def: 0};
|
|
98
|
+
|
|
99
|
+
generatorLimits[generatorTypes.endAddrsCoarseOffset] = {min: -32768, max: 32768, def: 0};
|
|
100
|
+
|
|
101
|
+
generatorLimits[generatorTypes.modLfoToVolume] = {min: -960, max: 960, def: 0};
|
|
102
|
+
|
|
103
|
+
// effects, pan
|
|
104
|
+
generatorLimits[generatorTypes.chorusEffectsSend] = {min: 0, max: 1000, def: 0};
|
|
105
|
+
generatorLimits[generatorTypes.reverbEffectsSend] = {min: 0, max: 1000, def: 0};
|
|
106
|
+
generatorLimits[generatorTypes.pan] = {min: -500, max: 500, def: 0};
|
|
107
|
+
|
|
108
|
+
// lfo
|
|
109
|
+
generatorLimits[generatorTypes.delayModLFO] = {min: -12000, max: 5000, def: -12000};
|
|
110
|
+
generatorLimits[generatorTypes.freqModLFO] = {min: -16000, max: 4500, def: 0};
|
|
111
|
+
generatorLimits[generatorTypes.delayVibLFO] = {min: -12000, max: 5000, def: -12000};
|
|
112
|
+
generatorLimits[generatorTypes.freqVibLFO] = {min: -16000, max: 4500, def: 0};
|
|
113
|
+
|
|
114
|
+
// mod env
|
|
115
|
+
generatorLimits[generatorTypes.delayModEnv] = {min: -12000, max: 5000, def: -12000};
|
|
116
|
+
generatorLimits[generatorTypes.attackModEnv] = {min: -12000, max: 8000, def: -12000};
|
|
117
|
+
generatorLimits[generatorTypes.holdModEnv] = {min: -12000, max: 5000, def: -12000};
|
|
118
|
+
generatorLimits[generatorTypes.decayModEnv] = {min: -12000, max: 8000, def: -12000};
|
|
119
|
+
generatorLimits[generatorTypes.sustainModEnv] = {min: 0, max: 1000, def: 0};
|
|
120
|
+
generatorLimits[generatorTypes.releaseModEnv] = {min: -12000, max: 8000, def: -12000};
|
|
121
|
+
// keynum to mod env
|
|
122
|
+
generatorLimits[generatorTypes.keyNumToModEnvHold] = {min: -1200, max: 1200, def: 0};
|
|
123
|
+
generatorLimits[generatorTypes.keyNumToModEnvDecay] = {min: -1200, max: 1200, def: 0};
|
|
124
|
+
|
|
125
|
+
// vol env
|
|
126
|
+
generatorLimits[generatorTypes.delayVolEnv] = {min: -12000, max: 5000, def: -12000};
|
|
127
|
+
generatorLimits[generatorTypes.attackVolEnv] = {min: -12000, max: 8000, def: -12000};
|
|
128
|
+
generatorLimits[generatorTypes.holdVolEnv] = {min: -12000, max: 5000, def: -12000};
|
|
129
|
+
generatorLimits[generatorTypes.decayVolEnv] = {min: -12000, max: 8000, def: -12000};
|
|
130
|
+
generatorLimits[generatorTypes.sustainVolEnv] = {min: 0, max: 1440, def: 0};
|
|
131
|
+
generatorLimits[generatorTypes.releaseVolEnv] = {min: -7200, max: 8000, def: -12000}; // prevent clicks
|
|
132
|
+
// keynum to vol env
|
|
133
|
+
generatorLimits[generatorTypes.keyNumToVolEnvHold] = {min: -1200, max: 1200, def: 0};
|
|
134
|
+
generatorLimits[generatorTypes.keyNumToVolEnvDecay] = {min: -1200, max: 1200, def: 0};
|
|
135
|
+
|
|
136
|
+
generatorLimits[generatorTypes.startloopAddrsCoarseOffset] = {min: -32768, max: 32768, def: 0};
|
|
137
|
+
generatorLimits[generatorTypes.keyNum] = {min: -1, max: 127, def: -1};
|
|
138
|
+
generatorLimits[generatorTypes.velocity] = {min: -1, max: 127, def: -1};
|
|
139
|
+
|
|
140
|
+
generatorLimits[generatorTypes.initialAttenuation] = {min: -250, max: 1440, def: 0}; // soundblaster allows 10dB of gain (divide by 0.4)
|
|
141
|
+
|
|
142
|
+
generatorLimits[generatorTypes.endloopAddrsCoarseOffset] = {min: -32768, max: 32768, def: 0};
|
|
143
|
+
|
|
144
|
+
generatorLimits[generatorTypes.coarseTune] = {min: -120, max: 120, def: 0};
|
|
145
|
+
generatorLimits[generatorTypes.fineTune] = {min: -99, max: 99, def: 0};
|
|
146
|
+
generatorLimits[generatorTypes.scaleTuning] = {min: 0, max: 1200, def: 100};
|
|
147
|
+
generatorLimits[generatorTypes.exclusiveClass] = {min: 0, max: 99999, def: 0};
|
|
148
|
+
generatorLimits[generatorTypes.overridingRootKey] = {min: 0-1, max: 127, def: -1};
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param generatorType {number}
|
|
153
|
+
* @param presetGens {Generator[]}
|
|
154
|
+
* @param instrumentGens {Generator[]}
|
|
155
|
+
*/
|
|
156
|
+
export function addAndClampGenerator(generatorType, presetGens, instrumentGens)
|
|
157
|
+
{
|
|
158
|
+
const limits = generatorLimits[generatorType] || {min: 0, max: 32768, def: 0};
|
|
159
|
+
let presetGen = presetGens.find(g => g.generatorType === generatorType);
|
|
160
|
+
let presetValue = 0;
|
|
161
|
+
if(presetGen)
|
|
162
|
+
{
|
|
163
|
+
presetValue = presetGen.generatorValue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let instruGen = instrumentGens.find(g => g.generatorType === generatorType);
|
|
167
|
+
let instruValue = limits.def;
|
|
168
|
+
if(instruGen)
|
|
169
|
+
{
|
|
170
|
+
instruValue = instruGen.generatorValue;
|
|
171
|
+
}
|
|
172
|
+
return Math.max(limits.min, Math.min(limits.max, instruValue + presetValue));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
export class Generator{
|
|
177
|
+
/**
|
|
178
|
+
* Creates a generator
|
|
179
|
+
* @param dataArray {IndexedByteArray}
|
|
180
|
+
*/
|
|
181
|
+
constructor(dataArray) {
|
|
182
|
+
// 4 bytes:
|
|
183
|
+
// type, type, type, value
|
|
184
|
+
const i = dataArray.currentIndex;
|
|
185
|
+
/**
|
|
186
|
+
* @type {generatorTypes}
|
|
187
|
+
**/
|
|
188
|
+
this.generatorType = (dataArray[i + 1] << 8) | dataArray[i];
|
|
189
|
+
this.generatorValue = signedInt16(dataArray[i + 2], dataArray[i + 3]);
|
|
190
|
+
dataArray.currentIndex += 4;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Reads the generator read
|
|
196
|
+
* @param generatorChunk {RiffChunk}
|
|
197
|
+
* @returns {Generator[]}
|
|
198
|
+
*/
|
|
199
|
+
export function readGenerators(generatorChunk)
|
|
200
|
+
{
|
|
201
|
+
let gens = [];
|
|
202
|
+
while(generatorChunk.chunkData.length > generatorChunk.chunkData.currentIndex)
|
|
203
|
+
{
|
|
204
|
+
gens.push(new Generator(generatorChunk.chunkData));
|
|
205
|
+
}
|
|
206
|
+
if(gens.length > 1)
|
|
207
|
+
{
|
|
208
|
+
// remove terminal
|
|
209
|
+
gens.pop();
|
|
210
|
+
}
|
|
211
|
+
return gens;
|
|
212
|
+
}
|