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.
Files changed (55) hide show
  1. package/.idea/inspectionProfiles/Project_Default.xml +10 -0
  2. package/.idea/jsLibraryMappings.xml +6 -0
  3. package/.idea/modules.xml +8 -0
  4. package/.idea/spessasynth_core.iml +12 -0
  5. package/.idea/vcs.xml +6 -0
  6. package/README.md +376 -0
  7. package/index.js +7 -0
  8. package/package.json +34 -0
  9. package/spessasynth_core/midi_parser/README.md +3 -0
  10. package/spessasynth_core/midi_parser/midi_loader.js +381 -0
  11. package/spessasynth_core/midi_parser/midi_message.js +231 -0
  12. package/spessasynth_core/sequencer/sequencer.js +192 -0
  13. package/spessasynth_core/sequencer/worklet_sequencer/play.js +221 -0
  14. package/spessasynth_core/sequencer/worklet_sequencer/process_event.js +138 -0
  15. package/spessasynth_core/sequencer/worklet_sequencer/process_tick.js +85 -0
  16. package/spessasynth_core/sequencer/worklet_sequencer/song_control.js +90 -0
  17. package/spessasynth_core/soundfont/README.md +4 -0
  18. package/spessasynth_core/soundfont/chunk/generators.js +205 -0
  19. package/spessasynth_core/soundfont/chunk/instruments.js +60 -0
  20. package/spessasynth_core/soundfont/chunk/modulators.js +232 -0
  21. package/spessasynth_core/soundfont/chunk/presets.js +264 -0
  22. package/spessasynth_core/soundfont/chunk/riff_chunk.js +46 -0
  23. package/spessasynth_core/soundfont/chunk/samples.js +250 -0
  24. package/spessasynth_core/soundfont/chunk/zones.js +264 -0
  25. package/spessasynth_core/soundfont/soundfont_parser.js +301 -0
  26. package/spessasynth_core/synthetizer/README.md +6 -0
  27. package/spessasynth_core/synthetizer/synthesizer.js +303 -0
  28. package/spessasynth_core/synthetizer/worklet_system/README.md +3 -0
  29. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/controller_control.js +285 -0
  30. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/data_entry.js +280 -0
  31. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/note_off.js +102 -0
  32. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/note_on.js +75 -0
  33. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/program_control.js +140 -0
  34. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/system_exclusive.js +265 -0
  35. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/tuning_control.js +105 -0
  36. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/vibrato_control.js +29 -0
  37. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/voice_control.js +186 -0
  38. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/lfo.js +23 -0
  39. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/lowpass_filter.js +95 -0
  40. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/modulation_envelope.js +73 -0
  41. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/modulator_curves.js +86 -0
  42. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/stereo_panner.js +76 -0
  43. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/unit_converter.js +66 -0
  44. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/volume_envelope.js +194 -0
  45. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js +83 -0
  46. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js +173 -0
  47. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js +105 -0
  48. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/worklet_voice.js +313 -0
  49. package/spessasynth_core/utils/README.md +4 -0
  50. package/spessasynth_core/utils/buffer_to_wav.js +70 -0
  51. package/spessasynth_core/utils/byte_functions.js +141 -0
  52. package/spessasynth_core/utils/loggin.js +79 -0
  53. package/spessasynth_core/utils/other.js +49 -0
  54. package/spessasynth_core/utils/shiftable_array.js +26 -0
  55. 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
+ }