spessasynth_lib 3.20.19 → 3.20.20

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/@types/index.d.ts CHANGED
@@ -14,6 +14,7 @@ import { writeMIDIFile } from './midi_parser/midi_writer.js';
14
14
  import { writeRMIDI } from './midi_parser/rmidi_writer.js';
15
15
  import { applySnapshotToMIDI } from './midi_parser/midi_editor.js';
16
16
  import { modifyMIDI } from './midi_parser/midi_editor.js';
17
+ import { MIDIticksToSeconds } from './midi_parser/basic_midi.js';
17
18
  import { audioBufferToWav } from './utils/buffer_to_wav.js';
18
19
  import { SpessaSynthLogging } from './utils/loggin.js';
19
20
  import { SpessaSynthGroup } from './utils/loggin.js';
@@ -34,4 +35,4 @@ import { readBytesAsUintBigEndian } from './utils/byte_functions/big_endian.js';
34
35
  import { NON_CC_INDEX_OFFSET } from './synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js';
35
36
  import { ALL_CHANNELS_OR_DIFFERENT_ACTION } from './synthetizer/worklet_system/message_protocol/worklet_message.js';
36
37
  import { WORKLET_URL_ABSOLUTE } from './synthetizer/worklet_url.js';
37
- export { Sequencer, Synthetizer, DEFAULT_PERCUSSION, VOICE_CAP, BasicSoundFont, loadSoundFont, trimSoundfont, modulatorSources, encodeVorbis, MIDI, MIDIBuilder, IndexedByteArray, writeMIDIFile, writeRMIDI, applySnapshotToMIDI, modifyMIDI, audioBufferToWav, SpessaSynthLogging, SpessaSynthGroup, SpessaSynthTable, SpessaSynthGroupEnd, SpessaSynthInfo, SpessaSynthWarn, SpessaSynthGroupCollapsed, midiControllers, messageTypes, MIDIDeviceHandler, WebMidiLinkHandler, arrayToHexString, consoleColors, formatTitle, formatTime, readBytesAsUintBigEndian, NON_CC_INDEX_OFFSET, ALL_CHANNELS_OR_DIFFERENT_ACTION, WORKLET_URL_ABSOLUTE };
38
+ export { Sequencer, Synthetizer, DEFAULT_PERCUSSION, VOICE_CAP, BasicSoundFont, loadSoundFont, trimSoundfont, modulatorSources, encodeVorbis, MIDI, MIDIBuilder, IndexedByteArray, writeMIDIFile, writeRMIDI, applySnapshotToMIDI, modifyMIDI, MIDIticksToSeconds, audioBufferToWav, SpessaSynthLogging, SpessaSynthGroup, SpessaSynthTable, SpessaSynthGroupEnd, SpessaSynthInfo, SpessaSynthWarn, SpessaSynthGroupCollapsed, midiControllers, messageTypes, MIDIDeviceHandler, WebMidiLinkHandler, arrayToHexString, consoleColors, formatTitle, formatTime, readBytesAsUintBigEndian, NON_CC_INDEX_OFFSET, ALL_CHANNELS_OR_DIFFERENT_ACTION, WORKLET_URL_ABSOLUTE };
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Converts ticks to time in seconds
3
+ * @param ticks {number} time in MIDI ticks
4
+ * @param mid {BasicMIDI} the MIDI
5
+ * @returns {number} time in seconds
6
+ */
7
+ export function MIDIticksToSeconds(ticks: number, mid: BasicMIDI): number;
1
8
  export class BasicMIDI {
2
9
  /**
3
10
  * The time division of the sequence
@@ -115,11 +122,4 @@ export class BasicMIDI {
115
122
  * @type {MidiMessage[][]}
116
123
  */
117
124
  tracks: MidiMessage[][];
118
- /**
119
- * Converts ticks to time in seconds
120
- * @param ticks {number} time in MIDI ticks
121
- * @returns {number} time in seconds
122
- * @protected
123
- */
124
- protected _ticksToSeconds(ticks: number): number;
125
125
  }
@@ -7,6 +7,7 @@
7
7
  * @property {BasicMIDI} parsedMIDI - the MIDI to render
8
8
  * @property {SynthesizerSnapshot} snapshot - the snapshot to apply
9
9
  * @property {boolean|undefined} oneOutput - if synth should use one output with 32 channels (2 audio channels for each midi channel). this disables chorus and reverb.
10
+ * @property {number|undefined} loopCount - the times to loop the song
10
11
  */
11
12
  export const WORKLET_PROCESSOR_NAME: "spessasynth-worklet-system";
12
13
  export const VOICE_CAP: 450;
@@ -312,6 +313,10 @@ export type StartRenderingDataConfig = {
312
313
  * - if synth should use one output with 32 channels (2 audio channels for each midi channel). this disables chorus and reverb.
313
314
  */
314
315
  oneOutput: boolean | undefined;
316
+ /**
317
+ * - the times to loop the song
318
+ */
319
+ loopCount: number | undefined;
315
320
  };
316
321
  import { EventHandler } from './synth_event_handler.js';
317
322
  import { SoundfontManager } from './synth_soundfont_manager.js';
@@ -6,6 +6,7 @@
6
6
  *
7
7
  * @property {number} channelTransposeKeyShift - key shift of the channel
8
8
  * @property {Int8Array} channelOctaveTuning - the tuning for octave on this channel
9
+ * @property {Int16Array} keyCentTuning - tuning of individual keys in cents
9
10
  * @property {boolean} holdPedal - indicates whether the hold pedal is active
10
11
  * @property {boolean} drumChannel - indicates whether the channel is a drum channel
11
12
  *
@@ -77,6 +78,10 @@ export type WorkletProcessorChannel = {
77
78
  * - the tuning for octave on this channel
78
79
  */
79
80
  channelOctaveTuning: Int8Array;
81
+ /**
82
+ * - tuning of individual keys in cents
83
+ */
84
+ keyCentTuning: Int16Array;
80
85
  /**
81
86
  * - indicates whether the hold pedal is active
82
87
  */
package/README.md CHANGED
@@ -41,7 +41,8 @@ document.getElementById("button").onclick = async () => {
41
41
  - **SoundFont3 Support:** Play compressed SoundFonts!
42
42
  - **Experimental SF2Pack Support:** Play soundfonts compressed with BASSMIDI! (*Note: only works with vorbis compression*)
43
43
  - **Can load very large SoundFonts:** up to 4GB! *Note: Only Firefox handles this well; Chromium has a hard-coded memory limit*
44
- - **Soundfont manager:** Stack multiple soundfonts!
44
+ - **Soundfont manager:** Stack multiple soundfonts!
45
+ - **DLS Level 1 and 2 Support:** *internally converted to sf2*
45
46
  - **Reverb and chorus support:** [customizable!](https://github.com/spessasus/SpessaSynth/wiki/Synthetizer-Class#effects-configuration-object)
46
47
  - **Export audio files** using [OfflineAudioContext](https://developer.mozilla.org/en-US/docs/Web/API/OfflineAudioContext)
47
48
  - **[Custom modulators for additional controllers](https://github.com/spessasus/SpessaSynth/wiki/Modulator-Class#default-modulators):** Why not?
@@ -101,7 +102,7 @@ document.getElementById("button").onclick = async () => {
101
102
 
102
103
  #### Read and play DLS Level 1 or 2 files
103
104
  - Read DLS (DownLoadable Sounds) files as SF2 files!
104
- - **Works like a normal soundfont:** *Saving is still [just one function!](https://github.com/spessasus/SpessaSynth/wiki/SoundFont2-Class#write)*
105
+ - **Works like a normal soundfont:** *Saving it as sf2 is still [just one function!](https://github.com/spessasus/SpessaSynth/wiki/SoundFont2-Class#write)*
105
106
  - Converts articulators to both **modulators** and **generators**!
106
107
  - Works with both unsigned 8-bit samples and signed 16-bit samples!
107
108
  - **Covers special generator cases:** *such as modLfoToPitch*!
package/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { loadSoundFont } from "./soundfont/load_soundfont.js";
3
3
  import { BasicSoundFont } from "./soundfont/basic_soundfont/basic_soundfont.js";
4
4
  import { MIDI } from './midi_parser/midi_loader.js';
5
+ import { MIDIticksToSeconds } from './midi_parser/basic_midi.js';
5
6
  import { MIDIBuilder } from "./midi_parser/midi_builder.js";
6
7
  import { Synthetizer, VOICE_CAP, DEFAULT_PERCUSSION } from './synthetizer/synthetizer.js';
7
8
  import { Sequencer } from './sequencer/sequencer.js';
@@ -54,6 +55,7 @@ export {
54
55
  writeRMIDI,
55
56
  applySnapshotToMIDI,
56
57
  modifyMIDI,
58
+ MIDIticksToSeconds,
57
59
 
58
60
  // Utilities
59
61
  audioBufferToWav,
@@ -124,31 +124,30 @@ export class BasicMIDI
124
124
  */
125
125
  this.tracks = [];
126
126
  }
127
-
128
- /**
129
- * Converts ticks to time in seconds
130
- * @param ticks {number} time in MIDI ticks
131
- * @returns {number} time in seconds
132
- * @protected
133
- */
134
- _ticksToSeconds(ticks) {
135
- let totalSeconds = 0;
136
-
137
- while (ticks > 0)
138
- {
139
- // tempo changes are reversed so the first element is the last tempo change and the last element is the first tempo change
140
- // (always at tick 0 and tempo 120)
141
- // find the last tempo change that has occurred
142
- let tempo = this.tempoChanges.find(v => v.ticks < ticks);
143
-
144
- // calculate the difference and tempo time
145
- let timeSinceLastTempo = ticks - tempo.ticks;
146
- totalSeconds += (timeSinceLastTempo * 60) / (tempo.tempo * this.timeDivision);
147
- ticks -= timeSinceLastTempo;
148
- }
149
-
150
- return totalSeconds;
127
+ }
128
+
129
+ /**
130
+ * Converts ticks to time in seconds
131
+ * @param ticks {number} time in MIDI ticks
132
+ * @param mid {BasicMIDI} the MIDI
133
+ * @returns {number} time in seconds
134
+ */
135
+ export function MIDIticksToSeconds(ticks, mid) {
136
+ let totalSeconds = 0;
137
+
138
+ while (ticks > 0)
139
+ {
140
+ // tempo changes are reversed so the first element is the last tempo change
141
+ // and the last element is the first tempo change
142
+ // (always at tick 0 and tempo 120)
143
+ // find the last tempo change that has occurred
144
+ let tempo = mid.tempoChanges.find(v => v.ticks < ticks);
145
+
146
+ // calculate the difference and tempo time
147
+ let timeSinceLastTempo = ticks - tempo.ticks;
148
+ totalSeconds += (timeSinceLastTempo * 60) / (tempo.tempo * mid.timeDivision);
149
+ ticks -= timeSinceLastTempo;
151
150
  }
152
151
 
153
-
152
+ return totalSeconds;
154
153
  }
@@ -1,4 +1,4 @@
1
- import { BasicMIDI } from './basic_midi.js'
1
+ import { BasicMIDI, MIDIticksToSeconds } from './basic_midi.js'
2
2
  import { messageTypes, MidiMessage } from './midi_message.js'
3
3
  import { IndexedByteArray } from '../utils/indexed_array.js'
4
4
  import { readBytesAsUintBigEndian } from '../utils/byte_functions/big_endian.js'
@@ -101,7 +101,7 @@ export class MIDIBuilder extends BasicMIDI
101
101
 
102
102
  // reverse tempo and compute duration
103
103
  this.tempoChanges.reverse();
104
- this.duration = this._ticksToSeconds(this.lastVoiceEventTick);
104
+ this.duration = MIDIticksToSeconds(this.lastVoiceEventTick, this);
105
105
 
106
106
  // fix midi ports:
107
107
  // midi tracks without ports will have a value of -1
@@ -8,7 +8,7 @@ import { readBytesAsUintBigEndian } from '../utils/byte_functions/big_endian.js'
8
8
  import { readBytesAsString } from '../utils/byte_functions/string.js'
9
9
  import { readLittleEndian } from '../utils/byte_functions/little_endian.js'
10
10
  import { RMIDINFOChunks } from './rmidi_writer.js'
11
- import { BasicMIDI } from './basic_midi.js'
11
+ import { BasicMIDI, MIDIticksToSeconds } from './basic_midi.js'
12
12
 
13
13
  /**
14
14
  * midi_loader.js
@@ -573,7 +573,7 @@ class MIDI extends BasicMIDI
573
573
  * The total playback time, in seconds
574
574
  * @type {number}
575
575
  */
576
- this.duration = this._ticksToSeconds(this.lastVoiceEventTick);
576
+ this.duration = MIDIticksToSeconds(this.lastVoiceEventTick, this);
577
577
 
578
578
  SpessaSynthGroupEnd();
579
579
  SpessaSynthInfo(`%cMIDI file parsed. Total tick time: %c${this.lastVoiceEventTick}%c, total seconds time: %c${this.duration}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spessasynth_lib",
3
- "version": "3.20.19",
3
+ "version": "3.20.20",
4
4
  "description": "MIDI and SoundFont2 or DLS Synthesizer library with no compromises",
5
5
  "browser": "index.js",
6
6
  "types": "@types/index.d.ts",
@@ -1,6 +1,7 @@
1
1
  import { getEvent, messageTypes, midiControllers } from '../../midi_parser/midi_message.js'
2
2
  import { WorkletSequencerReturnMessageType } from './sequencer_message.js'
3
3
  import { MIDI_CHANNEL_COUNT } from '../../synthetizer/synthetizer.js'
4
+ import { MIDIticksToSeconds } from '../../midi_parser/basic_midi.js'
4
5
 
5
6
 
6
7
  // an array with preset default values
@@ -271,26 +272,6 @@ export function play(resetTime = false)
271
272
  this.setProcessHandler();
272
273
  }
273
274
 
274
- /**
275
- * Coverts ticks to time in seconds
276
- * @param changes {{tempo: number, ticks: number}[]}
277
- * @param ticks {number}
278
- * @param division {number}
279
- * @returns {number}
280
- */
281
- export function ticksToSeconds(changes, ticks, division)
282
- {
283
- if (ticks <= 0) {
284
- return 0;
285
- }
286
-
287
- // find the last tempo change that has occured
288
- let tempo = changes.find(v => v.ticks < ticks);
289
-
290
- let timeSinceLastTempo = ticks - tempo.ticks;
291
- return ticksToSeconds(changes, ticks - timeSinceLastTempo, division) + (timeSinceLastTempo * 60) / (tempo.tempo * division);
292
- }
293
-
294
275
  /**
295
276
  * @this {WorkletSequencer}
296
277
  * @param ticks {number}
@@ -302,7 +283,7 @@ export function setTimeTicks(ticks)
302
283
  this.pausedTime = undefined;
303
284
  this.post(
304
285
  WorkletSequencerReturnMessageType.timeChange,
305
- currentTime - ticksToSeconds(this.midiData.tempoChanges, ticks, this.midiData.timeDivision)
286
+ currentTime - MIDIticksToSeconds(ticks, this.midiData)
306
287
  );
307
288
  const isNotFinished = this._playTo(0, ticks);
308
289
  this._recalculateStartTime(this.playedTime);
@@ -37,16 +37,18 @@ export function _processTick()
37
37
  this.playedTime += this.oneTickToSeconds * (eventNext.ticks - event.ticks);
38
38
 
39
39
  // loop
40
- if((this.midiData.loop.end <= event.ticks) && this.loop)
40
+ if((this.midiData.loop.end <= event.ticks) && this.loop && this.currentLoopCount > 0)
41
41
  {
42
+ this.currentLoopCount--;
42
43
  this.setTimeTicks(this.midiData.loop.start);
43
44
  return;
44
45
  }
45
46
  // if song has ended
46
47
  else if(current >= this.duration)
47
48
  {
48
- if(this.loop)
49
+ if(this.loop && this.currentLoopCount > 0)
49
50
  {
51
+ this.currentLoopCount--;
50
52
  this.setTimeTicks(this.midiData.loop.start);
51
53
  return;
52
54
  }
@@ -1,10 +1,10 @@
1
1
  import { WorkletSequencerReturnMessageType } from './sequencer_message.js'
2
2
  import { consoleColors, formatTime } from '../../utils/other.js'
3
3
  import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo, SpessaSynthWarn } from '../../utils/loggin.js'
4
- import { ticksToSeconds } from './play.js'
5
4
  import { MidiData } from '../../midi_parser/midi_data.js'
6
5
  import { MIDI } from '../../midi_parser/midi_loader.js'
7
6
  import { getUsedProgramsAndKeys } from '../../midi_parser/used_keys_loaded.js'
7
+ import { MIDIticksToSeconds } from '../../midi_parser/basic_midi.js'
8
8
 
9
9
  /**
10
10
  * @param trackNum {number}
@@ -53,6 +53,8 @@ export function loadNewSequence(parsedMidi)
53
53
  */
54
54
  this.midiData = parsedMidi;
55
55
 
56
+ this.currentLoopCount = this.loopCount;
57
+
56
58
  // check for embedded soundfont
57
59
  if(this.midiData.embeddedSoundFont !== undefined)
58
60
  {
@@ -108,7 +110,7 @@ export function loadNewSequence(parsedMidi)
108
110
  * @type {number}
109
111
  */
110
112
  this.duration = this.midiData.duration;
111
- this.firstNoteTime = ticksToSeconds(this.midiData.tempoChanges, this.midiData.firstNoteOn, this.midiData.timeDivision);
113
+ this.firstNoteTime = MIDIticksToSeconds(this.midiData.firstNoteOn, this.midiData);
112
114
  SpessaSynthInfo(`%cTotal song time: ${formatTime(Math.ceil(this.duration)).time}`, consoleColors.recognized);
113
115
 
114
116
  this.post(WorkletSequencerReturnMessageType.songChange, [new MidiData(this.midiData), this.songIndex]);
@@ -24,6 +24,9 @@ class WorkletSequencer
24
24
  */
25
25
  this.sendMIDIMessages = false;
26
26
 
27
+ this.loopCount = Infinity;
28
+ this.currentLoopCount = this.loopCount;
29
+
27
30
  // event's number in this.events
28
31
  /**
29
32
  * @type {number[]}
@@ -24,6 +24,7 @@ import { SoundfontManager } from './synth_soundfont_manager.js'
24
24
  * @property {BasicMIDI} parsedMIDI - the MIDI to render
25
25
  * @property {SynthesizerSnapshot} snapshot - the snapshot to apply
26
26
  * @property {boolean|undefined} oneOutput - if synth should use one output with 32 channels (2 audio channels for each midi channel). this disables chorus and reverb.
27
+ * @property {number|undefined} loopCount - the times to loop the song
27
28
  */
28
29
 
29
30
  export const WORKLET_PROCESSOR_NAME = "spessasynth-worklet-system";