spessasynth_core 1.1.3 → 1.1.5
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/LICENSE +3 -26
- package/README.md +156 -474
- package/index.js +74 -8
- package/package.json +21 -8
- package/src/externals/fflate/LICENSE +21 -0
- package/src/externals/fflate/fflate.min.js +1 -0
- package/src/externals/stbvorbis_sync/@types/stbvorbis_sync.d.ts +12 -0
- package/src/externals/stbvorbis_sync/LICENSE +202 -0
- package/src/externals/stbvorbis_sync/NOTICE +6 -0
- package/src/externals/stbvorbis_sync/stbvorbis_sync.min.js +1 -0
- package/src/midi/README.md +32 -0
- package/src/midi/basic_midi.js +567 -0
- package/src/midi/midi_builder.js +202 -0
- package/src/midi/midi_loader.js +324 -0
- package/{spessasynth_core/midi_parser → src/midi}/midi_message.js +58 -35
- package/src/midi/midi_sequence.js +224 -0
- package/src/midi/midi_tools/get_note_times.js +154 -0
- package/src/midi/midi_tools/midi_editor.js +611 -0
- package/src/midi/midi_tools/midi_writer.js +99 -0
- package/src/midi/midi_tools/rmidi_writer.js +567 -0
- package/src/midi/midi_tools/used_keys_loaded.js +238 -0
- package/src/midi/xmf_loader.js +454 -0
- package/src/sequencer/README.md +5 -0
- package/src/sequencer/events.js +81 -0
- package/src/sequencer/play.js +349 -0
- package/src/sequencer/process_event.js +165 -0
- package/{spessasynth_core/sequencer/worklet_sequencer → src/sequencer}/process_tick.js +103 -84
- package/src/sequencer/sequencer_engine.js +367 -0
- package/src/sequencer/song_control.js +201 -0
- package/src/soundfont/README.md +13 -0
- package/src/soundfont/basic_soundfont/basic_instrument.js +77 -0
- package/src/soundfont/basic_soundfont/basic_preset.js +336 -0
- package/src/soundfont/basic_soundfont/basic_sample.js +206 -0
- package/src/soundfont/basic_soundfont/basic_soundfont.js +565 -0
- package/src/soundfont/basic_soundfont/basic_zone.js +64 -0
- package/src/soundfont/basic_soundfont/basic_zones.js +43 -0
- package/src/soundfont/basic_soundfont/generator.js +220 -0
- package/src/soundfont/basic_soundfont/modulator.js +378 -0
- package/src/soundfont/basic_soundfont/riff_chunk.js +149 -0
- package/src/soundfont/basic_soundfont/write_dls/art2.js +173 -0
- package/src/soundfont/basic_soundfont/write_dls/articulator.js +49 -0
- package/src/soundfont/basic_soundfont/write_dls/combine_zones.js +400 -0
- package/src/soundfont/basic_soundfont/write_dls/ins.js +103 -0
- package/src/soundfont/basic_soundfont/write_dls/lins.js +18 -0
- package/src/soundfont/basic_soundfont/write_dls/modulator_converter.js +330 -0
- package/src/soundfont/basic_soundfont/write_dls/rgn2.js +121 -0
- package/src/soundfont/basic_soundfont/write_dls/wave.js +94 -0
- package/src/soundfont/basic_soundfont/write_dls/write_dls.js +119 -0
- package/src/soundfont/basic_soundfont/write_dls/wsmp.js +78 -0
- package/src/soundfont/basic_soundfont/write_dls/wvpl.js +32 -0
- package/src/soundfont/basic_soundfont/write_sf2/ibag.js +39 -0
- package/src/soundfont/basic_soundfont/write_sf2/igen.js +80 -0
- package/src/soundfont/basic_soundfont/write_sf2/imod.js +46 -0
- package/src/soundfont/basic_soundfont/write_sf2/inst.js +34 -0
- package/src/soundfont/basic_soundfont/write_sf2/pbag.js +39 -0
- package/src/soundfont/basic_soundfont/write_sf2/pgen.js +82 -0
- package/src/soundfont/basic_soundfont/write_sf2/phdr.js +42 -0
- package/src/soundfont/basic_soundfont/write_sf2/pmod.js +46 -0
- package/src/soundfont/basic_soundfont/write_sf2/sdta.js +80 -0
- package/src/soundfont/basic_soundfont/write_sf2/shdr.js +55 -0
- package/src/soundfont/basic_soundfont/write_sf2/write.js +222 -0
- package/src/soundfont/dls/articulator_converter.js +396 -0
- package/src/soundfont/dls/dls_destinations.js +38 -0
- package/src/soundfont/dls/dls_preset.js +44 -0
- package/src/soundfont/dls/dls_sample.js +75 -0
- package/src/soundfont/dls/dls_soundfont.js +186 -0
- package/src/soundfont/dls/dls_sources.js +62 -0
- package/src/soundfont/dls/dls_zone.js +95 -0
- package/src/soundfont/dls/read_articulation.js +299 -0
- package/src/soundfont/dls/read_instrument.js +121 -0
- package/src/soundfont/dls/read_instrument_list.js +17 -0
- package/src/soundfont/dls/read_lart.js +35 -0
- package/src/soundfont/dls/read_region.js +152 -0
- package/src/soundfont/dls/read_samples.js +270 -0
- package/src/soundfont/load_soundfont.js +21 -0
- package/src/soundfont/read_sf2/generators.js +46 -0
- package/{spessasynth_core/soundfont/chunk → src/soundfont/read_sf2}/instruments.js +20 -14
- package/src/soundfont/read_sf2/modulators.js +36 -0
- package/src/soundfont/read_sf2/presets.js +80 -0
- package/src/soundfont/read_sf2/samples.js +304 -0
- package/src/soundfont/read_sf2/soundfont.js +305 -0
- package/{spessasynth_core/soundfont/chunk → src/soundfont/read_sf2}/zones.js +68 -69
- package/src/synthetizer/README.md +7 -0
- package/src/synthetizer/audio_engine/README.md +9 -0
- package/src/synthetizer/audio_engine/engine_components/compute_modulator.js +266 -0
- package/src/synthetizer/audio_engine/engine_components/controller_tables.js +88 -0
- package/src/synthetizer/audio_engine/engine_components/key_modifier_manager.js +150 -0
- package/{spessasynth_core/synthetizer/worklet_system/worklet_utilities → src/synthetizer/audio_engine/engine_components}/lfo.js +9 -6
- package/src/synthetizer/audio_engine/engine_components/lowpass_filter.js +282 -0
- package/src/synthetizer/audio_engine/engine_components/midi_audio_channel.js +467 -0
- package/src/synthetizer/audio_engine/engine_components/modulation_envelope.js +181 -0
- package/{spessasynth_core/synthetizer/worklet_system/worklet_utilities → src/synthetizer/audio_engine/engine_components}/modulator_curves.js +33 -30
- package/src/synthetizer/audio_engine/engine_components/soundfont_manager.js +221 -0
- package/src/synthetizer/audio_engine/engine_components/stereo_panner.js +120 -0
- package/{spessasynth_core/synthetizer/worklet_system/worklet_utilities → src/synthetizer/audio_engine/engine_components}/unit_converter.js +11 -4
- package/src/synthetizer/audio_engine/engine_components/voice.js +519 -0
- package/src/synthetizer/audio_engine/engine_components/volume_envelope.js +401 -0
- package/src/synthetizer/audio_engine/engine_components/wavetable_oscillator.js +263 -0
- package/src/synthetizer/audio_engine/engine_methods/controller_control/controller_change.js +132 -0
- package/src/synthetizer/audio_engine/engine_methods/controller_control/master_parameters.js +48 -0
- package/src/synthetizer/audio_engine/engine_methods/controller_control/reset_controllers.js +241 -0
- package/src/synthetizer/audio_engine/engine_methods/create_midi_channel.js +27 -0
- package/src/synthetizer/audio_engine/engine_methods/data_entry/data_entry_coarse.js +253 -0
- package/src/synthetizer/audio_engine/engine_methods/data_entry/data_entry_fine.js +66 -0
- package/src/synthetizer/audio_engine/engine_methods/mute_channel.js +17 -0
- package/src/synthetizer/audio_engine/engine_methods/note_on.js +175 -0
- package/src/synthetizer/audio_engine/engine_methods/portamento_time.js +92 -0
- package/src/synthetizer/audio_engine/engine_methods/program_change.js +61 -0
- package/src/synthetizer/audio_engine/engine_methods/render_voice.js +196 -0
- package/src/synthetizer/audio_engine/engine_methods/soundfont_management/clear_sound_font.js +30 -0
- package/src/synthetizer/audio_engine/engine_methods/soundfont_management/get_preset.js +22 -0
- package/src/synthetizer/audio_engine/engine_methods/soundfont_management/reload_sound_font.js +28 -0
- package/src/synthetizer/audio_engine/engine_methods/soundfont_management/send_preset_list.js +31 -0
- package/src/synthetizer/audio_engine/engine_methods/soundfont_management/set_embedded_sound_font.js +21 -0
- package/src/synthetizer/audio_engine/engine_methods/stopping_notes/kill_note.js +20 -0
- package/src/synthetizer/audio_engine/engine_methods/stopping_notes/note_off.js +55 -0
- package/src/synthetizer/audio_engine/engine_methods/stopping_notes/stop_all_channels.js +16 -0
- package/src/synthetizer/audio_engine/engine_methods/stopping_notes/stop_all_notes.js +30 -0
- package/src/synthetizer/audio_engine/engine_methods/stopping_notes/voice_killing.js +63 -0
- package/src/synthetizer/audio_engine/engine_methods/system_exclusive.js +776 -0
- package/src/synthetizer/audio_engine/engine_methods/tuning_control/channel_pressure.js +24 -0
- package/src/synthetizer/audio_engine/engine_methods/tuning_control/pitch_wheel.js +33 -0
- package/src/synthetizer/audio_engine/engine_methods/tuning_control/poly_pressure.js +31 -0
- package/src/synthetizer/audio_engine/engine_methods/tuning_control/set_master_tuning.js +15 -0
- package/src/synthetizer/audio_engine/engine_methods/tuning_control/set_modulation_depth.js +27 -0
- package/src/synthetizer/audio_engine/engine_methods/tuning_control/set_octave_tuning.js +19 -0
- package/src/synthetizer/audio_engine/engine_methods/tuning_control/set_tuning.js +27 -0
- package/src/synthetizer/audio_engine/engine_methods/tuning_control/transpose_all_channels.js +15 -0
- package/src/synthetizer/audio_engine/engine_methods/tuning_control/transpose_channel.js +34 -0
- package/src/synthetizer/audio_engine/main_processor.js +804 -0
- package/src/synthetizer/audio_engine/snapshot/apply_synthesizer_snapshot.js +15 -0
- package/src/synthetizer/audio_engine/snapshot/channel_snapshot.js +175 -0
- package/src/synthetizer/audio_engine/snapshot/synthesizer_snapshot.js +116 -0
- package/src/synthetizer/synth_constants.js +22 -0
- package/{spessasynth_core → src}/utils/README.md +1 -0
- package/src/utils/buffer_to_wav.js +185 -0
- package/src/utils/byte_functions/big_endian.js +32 -0
- package/src/utils/byte_functions/little_endian.js +77 -0
- package/src/utils/byte_functions/string.js +107 -0
- package/src/utils/byte_functions/variable_length_quantity.js +42 -0
- package/src/utils/fill_with_defaults.js +21 -0
- package/src/utils/indexed_array.js +52 -0
- package/{spessasynth_core → src}/utils/loggin.js +70 -78
- package/src/utils/other.js +92 -0
- package/src/utils/sysex_detector.js +58 -0
- package/src/utils/xg_hacks.js +193 -0
- package/.idea/inspectionProfiles/Project_Default.xml +0 -10
- package/.idea/jsLibraryMappings.xml +0 -6
- package/.idea/modules.xml +0 -8
- package/.idea/spessasynth_core.iml +0 -12
- package/.idea/vcs.xml +0 -6
- package/spessasynth_core/midi_parser/README.md +0 -3
- package/spessasynth_core/midi_parser/midi_loader.js +0 -386
- package/spessasynth_core/sequencer/sequencer.js +0 -202
- package/spessasynth_core/sequencer/worklet_sequencer/play.js +0 -209
- package/spessasynth_core/sequencer/worklet_sequencer/process_event.js +0 -120
- package/spessasynth_core/sequencer/worklet_sequencer/song_control.js +0 -112
- package/spessasynth_core/soundfont/README.md +0 -4
- package/spessasynth_core/soundfont/chunk/generators.js +0 -205
- package/spessasynth_core/soundfont/chunk/modulators.js +0 -232
- package/spessasynth_core/soundfont/chunk/presets.js +0 -264
- package/spessasynth_core/soundfont/chunk/riff_chunk.js +0 -46
- package/spessasynth_core/soundfont/chunk/samples.js +0 -250
- package/spessasynth_core/soundfont/soundfont_parser.js +0 -301
- package/spessasynth_core/synthetizer/README.md +0 -6
- package/spessasynth_core/synthetizer/synthesizer.js +0 -313
- package/spessasynth_core/synthetizer/worklet_system/README.md +0 -3
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/controller_control.js +0 -290
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/data_entry.js +0 -280
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/note_off.js +0 -102
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/note_on.js +0 -77
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/program_control.js +0 -140
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/system_exclusive.js +0 -266
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/tuning_control.js +0 -104
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/vibrato_control.js +0 -29
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/voice_control.js +0 -223
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/lowpass_filter.js +0 -133
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/modulation_envelope.js +0 -73
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/stereo_panner.js +0 -76
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/volume_envelope.js +0 -272
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js +0 -83
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js +0 -175
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js +0 -106
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/worklet_voice.js +0 -285
- package/spessasynth_core/utils/buffer_to_wav.js +0 -70
- package/spessasynth_core/utils/byte_functions.js +0 -141
- package/spessasynth_core/utils/other.js +0 -49
- package/spessasynth_core/utils/shiftable_array.js +0 -26
- package/spessasynth_core/utils/stbvorbis_sync.js +0 -1877
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { SpessaSynthInfo } from "./loggin.js";
|
|
2
|
+
import { consoleColors } from "./other.js";
|
|
3
|
+
import { DEFAULT_PERCUSSION } from "../synthetizer/synth_constants.js";
|
|
4
|
+
|
|
5
|
+
export const XG_SFX_VOICE = 64;
|
|
6
|
+
|
|
7
|
+
const GM2_DEFAULT_BANK = 121;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param sys {SynthSystem}
|
|
11
|
+
* @returns {number}
|
|
12
|
+
*/
|
|
13
|
+
export function getDefaultBank(sys)
|
|
14
|
+
{
|
|
15
|
+
return sys === "gm2" ? GM2_DEFAULT_BANK : 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param bankNr {number}
|
|
20
|
+
* @returns {boolean}
|
|
21
|
+
*/
|
|
22
|
+
export function isXGDrums(bankNr)
|
|
23
|
+
{
|
|
24
|
+
return bankNr === 120 || bankNr === 126 || bankNr === 127;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param bank {number}
|
|
29
|
+
* @returns {boolean}
|
|
30
|
+
*/
|
|
31
|
+
export function isValidXGMSB(bank)
|
|
32
|
+
{
|
|
33
|
+
return isXGDrums(bank) || bank === XG_SFX_VOICE || bank === GM2_DEFAULT_BANK;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Bank select hacks abstracted here
|
|
38
|
+
* @param bankBefore {number} the current bank number
|
|
39
|
+
* @param bank {number} the cc change bank number
|
|
40
|
+
* @param system {SynthSystem} MIDI system
|
|
41
|
+
* @param isLSB {boolean} is bank LSB?
|
|
42
|
+
* @param isDrums {boolean} is drum channel?
|
|
43
|
+
* @param channelNumber {number} channel number
|
|
44
|
+
* @returns {{
|
|
45
|
+
* newBank: number,
|
|
46
|
+
* drumsStatus: 0|1|2
|
|
47
|
+
* }} 0 - unchanged, 1 - OFF, 2 - ON
|
|
48
|
+
*/
|
|
49
|
+
export function parseBankSelect(bankBefore, bank, system, isLSB, isDrums, channelNumber)
|
|
50
|
+
{
|
|
51
|
+
// 64 means SFX in MSB, so it is allowed
|
|
52
|
+
let out = bankBefore;
|
|
53
|
+
let drumsStatus = 0;
|
|
54
|
+
if (isLSB)
|
|
55
|
+
{
|
|
56
|
+
if (isSystemXG(system))
|
|
57
|
+
{
|
|
58
|
+
if (!isValidXGMSB(bank))
|
|
59
|
+
{
|
|
60
|
+
out = bank;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else if (system === "gm2")
|
|
64
|
+
{
|
|
65
|
+
out = bank;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else
|
|
69
|
+
{
|
|
70
|
+
let canSetBankSelect = true;
|
|
71
|
+
switch (system)
|
|
72
|
+
{
|
|
73
|
+
case "gm":
|
|
74
|
+
// gm ignores bank select
|
|
75
|
+
SpessaSynthInfo(
|
|
76
|
+
`%cIgnoring the Bank Select (${bank}), as the synth is in GM mode.`,
|
|
77
|
+
consoleColors.info
|
|
78
|
+
);
|
|
79
|
+
canSetBankSelect = false;
|
|
80
|
+
break;
|
|
81
|
+
|
|
82
|
+
case "xg":
|
|
83
|
+
canSetBankSelect = isValidXGMSB(bank);
|
|
84
|
+
// for xg, if msb is 120, 126 or 127, then it's drums
|
|
85
|
+
if (isXGDrums(bank))
|
|
86
|
+
{
|
|
87
|
+
drumsStatus = 2;
|
|
88
|
+
}
|
|
89
|
+
else
|
|
90
|
+
{
|
|
91
|
+
// drums shall not be disabled on channel 9
|
|
92
|
+
if (channelNumber % 16 !== DEFAULT_PERCUSSION)
|
|
93
|
+
{
|
|
94
|
+
drumsStatus = 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
|
|
99
|
+
case "gm2":
|
|
100
|
+
if (bank === 120)
|
|
101
|
+
{
|
|
102
|
+
drumsStatus = 2;
|
|
103
|
+
}
|
|
104
|
+
else
|
|
105
|
+
{
|
|
106
|
+
if (channelNumber % 16 !== DEFAULT_PERCUSSION)
|
|
107
|
+
{
|
|
108
|
+
drumsStatus = 1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (isDrums)
|
|
114
|
+
{
|
|
115
|
+
// 128 for percussion channel
|
|
116
|
+
bank = 128;
|
|
117
|
+
}
|
|
118
|
+
if (bank === 128 && !isDrums)
|
|
119
|
+
{
|
|
120
|
+
// if a channel is not for percussion, default to bank current
|
|
121
|
+
bank = bankBefore;
|
|
122
|
+
}
|
|
123
|
+
if (canSetBankSelect)
|
|
124
|
+
{
|
|
125
|
+
out = bank;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
newBank: out,
|
|
130
|
+
drumsStatus: drumsStatus
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Chooses a bank number according to spessasynth logic
|
|
137
|
+
* That is:
|
|
138
|
+
* for GS, bank MSB if not drum, otherwise 128
|
|
139
|
+
* for XG: bank MSB if drum and MSB is valid, 128 othewise, bank MSB if it is SFX voice, LSB otherwise
|
|
140
|
+
* @param msb {number}
|
|
141
|
+
* @param lsb {number}
|
|
142
|
+
* @param isDrums {boolean}
|
|
143
|
+
* @param isXG {boolean}
|
|
144
|
+
* @returns {number}
|
|
145
|
+
*/
|
|
146
|
+
export function chooseBank(msb, lsb, isDrums, isXG)
|
|
147
|
+
{
|
|
148
|
+
if (isXG)
|
|
149
|
+
{
|
|
150
|
+
if (isDrums)
|
|
151
|
+
{
|
|
152
|
+
if (isXGDrums(msb))
|
|
153
|
+
{
|
|
154
|
+
return msb;
|
|
155
|
+
}
|
|
156
|
+
else
|
|
157
|
+
{
|
|
158
|
+
return 128;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else
|
|
162
|
+
{
|
|
163
|
+
// check for SFX
|
|
164
|
+
if (isValidXGMSB(msb))
|
|
165
|
+
{
|
|
166
|
+
return msb;
|
|
167
|
+
}
|
|
168
|
+
// if lsb is 0 and msb is not, use that
|
|
169
|
+
if (lsb === 0 && msb !== 0)
|
|
170
|
+
{
|
|
171
|
+
return msb;
|
|
172
|
+
}
|
|
173
|
+
if (!isValidXGMSB(lsb))
|
|
174
|
+
{
|
|
175
|
+
return lsb;
|
|
176
|
+
}
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else
|
|
181
|
+
{
|
|
182
|
+
return isDrums ? 128 : msb;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* @param system {SynthSystem}
|
|
188
|
+
* @returns boolean
|
|
189
|
+
*/
|
|
190
|
+
export function isSystemXG(system)
|
|
191
|
+
{
|
|
192
|
+
return system === "gm2" || system === "xg";
|
|
193
|
+
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
<component name="InspectionProjectProfileManager">
|
|
2
|
-
<profile version="1.0">
|
|
3
|
-
<option name="myName" value="Project Default" />
|
|
4
|
-
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
|
|
5
|
-
<option name="processCode" value="true" />
|
|
6
|
-
<option name="processLiterals" value="true" />
|
|
7
|
-
<option name="processComments" value="true" />
|
|
8
|
-
</inspection_tool>
|
|
9
|
-
</profile>
|
|
10
|
-
</component>
|
package/.idea/modules.xml
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<project version="4">
|
|
3
|
-
<component name="ProjectModuleManager">
|
|
4
|
-
<modules>
|
|
5
|
-
<module fileurl="file://$PROJECT_DIR$/.idea/spessasynth_core.iml" filepath="$PROJECT_DIR$/.idea/spessasynth_core.iml" />
|
|
6
|
-
</modules>
|
|
7
|
-
</component>
|
|
8
|
-
</project>
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<module type="WEB_MODULE" version="4">
|
|
3
|
-
<component name="NewModuleRootManager">
|
|
4
|
-
<content url="file://$MODULE_DIR$">
|
|
5
|
-
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
|
6
|
-
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
|
7
|
-
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
|
8
|
-
</content>
|
|
9
|
-
<orderEntry type="inheritedJdk" />
|
|
10
|
-
<orderEntry type="sourceFolder" forTests="false" />
|
|
11
|
-
</component>
|
|
12
|
-
</module>
|
package/.idea/vcs.xml
DELETED
|
@@ -1,386 +0,0 @@
|
|
|
1
|
-
import { dataBytesAmount, getChannel, messageTypes, MidiMessage } from './midi_message.js'
|
|
2
|
-
import {ShiftableByteArray} from "../utils/shiftable_array.js";
|
|
3
|
-
import {
|
|
4
|
-
readByte,
|
|
5
|
-
readBytesAsString,
|
|
6
|
-
readBytesAsUintBigEndian,
|
|
7
|
-
readVariableLengthQuantity
|
|
8
|
-
} from "../utils/byte_functions.js";
|
|
9
|
-
import { arrayToHexString, consoleColors, formatTitle } from '../utils/other.js'
|
|
10
|
-
import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo } from '../utils/loggin.js'
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* midi_loader.js
|
|
14
|
-
* purpose: parses a midi file for the seqyencer, including things like marker or CC 2/4 loop detection, copyright detection etc.
|
|
15
|
-
*/
|
|
16
|
-
export class MIDI{
|
|
17
|
-
/**
|
|
18
|
-
* Parses a given midi file
|
|
19
|
-
* @param arrayBuffer {Buffer}
|
|
20
|
-
* @param fileName {string} optional, replaces the decoded title if empty
|
|
21
|
-
*/
|
|
22
|
-
constructor(arrayBuffer, fileName="") {
|
|
23
|
-
SpessaSynthGroupCollapsed(`%cParsing MIDI File...`, consoleColors.info);
|
|
24
|
-
|
|
25
|
-
const fileByteArray = new ShiftableByteArray(arrayBuffer);
|
|
26
|
-
const headerChunk = this.readMIDIChunk(fileByteArray);
|
|
27
|
-
if(headerChunk.type !== "MThd")
|
|
28
|
-
{
|
|
29
|
-
SpessaSynthGroupEnd();
|
|
30
|
-
throw new SyntaxError(`Invalid MIDI Header! Expected "MThd", got "${headerChunk.type}"`);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if(headerChunk.size !== 6)
|
|
34
|
-
{
|
|
35
|
-
SpessaSynthGroupEnd();
|
|
36
|
-
throw new RangeError(`Invalid MIDI header chunk size! Expected 6, got ${headerChunk.size}`);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// format
|
|
40
|
-
const format = readBytesAsUintBigEndian(headerChunk.data, 2);
|
|
41
|
-
// tracks count
|
|
42
|
-
this.tracksAmount = readBytesAsUintBigEndian(headerChunk.data, 2);
|
|
43
|
-
// time division
|
|
44
|
-
this.timeDivision = readBytesAsUintBigEndian(headerChunk.data, 2);
|
|
45
|
-
|
|
46
|
-
const decoder = new TextDecoder('shift-jis');
|
|
47
|
-
|
|
48
|
-
// read the copyright
|
|
49
|
-
this.copyright = "";
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Contains all the tempo changes in the file. (Ordered from last to first)
|
|
53
|
-
* @type {{
|
|
54
|
-
* ticks: number,
|
|
55
|
-
* tempo: number
|
|
56
|
-
* }[]}
|
|
57
|
-
*/
|
|
58
|
-
this.tempoChanges = [{ticks: 0, tempo: 120}];
|
|
59
|
-
|
|
60
|
-
let loopStart = null;
|
|
61
|
-
let loopEnd = null;
|
|
62
|
-
|
|
63
|
-
this.lastVoiceEventTick = 0;
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Midi port numbers for each tracks
|
|
67
|
-
* @type {number[]}
|
|
68
|
-
*/
|
|
69
|
-
this.midiPorts = [];
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Read all the tracks
|
|
73
|
-
* @type {MidiMessage[][]}
|
|
74
|
-
*/
|
|
75
|
-
this.tracks = [];
|
|
76
|
-
for(let i = 0; i < this.tracksAmount; i++)
|
|
77
|
-
{
|
|
78
|
-
/**
|
|
79
|
-
* @type {MidiMessage[]}
|
|
80
|
-
*/
|
|
81
|
-
const track = [];
|
|
82
|
-
const trackChunk = this.readMIDIChunk(fileByteArray);
|
|
83
|
-
this.midiPorts.push(0)
|
|
84
|
-
|
|
85
|
-
if(trackChunk.type !== "MTrk")
|
|
86
|
-
{
|
|
87
|
-
SpessaSynthGroupEnd();
|
|
88
|
-
throw new SyntaxError(`Invalid track header! Expected "MTrk" got "${trackChunk.type}"`);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* MIDI running byte
|
|
93
|
-
* @type {number}
|
|
94
|
-
*/
|
|
95
|
-
let runningByte = undefined;
|
|
96
|
-
|
|
97
|
-
let totalTicks = 0;
|
|
98
|
-
// format 2 plays sequentially
|
|
99
|
-
if(format === 2 && i > 0)
|
|
100
|
-
{
|
|
101
|
-
totalTicks += this.tracks[i - 1][this.tracks[i - 1].length - 1].ticks;
|
|
102
|
-
}
|
|
103
|
-
// loop until we reach the end of track
|
|
104
|
-
while(trackChunk.data.currentIndex < trackChunk.size)
|
|
105
|
-
{
|
|
106
|
-
totalTicks += readVariableLengthQuantity(trackChunk.data);
|
|
107
|
-
|
|
108
|
-
// check if the status byte is valid (IE. larger than 127)
|
|
109
|
-
const statusByteCheck = trackChunk.data[trackChunk.data.currentIndex];
|
|
110
|
-
|
|
111
|
-
let statusByte;
|
|
112
|
-
// if we have a running byte and the status byte isn't valid
|
|
113
|
-
if(runningByte !== undefined && statusByteCheck < 0x80)
|
|
114
|
-
{
|
|
115
|
-
statusByte = runningByte;
|
|
116
|
-
}
|
|
117
|
-
else if(!runningByte && statusByteCheck < 0x80)
|
|
118
|
-
{
|
|
119
|
-
// if we don't have a running byte and the status byte isn't valid, it's an error.
|
|
120
|
-
SpessaSynthGroupEnd();
|
|
121
|
-
throw new SyntaxError(`Unexpected byte with no running byte. (${statusByteCheck})`);
|
|
122
|
-
}
|
|
123
|
-
else
|
|
124
|
-
{
|
|
125
|
-
// if the status byte is valid, just use that
|
|
126
|
-
statusByte = readByte(trackChunk.data);
|
|
127
|
-
}
|
|
128
|
-
const statusByteChannel = getChannel(statusByte);
|
|
129
|
-
|
|
130
|
-
let eventDataLength;
|
|
131
|
-
|
|
132
|
-
// determine the message's length;
|
|
133
|
-
switch(statusByteChannel)
|
|
134
|
-
{
|
|
135
|
-
case -1:
|
|
136
|
-
// system common/realtime (no length)
|
|
137
|
-
eventDataLength = 0;
|
|
138
|
-
break;
|
|
139
|
-
case -2:
|
|
140
|
-
// meta (the next is the actual status byte)
|
|
141
|
-
statusByte = readByte(trackChunk.data);
|
|
142
|
-
eventDataLength = readVariableLengthQuantity(trackChunk.data);
|
|
143
|
-
break;
|
|
144
|
-
case -3:
|
|
145
|
-
// sysex
|
|
146
|
-
eventDataLength = readVariableLengthQuantity(trackChunk.data);
|
|
147
|
-
break;
|
|
148
|
-
default:
|
|
149
|
-
// voice message
|
|
150
|
-
// get the midi message length
|
|
151
|
-
if(totalTicks > this.lastVoiceEventTick)
|
|
152
|
-
{
|
|
153
|
-
this.lastVoiceEventTick = totalTicks;
|
|
154
|
-
}
|
|
155
|
-
eventDataLength = dataBytesAmount[statusByte >> 4];
|
|
156
|
-
break;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// put the event data into the array
|
|
160
|
-
const eventData = new ShiftableByteArray(eventDataLength);
|
|
161
|
-
const messageData = trackChunk.data.slice(trackChunk.data.currentIndex, trackChunk.data.currentIndex + eventDataLength);
|
|
162
|
-
trackChunk.data.currentIndex += eventDataLength;
|
|
163
|
-
eventData.set(messageData, 0);
|
|
164
|
-
|
|
165
|
-
runningByte = statusByte;
|
|
166
|
-
|
|
167
|
-
const message = new MidiMessage(totalTicks, statusByte, eventData);
|
|
168
|
-
track.push(message);
|
|
169
|
-
|
|
170
|
-
// check for tempo change
|
|
171
|
-
if(statusByte === messageTypes.setTempo)
|
|
172
|
-
{
|
|
173
|
-
this.tempoChanges.push({
|
|
174
|
-
ticks: totalTicks,
|
|
175
|
-
tempo: 60000000 / readBytesAsUintBigEndian(messageData, 3)
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
else
|
|
179
|
-
// check for loop start (Marker "start")
|
|
180
|
-
|
|
181
|
-
if(statusByte === messageTypes.marker)
|
|
182
|
-
{
|
|
183
|
-
const text = readBytesAsString(eventData, eventData.length).trim().toLowerCase();
|
|
184
|
-
switch (text)
|
|
185
|
-
{
|
|
186
|
-
default:
|
|
187
|
-
break;
|
|
188
|
-
|
|
189
|
-
case "start":
|
|
190
|
-
case "loopstart":
|
|
191
|
-
loopStart = totalTicks;
|
|
192
|
-
break;
|
|
193
|
-
|
|
194
|
-
case "loopend":
|
|
195
|
-
loopEnd = totalTicks;
|
|
196
|
-
}
|
|
197
|
-
eventData.currentIndex = 0;
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
else
|
|
201
|
-
// check for loop (CC 2/4)
|
|
202
|
-
if((statusByte & 0xF0) === messageTypes.controllerChange)
|
|
203
|
-
{
|
|
204
|
-
switch(eventData[0])
|
|
205
|
-
{
|
|
206
|
-
case 2:
|
|
207
|
-
case 116:
|
|
208
|
-
loopStart = totalTicks;
|
|
209
|
-
break;
|
|
210
|
-
|
|
211
|
-
case 4:
|
|
212
|
-
case 117:
|
|
213
|
-
if(loopEnd === null)
|
|
214
|
-
{
|
|
215
|
-
loopEnd = totalTicks;
|
|
216
|
-
}
|
|
217
|
-
else
|
|
218
|
-
{
|
|
219
|
-
// this controller has occured more than once, this means that it doesn't indicate the loop
|
|
220
|
-
loopEnd = 0;
|
|
221
|
-
}
|
|
222
|
-
break;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
else
|
|
226
|
-
// check for midi port
|
|
227
|
-
if(statusByte === messageTypes.midiPort)
|
|
228
|
-
{
|
|
229
|
-
this.midiPorts[i] = eventData[0];
|
|
230
|
-
}
|
|
231
|
-
else
|
|
232
|
-
// check for copyright
|
|
233
|
-
if(statusByte === messageTypes.copyright)
|
|
234
|
-
{
|
|
235
|
-
this.copyright += decoder.decode(eventData) + "\n";
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// check for embedded copyright (roland SC display sysex) http://www.bandtrax.com.au/sysex.htm
|
|
239
|
-
if(statusByte === messageTypes.systemExclusive)
|
|
240
|
-
{
|
|
241
|
-
// header goes like this: 41 10 45 12 10 00 00
|
|
242
|
-
if(arrayToHexString(messageData.slice(0, 7)).trim() === "41 10 45 12 10 00 00")
|
|
243
|
-
{
|
|
244
|
-
const decoded = decoder.decode(messageData.slice(7, messageData.length - 3)) + "\n";
|
|
245
|
-
this.copyright += decoded;
|
|
246
|
-
SpessaSynthInfo(`%cDecoded Roland SC message! %c${decoded}`,
|
|
247
|
-
consoleColors.recognized,
|
|
248
|
-
consoleColors.value)
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
this.tracks.push(track);
|
|
253
|
-
SpessaSynthInfo(`%cParsed %c${this.tracks.length}%c / %c${this.tracksAmount}`,
|
|
254
|
-
consoleColors.info,
|
|
255
|
-
consoleColors.value,
|
|
256
|
-
consoleColors.info,
|
|
257
|
-
consoleColors.value);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
//this.lastVoiceEventTick = Math.max(...this.tracks.map(track =>
|
|
261
|
-
//track[track.length - 1].ticks));
|
|
262
|
-
const firstNoteOns = [];
|
|
263
|
-
for(const t of this.tracks)
|
|
264
|
-
{
|
|
265
|
-
const firstNoteOn = t.find(e => (e.messageStatusByte & 0xF0) === messageTypes.noteOn);
|
|
266
|
-
if(firstNoteOn)
|
|
267
|
-
{
|
|
268
|
-
firstNoteOns.push(firstNoteOn.ticks);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
this.firstNoteOn = Math.min(...firstNoteOns);
|
|
272
|
-
|
|
273
|
-
SpessaSynthInfo(`%cMIDI file parsed. Total tick time: %c${this.lastVoiceEventTick}`,
|
|
274
|
-
consoleColors.info,
|
|
275
|
-
consoleColors.recognized);
|
|
276
|
-
SpessaSynthGroupEnd();
|
|
277
|
-
|
|
278
|
-
if(loopStart !== null && loopEnd === null)
|
|
279
|
-
{
|
|
280
|
-
// not a loop
|
|
281
|
-
loopStart = this.firstNoteOn;
|
|
282
|
-
loopEnd = this.lastVoiceEventTick;
|
|
283
|
-
}
|
|
284
|
-
else {
|
|
285
|
-
if (loopStart === null) {
|
|
286
|
-
loopStart = this.firstNoteOn;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if (loopEnd === null || loopEnd === 0) {
|
|
290
|
-
loopEnd = this.lastVoiceEventTick;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
*
|
|
296
|
-
* @type {{start: number, end: number}}
|
|
297
|
-
*/
|
|
298
|
-
this.loop = {start: loopStart, end: loopEnd};
|
|
299
|
-
|
|
300
|
-
// get track name
|
|
301
|
-
this.midiName = "";
|
|
302
|
-
|
|
303
|
-
// midi name
|
|
304
|
-
if(this.tracks.length > 1)
|
|
305
|
-
{
|
|
306
|
-
// if more than 1 track and the first track has no notes, just find the first trackName in the first track
|
|
307
|
-
if(this.tracks[0].find(
|
|
308
|
-
message => message.messageStatusByte >= messageTypes.noteOn
|
|
309
|
-
&&
|
|
310
|
-
message.messageStatusByte < messageTypes.systemExclusive
|
|
311
|
-
) === undefined)
|
|
312
|
-
{
|
|
313
|
-
let name = this.tracks[0].find(message => message.messageStatusByte === messageTypes.trackName);
|
|
314
|
-
if(name)
|
|
315
|
-
{
|
|
316
|
-
this.midiName = decoder.decode(name.messageData);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
else
|
|
321
|
-
{
|
|
322
|
-
// if only 1 track, find the first "track name" event
|
|
323
|
-
let name = this.tracks[0].find(message => message.messageStatusByte === messageTypes.trackName);
|
|
324
|
-
if(name)
|
|
325
|
-
{
|
|
326
|
-
this.midiName = decoder.decode(name.messageData);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
this.fileName = fileName;
|
|
331
|
-
|
|
332
|
-
// if midiName is "", use the file name
|
|
333
|
-
if(this.midiName.trim().length === 0 && fileName.length > 0)
|
|
334
|
-
{
|
|
335
|
-
this.midiName = formatTitle(fileName);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// reverse the tempo changes
|
|
339
|
-
this.tempoChanges.reverse();
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* The total playback time, in seconds
|
|
343
|
-
* @type {number}
|
|
344
|
-
*/
|
|
345
|
-
this.duration = this._ticksToSeconds(this.lastVoiceEventTick);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
/**
|
|
349
|
-
* @param fileByteArray {ShiftableByteArray}
|
|
350
|
-
* @returns {{type: string, size: number, data: ShiftableByteArray}}
|
|
351
|
-
*/
|
|
352
|
-
readMIDIChunk(fileByteArray)
|
|
353
|
-
{
|
|
354
|
-
const chunk = {};
|
|
355
|
-
// type
|
|
356
|
-
chunk.type = readBytesAsString(fileByteArray, 4);
|
|
357
|
-
// size
|
|
358
|
-
chunk.size = readBytesAsUintBigEndian(fileByteArray, 4);
|
|
359
|
-
// data
|
|
360
|
-
chunk.data = new ShiftableByteArray(chunk.size);
|
|
361
|
-
const dataSlice = fileByteArray.slice(fileByteArray.currentIndex, fileByteArray.currentIndex + chunk.size);
|
|
362
|
-
chunk.data.set(dataSlice, 0);
|
|
363
|
-
fileByteArray.currentIndex += chunk.size;
|
|
364
|
-
return chunk;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* Coverts ticks to time in seconds
|
|
370
|
-
* @param ticks {number}
|
|
371
|
-
* @returns {number}
|
|
372
|
-
* @private
|
|
373
|
-
*/
|
|
374
|
-
_ticksToSeconds(ticks)
|
|
375
|
-
{
|
|
376
|
-
if (ticks <= 0) {
|
|
377
|
-
return 0;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// find the last tempo change that has occured
|
|
381
|
-
let tempo = this.tempoChanges.find(v => v.ticks < ticks);
|
|
382
|
-
|
|
383
|
-
let timeSinceLastTempo = ticks - tempo.ticks;
|
|
384
|
-
return this._ticksToSeconds(ticks - timeSinceLastTempo) + (timeSinceLastTempo * 60) / (tempo.tempo * this.timeDivision);
|
|
385
|
-
}
|
|
386
|
-
}
|