@ugfoundation/swaralipi-js 0.5.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 (46) hide show
  1. package/dist/{batch-UETOBPHT.js → batch-RUZGM2NR.js} +4 -4
  2. package/dist/{batch-UETOBPHT.js.map → batch-RUZGM2NR.js.map} +1 -1
  3. package/dist/{chunk-C6TRCMPL.js → chunk-GHWURC4U.js} +97 -5
  4. package/dist/chunk-GHWURC4U.js.map +1 -0
  5. package/dist/{chunk-UD2K4UIC.js → chunk-ULK7NVKH.js} +8 -6
  6. package/dist/chunk-ULK7NVKH.js.map +1 -0
  7. package/dist/{chunk-ID44OP2N.js → chunk-ZNVTOY7J.js} +7 -6
  8. package/dist/chunk-ZNVTOY7J.js.map +1 -0
  9. package/dist/cli.cjs +11 -8
  10. package/dist/cli.cjs.map +1 -1
  11. package/dist/cli.js +2 -2
  12. package/dist/convert/index.cjs +10 -7
  13. package/dist/convert/index.cjs.map +1 -1
  14. package/dist/convert/index.d.cts +4 -3
  15. package/dist/convert/index.d.ts +4 -3
  16. package/dist/convert/index.js +2 -2
  17. package/dist/index.cjs +6 -4
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +1 -1
  22. package/dist/player/index.cjs +212 -20
  23. package/dist/player/index.cjs.map +1 -1
  24. package/dist/player/index.d.cts +16 -4
  25. package/dist/player/index.d.ts +16 -4
  26. package/dist/player/index.js +111 -20
  27. package/dist/player/index.js.map +1 -1
  28. package/dist/{schedule-BaYIJf8y.d.ts → schedule-8WFUBJqB.d.ts} +78 -3
  29. package/dist/{schedule-bBvy18MO.d.cts → schedule-CjPcD-Hf.d.cts} +78 -3
  30. package/dist/{schema-D3wyNRqM.d.ts → schema-D_86x4oV.d.cts} +21 -6
  31. package/dist/{schema-D3wyNRqM.d.cts → schema-D_86x4oV.d.ts} +21 -6
  32. package/dist/spec/index.cjs +6 -4
  33. package/dist/spec/index.cjs.map +1 -1
  34. package/dist/spec/index.d.cts +3 -3
  35. package/dist/spec/index.d.ts +3 -3
  36. package/dist/spec/index.js +1 -1
  37. package/dist/viewer/index.cjs +240 -161
  38. package/dist/viewer/index.cjs.map +1 -1
  39. package/dist/viewer/index.d.cts +16 -23
  40. package/dist/viewer/index.d.ts +16 -23
  41. package/dist/viewer/index.js +75 -36
  42. package/dist/viewer/index.js.map +1 -1
  43. package/package.json +1 -1
  44. package/dist/chunk-C6TRCMPL.js.map +0 -1
  45. package/dist/chunk-ID44OP2N.js.map +0 -1
  46. package/dist/chunk-UD2K4UIC.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-bBvy18MO.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-bBvy18MO.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-BaYIJf8y.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-BaYIJf8y.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-C6TRCMPL.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-C6TRCMPL.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
@@ -39,6 +39,8 @@ var SwaralipiPlayer = class {
39
39
  /** Cached melody samplers keyed by instrument id, with the MIDI range each
40
40
  * was loaded for (rebuilt when a song needs a wider range). */
41
41
  #samplers = /* @__PURE__ */ new Map();
42
+ /** Raw sample buffers for the মীড় glide voice, keyed like #samplers. */
43
+ #glide = /* @__PURE__ */ new Map();
42
44
  #tanpura;
43
45
  #tanpuraKey = "";
44
46
  #metronome;
@@ -290,6 +292,8 @@ var SwaralipiPlayer = class {
290
292
  this.#halt();
291
293
  for (const { sampler } of this.#samplers.values()) sampler.dispose();
292
294
  this.#samplers.clear();
295
+ for (const { buffers } of this.#glide.values()) buffers.dispose();
296
+ this.#glide.clear();
293
297
  this.#tanpura?.dispose();
294
298
  this.#metronome?.dispose();
295
299
  this.#tabla?.dispose();
@@ -392,7 +396,10 @@ var SwaralipiPlayer = class {
392
396
  baseOctave: this.#opts.baseOctave
393
397
  });
394
398
  const range = midiRange(schedule);
395
- 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;
396
403
  this.#buildRhythm(Tone, transport, base, spb);
397
404
  this.#buildTanpura(Tone, transport, base, spb);
398
405
  await loadedOrWarn(Tone);
@@ -401,21 +408,42 @@ var SwaralipiPlayer = class {
401
408
  }
402
409
  const draw = Tone.getDraw();
403
410
  const shift = this.#opts.rhythm !== "none" ? schedule.pickupBeats * spb : 0;
404
- for (const ev of schedule.events) {
411
+ schedule.events.forEach((ev, i) => {
405
412
  const at = ev.beat * spb + shift;
406
- if (ev.midi !== null) {
413
+ if (ev.midi !== null && ev.glide !== true) {
407
414
  const name = midiToSampleName(ev.midi);
408
415
  const dur = Math.max(ev.durationBeats * spb, 0.05);
409
416
  if (ev.kan !== null) {
410
417
  const kanName = midiToSampleName(ev.kan);
411
418
  transport.scheduleOnce((time) => melody.triggerAttackRelease(kanName, spb * 0.4, time), Math.max(at - spb * 0.12, 0));
412
419
  }
413
- 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
+ }
414
442
  }
415
443
  transport.scheduleOnce((time) => {
416
444
  draw.schedule(() => this.#emitHighlight(ev.path), time);
417
445
  }, at);
418
- }
446
+ });
419
447
  const endAt = schedule.totalBeats * spb + shift + spb;
420
448
  transport.scheduleOnce((time) => {
421
449
  draw.schedule(() => this.#emitHighlight(null), time);
@@ -471,6 +499,19 @@ var SwaralipiPlayer = class {
471
499
  this.#samplers.set(id, { sampler, lo: range?.lo ?? -Infinity, hi: range?.hi ?? Infinity });
472
500
  return sampler;
473
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
+ }
474
515
  #buildRhythm(Tone, transport, base, spb) {
475
516
  this.#tabla?.dispose();
476
517
  this.#tabla = void 0;
@@ -599,17 +640,39 @@ async function exportAudio(doc, options = {}) {
599
640
  const master = new Tone.Gain(1).toDestination();
600
641
  if (withMelody) {
601
642
  const sampler = createSampler(Tone, def, base, range).connect(master);
602
- for (const ev of schedule.events) {
603
- 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;
604
646
  const at = ev.beat * spb + shift;
605
647
  const dur = Math.max(ev.durationBeats * spb, 0.05);
606
648
  if (ev.kan !== null) {
607
- const name2 = midiToSampleName(ev.kan);
608
- 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));
609
651
  }
610
- const name = midiToSampleName(ev.midi);
611
- transport.scheduleOnce((t) => sampler.triggerAttackRelease(name, dur, t), at);
612
- }
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
+ });
613
676
  }
614
677
  let tabla;
615
678
  if (rhythm === "drums" && base) {
@@ -781,6 +844,17 @@ function utf8(s) {
781
844
  function u32be(n) {
782
845
  return [n >>> 24 & 255, n >>> 16 & 255, n >>> 8 & 255, n & 255];
783
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
+ }
784
858
  var DRUM_NOTE = {
785
859
  kick: 36,
786
860
  snare: 38,
@@ -791,11 +865,12 @@ var DRUM_NOTE = {
791
865
  tom3: 41
792
866
  };
793
867
  function trackChunk(raw, head) {
794
- 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));
795
870
  const track = [...head];
796
871
  let last = 0;
797
872
  for (const e of raw) {
798
- 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;
799
874
  track.push(...vlq(e.tick - last), status, e.note & 127, e.vel & 127);
800
875
  last = e.tick;
801
876
  }
@@ -804,10 +879,14 @@ function trackChunk(raw, head) {
804
879
  }
805
880
  function encodeMidi(schedule, opts) {
806
881
  const melody = [];
807
- for (const ev of schedule.events) {
808
- 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;
809
886
  const start = Math.round(ev.beat * PPQ);
810
- 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);
811
890
  if (ev.kan !== null) {
812
891
  const kOn = Math.max(start - Math.round(0.12 * PPQ), 0);
813
892
  melody.push({ tick: kOn, on: true, note: ev.kan, vel: 70, ch: 0 });
@@ -815,7 +894,19 @@ function encodeMidi(schedule, opts) {
815
894
  }
816
895
  melody.push({ tick: start, on: true, note: ev.midi, vel: 100, ch: 0 });
817
896
  melody.push({ tick: end, on: false, note: ev.midi, vel: 0, ch: 0 });
818
- }
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
+ });
819
910
  const usPerBeat = Math.round(6e7 / opts.bpm);
820
911
  const head = [];
821
912
  if (opts.name) head.push(...vlq(0), 255, 3, ...vlq(utf8(opts.name).length), ...utf8(opts.name));