@ugfoundation/swaralipi-js 0.4.0 → 0.6.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 (47) hide show
  1. package/README.md +8 -8
  2. package/dist/{batch-V4X3GN4N.js → batch-RUZGM2NR.js} +4 -4
  3. package/dist/{batch-V4X3GN4N.js.map → batch-RUZGM2NR.js.map} +1 -1
  4. package/dist/{chunk-6QW7MSJO.js → chunk-GHWURC4U.js} +98 -6
  5. package/dist/chunk-GHWURC4U.js.map +1 -0
  6. package/dist/{chunk-RTKWFJFI.js → chunk-ULK7NVKH.js} +8 -6
  7. package/dist/chunk-ULK7NVKH.js.map +1 -0
  8. package/dist/{chunk-ZS43BDP7.js → chunk-ZNVTOY7J.js} +7 -6
  9. package/dist/chunk-ZNVTOY7J.js.map +1 -0
  10. package/dist/cli.cjs +11 -8
  11. package/dist/cli.cjs.map +1 -1
  12. package/dist/cli.js +2 -2
  13. package/dist/convert/index.cjs +10 -7
  14. package/dist/convert/index.cjs.map +1 -1
  15. package/dist/convert/index.d.cts +4 -3
  16. package/dist/convert/index.d.ts +4 -3
  17. package/dist/convert/index.js +2 -2
  18. package/dist/index.cjs +6 -4
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +1 -1
  23. package/dist/player/index.cjs +222 -23
  24. package/dist/player/index.cjs.map +1 -1
  25. package/dist/player/index.d.cts +16 -4
  26. package/dist/player/index.d.ts +16 -4
  27. package/dist/player/index.js +120 -22
  28. package/dist/player/index.js.map +1 -1
  29. package/dist/{schedule-CAns78TD.d.ts → schedule-8WFUBJqB.d.ts} +82 -7
  30. package/dist/{schedule-D3JyEhuO.d.cts → schedule-CjPcD-Hf.d.cts} +82 -7
  31. package/dist/{schema-D3wyNRqM.d.ts → schema-D_86x4oV.d.cts} +21 -6
  32. package/dist/{schema-D3wyNRqM.d.cts → schema-D_86x4oV.d.ts} +21 -6
  33. package/dist/spec/index.cjs +6 -4
  34. package/dist/spec/index.cjs.map +1 -1
  35. package/dist/spec/index.d.cts +3 -3
  36. package/dist/spec/index.d.ts +3 -3
  37. package/dist/spec/index.js +1 -1
  38. package/dist/viewer/index.cjs +253 -172
  39. package/dist/viewer/index.cjs.map +1 -1
  40. package/dist/viewer/index.d.cts +16 -23
  41. package/dist/viewer/index.d.ts +16 -23
  42. package/dist/viewer/index.js +88 -47
  43. package/dist/viewer/index.js.map +1 -1
  44. package/package.json +1 -1
  45. package/dist/chunk-6QW7MSJO.js.map +0 -1
  46. package/dist/chunk-RTKWFJFI.js.map +0 -1
  47. package/dist/chunk-ZS43BDP7.js.map +0 -1
@@ -1,7 +1,7 @@
1
- import { S as Swaralipi, T as Taal, O as Octave, a as SvaraLetter, N as Note } from '../schema-D3wyNRqM.cjs';
1
+ import { S as Swaralipi, T as Taal, O as Octave, a as SvaraLetter, N as Note } from '../schema-D_86x4oV.cjs';
2
2
  import { T as TaalId } from '../taals-CjzBuZ35.cjs';
3
- import { I as InstrumentId, a as InstrumentDef, S as Schedule, D as DrumVoice } from '../schedule-D3JyEhuO.cjs';
4
- export { b as DEFAULT_SOUNDS_BASE, c as DRUM_KIT_BASE, d as INSTRUMENTS, P as PlayEvent, e as SALAMANDER_BASE, f as SAMPLED_TAALS, g as ScheduleOptions, T as TABLA_BPMS, h as TABLA_VARIATIONS, i as TONEJS_INSTRUMENTS_BASE, j as ToneModule, k as buildSchedule, l as drumPattern, m as instrumentSampleUrl, n as nearestTablaBpm, o as normalizeBase, p as probeUrl, q as quantizeLoopEnd, s as santoorUrls, r as subsetUrls, t as tablaBpms, u as tablaUrl } from '../schedule-D3JyEhuO.cjs';
3
+ import { I as InstrumentId, a as InstrumentDef, S as Schedule, D as DrumVoice } from '../schedule-CjPcD-Hf.cjs';
4
+ export { A as ARC_RAW_RE, b as DEFAULT_SOUNDS_BASE, c as DRUM_KIT_BASE, G as GlideLeg, d as INSTRUMENTS, M as MEEND_HOLD, P as PlayEvent, e as SALAMANDER_BASE, f as SAMPLED_TAALS, g as ScheduleOptions, T as TABLA_BPMS, h as TABLA_VARIATIONS, i as TONEJS_INSTRUMENTS_BASE, j as ToneModule, k as buildSchedule, l as createBuffers, m as drumPattern, n as glideTargets, o as instrumentSampleUrl, p as isArcRaw, q as meendContinuations, r as nearestSampleKey, s as nearestTablaBpm, t as normalizeBase, u as probeUrl, v as quantizeLoopEnd, w as santoorUrls, x as subsetUrls, y as tablaBpms, z as tablaUrl, B as triggerGlide } from '../schedule-CjPcD-Hf.cjs';
5
5
  import 'zod';
6
6
  import 'tone';
7
7
 
@@ -301,5 +301,17 @@ declare function svaraToMidi(note: Note, tonicPc: number, baseOctave?: number):
301
301
  declare function midiToSampleName(midi: number): string;
302
302
  /** MIDI → santoor sample filename stem (`cs4`): lowercase, `#` → `s`. */
303
303
  declare function sampleFileName(midi: number): string;
304
+ /**
305
+ * Logistic S-curve on [0, 1], normalised so f(0) = 0 and f(1) = 1.
306
+ *
307
+ * A মীড় is **not** a linear ramp. The hand (or the voice) accelerates away
308
+ * from the source pitch and decelerates into the target — "if the pull is too
309
+ * fast, the melody loses its vocal quality; if it is too slow, the note dies
310
+ * before reaching its destination". That S shape is the established model for
311
+ * portamento pitch contours (Yang & Chew's logistic fit, ISMIR 2016), and its
312
+ * coefficient has a direct musical meaning: `steepness` is how abruptly the
313
+ * pitch leaves one svara for the next.
314
+ */
315
+ declare function sCurve(x: number, steepness?: number): number;
304
316
 
305
- export { type AudioExportOptions, type AudioExportResult, type AudioFormat, type BindSheetOptions, DrumVoice, InstrumentDef, InstrumentId, type MidiExportOptions, type MidiExportResult, OCTAVE_OFFSET, type PlayerOptions, type PlayerState, type RhythmMode, SHUDDHA, Schedule, SwaralipiPlayer, encodeMidi, encodeMp3, encodeWav, exportAudio, exportMidi, midiToSampleName, parseTonic, sampleFileName, svaraSemitone, svaraToMidi };
317
+ export { type AudioExportOptions, type AudioExportResult, type AudioFormat, type BindSheetOptions, DrumVoice, InstrumentDef, InstrumentId, type MidiExportOptions, type MidiExportResult, OCTAVE_OFFSET, type PlayerOptions, type PlayerState, type RhythmMode, SHUDDHA, Schedule, SwaralipiPlayer, encodeMidi, encodeMp3, encodeWav, exportAudio, exportMidi, midiToSampleName, parseTonic, sCurve, sampleFileName, svaraSemitone, svaraToMidi };
@@ -1,7 +1,7 @@
1
- import { S as Swaralipi, T as Taal, O as Octave, a as SvaraLetter, N as Note } from '../schema-D3wyNRqM.js';
1
+ import { S as Swaralipi, T as Taal, O as Octave, a as SvaraLetter, N as Note } from '../schema-D_86x4oV.js';
2
2
  import { T as TaalId } from '../taals-CjzBuZ35.js';
3
- import { I as InstrumentId, a as InstrumentDef, S as Schedule, D as DrumVoice } from '../schedule-CAns78TD.js';
4
- export { b as DEFAULT_SOUNDS_BASE, c as DRUM_KIT_BASE, d as INSTRUMENTS, P as PlayEvent, e as SALAMANDER_BASE, f as SAMPLED_TAALS, g as ScheduleOptions, T as TABLA_BPMS, h as TABLA_VARIATIONS, i as TONEJS_INSTRUMENTS_BASE, j as ToneModule, k as buildSchedule, l as drumPattern, m as instrumentSampleUrl, n as nearestTablaBpm, o as normalizeBase, p as probeUrl, q as quantizeLoopEnd, s as santoorUrls, r as subsetUrls, t as tablaBpms, u as tablaUrl } from '../schedule-CAns78TD.js';
3
+ import { I as InstrumentId, a as InstrumentDef, S as Schedule, D as DrumVoice } from '../schedule-8WFUBJqB.js';
4
+ export { A as ARC_RAW_RE, b as DEFAULT_SOUNDS_BASE, c as DRUM_KIT_BASE, G as GlideLeg, d as INSTRUMENTS, M as MEEND_HOLD, P as PlayEvent, e as SALAMANDER_BASE, f as SAMPLED_TAALS, g as ScheduleOptions, T as TABLA_BPMS, h as TABLA_VARIATIONS, i as TONEJS_INSTRUMENTS_BASE, j as ToneModule, k as buildSchedule, l as createBuffers, m as drumPattern, n as glideTargets, o as instrumentSampleUrl, p as isArcRaw, q as meendContinuations, r as nearestSampleKey, s as nearestTablaBpm, t as normalizeBase, u as probeUrl, v as quantizeLoopEnd, w as santoorUrls, x as subsetUrls, y as tablaBpms, z as tablaUrl, B as triggerGlide } from '../schedule-8WFUBJqB.js';
5
5
  import 'zod';
6
6
  import 'tone';
7
7
 
@@ -301,5 +301,17 @@ declare function svaraToMidi(note: Note, tonicPc: number, baseOctave?: number):
301
301
  declare function midiToSampleName(midi: number): string;
302
302
  /** MIDI → santoor sample filename stem (`cs4`): lowercase, `#` → `s`. */
303
303
  declare function sampleFileName(midi: number): string;
304
+ /**
305
+ * Logistic S-curve on [0, 1], normalised so f(0) = 0 and f(1) = 1.
306
+ *
307
+ * A মীড় is **not** a linear ramp. The hand (or the voice) accelerates away
308
+ * from the source pitch and decelerates into the target — "if the pull is too
309
+ * fast, the melody loses its vocal quality; if it is too slow, the note dies
310
+ * before reaching its destination". That S shape is the established model for
311
+ * portamento pitch contours (Yang & Chew's logistic fit, ISMIR 2016), and its
312
+ * coefficient has a direct musical meaning: `steepness` is how abruptly the
313
+ * pitch leaves one svara for the next.
314
+ */
315
+ declare function sCurve(x: number, steepness?: number): number;
304
316
 
305
- export { type AudioExportOptions, type AudioExportResult, type AudioFormat, type BindSheetOptions, DrumVoice, InstrumentDef, InstrumentId, type MidiExportOptions, type MidiExportResult, OCTAVE_OFFSET, type PlayerOptions, type PlayerState, type RhythmMode, SHUDDHA, Schedule, SwaralipiPlayer, encodeMidi, encodeMp3, encodeWav, exportAudio, exportMidi, midiToSampleName, parseTonic, sampleFileName, svaraSemitone, svaraToMidi };
317
+ export { type AudioExportOptions, type AudioExportResult, type AudioFormat, type BindSheetOptions, DrumVoice, InstrumentDef, InstrumentId, type MidiExportOptions, type MidiExportResult, OCTAVE_OFFSET, type PlayerOptions, type PlayerState, type RhythmMode, SHUDDHA, Schedule, SwaralipiPlayer, encodeMidi, encodeMp3, encodeWav, exportAudio, exportMidi, midiToSampleName, parseTonic, sCurve, sampleFileName, svaraSemitone, svaraToMidi };
@@ -1,5 +1,5 @@
1
- import { DEFAULT_SOUNDS_BASE, INSTRUMENTS, SAMPLED_TAALS, instrumentSampleUrl, probeUrl, normalizeBase, nearestTablaBpm, tablaBpms, buildSchedule, quantizeLoopEnd, midiToSampleName, createSampler, tablaUrl, tablaShiftSemitones, TABLA_GRAIN_OVERLAP_SEC, TABLA_GRAIN_SEC, createDrumKit, drumPattern, createMetronome, TANPURA_PCS, createTanpura, parseTonic } from '../chunk-6QW7MSJO.js';
2
- export { DEFAULT_SOUNDS_BASE, DRUM_KIT_BASE, INSTRUMENTS, OCTAVE_OFFSET, SALAMANDER_BASE, SAMPLED_TAALS, SHUDDHA, TABLA_BPMS, TABLA_VARIATIONS, TONEJS_INSTRUMENTS_BASE, buildSchedule, drumPattern, instrumentSampleUrl, midiToSampleName, nearestTablaBpm, normalizeBase, parseTonic, probeUrl, quantizeLoopEnd, sampleFileName, santoorUrls, subsetUrls, svaraSemitone, svaraToMidi, tablaBpms, tablaUrl } from '../chunk-6QW7MSJO.js';
1
+ import { DEFAULT_SOUNDS_BASE, INSTRUMENTS, SAMPLED_TAALS, instrumentSampleUrl, probeUrl, normalizeBase, nearestTablaBpm, tablaBpms, buildSchedule, quantizeLoopEnd, midiToSampleName, glideTargets, triggerGlide, createSampler, createBuffers, tablaUrl, tablaShiftSemitones, TABLA_GRAIN_OVERLAP_SEC, TABLA_GRAIN_SEC, createDrumKit, drumPattern, createMetronome, TANPURA_PCS, createTanpura, parseTonic, MEEND_HOLD, sCurve } from '../chunk-GHWURC4U.js';
2
+ export { ARC_RAW_RE, DEFAULT_SOUNDS_BASE, DRUM_KIT_BASE, INSTRUMENTS, MEEND_HOLD, OCTAVE_OFFSET, SALAMANDER_BASE, SAMPLED_TAALS, SHUDDHA, TABLA_BPMS, TABLA_VARIATIONS, TONEJS_INSTRUMENTS_BASE, buildSchedule, createBuffers, drumPattern, glideTargets, instrumentSampleUrl, isArcRaw, meendContinuations, midiToSampleName, nearestSampleKey, nearestTablaBpm, normalizeBase, parseTonic, probeUrl, quantizeLoopEnd, sCurve, sampleFileName, santoorUrls, subsetUrls, svaraSemitone, svaraToMidi, tablaBpms, tablaUrl, triggerGlide } from '../chunk-GHWURC4U.js';
3
3
  import { TAALS } from '../chunk-6ETPFHZ6.js';
4
4
 
5
5
  // src/player/player.ts
@@ -17,6 +17,13 @@ function midiRange(schedule) {
17
17
  }
18
18
  return lo <= hi ? { lo, hi } : void 0;
19
19
  }
20
+ async function loadedOrWarn(Tone) {
21
+ try {
22
+ await Tone.loaded();
23
+ } catch (err) {
24
+ console.warn("[swaralipi] some audio samples failed to load; continuing without them", err);
25
+ }
26
+ }
20
27
  var SwaralipiPlayer = class {
21
28
  #opts;
22
29
  /** Per-song setBpm tweak: beats meta.bpm but is cleared by the next load(). */
@@ -32,6 +39,8 @@ var SwaralipiPlayer = class {
32
39
  /** Cached melody samplers keyed by instrument id, with the MIDI range each
33
40
  * was loaded for (rebuilt when a song needs a wider range). */
34
41
  #samplers = /* @__PURE__ */ new Map();
42
+ /** Raw sample buffers for the মীড় glide voice, keyed like #samplers. */
43
+ #glide = /* @__PURE__ */ new Map();
35
44
  #tanpura;
36
45
  #tanpuraKey = "";
37
46
  #metronome;
@@ -283,6 +292,8 @@ var SwaralipiPlayer = class {
283
292
  this.#halt();
284
293
  for (const { sampler } of this.#samplers.values()) sampler.dispose();
285
294
  this.#samplers.clear();
295
+ for (const { buffers } of this.#glide.values()) buffers.dispose();
296
+ this.#glide.clear();
286
297
  this.#tanpura?.dispose();
287
298
  this.#metronome?.dispose();
288
299
  this.#tabla?.dispose();
@@ -385,30 +396,54 @@ var SwaralipiPlayer = class {
385
396
  baseOctave: this.#opts.baseOctave
386
397
  });
387
398
  const range = midiRange(schedule);
388
- const melody = this.#buildMelody(Tone, this.#effectiveInstrument(), base, range);
399
+ const id = this.#effectiveInstrument();
400
+ const def = this.#resolveDef(id) ?? INSTRUMENTS.harmonium;
401
+ const melody = this.#buildMelody(Tone, id, base, range);
402
+ const glide = schedule.events.some((ev) => ev.glide === true) ? this.#buildGlide(Tone, id, base, range) : void 0;
389
403
  this.#buildRhythm(Tone, transport, base, spb);
390
404
  this.#buildTanpura(Tone, transport, base, spb);
391
- await Tone.loaded();
405
+ await loadedOrWarn(Tone);
392
406
  if (this.#tabla?.buffer?.loaded) {
393
407
  this.#tabla.loopEnd = quantizeLoopEnd(this.#tabla.buffer.duration, spb);
394
408
  }
395
409
  const draw = Tone.getDraw();
396
410
  const shift = this.#opts.rhythm !== "none" ? schedule.pickupBeats * spb : 0;
397
- for (const ev of schedule.events) {
411
+ schedule.events.forEach((ev, i) => {
398
412
  const at = ev.beat * spb + shift;
399
- if (ev.midi !== null) {
413
+ if (ev.midi !== null && ev.glide !== true) {
400
414
  const name = midiToSampleName(ev.midi);
401
415
  const dur = Math.max(ev.durationBeats * spb, 0.05);
402
416
  if (ev.kan !== null) {
403
417
  const kanName = midiToSampleName(ev.kan);
404
418
  transport.scheduleOnce((time) => melody.triggerAttackRelease(kanName, spb * 0.4, time), Math.max(at - spb * 0.12, 0));
405
419
  }
406
- transport.scheduleOnce((time) => melody.triggerAttackRelease(name, dur, time), at);
420
+ const targets = glide !== void 0 ? glideTargets(schedule.events, i) : [];
421
+ if (targets.length > 0 && glide !== void 0) {
422
+ const last = targets[targets.length - 1];
423
+ const legs = [
424
+ { midi: ev.midi, at: 0 },
425
+ ...targets.map((t) => ({ midi: t.midi, at: (t.beat - ev.beat) * spb }))
426
+ ];
427
+ const end = (last.beat + last.durationBeats - ev.beat) * spb;
428
+ transport.scheduleOnce((time) => {
429
+ triggerGlide(
430
+ Tone,
431
+ glide.buffers,
432
+ glide.urls,
433
+ this.#ensureMaster(Tone),
434
+ legs.map((l) => ({ midi: l.midi, at: time + l.at })),
435
+ time + end,
436
+ def.release ?? 1
437
+ );
438
+ }, at);
439
+ } else {
440
+ transport.scheduleOnce((time) => melody.triggerAttackRelease(name, dur, time), at);
441
+ }
407
442
  }
408
443
  transport.scheduleOnce((time) => {
409
444
  draw.schedule(() => this.#emitHighlight(ev.path), time);
410
445
  }, at);
411
- }
446
+ });
412
447
  const endAt = schedule.totalBeats * spb + shift + spb;
413
448
  transport.scheduleOnce((time) => {
414
449
  draw.schedule(() => this.#emitHighlight(null), time);
@@ -432,7 +467,7 @@ var SwaralipiPlayer = class {
432
467
  transport.bpm.value = bpm;
433
468
  const spb = 60 / bpm;
434
469
  this.#buildRhythm(Tone, transport, base, spb);
435
- await Tone.loaded();
470
+ await loadedOrWarn(Tone);
436
471
  if (this.#tabla?.buffer?.loaded) {
437
472
  this.#tabla.loopEnd = quantizeLoopEnd(this.#tabla.buffer.duration, spb);
438
473
  }
@@ -464,6 +499,19 @@ var SwaralipiPlayer = class {
464
499
  this.#samplers.set(id, { sampler, lo: range?.lo ?? -Infinity, hi: range?.hi ?? Infinity });
465
500
  return sampler;
466
501
  }
502
+ /** Build (or reuse) the মীড় glide buffers for `id` — the same samples the
503
+ * Sampler uses, kept as raw buffers so a note can be started once and its
504
+ * playbackRate automated. Cached and range-widened exactly like #buildMelody. */
505
+ #buildGlide(Tone, id, base, range) {
506
+ const cached = this.#glide.get(id);
507
+ if (cached && (range === void 0 || cached.lo <= range.lo && cached.hi >= range.hi)) return cached;
508
+ cached?.buffers.dispose();
509
+ const def = this.#resolveDef(id) ?? INSTRUMENTS.harmonium;
510
+ const built = createBuffers(Tone, def, base, range);
511
+ const entry = { ...built, lo: range?.lo ?? -Infinity, hi: range?.hi ?? Infinity };
512
+ this.#glide.set(id, entry);
513
+ return entry;
514
+ }
467
515
  #buildRhythm(Tone, transport, base, spb) {
468
516
  this.#tabla?.dispose();
469
517
  this.#tabla = void 0;
@@ -592,17 +640,39 @@ async function exportAudio(doc, options = {}) {
592
640
  const master = new Tone.Gain(1).toDestination();
593
641
  if (withMelody) {
594
642
  const sampler = createSampler(Tone, def, base, range).connect(master);
595
- for (const ev of schedule.events) {
596
- if (ev.midi === null) continue;
643
+ const glide = schedule.events.some((ev) => ev.glide === true) ? createBuffers(Tone, def, base, range) : void 0;
644
+ schedule.events.forEach((ev, i) => {
645
+ if (ev.midi === null || ev.glide === true) return;
597
646
  const at = ev.beat * spb + shift;
598
647
  const dur = Math.max(ev.durationBeats * spb, 0.05);
599
648
  if (ev.kan !== null) {
600
- const name2 = midiToSampleName(ev.kan);
601
- transport.scheduleOnce((t) => sampler.triggerAttackRelease(name2, spb * 0.4, t), Math.max(at - spb * 0.12, 0));
649
+ const name = midiToSampleName(ev.kan);
650
+ transport.scheduleOnce((t) => sampler.triggerAttackRelease(name, spb * 0.4, t), Math.max(at - spb * 0.12, 0));
602
651
  }
603
- const name = midiToSampleName(ev.midi);
604
- transport.scheduleOnce((t) => sampler.triggerAttackRelease(name, dur, t), at);
605
- }
652
+ const targets = glide !== void 0 ? glideTargets(schedule.events, i) : [];
653
+ if (targets.length > 0 && glide !== void 0) {
654
+ const last = targets[targets.length - 1];
655
+ const legs = [
656
+ { midi: ev.midi, at: 0 },
657
+ ...targets.map((t) => ({ midi: t.midi, at: (t.beat - ev.beat) * spb }))
658
+ ];
659
+ const end = (last.beat + last.durationBeats - ev.beat) * spb;
660
+ transport.scheduleOnce((t) => {
661
+ triggerGlide(
662
+ Tone,
663
+ glide.buffers,
664
+ glide.urls,
665
+ master,
666
+ legs.map((l) => ({ midi: l.midi, at: t + l.at })),
667
+ t + end,
668
+ def.release ?? 1
669
+ );
670
+ }, at);
671
+ } else {
672
+ const name = midiToSampleName(ev.midi);
673
+ transport.scheduleOnce((t) => sampler.triggerAttackRelease(name, dur, t), at);
674
+ }
675
+ });
606
676
  }
607
677
  let tabla;
608
678
  if (rhythm === "drums" && base) {
@@ -774,6 +844,17 @@ function utf8(s) {
774
844
  function u32be(n) {
775
845
  return [n >>> 24 & 255, n >>> 16 & 255, n >>> 8 & 255, n & 255];
776
846
  }
847
+ var BEND_RANGE = 12;
848
+ var BEND_STEPS = 16;
849
+ function bendEvent(tick, semitones) {
850
+ const raw = Math.round(8192 + semitones / BEND_RANGE * 8192);
851
+ const value = Math.min(Math.max(raw, 0), 16383);
852
+ return { tick, on: false, status: 224, note: value & 127, vel: value >> 7 & 127, ch: 0 };
853
+ }
854
+ function bendRangeSetup() {
855
+ const cc = (num, val) => ({ tick: 0, on: false, status: 176, note: num, vel: val, ch: 0 });
856
+ return [cc(101, 0), cc(100, 0), cc(6, BEND_RANGE), cc(38, 0), cc(101, 127), cc(100, 127)];
857
+ }
777
858
  var DRUM_NOTE = {
778
859
  kick: 36,
779
860
  snare: 38,
@@ -784,11 +865,12 @@ var DRUM_NOTE = {
784
865
  tom3: 41
785
866
  };
786
867
  function trackChunk(raw, head) {
787
- raw.sort((a, b) => a.tick - b.tick || Number(a.on) - Number(b.on));
868
+ const rank = (e) => e.status !== void 0 ? 1 : e.on ? 2 : 0;
869
+ raw.sort((a, b) => a.tick - b.tick || rank(a) - rank(b));
788
870
  const track = [...head];
789
871
  let last = 0;
790
872
  for (const e of raw) {
791
- const status = (e.on ? 144 : 128) | e.ch & 15;
873
+ const status = e.status !== void 0 ? e.status | e.ch & 15 : (e.on ? 144 : 128) | e.ch & 15;
792
874
  track.push(...vlq(e.tick - last), status, e.note & 127, e.vel & 127);
793
875
  last = e.tick;
794
876
  }
@@ -797,10 +879,14 @@ function trackChunk(raw, head) {
797
879
  }
798
880
  function encodeMidi(schedule, opts) {
799
881
  const melody = [];
800
- for (const ev of schedule.events) {
801
- if (ev.midi === null) continue;
882
+ const hasGlide = schedule.events.some((ev) => ev.glide === true);
883
+ if (hasGlide) melody.push(...bendRangeSetup());
884
+ schedule.events.forEach((ev, i) => {
885
+ if (ev.midi === null || ev.glide === true) return;
802
886
  const start = Math.round(ev.beat * PPQ);
803
- const end = Math.max(Math.round((ev.beat + ev.durationBeats) * PPQ), start + 1);
887
+ const targets = glideTargets(schedule.events, i);
888
+ const last = targets[targets.length - 1] ?? ev;
889
+ const end = Math.max(Math.round((last.beat + last.durationBeats) * PPQ), start + 1);
804
890
  if (ev.kan !== null) {
805
891
  const kOn = Math.max(start - Math.round(0.12 * PPQ), 0);
806
892
  melody.push({ tick: kOn, on: true, note: ev.kan, vel: 70, ch: 0 });
@@ -808,7 +894,19 @@ function encodeMidi(schedule, opts) {
808
894
  }
809
895
  melody.push({ tick: start, on: true, note: ev.midi, vel: 100, ch: 0 });
810
896
  melody.push({ tick: end, on: false, note: ev.midi, vel: 0, ch: 0 });
811
- }
897
+ let from = ev;
898
+ for (const to of targets) {
899
+ const glideStart = from.beat + (to.beat - from.beat) * MEEND_HOLD;
900
+ for (let n = 0; n <= BEND_STEPS; n++) {
901
+ const u = n / BEND_STEPS;
902
+ const beat = glideStart + (to.beat - glideStart) * u;
903
+ const semis = from.midi - ev.midi + (to.midi - from.midi) * sCurve(u);
904
+ melody.push(bendEvent(Math.round(beat * PPQ), semis));
905
+ }
906
+ from = to;
907
+ }
908
+ if (targets.length > 0) melody.push(bendEvent(end, 0));
909
+ });
812
910
  const usPerBeat = Math.round(6e7 / opts.bpm);
813
911
  const head = [];
814
912
  if (opts.name) head.push(...vlq(0), 255, 3, ...vlq(utf8(opts.name).length), ...utf8(opts.name));