spessasynth_core 4.3.5 → 4.3.6

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/dist/index.d.ts CHANGED
@@ -1663,7 +1663,8 @@ interface ChannelMIDIParameter {
1663
1663
  /**
1664
1664
  * If the channel is in the poly mode.
1665
1665
  * - `true` - POLY ON - regular playback.
1666
- * - `false` - MONO ON - one note per channel, others are killed on Note On.
1666
+ * - `false` - MONO ON - one note per channel,
1667
+ * highest still pressed note is restored after releasing the currently playing one.
1667
1668
  */
1668
1669
  polyMode: boolean;
1669
1670
  /**
@@ -2045,6 +2046,20 @@ declare class MIDIChannel {
2045
2046
  */
2046
2047
  protected readonly octaveTuning: Int8Array;
2047
2048
  protected readonly _midiParameters: Readonly<ChannelMIDIParameter>;
2049
+ /**
2050
+ * Note On message tracking, for grouping voices for specific Note On messages.
2051
+ * Used for overlapping Note Ons.
2052
+ * MIDI note: current note on ID
2053
+ * @protected
2054
+ */
2055
+ protected readonly noteOnID: number[];
2056
+ /**
2057
+ * Note Off message tracking, for grouping voices for specific Note On messages.
2058
+ * Used for overlapping Note Ons.
2059
+ * MIDI note: current note on ID
2060
+ * @protected
2061
+ */
2062
+ protected readonly noteOffID: number[];
2048
2063
  /**
2049
2064
  * If the last Parameter was RPN.
2050
2065
  * If false then the last parameter was NRPN.
@@ -2080,21 +2095,39 @@ declare class MIDIChannel {
2080
2095
  */
2081
2096
  protected currentGain: number;
2082
2097
  /**
2083
- * The last pressed note on this channel.
2098
+ * The last pressed note on this channel for portamento tracking.
2084
2099
  * -1 means none.
2085
2100
  * This is not a `ChannelMIDIParameter` and is strictly internal,
2086
2101
  * mostly because we don't want to send events for every note on message.
2087
2102
  * It can be set with Portamento Control CC anyway.
2088
2103
  * @protected
2089
2104
  */
2090
- protected lastNote: number;
2105
+ protected lastPortamentoNote: number;
2091
2106
  /**
2092
2107
  * If the portamento should be executed once regardless of Portamento on/off.
2093
2108
  * Adhering to the MIDI spec, CC#84 ignores on/off.
2094
- * This is also not a `ChannelMIDIParameter` for the same reason as `lastNote`
2109
+ * This is also not a `ChannelMIDIParameter` for the same reason as `lastPortamentoNote`
2095
2110
  * @protected
2096
2111
  */
2097
2112
  protected portamentoForce: boolean;
2113
+ /**
2114
+ * The last pressed note on this channel in mono mode.
2115
+ * Used for tracking and releasing this note on a new Note On event.
2116
+ * -1 means none.
2117
+ * @protected
2118
+ */
2119
+ protected lastMonoNote: number;
2120
+ /**
2121
+ * The last pressed note's velocity on this channel in mono mode.
2122
+ * @protected
2123
+ */
2124
+ protected lastMonoVelocity: number;
2125
+ /**
2126
+ * For Mono Mode restoring notes.
2127
+ * playingNotes[midiNote]
2128
+ * @protected
2129
+ */
2130
+ protected readonly playingNotes: boolean[];
2098
2131
  protected readonly generators: ChannelGenerators;
2099
2132
  protected readonly computeModulator: (voice: Voice, pitchWheel: number, modulatorIndex: number) => number;
2100
2133
  protected readonly computeModulators: (voice: Voice, sourceUsesCC?: 0 | 1 | -1 | undefined, sourceIndex?: number | undefined) => void;
@@ -3070,6 +3103,11 @@ declare class Voice {
3070
3103
  * MIDI channel number of the voice.
3071
3104
  */
3072
3105
  channel: number;
3106
+ /**
3107
+ * Grouping voices for specific Note On messages.
3108
+ * Used for overlapping Note Ons.
3109
+ */
3110
+ noteID: number;
3073
3111
  /**
3074
3112
  * MIDI note number of the voice.
3075
3113
  * Direct number from the Note On message and is
@@ -3183,7 +3221,7 @@ declare class Voice {
3183
3221
  * @param minNoteLength minimum note length in seconds
3184
3222
  */
3185
3223
  releaseVoice(currentTime: number, minNoteLength?: number): void;
3186
- setup(currentTime: number, channel: number, midiNote: number): void;
3224
+ setup(currentTime: number, channel: number, midiNote: number, noteID: number): void;
3187
3225
  }
3188
3226
  //#endregion
3189
3227
  //#region src/soundbank/basic_soundbank/modulator_source.d.ts
package/dist/index.js CHANGED
@@ -2585,13 +2585,13 @@ function getNoteTimesInternal(midi, minDrumLength = 0) {
2585
2585
  const unfinishedNotes = [];
2586
2586
  for (let i = 0; i < 16; i++) unfinishedNotes.push(/* @__PURE__ */ new Map());
2587
2587
  const noteOff = (midiNote, channel) => {
2588
- const ch = unfinishedNotes[channel];
2589
- const noteIndex = ch.get(midiNote);
2588
+ const noteIndexes = unfinishedNotes[channel].get(midiNote);
2589
+ if (noteIndexes === void 0) return;
2590
+ const noteIndex = noteIndexes.shift();
2590
2591
  if (noteIndex === void 0) return;
2591
2592
  const note = noteTimes[channel][noteIndex];
2592
2593
  const time = elapsedTime - note.start;
2593
2594
  note.length = channel === 9 ? Math.max(time, minDrumLength) : time;
2594
- ch.delete(midiNote);
2595
2595
  unfinished--;
2596
2596
  };
2597
2597
  const { timeline, tracks } = midi;
@@ -2606,7 +2606,6 @@ function getNoteTimesInternal(midi, minDrumLength = 0) {
2606
2606
  const velocity = event.data[1];
2607
2607
  if (velocity === 0) noteOff(midiNote, channel);
2608
2608
  else {
2609
- noteOff(midiNote, channel);
2610
2609
  const noteTime = {
2611
2610
  midiNote,
2612
2611
  start: elapsedTime,
@@ -2615,14 +2614,17 @@ function getNoteTimesInternal(midi, minDrumLength = 0) {
2615
2614
  };
2616
2615
  const times = noteTimes[channel];
2617
2616
  times.push(noteTime);
2618
- unfinishedNotes[channel].set(midiNote, times.length - 1);
2617
+ const unfinishedChannel = unfinishedNotes[channel];
2618
+ if (!unfinishedChannel.has(midiNote)) unfinishedChannel.set(midiNote, []);
2619
+ unfinishedNotes[channel].get(midiNote)?.push(times.length - 1);
2619
2620
  unfinished++;
2620
2621
  }
2621
2622
  } else if (event.statusByte === 81) oneTickToSeconds = 60 / (getTempo(event) * midi.timeDivision);
2622
2623
  if (++i >= timeline.length) break;
2623
- elapsedTime += oneTickToSeconds * (tracks[timeline[i].tr].events[timeline[i].ev].ticks - event.ticks);
2624
+ const nextTimeline = timeline[i];
2625
+ elapsedTime += oneTickToSeconds * (tracks[nextTimeline.tr].events[nextTimeline.ev].ticks - event.ticks);
2624
2626
  }
2625
- if (unfinished > 0) for (let channel = 0; channel < unfinishedNotes.length; channel++) for (const noteIndex of unfinishedNotes[channel].values()) {
2627
+ if (unfinished > 0) for (let channel = 0; channel < unfinishedNotes.length; channel++) for (const noteIndexes of unfinishedNotes[channel].values()) for (const noteIndex of noteIndexes) {
2626
2628
  const note = noteTimes[channel][noteIndex];
2627
2629
  note.length = elapsedTime - note.start;
2628
2630
  }
@@ -4665,7 +4667,7 @@ function loadNewSequenceInternal(parsedMIDI) {
4665
4667
  //#endregion
4666
4668
  //#region src/synthesizer/audio_engine/channel/reset.ts
4667
4669
  function resetPortamento() {
4668
- this.lastNote = this.channelSystem === "xg" ? 60 : -1;
4670
+ this.lastPortamentoNote = this.channelSystem === "xg" ? 60 : -1;
4669
4671
  }
4670
4672
  /**
4671
4673
  * An array with the default MIDI controller values.
@@ -4733,6 +4735,7 @@ function resetChannelInternal(sendCCEvents = true) {
4733
4735
  this.resetGeneratorOffsets();
4734
4736
  this.dynamicModulators.resetModulators();
4735
4737
  this.sf2NRPNGeneratorLSB = 0;
4738
+ this.playingNotes.fill(false);
4736
4739
  this.lastParameterIsRegistered = true;
4737
4740
  this._midiControllers[MIDIControllers.nonRegisteredParameterLSB] = 0;
4738
4741
  this._midiControllers[MIDIControllers.nonRegisteredParameterMSB] = 0;
@@ -7389,6 +7392,11 @@ var Voice = class {
7389
7392
  */
7390
7393
  channel = 0;
7391
7394
  /**
7395
+ * Grouping voices for specific Note On messages.
7396
+ * Used for overlapping Note Ons.
7397
+ */
7398
+ noteID = 0;
7399
+ /**
7392
7400
  * MIDI note number of the voice.
7393
7401
  * Direct number from the Note On message and is
7394
7402
  * used for Note Off and external parameters:
@@ -7511,7 +7519,7 @@ var Voice = class {
7511
7519
  this.releaseStartTime = currentTime;
7512
7520
  if (this.releaseStartTime - this.startTime < minNoteLength) this.releaseStartTime = this.startTime + minNoteLength;
7513
7521
  }
7514
- setup(currentTime, channel, midiNote) {
7522
+ setup(currentTime, channel, midiNote, noteID) {
7515
7523
  this.isActive = true;
7516
7524
  this.isInRelease = false;
7517
7525
  this.hasRendered = false;
@@ -7526,6 +7534,7 @@ var Voice = class {
7526
7534
  this.startTime = currentTime;
7527
7535
  this.channel = channel;
7528
7536
  this.midiNote = midiNote;
7537
+ this.noteID = noteID;
7529
7538
  }
7530
7539
  };
7531
7540
  //#endregion
@@ -12170,7 +12179,7 @@ function controllerChange(controller, value, sendEvent = true) {
12170
12179
  }
12171
12180
  break;
12172
12181
  case MIDIControllers.portamentoControl:
12173
- this.lastNote = value;
12182
+ this.lastPortamentoNote = value;
12174
12183
  this.portamentoForce = true;
12175
12184
  break;
12176
12185
  default:
@@ -12583,8 +12592,9 @@ const clamp = (num, min, max) => Math.max(min, Math.min(max, num));
12583
12592
  * Sends a "MIDI Note on" message and starts a note.
12584
12593
  * @param midiNote The MIDI note number (0-127).
12585
12594
  * @param velocity The velocity of the note (0-127). If less than 1, it will send a note off instead.
12595
+ * @param emit If the note on should be updated and emitted (non-internal)
12586
12596
  */
12587
- function noteOn(midiNote, velocity) {
12597
+ function noteOn(midiNote, velocity, emit = true) {
12588
12598
  if (velocity < 1) {
12589
12599
  this.noteOff(midiNote);
12590
12600
  return;
@@ -12601,7 +12611,7 @@ function noteOn(midiNote, velocity) {
12601
12611
  const keyVel = this.synthCore.keyModifierManager.getVelocity(this.channel, midiNote);
12602
12612
  if (keyVel > -1) velocity = keyVel;
12603
12613
  let voiceGain = this.synthCore.keyModifierManager.getGain(this.channel, midiNote);
12604
- const previousNote = this.lastNote;
12614
+ const previousNote = this.lastPortamentoNote;
12605
12615
  const portamentoEnabled = this.portamentoForce || this._midiControllers[MIDIControllers.portamentoOnOff] >= 8192;
12606
12616
  const portamentoTime = this._midiControllers[MIDIControllers.portamentoTime] >> 7;
12607
12617
  const canApplyPortamento = portamentoEnabled && !this._drumChannel && previousNote >= 0 && previousNote !== midiNote && portamentoTime > 0;
@@ -12613,15 +12623,12 @@ function noteOn(midiNote, velocity) {
12613
12623
  portaTime = portamentoTimeToSeconds(portamentoTime, keyDistance);
12614
12624
  this.portamentoForce = false;
12615
12625
  }
12616
- this.lastNote = midiNote;
12626
+ this.lastPortamentoNote = midiNote;
12627
+ this.playingNotes[midiNote] = true;
12617
12628
  if (!this._midiParameters.polyMode) {
12618
- let vc = 0;
12619
- if (this._voiceCount > 0) {
12620
- for (const v of this.synthCore.voices) if (v.isActive && v.channel === this.channel) {
12621
- v.exclusiveRelease(this.synthCore.currentTime, 0);
12622
- if (++vc >= this._voiceCount) break;
12623
- }
12624
- }
12629
+ if (this.lastMonoNote >= 0 && this.lastMonoNote !== midiNote) this.killNote(this.lastMonoNote);
12630
+ this.lastMonoNote = midiNote;
12631
+ this.lastMonoVelocity = velocity;
12625
12632
  }
12626
12633
  const voices = this.synthCore.getVoices(this.channel, soundBankNote, velocity);
12627
12634
  let panOverride = 0;
@@ -12647,10 +12654,11 @@ function noteOn(midiNote, velocity) {
12647
12654
  delaySend = p.delayGain;
12648
12655
  if (voiceGain === 1) voiceGain = p.gain;
12649
12656
  }
12657
+ const noteID = emit ? this.noteOnID[midiNote]++ : this.noteOnID[midiNote];
12650
12658
  for (const cached of voices) {
12651
12659
  const voice = this.synthCore.assignVoice();
12652
12660
  const now = this.synthCore.currentTime;
12653
- voice.setup(now, this.channel, midiNote);
12661
+ voice.setup(now, this.channel, midiNote, noteID);
12654
12662
  voice.wavetable = voice.oscillators[this._systemParameters.interpolationType ?? this.synthCore.systemParameters.interpolationType];
12655
12663
  voice.targetKey = cached.targetKey;
12656
12664
  voice.velocity = cached.velocity;
@@ -12723,7 +12731,7 @@ function noteOn(midiNote, velocity) {
12723
12731
  voice.currentPan = Math.max(-500, Math.min(500, panOverride || voice.modulatedGenerators[GeneratorTypes.pan]));
12724
12732
  }
12725
12733
  this._voiceCount += voices.length;
12726
- this.synthCore.callEvent("noteOn", {
12734
+ if (emit) this.synthCore.callEvent("noteOn", {
12727
12735
  midiNote,
12728
12736
  channel: this.channel,
12729
12737
  velocity
@@ -12747,10 +12755,14 @@ function noteOff(midiNote) {
12747
12755
  });
12748
12756
  return;
12749
12757
  }
12750
- const sustain = this._midiControllers[MIDIControllers.sustainPedal] >= 8192;
12758
+ this.playingNotes[midiNote] = false;
12759
+ const mono = !this._midiParameters.polyMode;
12760
+ const sustain = this._midiControllers[MIDIControllers.sustainPedal] >= 8192 && !mono;
12751
12761
  let vc = 0;
12762
+ const noteID = this.noteOffID[midiNote];
12763
+ if (noteID < this.noteOnID[midiNote]) this.noteOffID[midiNote]++;
12752
12764
  if (this._voiceCount > 0) {
12753
- for (const v of this.synthCore.voices) if (v.channel === this.channel && v.isActive && v.midiNote === midiNote && !v.isInRelease) {
12765
+ for (const v of this.synthCore.voices) if (v.channel === this.channel && v.isActive && v.midiNote === midiNote && v.noteID === noteID && !v.isInRelease) {
12754
12766
  if (sustain) v.isHeld = true;
12755
12767
  else v.releaseVoice(this.synthCore.currentTime);
12756
12768
  if (++vc >= this._voiceCount) break;
@@ -12760,6 +12772,11 @@ function noteOff(midiNote) {
12760
12772
  midiNote,
12761
12773
  channel: this.channel
12762
12774
  });
12775
+ if (mono) {
12776
+ const highest = this.playingNotes.lastIndexOf(true);
12777
+ if (highest === -1) this.lastMonoNote = -1;
12778
+ else if (this.lastMonoNote === midiNote) this.noteOn(highest, this.lastMonoVelocity, false);
12779
+ }
12763
12780
  }
12764
12781
  //#endregion
12765
12782
  //#region src/synthesizer/audio_engine/channel/program_change.ts
@@ -13281,6 +13298,20 @@ var MIDIChannel = class {
13281
13298
  */
13282
13299
  _systemParameters = { ...DEFAULT_CHANNEL_SYSTEM_PARAMETERS };
13283
13300
  /**
13301
+ * Note On message tracking, for grouping voices for specific Note On messages.
13302
+ * Used for overlapping Note Ons.
13303
+ * MIDI note: current note on ID
13304
+ * @protected
13305
+ */
13306
+ noteOnID = new Array(128).fill(0);
13307
+ /**
13308
+ * Note Off message tracking, for grouping voices for specific Note On messages.
13309
+ * Used for overlapping Note Ons.
13310
+ * MIDI note: current note on ID
13311
+ * @protected
13312
+ */
13313
+ noteOffID = new Array(128).fill(0);
13314
+ /**
13284
13315
  * If the last Parameter was RPN.
13285
13316
  * If false then the last parameter was NRPN.
13286
13317
  * @protected
@@ -13315,21 +13346,39 @@ var MIDIChannel = class {
13315
13346
  */
13316
13347
  currentGain = 0;
13317
13348
  /**
13318
- * The last pressed note on this channel.
13349
+ * The last pressed note on this channel for portamento tracking.
13319
13350
  * -1 means none.
13320
13351
  * This is not a `ChannelMIDIParameter` and is strictly internal,
13321
13352
  * mostly because we don't want to send events for every note on message.
13322
13353
  * It can be set with Portamento Control CC anyway.
13323
13354
  * @protected
13324
13355
  */
13325
- lastNote = -1;
13356
+ lastPortamentoNote = -1;
13326
13357
  /**
13327
13358
  * If the portamento should be executed once regardless of Portamento on/off.
13328
13359
  * Adhering to the MIDI spec, CC#84 ignores on/off.
13329
- * This is also not a `ChannelMIDIParameter` for the same reason as `lastNote`
13360
+ * This is also not a `ChannelMIDIParameter` for the same reason as `lastPortamentoNote`
13330
13361
  * @protected
13331
13362
  */
13332
13363
  portamentoForce = false;
13364
+ /**
13365
+ * The last pressed note on this channel in mono mode.
13366
+ * Used for tracking and releasing this note on a new Note On event.
13367
+ * -1 means none.
13368
+ * @protected
13369
+ */
13370
+ lastMonoNote = -1;
13371
+ /**
13372
+ * The last pressed note's velocity on this channel in mono mode.
13373
+ * @protected
13374
+ */
13375
+ lastMonoVelocity = 0;
13376
+ /**
13377
+ * For Mono Mode restoring notes.
13378
+ * playingNotes[midiNote]
13379
+ * @protected
13380
+ */
13381
+ playingNotes = new Array(128).fill(false);
13333
13382
  generators = {
13334
13383
  offsets: new Int16Array(GENERATORS_AMOUNT),
13335
13384
  offsetsEnabled: false,
@@ -13442,6 +13491,9 @@ var MIDIChannel = class {
13442
13491
  * @param force If true, stops all notes immediately, otherwise applies release time.
13443
13492
  */
13444
13493
  stopAllNotes(force = false) {
13494
+ this.noteOnID.fill(0);
13495
+ this.noteOffID.fill(0);
13496
+ this.playingNotes.fill(false);
13445
13497
  if (force) {
13446
13498
  let vc = 0;
13447
13499
  if (this._voiceCount > 0) {
@@ -13644,6 +13696,8 @@ var MIDIChannel = class {
13644
13696
  */
13645
13697
  killNote(midiNote, releaseTime = -12e3) {
13646
13698
  let vc = 0;
13699
+ this.noteOffID[midiNote] = 0;
13700
+ this.noteOnID[midiNote] = 0;
13647
13701
  if (this._voiceCount > 0) {
13648
13702
  for (const v of this.synthCore.voices) if (v.channel === this.channel && v.isActive && v.midiNote === midiNote) {
13649
13703
  v.overrideReleaseVolEnv = releaseTime;
@@ -13674,7 +13728,7 @@ var MIDIChannel = class {
13674
13728
  * @param midiNote
13675
13729
  */
13676
13730
  setLastNote(midiNote) {
13677
- this.lastNote = midiNote;
13731
+ this.lastPortamentoNote = midiNote;
13678
13732
  }
13679
13733
  /**
13680
13734
  * @internal