spessasynth_core 4.3.4 → 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
  }
@@ -4490,20 +4492,31 @@ function processEventInternal(event, trackIndex) {
4490
4492
  channel += offset;
4491
4493
  switch (status) {
4492
4494
  case MIDIMessageTypes.noteOn: {
4495
+ let playingNotes = this.playingNotes[channel];
4496
+ if (!playingNotes) {
4497
+ while (this.playingNotes.length <= channel) this.playingNotes.push(/* @__PURE__ */ new Map());
4498
+ playingNotes = this.playingNotes[channel];
4499
+ }
4493
4500
  const velocity = event.data[1];
4494
4501
  if (velocity > 0) {
4495
4502
  this.synth.noteOn(channel, event.data[0], velocity);
4496
- this.playingNotes[channel].set(event.data[0], velocity);
4503
+ playingNotes.set(event.data[0], velocity);
4497
4504
  } else {
4498
4505
  this.synth.noteOff(channel, event.data[0]);
4499
- this.playingNotes[channel].delete(event.data[0]);
4506
+ playingNotes.delete(event.data[0]);
4500
4507
  }
4501
4508
  break;
4502
4509
  }
4503
- case MIDIMessageTypes.noteOff:
4510
+ case MIDIMessageTypes.noteOff: {
4511
+ let playingNotes = this.playingNotes[channel];
4512
+ if (!playingNotes) {
4513
+ while (this.playingNotes.length <= channel) this.playingNotes.push(/* @__PURE__ */ new Map());
4514
+ playingNotes = this.playingNotes[channel];
4515
+ }
4504
4516
  this.synth.noteOff(channel, event.data[0]);
4505
- this.playingNotes[channel].delete(event.data[0]);
4517
+ playingNotes.delete(event.data[0]);
4506
4518
  break;
4519
+ }
4507
4520
  case MIDIMessageTypes.pitchWheel:
4508
4521
  this.synth.pitchWheel(channel, event.data[1] << 7 | event.data[0]);
4509
4522
  break;
@@ -4654,7 +4667,7 @@ function loadNewSequenceInternal(parsedMIDI) {
4654
4667
  //#endregion
4655
4668
  //#region src/synthesizer/audio_engine/channel/reset.ts
4656
4669
  function resetPortamento() {
4657
- this.lastNote = this.channelSystem === "xg" ? 60 : -1;
4670
+ this.lastPortamentoNote = this.channelSystem === "xg" ? 60 : -1;
4658
4671
  }
4659
4672
  /**
4660
4673
  * An array with the default MIDI controller values.
@@ -4722,6 +4735,7 @@ function resetChannelInternal(sendCCEvents = true) {
4722
4735
  this.resetGeneratorOffsets();
4723
4736
  this.dynamicModulators.resetModulators();
4724
4737
  this.sf2NRPNGeneratorLSB = 0;
4738
+ this.playingNotes.fill(false);
4725
4739
  this.lastParameterIsRegistered = true;
4726
4740
  this._midiControllers[MIDIControllers.nonRegisteredParameterLSB] = 0;
4727
4741
  this._midiControllers[MIDIControllers.nonRegisteredParameterMSB] = 0;
@@ -5041,7 +5055,7 @@ var SpessaSynthSequencer = class {
5041
5055
  constructor(spessasynthProcessor) {
5042
5056
  this.synth = spessasynthProcessor;
5043
5057
  this.absoluteStartTime = this.synth.currentTime;
5044
- for (let i = 0; i < 16; i++) this.playingNotes.push(/* @__PURE__ */ new Map());
5058
+ this.playingNotes = this.synth.midiChannels.map(() => /* @__PURE__ */ new Map());
5045
5059
  }
5046
5060
  _midiData;
5047
5061
  /**
@@ -7378,6 +7392,11 @@ var Voice = class {
7378
7392
  */
7379
7393
  channel = 0;
7380
7394
  /**
7395
+ * Grouping voices for specific Note On messages.
7396
+ * Used for overlapping Note Ons.
7397
+ */
7398
+ noteID = 0;
7399
+ /**
7381
7400
  * MIDI note number of the voice.
7382
7401
  * Direct number from the Note On message and is
7383
7402
  * used for Note Off and external parameters:
@@ -7500,7 +7519,7 @@ var Voice = class {
7500
7519
  this.releaseStartTime = currentTime;
7501
7520
  if (this.releaseStartTime - this.startTime < minNoteLength) this.releaseStartTime = this.startTime + minNoteLength;
7502
7521
  }
7503
- setup(currentTime, channel, midiNote) {
7522
+ setup(currentTime, channel, midiNote, noteID) {
7504
7523
  this.isActive = true;
7505
7524
  this.isInRelease = false;
7506
7525
  this.hasRendered = false;
@@ -7515,6 +7534,7 @@ var Voice = class {
7515
7534
  this.startTime = currentTime;
7516
7535
  this.channel = channel;
7517
7536
  this.midiNote = midiNote;
7537
+ this.noteID = noteID;
7518
7538
  }
7519
7539
  };
7520
7540
  //#endregion
@@ -12159,7 +12179,7 @@ function controllerChange(controller, value, sendEvent = true) {
12159
12179
  }
12160
12180
  break;
12161
12181
  case MIDIControllers.portamentoControl:
12162
- this.lastNote = value;
12182
+ this.lastPortamentoNote = value;
12163
12183
  this.portamentoForce = true;
12164
12184
  break;
12165
12185
  default:
@@ -12572,8 +12592,9 @@ const clamp = (num, min, max) => Math.max(min, Math.min(max, num));
12572
12592
  * Sends a "MIDI Note on" message and starts a note.
12573
12593
  * @param midiNote The MIDI note number (0-127).
12574
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)
12575
12596
  */
12576
- function noteOn(midiNote, velocity) {
12597
+ function noteOn(midiNote, velocity, emit = true) {
12577
12598
  if (velocity < 1) {
12578
12599
  this.noteOff(midiNote);
12579
12600
  return;
@@ -12590,7 +12611,7 @@ function noteOn(midiNote, velocity) {
12590
12611
  const keyVel = this.synthCore.keyModifierManager.getVelocity(this.channel, midiNote);
12591
12612
  if (keyVel > -1) velocity = keyVel;
12592
12613
  let voiceGain = this.synthCore.keyModifierManager.getGain(this.channel, midiNote);
12593
- const previousNote = this.lastNote;
12614
+ const previousNote = this.lastPortamentoNote;
12594
12615
  const portamentoEnabled = this.portamentoForce || this._midiControllers[MIDIControllers.portamentoOnOff] >= 8192;
12595
12616
  const portamentoTime = this._midiControllers[MIDIControllers.portamentoTime] >> 7;
12596
12617
  const canApplyPortamento = portamentoEnabled && !this._drumChannel && previousNote >= 0 && previousNote !== midiNote && portamentoTime > 0;
@@ -12602,15 +12623,12 @@ function noteOn(midiNote, velocity) {
12602
12623
  portaTime = portamentoTimeToSeconds(portamentoTime, keyDistance);
12603
12624
  this.portamentoForce = false;
12604
12625
  }
12605
- this.lastNote = midiNote;
12626
+ this.lastPortamentoNote = midiNote;
12627
+ this.playingNotes[midiNote] = true;
12606
12628
  if (!this._midiParameters.polyMode) {
12607
- let vc = 0;
12608
- if (this._voiceCount > 0) {
12609
- for (const v of this.synthCore.voices) if (v.isActive && v.channel === this.channel) {
12610
- v.exclusiveRelease(this.synthCore.currentTime, 0);
12611
- if (++vc >= this._voiceCount) break;
12612
- }
12613
- }
12629
+ if (this.lastMonoNote >= 0 && this.lastMonoNote !== midiNote) this.killNote(this.lastMonoNote);
12630
+ this.lastMonoNote = midiNote;
12631
+ this.lastMonoVelocity = velocity;
12614
12632
  }
12615
12633
  const voices = this.synthCore.getVoices(this.channel, soundBankNote, velocity);
12616
12634
  let panOverride = 0;
@@ -12636,10 +12654,11 @@ function noteOn(midiNote, velocity) {
12636
12654
  delaySend = p.delayGain;
12637
12655
  if (voiceGain === 1) voiceGain = p.gain;
12638
12656
  }
12657
+ const noteID = emit ? this.noteOnID[midiNote]++ : this.noteOnID[midiNote];
12639
12658
  for (const cached of voices) {
12640
12659
  const voice = this.synthCore.assignVoice();
12641
12660
  const now = this.synthCore.currentTime;
12642
- voice.setup(now, this.channel, midiNote);
12661
+ voice.setup(now, this.channel, midiNote, noteID);
12643
12662
  voice.wavetable = voice.oscillators[this._systemParameters.interpolationType ?? this.synthCore.systemParameters.interpolationType];
12644
12663
  voice.targetKey = cached.targetKey;
12645
12664
  voice.velocity = cached.velocity;
@@ -12712,7 +12731,7 @@ function noteOn(midiNote, velocity) {
12712
12731
  voice.currentPan = Math.max(-500, Math.min(500, panOverride || voice.modulatedGenerators[GeneratorTypes.pan]));
12713
12732
  }
12714
12733
  this._voiceCount += voices.length;
12715
- this.synthCore.callEvent("noteOn", {
12734
+ if (emit) this.synthCore.callEvent("noteOn", {
12716
12735
  midiNote,
12717
12736
  channel: this.channel,
12718
12737
  velocity
@@ -12736,10 +12755,14 @@ function noteOff(midiNote) {
12736
12755
  });
12737
12756
  return;
12738
12757
  }
12739
- 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;
12740
12761
  let vc = 0;
12762
+ const noteID = this.noteOffID[midiNote];
12763
+ if (noteID < this.noteOnID[midiNote]) this.noteOffID[midiNote]++;
12741
12764
  if (this._voiceCount > 0) {
12742
- 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) {
12743
12766
  if (sustain) v.isHeld = true;
12744
12767
  else v.releaseVoice(this.synthCore.currentTime);
12745
12768
  if (++vc >= this._voiceCount) break;
@@ -12749,6 +12772,11 @@ function noteOff(midiNote) {
12749
12772
  midiNote,
12750
12773
  channel: this.channel
12751
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
+ }
12752
12780
  }
12753
12781
  //#endregion
12754
12782
  //#region src/synthesizer/audio_engine/channel/program_change.ts
@@ -13270,6 +13298,20 @@ var MIDIChannel = class {
13270
13298
  */
13271
13299
  _systemParameters = { ...DEFAULT_CHANNEL_SYSTEM_PARAMETERS };
13272
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
+ /**
13273
13315
  * If the last Parameter was RPN.
13274
13316
  * If false then the last parameter was NRPN.
13275
13317
  * @protected
@@ -13304,21 +13346,39 @@ var MIDIChannel = class {
13304
13346
  */
13305
13347
  currentGain = 0;
13306
13348
  /**
13307
- * The last pressed note on this channel.
13349
+ * The last pressed note on this channel for portamento tracking.
13308
13350
  * -1 means none.
13309
13351
  * This is not a `ChannelMIDIParameter` and is strictly internal,
13310
13352
  * mostly because we don't want to send events for every note on message.
13311
13353
  * It can be set with Portamento Control CC anyway.
13312
13354
  * @protected
13313
13355
  */
13314
- lastNote = -1;
13356
+ lastPortamentoNote = -1;
13315
13357
  /**
13316
13358
  * If the portamento should be executed once regardless of Portamento on/off.
13317
13359
  * Adhering to the MIDI spec, CC#84 ignores on/off.
13318
- * 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`
13319
13361
  * @protected
13320
13362
  */
13321
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);
13322
13382
  generators = {
13323
13383
  offsets: new Int16Array(GENERATORS_AMOUNT),
13324
13384
  offsetsEnabled: false,
@@ -13431,6 +13491,9 @@ var MIDIChannel = class {
13431
13491
  * @param force If true, stops all notes immediately, otherwise applies release time.
13432
13492
  */
13433
13493
  stopAllNotes(force = false) {
13494
+ this.noteOnID.fill(0);
13495
+ this.noteOffID.fill(0);
13496
+ this.playingNotes.fill(false);
13434
13497
  if (force) {
13435
13498
  let vc = 0;
13436
13499
  if (this._voiceCount > 0) {
@@ -13633,6 +13696,8 @@ var MIDIChannel = class {
13633
13696
  */
13634
13697
  killNote(midiNote, releaseTime = -12e3) {
13635
13698
  let vc = 0;
13699
+ this.noteOffID[midiNote] = 0;
13700
+ this.noteOnID[midiNote] = 0;
13636
13701
  if (this._voiceCount > 0) {
13637
13702
  for (const v of this.synthCore.voices) if (v.channel === this.channel && v.isActive && v.midiNote === midiNote) {
13638
13703
  v.overrideReleaseVolEnv = releaseTime;
@@ -13663,7 +13728,7 @@ var MIDIChannel = class {
13663
13728
  * @param midiNote
13664
13729
  */
13665
13730
  setLastNote(midiNote) {
13666
- this.lastNote = midiNote;
13731
+ this.lastPortamentoNote = midiNote;
13667
13732
  }
13668
13733
  /**
13669
13734
  * @internal
@@ -20467,7 +20532,6 @@ var SpessaSynthProcessor = class {
20467
20532
  applySnapshot(snapshot) {
20468
20533
  this.savedSnapshot = snapshot;
20469
20534
  applySnapshot$1.call(this.synthCore, snapshot);
20470
- this.reset();
20471
20535
  }
20472
20536
  /**
20473
20537
  * Gets a synthesizer snapshot from this processor instance.