spessasynth_core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.idea/inspectionProfiles/Project_Default.xml +10 -0
- package/.idea/jsLibraryMappings.xml +6 -0
- package/.idea/modules.xml +8 -0
- package/.idea/spessasynth_core.iml +12 -0
- package/.idea/vcs.xml +6 -0
- package/README.md +376 -0
- package/index.js +7 -0
- package/package.json +34 -0
- package/spessasynth_core/midi_parser/README.md +3 -0
- package/spessasynth_core/midi_parser/midi_loader.js +381 -0
- package/spessasynth_core/midi_parser/midi_message.js +231 -0
- package/spessasynth_core/sequencer/sequencer.js +192 -0
- package/spessasynth_core/sequencer/worklet_sequencer/play.js +221 -0
- package/spessasynth_core/sequencer/worklet_sequencer/process_event.js +138 -0
- package/spessasynth_core/sequencer/worklet_sequencer/process_tick.js +85 -0
- package/spessasynth_core/sequencer/worklet_sequencer/song_control.js +90 -0
- package/spessasynth_core/soundfont/README.md +4 -0
- package/spessasynth_core/soundfont/chunk/generators.js +205 -0
- package/spessasynth_core/soundfont/chunk/instruments.js +60 -0
- package/spessasynth_core/soundfont/chunk/modulators.js +232 -0
- package/spessasynth_core/soundfont/chunk/presets.js +264 -0
- package/spessasynth_core/soundfont/chunk/riff_chunk.js +46 -0
- package/spessasynth_core/soundfont/chunk/samples.js +250 -0
- package/spessasynth_core/soundfont/chunk/zones.js +264 -0
- package/spessasynth_core/soundfont/soundfont_parser.js +301 -0
- package/spessasynth_core/synthetizer/README.md +6 -0
- package/spessasynth_core/synthetizer/synthesizer.js +303 -0
- package/spessasynth_core/synthetizer/worklet_system/README.md +3 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/controller_control.js +285 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/data_entry.js +280 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/note_off.js +102 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/note_on.js +75 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/program_control.js +140 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/system_exclusive.js +265 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/tuning_control.js +105 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/vibrato_control.js +29 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/voice_control.js +186 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/lfo.js +23 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/lowpass_filter.js +95 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/modulation_envelope.js +73 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/modulator_curves.js +86 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/stereo_panner.js +76 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/unit_converter.js +66 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/volume_envelope.js +194 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js +83 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js +173 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js +105 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/worklet_voice.js +313 -0
- package/spessasynth_core/utils/README.md +4 -0
- package/spessasynth_core/utils/buffer_to_wav.js +70 -0
- package/spessasynth_core/utils/byte_functions.js +141 -0
- package/spessasynth_core/utils/loggin.js +79 -0
- package/spessasynth_core/utils/other.js +49 -0
- package/spessasynth_core/utils/shiftable_array.js +26 -0
- package/spessasynth_core/utils/stbvorbis_sync.js +1877 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { _addNewMidiPort, _processEvent } from './worklet_sequencer/process_event.js'
|
|
2
|
+
import { _findFirstEventIndex, _processTick } from './worklet_sequencer/process_tick.js'
|
|
3
|
+
import { loadNewSequence, loadNewSongList, nextSong, previousSong } from './worklet_sequencer/song_control.js'
|
|
4
|
+
import { _playTo, _recalculateStartTime, play, setTimeTicks } from './worklet_sequencer/play.js'
|
|
5
|
+
import { midiControllers } from '../midi_parser/midi_message.js'
|
|
6
|
+
import { SpessaSynthWarn } from '../utils/loggin.js'
|
|
7
|
+
|
|
8
|
+
class Sequencer
|
|
9
|
+
{
|
|
10
|
+
/**
|
|
11
|
+
* @param Synthesizer {Synthesizer}
|
|
12
|
+
*/
|
|
13
|
+
constructor(Synthesizer)
|
|
14
|
+
{
|
|
15
|
+
this.synth = Synthesizer;
|
|
16
|
+
this.ignoreEvents = false;
|
|
17
|
+
|
|
18
|
+
// event's number in this.events
|
|
19
|
+
/**
|
|
20
|
+
* @type {number[]}
|
|
21
|
+
*/
|
|
22
|
+
this.eventIndex = [];
|
|
23
|
+
this.songIndex = 0;
|
|
24
|
+
|
|
25
|
+
// tracks the time that we have already played
|
|
26
|
+
/**
|
|
27
|
+
* @type {number}
|
|
28
|
+
*/
|
|
29
|
+
this.playedTime = 0;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The (relative) time when the sequencer was paused. If it's not paused then it's undefined.
|
|
33
|
+
* @type {number}
|
|
34
|
+
*/
|
|
35
|
+
this.pausedTime = undefined;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Absolute playback startTime, bases on the synth's time
|
|
39
|
+
* @type {number}
|
|
40
|
+
*/
|
|
41
|
+
this.absoluteStartTime = this.currentTime;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Controls the playback's rate
|
|
45
|
+
* @type {number}
|
|
46
|
+
*/
|
|
47
|
+
this._playbackRate = 1;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Currently playing notes (for pausing and resuming)
|
|
51
|
+
* @type {{
|
|
52
|
+
* midiNote: number,
|
|
53
|
+
* channel: number,
|
|
54
|
+
* velocity: number
|
|
55
|
+
* }[]}
|
|
56
|
+
*/
|
|
57
|
+
this.playingNotes = [];
|
|
58
|
+
|
|
59
|
+
// controls if the sequencer loops (defaults to true)
|
|
60
|
+
this.loop = true;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* the current track data
|
|
64
|
+
* @type {MIDI}
|
|
65
|
+
*/
|
|
66
|
+
this.midiData = undefined;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* midi port number for the corresponding track
|
|
70
|
+
* @type {number[]}
|
|
71
|
+
*/
|
|
72
|
+
this.midiPorts = [];
|
|
73
|
+
|
|
74
|
+
this.midiPortChannelOffset = 0;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* midi port: channel offset
|
|
78
|
+
* @type {Object<number, number>}
|
|
79
|
+
*/
|
|
80
|
+
this.midiPortChannelOffsets = {};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param value {number}
|
|
85
|
+
*/
|
|
86
|
+
set playbackRate(value)
|
|
87
|
+
{
|
|
88
|
+
const time = this.currentTime;
|
|
89
|
+
this._playbackRate = value;
|
|
90
|
+
this.currentTime = time;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get currentTime()
|
|
94
|
+
{
|
|
95
|
+
// return the paused time if it's set to something other than undefined
|
|
96
|
+
if(this.pausedTime)
|
|
97
|
+
{
|
|
98
|
+
return this.pausedTime;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return (this.synth.currentTime - this.absoluteStartTime) * this._playbackRate;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
set currentTime(time)
|
|
105
|
+
{
|
|
106
|
+
if(time < 0 || time > this.duration || time === 0)
|
|
107
|
+
{
|
|
108
|
+
// time is 0
|
|
109
|
+
this.setTimeTicks(this.midiData.firstNoteOn - 1);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
this.stop();
|
|
113
|
+
this.playingNotes = [];
|
|
114
|
+
this.pausedTime = undefined;
|
|
115
|
+
const isNotFinished = this._playTo(time);
|
|
116
|
+
this._recalculateStartTime(time);
|
|
117
|
+
if(!isNotFinished)
|
|
118
|
+
{
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
this.play();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Pauses the playback
|
|
126
|
+
*/
|
|
127
|
+
pause()
|
|
128
|
+
{
|
|
129
|
+
if(this.paused)
|
|
130
|
+
{
|
|
131
|
+
SpessaSynthWarn("Already paused");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
this.pausedTime = this.currentTime;
|
|
135
|
+
this.stop();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Stops the playback
|
|
140
|
+
*/
|
|
141
|
+
stop()
|
|
142
|
+
{
|
|
143
|
+
this.clearProcessHandler()
|
|
144
|
+
// disable sustain
|
|
145
|
+
for (let i = 0; i < 16; i++) {
|
|
146
|
+
this.synth.controllerChange(i, midiControllers.sustainPedal, 0);
|
|
147
|
+
}
|
|
148
|
+
this.synth.stopAllChannels();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
_resetTimers()
|
|
152
|
+
{
|
|
153
|
+
this.playedTime = 0
|
|
154
|
+
this.eventIndex = Array(this.tracks.length).fill(0);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* true if paused, false if playing or stopped
|
|
159
|
+
* @returns {boolean}
|
|
160
|
+
*/
|
|
161
|
+
get paused()
|
|
162
|
+
{
|
|
163
|
+
return this.pausedTime !== undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
setProcessHandler()
|
|
167
|
+
{
|
|
168
|
+
this.synth.processTickCallback = this._processTick.bind(this);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
clearProcessHandler()
|
|
172
|
+
{
|
|
173
|
+
this.synth.processTickCallback = undefined;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
Sequencer.prototype._processEvent = _processEvent;
|
|
178
|
+
Sequencer.prototype._addNewMidiPort = _addNewMidiPort;
|
|
179
|
+
Sequencer.prototype._processTick = _processTick;
|
|
180
|
+
Sequencer.prototype._findFirstEventIndex = _findFirstEventIndex;
|
|
181
|
+
|
|
182
|
+
Sequencer.prototype.loadNewSequence = loadNewSequence;
|
|
183
|
+
Sequencer.prototype.loadNewSongList = loadNewSongList;
|
|
184
|
+
Sequencer.prototype.nextSong = nextSong;
|
|
185
|
+
Sequencer.prototype.previousSong = previousSong;
|
|
186
|
+
|
|
187
|
+
Sequencer.prototype.play = play;
|
|
188
|
+
Sequencer.prototype._playTo = _playTo;
|
|
189
|
+
Sequencer.prototype.setTimeTicks = setTimeTicks;
|
|
190
|
+
Sequencer.prototype._recalculateStartTime = _recalculateStartTime;
|
|
191
|
+
|
|
192
|
+
export { Sequencer }
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { getEvent, messageTypes, midiControllers } from '../../midi_parser/midi_message.js'
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
// an array with preset default values
|
|
5
|
+
const defaultControllerArray = new Int16Array(127);
|
|
6
|
+
// default values
|
|
7
|
+
defaultControllerArray[midiControllers.mainVolume] = 100;
|
|
8
|
+
defaultControllerArray[midiControllers.expressionController] = 127;
|
|
9
|
+
defaultControllerArray[midiControllers.pan] = 64;
|
|
10
|
+
defaultControllerArray[midiControllers.releaseTime] = 64;
|
|
11
|
+
defaultControllerArray[midiControllers.brightness] = 64;
|
|
12
|
+
defaultControllerArray[midiControllers.effects1Depth] = 40;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* plays from start to the target time, excluding note messages (to get the synth to the correct state)
|
|
16
|
+
* @private
|
|
17
|
+
* @param time {number} in seconds
|
|
18
|
+
* @param ticks {number} optional MIDI ticks, when given is used instead of time
|
|
19
|
+
* @returns {boolean} true if the midi file is not finished
|
|
20
|
+
* @this {Sequencer}
|
|
21
|
+
*/
|
|
22
|
+
export function _playTo(time, ticks = undefined)
|
|
23
|
+
{
|
|
24
|
+
this.oneTickToSeconds = 60 / (120 * this.midiData.timeDivision);
|
|
25
|
+
// process every non note message from the start
|
|
26
|
+
this.synth.resetAllControllers();
|
|
27
|
+
|
|
28
|
+
this._resetTimers()
|
|
29
|
+
/**
|
|
30
|
+
* save pitch bends here and send them only after
|
|
31
|
+
* @type {number[]}
|
|
32
|
+
*/
|
|
33
|
+
const pitchBends = Array(16).fill(8192);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Save controllers here and send them only after
|
|
37
|
+
* @type {number[][]}
|
|
38
|
+
*/
|
|
39
|
+
const savedControllers = [];
|
|
40
|
+
for (let i = 0; i < 16; i++)
|
|
41
|
+
{
|
|
42
|
+
savedControllers.push(Array.from(defaultControllerArray));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
while(true)
|
|
46
|
+
{
|
|
47
|
+
// find next event
|
|
48
|
+
let trackIndex = this._findFirstEventIndex();
|
|
49
|
+
let event = this.tracks[trackIndex][this.eventIndex[trackIndex]];
|
|
50
|
+
if(ticks !== undefined)
|
|
51
|
+
{
|
|
52
|
+
if(event.ticks >= ticks)
|
|
53
|
+
{
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else
|
|
58
|
+
{
|
|
59
|
+
if(this.playedTime >= time)
|
|
60
|
+
{
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// skip note ons
|
|
66
|
+
const info = getEvent(event.messageStatusByte);
|
|
67
|
+
switch(info.status)
|
|
68
|
+
{
|
|
69
|
+
// skip note messages
|
|
70
|
+
case messageTypes.noteOn:
|
|
71
|
+
case messageTypes.noteOff:
|
|
72
|
+
break;
|
|
73
|
+
|
|
74
|
+
// skip pitch bend
|
|
75
|
+
case messageTypes.pitchBend:
|
|
76
|
+
pitchBends[info.channel] = event.messageData[1] << 7 | event.messageData[0];
|
|
77
|
+
break;
|
|
78
|
+
|
|
79
|
+
case messageTypes.controllerChange:
|
|
80
|
+
// do not skip data entries
|
|
81
|
+
const controllerNumber = event.messageData[0];
|
|
82
|
+
if(
|
|
83
|
+
controllerNumber === midiControllers.dataDecrement ||
|
|
84
|
+
controllerNumber === midiControllers.dataEntryMsb ||
|
|
85
|
+
controllerNumber === midiControllers.dataDecrement ||
|
|
86
|
+
controllerNumber === midiControllers.lsbForControl6DataEntry ||
|
|
87
|
+
controllerNumber === midiControllers.RPNLsb ||
|
|
88
|
+
controllerNumber === midiControllers.RPNMsb ||
|
|
89
|
+
controllerNumber === midiControllers.NRPNLsb ||
|
|
90
|
+
controllerNumber === midiControllers.NRPNMsb ||
|
|
91
|
+
controllerNumber === midiControllers.bankSelect ||
|
|
92
|
+
controllerNumber === midiControllers.lsbForControl0BankSelect||
|
|
93
|
+
controllerNumber === midiControllers.resetAllControllers
|
|
94
|
+
)
|
|
95
|
+
{
|
|
96
|
+
this.synth.controllerChange(info.channel, controllerNumber, event.messageData[1]);
|
|
97
|
+
}
|
|
98
|
+
else
|
|
99
|
+
{
|
|
100
|
+
// Keep in mind midi ports to determine channel!!
|
|
101
|
+
const channel = info.channel + (this.midiPortChannelOffsets[this.midiPorts[trackIndex]] || 0);
|
|
102
|
+
if(savedControllers[channel] === undefined)
|
|
103
|
+
{
|
|
104
|
+
savedControllers[channel] = Array.from(defaultControllerArray);
|
|
105
|
+
}
|
|
106
|
+
savedControllers[channel][controllerNumber] = event.messageData[1];
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
|
|
110
|
+
// midiport: handle it and make sure that the saved controllers table is the same size as synth channels
|
|
111
|
+
case messageTypes.midiPort:
|
|
112
|
+
this._processEvent(event, trackIndex);
|
|
113
|
+
if(this.synth.workletProcessorChannels.length > savedControllers.length)
|
|
114
|
+
{
|
|
115
|
+
while(this.synth.workletProcessorChannels.length > savedControllers.length)
|
|
116
|
+
{
|
|
117
|
+
savedControllers.push(Array.from(defaultControllerArray));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
|
|
122
|
+
default:
|
|
123
|
+
this._processEvent(event, trackIndex);
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this.eventIndex[trackIndex]++;
|
|
128
|
+
// find next event
|
|
129
|
+
trackIndex = this._findFirstEventIndex();
|
|
130
|
+
let nextEvent = this.tracks[trackIndex][this.eventIndex[trackIndex]];
|
|
131
|
+
if(nextEvent === undefined)
|
|
132
|
+
{
|
|
133
|
+
this.stop();
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
this.playedTime += this.oneTickToSeconds * (nextEvent.ticks - event.ticks);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// restoring saved controllers
|
|
140
|
+
// for all synth channels
|
|
141
|
+
for (let channelNumber = 0; channelNumber < this.synth.workletProcessorChannels.length; channelNumber++) {
|
|
142
|
+
// restore pitch bends
|
|
143
|
+
if(pitchBends[channelNumber] !== undefined) {
|
|
144
|
+
this.synth.pitchWheel(channelNumber, pitchBends[channelNumber] >> 7, pitchBends[channelNumber] & 0x7F);
|
|
145
|
+
}
|
|
146
|
+
if(savedControllers[channelNumber] !== undefined)
|
|
147
|
+
{
|
|
148
|
+
// every controller that has changed
|
|
149
|
+
savedControllers[channelNumber].forEach((value, index) => {
|
|
150
|
+
if(value !== defaultControllerArray[index])
|
|
151
|
+
{
|
|
152
|
+
this.synth.controllerChange(channelNumber, index, value);
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Starts the playback
|
|
162
|
+
* @param resetTime {boolean} If true, time is set to 0s
|
|
163
|
+
* @this {Sequencer}
|
|
164
|
+
*/
|
|
165
|
+
export function play(resetTime = false)
|
|
166
|
+
{
|
|
167
|
+
|
|
168
|
+
// reset the time if necesarry
|
|
169
|
+
if(resetTime)
|
|
170
|
+
{
|
|
171
|
+
this.currentTime = 0;
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if(this.currentTime >= this.duration)
|
|
176
|
+
{
|
|
177
|
+
this.currentTime = 0;
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// unpause if paused
|
|
182
|
+
if(this.paused)
|
|
183
|
+
{
|
|
184
|
+
// adjust the start time
|
|
185
|
+
this._recalculateStartTime(this.pausedTime)
|
|
186
|
+
this.pausedTime = undefined;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
this.playingNotes.forEach(n => {
|
|
190
|
+
this.synth.noteOn(n.channel, n.midiNote, n.velocity);
|
|
191
|
+
});
|
|
192
|
+
this.setProcessHandler();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @this {Sequencer}
|
|
197
|
+
* @param ticks {number}
|
|
198
|
+
*/
|
|
199
|
+
export function setTimeTicks(ticks)
|
|
200
|
+
{
|
|
201
|
+
this.stop();
|
|
202
|
+
this.playingNotes = [];
|
|
203
|
+
this.pausedTime = undefined;
|
|
204
|
+
const isNotFinished = this._playTo(0, ticks);
|
|
205
|
+
this._recalculateStartTime(this.playedTime);
|
|
206
|
+
if(!isNotFinished)
|
|
207
|
+
{
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
this.play();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* @param time
|
|
215
|
+
* @private
|
|
216
|
+
* @this {Sequencer}
|
|
217
|
+
*/
|
|
218
|
+
export function _recalculateStartTime(time)
|
|
219
|
+
{
|
|
220
|
+
this.absoluteStartTime = this.synth.currentTime - time / this._playbackRate;
|
|
221
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { getEvent, messageTypes } from '../../midi_parser/midi_message.js'
|
|
2
|
+
import { consoleColors } from '../../utils/other.js'
|
|
3
|
+
import { readBytesAsUintBigEndian } from '../../utils/byte_functions.js'
|
|
4
|
+
import { SpessaSynthWarn } from '../../utils/loggin.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Processes a single event
|
|
8
|
+
* @param event {MidiMessage}
|
|
9
|
+
* @param trackIndex {number}
|
|
10
|
+
* @this {Sequencer}
|
|
11
|
+
* @private
|
|
12
|
+
*/
|
|
13
|
+
export function _processEvent(event, trackIndex)
|
|
14
|
+
{
|
|
15
|
+
if(this.ignoreEvents) return;
|
|
16
|
+
const statusByteData = getEvent(event.messageStatusByte);
|
|
17
|
+
statusByteData.channel += this.midiPortChannelOffsets[this.midiPorts[trackIndex]] || 0;
|
|
18
|
+
// process the event
|
|
19
|
+
switch (statusByteData.status) {
|
|
20
|
+
case messageTypes.noteOn:
|
|
21
|
+
const velocity = event.messageData[1];
|
|
22
|
+
if(velocity > 0) {
|
|
23
|
+
this.synth.noteOn(statusByteData.channel, event.messageData[0], velocity);
|
|
24
|
+
this.playingNotes.push({
|
|
25
|
+
midiNote: event.messageData[0],
|
|
26
|
+
channel: statusByteData.channel,
|
|
27
|
+
velocity: velocity
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
else
|
|
31
|
+
{
|
|
32
|
+
this.synth.noteOff(statusByteData.channel, event.messageData[0]);
|
|
33
|
+
this.playingNotes.splice(this.playingNotes.findIndex(n =>
|
|
34
|
+
n.midiNote === event.messageData[0] && n.channel === statusByteData.channel), 1);
|
|
35
|
+
}
|
|
36
|
+
break;
|
|
37
|
+
|
|
38
|
+
case messageTypes.noteOff:
|
|
39
|
+
this.synth.noteOff(statusByteData.channel, event.messageData[0]);
|
|
40
|
+
this.playingNotes.splice(this.playingNotes.findIndex(n =>
|
|
41
|
+
n.midiNote === event.messageData[0] && n.channel === statusByteData.channel), 1);
|
|
42
|
+
break;
|
|
43
|
+
|
|
44
|
+
case messageTypes.setTempo:
|
|
45
|
+
this.oneTickToSeconds = 60 / (getTempo(event) * this.midiData.timeDivision);
|
|
46
|
+
if(this.oneTickToSeconds === 0)
|
|
47
|
+
{
|
|
48
|
+
this.oneTickToSeconds = 60 / (120 * this.midiData.timeDivision);
|
|
49
|
+
SpessaSynthWarn("invalid tempo! falling back to 120 BPM");
|
|
50
|
+
}
|
|
51
|
+
break;
|
|
52
|
+
|
|
53
|
+
case messageTypes.midiPort:
|
|
54
|
+
const port = event.messageData[0];
|
|
55
|
+
// assign new 16 channels if the port is not occupied yet
|
|
56
|
+
if(this.midiPortChannelOffset === 0)
|
|
57
|
+
{
|
|
58
|
+
this.midiPortChannelOffset += 16;
|
|
59
|
+
this.midiPortChannelOffsets[port] = 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if(this.midiPortChannelOffsets[port] === undefined)
|
|
63
|
+
{
|
|
64
|
+
if(this.synth.workletProcessorChannels.length < this.midiPortChannelOffset + 16) {
|
|
65
|
+
this._addNewMidiPort();
|
|
66
|
+
}
|
|
67
|
+
this.midiPortChannelOffsets[port] = this.midiPortChannelOffset;
|
|
68
|
+
this.midiPortChannelOffset += 16;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
this.midiPorts[trackIndex] = port;
|
|
72
|
+
break;
|
|
73
|
+
|
|
74
|
+
case messageTypes.endOfTrack:
|
|
75
|
+
case messageTypes.midiChannelPrefix:
|
|
76
|
+
case messageTypes.timeSignature:
|
|
77
|
+
case messageTypes.songPosition:
|
|
78
|
+
case messageTypes.activeSensing:
|
|
79
|
+
case messageTypes.keySignature:
|
|
80
|
+
break;
|
|
81
|
+
|
|
82
|
+
default:
|
|
83
|
+
SpessaSynthWarn(`%cUnrecognized Event: %c${event.messageStatusByte}%c status byte: %c${Object.keys(messageTypes).find(k => messageTypes[k] === statusByteData.status)}`,
|
|
84
|
+
consoleColors.warn,
|
|
85
|
+
consoleColors.unrecognized,
|
|
86
|
+
consoleColors.warn,
|
|
87
|
+
consoleColors.value);
|
|
88
|
+
break;
|
|
89
|
+
|
|
90
|
+
case messageTypes.pitchBend:
|
|
91
|
+
this.synth.pitchWheel(statusByteData.channel, event.messageData[1], event.messageData[0]);
|
|
92
|
+
break;
|
|
93
|
+
|
|
94
|
+
case messageTypes.controllerChange:
|
|
95
|
+
this.synth.controllerChange(statusByteData.channel, event.messageData[0], event.messageData[1]);
|
|
96
|
+
break;
|
|
97
|
+
|
|
98
|
+
case messageTypes.programChange:
|
|
99
|
+
this.synth.programChange(statusByteData.channel, event.messageData[0]);
|
|
100
|
+
break;
|
|
101
|
+
|
|
102
|
+
case messageTypes.systemExclusive:
|
|
103
|
+
this.synth.systemExclusive(event.messageData);
|
|
104
|
+
break;
|
|
105
|
+
|
|
106
|
+
case messageTypes.reset:
|
|
107
|
+
this.synth.stopAllChannels();
|
|
108
|
+
this.synth.resetAllControllers();
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Adds 16 channels to the synth
|
|
115
|
+
* @this {Sequencer}
|
|
116
|
+
* @private
|
|
117
|
+
*/
|
|
118
|
+
export function _addNewMidiPort()
|
|
119
|
+
{
|
|
120
|
+
for (let i = 0; i < 16; i++) {
|
|
121
|
+
this.synth.addNewChannel(true);
|
|
122
|
+
if(i === 9)
|
|
123
|
+
{
|
|
124
|
+
this.synth.setDrums(this.synth.workletProcessorChannels.length - 1, true);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* gets tempo from the midi message
|
|
131
|
+
* @param event {MidiMessage}
|
|
132
|
+
* @return {number} the tempo in bpm
|
|
133
|
+
*/
|
|
134
|
+
function getTempo(event)
|
|
135
|
+
{
|
|
136
|
+
event.messageData.currentIndex = 0;
|
|
137
|
+
return 60000000 / readBytesAsUintBigEndian(event.messageData, 3);
|
|
138
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Processes a single tick
|
|
3
|
+
* @private
|
|
4
|
+
* @this {Sequencer}
|
|
5
|
+
*/
|
|
6
|
+
export function _processTick()
|
|
7
|
+
{
|
|
8
|
+
let current = this.currentTime;
|
|
9
|
+
while(this.playedTime < current)
|
|
10
|
+
{
|
|
11
|
+
// find next event
|
|
12
|
+
let trackIndex = this._findFirstEventIndex();
|
|
13
|
+
let event = this.tracks[trackIndex][this.eventIndex[trackIndex]];
|
|
14
|
+
this._processEvent(event, trackIndex);
|
|
15
|
+
|
|
16
|
+
this.eventIndex[trackIndex]++;
|
|
17
|
+
|
|
18
|
+
// find next event
|
|
19
|
+
trackIndex = this._findFirstEventIndex();
|
|
20
|
+
if(this.tracks[trackIndex].length <= this.eventIndex[trackIndex])
|
|
21
|
+
{
|
|
22
|
+
// song has ended
|
|
23
|
+
if(this.loop)
|
|
24
|
+
{
|
|
25
|
+
this.setTimeTicks(this.midiData.loop.start);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
this.eventIndex[trackIndex]--;
|
|
29
|
+
this.pause(true);
|
|
30
|
+
if(this.songs.length > 1)
|
|
31
|
+
{
|
|
32
|
+
this.nextSong();
|
|
33
|
+
}
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
let eventNext = this.tracks[trackIndex][this.eventIndex[trackIndex]];
|
|
37
|
+
this.playedTime += this.oneTickToSeconds * (eventNext.ticks - event.ticks);
|
|
38
|
+
|
|
39
|
+
// loop
|
|
40
|
+
if((this.midiData.loop.end <= event.ticks) && this.loop)
|
|
41
|
+
{
|
|
42
|
+
this.setTimeTicks(this.midiData.loop.start);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// if song has ended
|
|
46
|
+
else if(current >= this.duration)
|
|
47
|
+
{
|
|
48
|
+
if(this.loop)
|
|
49
|
+
{
|
|
50
|
+
this.setTimeTicks(this.midiData.loop.start);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
this.eventIndex[trackIndex]--;
|
|
54
|
+
this.pause(true);
|
|
55
|
+
if(this.songs.length > 1)
|
|
56
|
+
{
|
|
57
|
+
this.nextSong();
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @returns {number} the index of the first to the current played time
|
|
67
|
+
* @this {Sequencer}
|
|
68
|
+
*/
|
|
69
|
+
export function _findFirstEventIndex()
|
|
70
|
+
{
|
|
71
|
+
let index = 0;
|
|
72
|
+
let ticks = Infinity;
|
|
73
|
+
this.tracks.forEach((track, i) => {
|
|
74
|
+
if(this.eventIndex[i] >= track.length)
|
|
75
|
+
{
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if(track[this.eventIndex[i]].ticks < ticks)
|
|
79
|
+
{
|
|
80
|
+
index = i;
|
|
81
|
+
ticks = track[this.eventIndex[i]].ticks;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
return index;
|
|
85
|
+
}
|