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,116 @@
|
|
|
1
|
+
import { midiControllers } from '../../../midi_parser/midi_message.js'
|
|
2
|
+
import { modulatorSources } from '../../../soundfont/read/modulators.js'
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} WorkletProcessorChannel
|
|
5
|
+
* @property {Int16Array} midiControllers - array of MIDI controller values + the values used by modulators as source (pitch bend, bend range etc.)
|
|
6
|
+
* @property {boolean[]} lockedControllers - array indicating if a controller is locked
|
|
7
|
+
* @property {Float32Array} customControllers - array of custom (not sf2) control values such as RPN pitch tuning, transpose, modulation depth, etc.
|
|
8
|
+
*
|
|
9
|
+
* @property {number} channelTransposeKeyShift - key shift of the channel
|
|
10
|
+
* @property {boolean} holdPedal - indicates whether the hold pedal is active
|
|
11
|
+
* @property {boolean} drumChannel - indicates whether the channel is a drum channel
|
|
12
|
+
*
|
|
13
|
+
* @property {dataEntryStates} dataEntryState - the current state of the data entry
|
|
14
|
+
* @property {number} NRPCoarse - the current coarse value of the Non-Registered Parameter
|
|
15
|
+
* @property {number} NRPFine - the current fine value of the Non-Registered Parameter
|
|
16
|
+
* @property {number} RPValue - the current value of the Registered Parameter
|
|
17
|
+
*
|
|
18
|
+
* @property {Preset} preset - the channel's preset
|
|
19
|
+
* @property {boolean} lockPreset - indicates whether the program on the channel is locked
|
|
20
|
+
*
|
|
21
|
+
* @property {boolean} lockVibrato - indicates whether the custom vibrato is locked
|
|
22
|
+
* @property {Object} channelVibrato - vibrato settings for the channel
|
|
23
|
+
* @property {number} channelVibrato.depth - depth of the vibrato effect (cents)
|
|
24
|
+
* @property {number} channelVibrato.delay - delay before the vibrato effect starts (seconds)
|
|
25
|
+
* @property {number} channelVibrato.rate - rate of the vibrato oscillation (Hz)
|
|
26
|
+
|
|
27
|
+
* @property {boolean} isMuted - indicates whether the channel is muted
|
|
28
|
+
* @property {WorkletVoice[]} voices - array of voices currently active on the channel
|
|
29
|
+
* @property {WorkletVoice[]} sustainedVoices - array of voices that are sustained on the channel
|
|
30
|
+
* @property {WorkletVoice[][][]} cachedVoices - first is midi note, second is velocity. output is an array of WorkletVoices
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param sendEvent {boolean}
|
|
35
|
+
* @this {SpessaSynthProcessor}
|
|
36
|
+
*/
|
|
37
|
+
export function createWorkletChannel(sendEvent = false)
|
|
38
|
+
{
|
|
39
|
+
/**
|
|
40
|
+
* @type {WorkletProcessorChannel}
|
|
41
|
+
*/
|
|
42
|
+
const channel = {
|
|
43
|
+
midiControllers: new Int16Array(CONTROLLER_TABLE_SIZE),
|
|
44
|
+
lockedControllers: Array(CONTROLLER_TABLE_SIZE).fill(false),
|
|
45
|
+
customControllers: new Float32Array(CUSTOM_CONTROLLER_TABLE_SIZE),
|
|
46
|
+
|
|
47
|
+
NRPCoarse: 0,
|
|
48
|
+
NRPFine: 0,
|
|
49
|
+
RPValue: 0,
|
|
50
|
+
dataEntryState: dataEntryStates.Idle,
|
|
51
|
+
|
|
52
|
+
voices: [],
|
|
53
|
+
sustainedVoices: [],
|
|
54
|
+
cachedVoices: [],
|
|
55
|
+
preset: this.defaultPreset,
|
|
56
|
+
|
|
57
|
+
channelTransposeKeyShift: 0,
|
|
58
|
+
channelVibrato: {delay: 0, depth: 0, rate: 0},
|
|
59
|
+
lockVibrato: false,
|
|
60
|
+
holdPedal: false,
|
|
61
|
+
isMuted: false,
|
|
62
|
+
drumChannel: false,
|
|
63
|
+
lockPreset: false,
|
|
64
|
+
|
|
65
|
+
}
|
|
66
|
+
for (let i = 0; i < 128; i++)
|
|
67
|
+
{
|
|
68
|
+
channel.cachedVoices.push([]);
|
|
69
|
+
}
|
|
70
|
+
this.workletProcessorChannels.push(channel);
|
|
71
|
+
this.resetControllers(this.workletProcessorChannels.length - 1);
|
|
72
|
+
this.sendChannelProperties();
|
|
73
|
+
if(sendEvent)
|
|
74
|
+
{
|
|
75
|
+
this.callEvent("newchannel", undefined);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const NON_CC_INDEX_OFFSET = 128;
|
|
80
|
+
export const CONTROLLER_TABLE_SIZE = 147;
|
|
81
|
+
// an array with preset default values so we can quickly use set() to reset the controllers
|
|
82
|
+
export const resetArray = new Int16Array(CONTROLLER_TABLE_SIZE).fill(0);
|
|
83
|
+
// default values (the array is 14 bit so shift the 7 bit values by 7 bits)
|
|
84
|
+
resetArray[midiControllers.mainVolume] = 100 << 7;
|
|
85
|
+
resetArray[midiControllers.expressionController] = 127 << 7;
|
|
86
|
+
resetArray[midiControllers.pan] = 64 << 7;
|
|
87
|
+
resetArray[midiControllers.releaseTime] = 64 << 7;
|
|
88
|
+
resetArray[midiControllers.brightness] = 64 << 7;
|
|
89
|
+
resetArray[midiControllers.effects1Depth] = 40 << 7;
|
|
90
|
+
resetArray[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel] = 8192;
|
|
91
|
+
resetArray[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheelRange] = 2 << 7;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @enum {number}
|
|
95
|
+
*/
|
|
96
|
+
export const dataEntryStates = {
|
|
97
|
+
Idle: 0,
|
|
98
|
+
RPCoarse: 1,
|
|
99
|
+
RPFine: 2,
|
|
100
|
+
NRPCoarse: 3,
|
|
101
|
+
NRPFine: 4,
|
|
102
|
+
DataCoarse: 5,
|
|
103
|
+
DataFine: 6
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
export const customControllers = {
|
|
108
|
+
channelTuning: 0, // cents, RPN for fine tuning
|
|
109
|
+
channelTransposeFine: 1, // cents, only the decimal tuning, (e.g. transpose is 4.5, then shift by 4 keys + tune by 50 cents)
|
|
110
|
+
modulationMultiplier: 2, // cents, set by moduldation depth RPN
|
|
111
|
+
masterTuning: 3, // cents, set by system exclusive
|
|
112
|
+
channelTuningSemitones: 4, // semitones, for RPN coarse tuning
|
|
113
|
+
}
|
|
114
|
+
export const CUSTOM_CONTROLLER_TABLE_SIZE = Object.keys(customControllers).length;
|
|
115
|
+
export const customResetArray = new Float32Array(CUSTOM_CONTROLLER_TABLE_SIZE);
|
|
116
|
+
customResetArray[customControllers.modulationMultiplier] = 1;
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* worklet_voice.js
|
|
3
|
+
* purpose: prepares workletvoices from sample and generator data and manages sample dumping
|
|
4
|
+
* note: sample dumping means sending it over to the AudioWorkletGlobalScope
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} WorkletSample
|
|
9
|
+
* @property {number} sampleID - ID of the sample
|
|
10
|
+
* @property {number} playbackStep - current playback step (rate)
|
|
11
|
+
* @property {number} cursor - current position in the sample
|
|
12
|
+
* @property {number} rootKey - root key of the sample
|
|
13
|
+
* @property {number} loopStart - start position of the loop
|
|
14
|
+
* @property {number} loopEnd - end position of the loop
|
|
15
|
+
* @property {number} end - end position of the sample
|
|
16
|
+
* @property {0|1|2} loopingMode - looping mode of the sample
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} WorkletVoice
|
|
21
|
+
* @property {WorkletSample} sample - sample ID for voice.
|
|
22
|
+
* @property {WorkletLowpassFilter} filter - lowpass filter applied to the voice
|
|
23
|
+
* @property {Int16Array} generators - the unmodulated (constant) generators of the voice
|
|
24
|
+
* @property {Modulator[]} modulators - the voice's modulators. Grouped by the destination
|
|
25
|
+
* @property {Int16Array} modulatedGenerators - the generators modulated by the modulators
|
|
26
|
+
*
|
|
27
|
+
* @property {boolean} finished - indicates if the voice has finished
|
|
28
|
+
* @property {boolean} isInRelease - indicates if the voice is in the release phase
|
|
29
|
+
*
|
|
30
|
+
* @property {number} channelNumber - MIDI channel number
|
|
31
|
+
* @property {number} velocity - velocity of the note
|
|
32
|
+
* @property {number} midiNote - MIDI note number
|
|
33
|
+
* @property {number} pressure - the pressure of the note
|
|
34
|
+
* @property {number} targetKey - target key for the note
|
|
35
|
+
*
|
|
36
|
+
* @property {WorkletVolumeEnvelope} volumeEnvelope
|
|
37
|
+
*
|
|
38
|
+
* @property {number} currentModEnvValue - current value of the modulation envelope
|
|
39
|
+
* @property {number} releaseStartModEnv - modenv value at the start of the release phase
|
|
40
|
+
*
|
|
41
|
+
* @property {number} startTime - start time of the voice
|
|
42
|
+
* @property {number} releaseStartTime - start time of the release phase
|
|
43
|
+
*
|
|
44
|
+
* @property {number} currentTuningCents - current tuning adjustment in cents
|
|
45
|
+
* @property {number} currentTuningCalculated - calculated tuning adjustment
|
|
46
|
+
* @property {number} currentPan - from 0 to 1
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
import { addAndClampGenerator, generatorTypes } from '../../../soundfont/read/generators.js'
|
|
50
|
+
import { SpessaSynthTable, SpessaSynthWarn } from '../../../utils/loggin.js'
|
|
51
|
+
import { DEFAULT_WORKLET_VOLUME_ENVELOPE } from './volume_envelope.js'
|
|
52
|
+
import { DEFAULT_WORKLET_LOWPASS_FILTER } from './lowpass_filter.js'
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* the sampleID is the index
|
|
57
|
+
* @type {boolean[]}
|
|
58
|
+
*/
|
|
59
|
+
let globalDumpedSamplesList = [];
|
|
60
|
+
|
|
61
|
+
export function clearSamplesList()
|
|
62
|
+
{
|
|
63
|
+
globalDumpedSamplesList = [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function /**
|
|
67
|
+
* @param channel {number} channel hint for the processor to recalculate cursor positions
|
|
68
|
+
* @param sample {Sample}
|
|
69
|
+
* @param id {number}
|
|
70
|
+
* @param sampleDumpCallback {function({channel: number, sampleID: number, sampleData: Float32Array})}
|
|
71
|
+
*/
|
|
72
|
+
dumpSample(channel, sample, id, sampleDumpCallback)
|
|
73
|
+
{
|
|
74
|
+
// flag as defined, so it's currently being dumped
|
|
75
|
+
globalDumpedSamplesList[id] = false;
|
|
76
|
+
|
|
77
|
+
// load the data
|
|
78
|
+
sampleDumpCallback({
|
|
79
|
+
channel: channel,
|
|
80
|
+
sampleID: id,
|
|
81
|
+
sampleData: sample.getAudioData()
|
|
82
|
+
});
|
|
83
|
+
globalDumpedSamplesList[id] = true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Deep clone function for the WorkletVoice object and its nested structures.
|
|
88
|
+
* This function handles Int16Array, objects, arrays, and primitives.
|
|
89
|
+
* It does not handle circular references.
|
|
90
|
+
* @param {WorkletVoice} obj - The object to clone.
|
|
91
|
+
* @returns {WorkletVoice} - Cloned object.
|
|
92
|
+
*/
|
|
93
|
+
function deepClone(obj) {
|
|
94
|
+
if (obj === null || typeof obj !== 'object') {
|
|
95
|
+
return obj;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Handle Int16Array separately
|
|
99
|
+
if (obj instanceof Int16Array) {
|
|
100
|
+
return new Int16Array(obj);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Handle objects and arrays
|
|
104
|
+
const clonedObj = Array.isArray(obj) ? [] : {};
|
|
105
|
+
for (let key in obj) {
|
|
106
|
+
if (obj.hasOwnProperty(key)) {
|
|
107
|
+
if (typeof obj[key] === 'object' && obj[key] !== null) {
|
|
108
|
+
clonedObj[key] = deepClone(obj[key]); // Recursively clone nested objects
|
|
109
|
+
} else if (obj[key] instanceof Int16Array) {
|
|
110
|
+
clonedObj[key] = new Int16Array(obj[key]); // Clone Int16Array
|
|
111
|
+
} else {
|
|
112
|
+
clonedObj[key] = obj[key]; // Copy primitives
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return clonedObj;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param channel {number} a hint for the processor to recalculate sample cursors when sample dumping
|
|
122
|
+
* @param midiNote {number}
|
|
123
|
+
* @param velocity {number}
|
|
124
|
+
* @param preset {Preset}
|
|
125
|
+
* @param currentTime {number}
|
|
126
|
+
* @param sampleRate {number}
|
|
127
|
+
* @param sampleDumpCallback {function({channel: number, sampleID: number, sampleData: Float32Array})}
|
|
128
|
+
* @param cachedVoices {WorkletVoice[][][]} first is midi note, second is velocity. output is an array of WorkletVoices
|
|
129
|
+
* @param debug {boolean}
|
|
130
|
+
* @returns {WorkletVoice[]}
|
|
131
|
+
*/
|
|
132
|
+
export function getWorkletVoices(channel,
|
|
133
|
+
midiNote,
|
|
134
|
+
velocity,
|
|
135
|
+
preset,
|
|
136
|
+
currentTime,
|
|
137
|
+
sampleRate,
|
|
138
|
+
sampleDumpCallback,
|
|
139
|
+
cachedVoices,
|
|
140
|
+
debug=false)
|
|
141
|
+
{
|
|
142
|
+
/**
|
|
143
|
+
* @type {WorkletVoice[]}
|
|
144
|
+
*/
|
|
145
|
+
let workletVoices;
|
|
146
|
+
|
|
147
|
+
const cached = cachedVoices[midiNote][velocity];
|
|
148
|
+
if(cached !== undefined)
|
|
149
|
+
{
|
|
150
|
+
workletVoices = cached.map(deepClone);
|
|
151
|
+
workletVoices.forEach(v => {
|
|
152
|
+
v.startTime = currentTime;
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
else
|
|
156
|
+
{
|
|
157
|
+
/**
|
|
158
|
+
* @returns {WorkletVoice[]}
|
|
159
|
+
*/
|
|
160
|
+
workletVoices = preset.getSamplesAndGenerators(midiNote, velocity).reduce((voices, sampleAndGenerators) => {
|
|
161
|
+
// dump the sample if haven't already
|
|
162
|
+
if (globalDumpedSamplesList[sampleAndGenerators.sampleID] !== true)
|
|
163
|
+
{
|
|
164
|
+
dumpSample(channel, sampleAndGenerators.sample, sampleAndGenerators.sampleID, sampleDumpCallback);
|
|
165
|
+
}
|
|
166
|
+
if(sampleAndGenerators.sample.sampleData === undefined)
|
|
167
|
+
{
|
|
168
|
+
SpessaSynthWarn(`Discarding invalid sample: ${sampleAndGenerators.sample.sampleName}`);
|
|
169
|
+
return voices;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// create the generator list
|
|
173
|
+
const generators = new Int16Array(60);
|
|
174
|
+
// apply and sum the gens
|
|
175
|
+
for (let i = 0; i < 60; i++)
|
|
176
|
+
{
|
|
177
|
+
generators[i] = addAndClampGenerator(i, sampleAndGenerators.presetGenerators, sampleAndGenerators.instrumentGenerators);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// !! EMU initial attenuation correction, multiply initial attenuation by 0.4
|
|
181
|
+
generators[generatorTypes.initialAttenuation] = Math.floor(generators[generatorTypes.initialAttenuation] * 0.4);
|
|
182
|
+
|
|
183
|
+
// key override
|
|
184
|
+
let rootKey = sampleAndGenerators.sample.samplePitch;
|
|
185
|
+
if (generators[generatorTypes.overridingRootKey] > -1) {
|
|
186
|
+
rootKey = generators[generatorTypes.overridingRootKey];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let targetKey = midiNote;
|
|
190
|
+
if (generators[generatorTypes.keyNum] > -1) {
|
|
191
|
+
targetKey = generators[generatorTypes.keyNum];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// determine looping mode now. if the loop is too small, disable
|
|
195
|
+
const loopStart = (sampleAndGenerators.sample.sampleLoopStartIndex / 2) + (generators[generatorTypes.startloopAddrsOffset] + (generators[generatorTypes.startloopAddrsCoarseOffset] * 32768));
|
|
196
|
+
const loopEnd = (sampleAndGenerators.sample.sampleLoopEndIndex / 2) + (generators[generatorTypes.endloopAddrsOffset] + (generators[generatorTypes.endloopAddrsCoarseOffset] * 32768));
|
|
197
|
+
let loopingMode = generators[generatorTypes.sampleModes];
|
|
198
|
+
if (loopEnd - loopStart < 1) {
|
|
199
|
+
loopingMode = 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// determine end
|
|
203
|
+
/**
|
|
204
|
+
* create the worklet sample
|
|
205
|
+
* @type {WorkletSample}
|
|
206
|
+
*/
|
|
207
|
+
const workletSample = {
|
|
208
|
+
sampleID: sampleAndGenerators.sampleID,
|
|
209
|
+
playbackStep: (sampleAndGenerators.sample.sampleRate / sampleRate) * Math.pow(2, sampleAndGenerators.sample.samplePitchCorrection / 1200),// cent tuning
|
|
210
|
+
cursor: generators[generatorTypes.startAddrsOffset] + (generators[generatorTypes.startAddrsCoarseOffset] * 32768),
|
|
211
|
+
rootKey: rootKey,
|
|
212
|
+
loopStart: loopStart,
|
|
213
|
+
loopEnd: loopEnd,
|
|
214
|
+
end: Math.floor( sampleAndGenerators.sample.sampleData.length) - 1 + (generators[generatorTypes.endAddrOffset] + (generators[generatorTypes.endAddrsCoarseOffset] * 32768)),
|
|
215
|
+
loopingMode: loopingMode
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// velocity override
|
|
219
|
+
if (generators[generatorTypes.velocity] > -1) {
|
|
220
|
+
velocity = generators[generatorTypes.velocity];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if(debug)
|
|
224
|
+
{
|
|
225
|
+
SpessaSynthTable([{
|
|
226
|
+
Sample: sampleAndGenerators.sample.sampleName,
|
|
227
|
+
Generators: generators,
|
|
228
|
+
Modulators: sampleAndGenerators.modulators.map(m => m.debugString()),
|
|
229
|
+
Velocity: velocity,
|
|
230
|
+
TargetKey: targetKey,
|
|
231
|
+
MidiNote: midiNote,
|
|
232
|
+
WorkletSample: workletSample
|
|
233
|
+
}]);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
voices.push({
|
|
238
|
+
filter: deepClone(DEFAULT_WORKLET_LOWPASS_FILTER),
|
|
239
|
+
// generators and modulators
|
|
240
|
+
generators: generators,
|
|
241
|
+
modulators: sampleAndGenerators.modulators,
|
|
242
|
+
modulatedGenerators: new Int16Array(60),
|
|
243
|
+
|
|
244
|
+
// sample and playback data
|
|
245
|
+
sample: workletSample,
|
|
246
|
+
velocity: velocity,
|
|
247
|
+
midiNote: midiNote,
|
|
248
|
+
pressure: 0,
|
|
249
|
+
channelNumber: channel,
|
|
250
|
+
startTime: currentTime,
|
|
251
|
+
targetKey: targetKey,
|
|
252
|
+
currentTuningCalculated: 1,
|
|
253
|
+
currentTuningCents: 0,
|
|
254
|
+
releaseStartTime: Infinity,
|
|
255
|
+
|
|
256
|
+
// envelope data
|
|
257
|
+
finished: false,
|
|
258
|
+
isInRelease: false,
|
|
259
|
+
currentModEnvValue: 0,
|
|
260
|
+
releaseStartModEnv: 1,
|
|
261
|
+
currentPan: 0.5,
|
|
262
|
+
|
|
263
|
+
volumeEnvelope: deepClone(DEFAULT_WORKLET_VOLUME_ENVELOPE)
|
|
264
|
+
});
|
|
265
|
+
return voices;
|
|
266
|
+
}, []);
|
|
267
|
+
// cache the voice
|
|
268
|
+
// clone it so the system won't mess with it!
|
|
269
|
+
cachedVoices[midiNote][velocity] = workletVoices.map(deepClone);
|
|
270
|
+
}
|
|
271
|
+
return workletVoices;
|
|
272
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* @param audioBuffer {AudioBuffer}
|
|
4
|
+
* @param normalizeAudio {boolean} find the max sample point and set it to 1, and scale others with it
|
|
5
|
+
* @param channelOffset {number} channel offset and channel offset + 1 get saved
|
|
6
|
+
* @returns {Blob}
|
|
7
|
+
*/
|
|
8
|
+
export function audioBufferToWav(audioBuffer, normalizeAudio = true, channelOffset = 0)
|
|
9
|
+
{
|
|
10
|
+
|
|
11
|
+
const channel1Data = audioBuffer.getChannelData(channelOffset);
|
|
12
|
+
const channel2Data = audioBuffer.getChannelData(channelOffset + 1);
|
|
13
|
+
const length = channel1Data.length;
|
|
14
|
+
|
|
15
|
+
const bytesPerSample = 2; // 16-bit PCM
|
|
16
|
+
|
|
17
|
+
// Prepare the header
|
|
18
|
+
const headerSize = 44;
|
|
19
|
+
const dataSize = length * 2 * bytesPerSample; // 2 channels, 16-bit per channel
|
|
20
|
+
const fileSize = headerSize + dataSize - 8; // total file size minus the first 8 bytes
|
|
21
|
+
const header = new Uint8Array(headerSize);
|
|
22
|
+
|
|
23
|
+
// 'RIFF'
|
|
24
|
+
header.set([82, 73, 70, 70], 0);
|
|
25
|
+
// file length
|
|
26
|
+
header.set(new Uint8Array([fileSize & 0xff, (fileSize >> 8) & 0xff, (fileSize >> 16) & 0xff, (fileSize >> 24) & 0xff]), 4);
|
|
27
|
+
// 'WAVE'
|
|
28
|
+
header.set([87, 65, 86, 69], 8);
|
|
29
|
+
// 'fmt '
|
|
30
|
+
header.set([102, 109, 116, 32], 12);
|
|
31
|
+
// fmt chunk length
|
|
32
|
+
header.set([16, 0, 0, 0], 16); // 16 for PCM
|
|
33
|
+
// audio format (PCM)
|
|
34
|
+
header.set([1, 0], 20);
|
|
35
|
+
// number of channels (2)
|
|
36
|
+
header.set([2, 0], 22);
|
|
37
|
+
// sample rate
|
|
38
|
+
const sampleRate = audioBuffer.sampleRate;
|
|
39
|
+
header.set(new Uint8Array([sampleRate & 0xff, (sampleRate >> 8) & 0xff, (sampleRate >> 16) & 0xff, (sampleRate >> 24) & 0xff]), 24);
|
|
40
|
+
// byte rate (sample rate * block align)
|
|
41
|
+
const byteRate = sampleRate * 2 * bytesPerSample; // 2 channels, 16-bit per channel
|
|
42
|
+
header.set(new Uint8Array([byteRate & 0xff, (byteRate >> 8) & 0xff, (byteRate >> 16) & 0xff, (byteRate >> 24) & 0xff]), 28);
|
|
43
|
+
// block align (channels * bytes per sample)
|
|
44
|
+
header.set([4, 0], 32); // 2 channels * 16-bit per channel / 8
|
|
45
|
+
// bits per sample
|
|
46
|
+
header.set([16, 0], 34); // 16-bit
|
|
47
|
+
|
|
48
|
+
// data chunk identifier 'data'
|
|
49
|
+
header.set([100, 97, 116, 97], 36);
|
|
50
|
+
// data chunk length
|
|
51
|
+
header.set(new Uint8Array([dataSize & 0xff, (dataSize >> 8) & 0xff, (dataSize >> 16) & 0xff, (dataSize >> 24) & 0xff]), 40);
|
|
52
|
+
|
|
53
|
+
const wavData = new Uint8Array(headerSize + dataSize);
|
|
54
|
+
wavData.set(header, 0);
|
|
55
|
+
// Interleave audio data (combine channels)
|
|
56
|
+
let offset = headerSize;
|
|
57
|
+
|
|
58
|
+
let multiplier;
|
|
59
|
+
if(normalizeAudio)
|
|
60
|
+
{
|
|
61
|
+
// find min and max values to prevent clipping when converting to 16 bits
|
|
62
|
+
const initialMultiplier = 32767;
|
|
63
|
+
|
|
64
|
+
const max = Math.max(
|
|
65
|
+
channel1Data.reduce((max, value) => (value > max ? value : max), -Infinity),
|
|
66
|
+
channel2Data.reduce((max, value) => (value > max ? value : max), -Infinity)
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const min = Math.min(
|
|
70
|
+
channel1Data.reduce((min, value) => (value < min ? value : min), Infinity),
|
|
71
|
+
channel2Data.reduce((min, value) => (value < min ? value : min), Infinity)
|
|
72
|
+
);
|
|
73
|
+
const maxAbsValue = Math.max(max, Math.abs(min));
|
|
74
|
+
multiplier = initialMultiplier / maxAbsValue;
|
|
75
|
+
}
|
|
76
|
+
else
|
|
77
|
+
{
|
|
78
|
+
multiplier = 32767;
|
|
79
|
+
// clip audio
|
|
80
|
+
for(let i = 0; i < length; i++)
|
|
81
|
+
{
|
|
82
|
+
channel1Data[i] = Math.min(1, Math.max(-1, channel1Data[i]));
|
|
83
|
+
channel2Data[i] = Math.min(1, Math.max(-1, channel2Data[i]));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for (let i = 0; i < length; i++)
|
|
87
|
+
{
|
|
88
|
+
// interleave both channels
|
|
89
|
+
const sample1 = channel1Data[i] * multiplier;
|
|
90
|
+
const sample2 = channel2Data[i] * multiplier;
|
|
91
|
+
|
|
92
|
+
// convert to 16-bit
|
|
93
|
+
wavData[offset++] = sample1 & 0xff;
|
|
94
|
+
wavData[offset++] = (sample1 >> 8) & 0xff;
|
|
95
|
+
wavData[offset++] = sample2 & 0xff;
|
|
96
|
+
wavData[offset++] = (sample2 >> 8) & 0xff;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
return new Blob([wavData.buffer], { type: 'audio/wav' });
|
|
101
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads as Big endian
|
|
3
|
+
* @param dataArray {IndexedByteArray}
|
|
4
|
+
* @param bytesAmount {number}
|
|
5
|
+
* @returns {number}
|
|
6
|
+
*/
|
|
7
|
+
export function readBytesAsUintBigEndian(dataArray, bytesAmount) {
|
|
8
|
+
let out = 0
|
|
9
|
+
for (let i = 8 * (bytesAmount - 1); i >= 0; i -= 8) {
|
|
10
|
+
out |= (dataArray[dataArray.currentIndex++] << i)
|
|
11
|
+
}
|
|
12
|
+
return out >>> 0
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param number {number}
|
|
17
|
+
* @param bytesAmount {number}
|
|
18
|
+
* @returns {number[]}
|
|
19
|
+
*/
|
|
20
|
+
export function writeBytesAsUintBigEndian(number, bytesAmount) {
|
|
21
|
+
const bytes = new Array(bytesAmount).fill(0)
|
|
22
|
+
for (let i = bytesAmount - 1; i >= 0; i--) {
|
|
23
|
+
bytes[i] = number & 0xFF
|
|
24
|
+
number >>= 8
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return bytes
|
|
28
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads as little endian
|
|
3
|
+
* @param dataArray {IndexedByteArray}
|
|
4
|
+
* @param bytesAmount {number}
|
|
5
|
+
* @returns {number}
|
|
6
|
+
*/
|
|
7
|
+
export function readBytesAsUintLittleEndian(dataArray, bytesAmount){
|
|
8
|
+
let out = 0;
|
|
9
|
+
for(let i = 0; i < bytesAmount; i++)
|
|
10
|
+
{
|
|
11
|
+
out |= (dataArray[dataArray.currentIndex++] << i * 8);
|
|
12
|
+
}
|
|
13
|
+
// make sure it stays unsigned
|
|
14
|
+
return out >>> 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Writes a number as little endian seems to also work for negative numbers so yay?
|
|
19
|
+
* @param dataArray {IndexedByteArray}
|
|
20
|
+
* @param number {number}
|
|
21
|
+
* @param byteTarget {number}
|
|
22
|
+
*/
|
|
23
|
+
export function writeLittleEndian(dataArray, number, byteTarget)
|
|
24
|
+
{
|
|
25
|
+
for(let i = 0; i < byteTarget; i++)
|
|
26
|
+
{
|
|
27
|
+
dataArray[dataArray.currentIndex++] = (number >> (i * 8)) & 0xFF;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param dataArray {IndexedByteArray}
|
|
33
|
+
* @param word {number}
|
|
34
|
+
*/
|
|
35
|
+
export function writeWord(dataArray, word)
|
|
36
|
+
{
|
|
37
|
+
dataArray[dataArray.currentIndex++] = word & 0xFF;
|
|
38
|
+
dataArray[dataArray.currentIndex++] = word >> 8;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param dataArray {IndexedByteArray}
|
|
43
|
+
* @param dword {number}
|
|
44
|
+
*/
|
|
45
|
+
export function writeDword(dataArray, dword)
|
|
46
|
+
{
|
|
47
|
+
writeLittleEndian(dataArray, dword, 4);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param byte1 {number}
|
|
52
|
+
* @param byte2 {number}
|
|
53
|
+
* @returns {number}
|
|
54
|
+
*/
|
|
55
|
+
export function signedInt16(byte1, byte2){
|
|
56
|
+
let val = (byte2 << 8) | byte1;
|
|
57
|
+
if(val > 32767)
|
|
58
|
+
{
|
|
59
|
+
return val - 65536;
|
|
60
|
+
}
|
|
61
|
+
return val;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param byte {number}
|
|
66
|
+
* @returns {number}
|
|
67
|
+
*/
|
|
68
|
+
export function signedInt8(byte) {
|
|
69
|
+
if(byte > 127)
|
|
70
|
+
{
|
|
71
|
+
return byte - 256;
|
|
72
|
+
}
|
|
73
|
+
return byte;
|
|
74
|
+
}
|