@mcptoolshop/ai_jam_session 0.1.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 (246) hide show
  1. package/LICENSE +21 -0
  2. package/README.es.md +212 -0
  3. package/README.fr.md +212 -0
  4. package/README.hi.md +212 -0
  5. package/README.it.md +212 -0
  6. package/README.ja.md +214 -0
  7. package/README.md +214 -0
  8. package/README.pt-BR.md +212 -0
  9. package/dist/audio-engine.d.ts +9 -0
  10. package/dist/audio-engine.d.ts.map +1 -0
  11. package/dist/audio-engine.js +422 -0
  12. package/dist/audio-engine.js.map +1 -0
  13. package/dist/cli.d.ts +3 -0
  14. package/dist/cli.d.ts.map +1 -0
  15. package/dist/cli.js +551 -0
  16. package/dist/cli.js.map +1 -0
  17. package/dist/index.d.ts +32 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +41 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/mcp-server.d.ts +3 -0
  22. package/dist/mcp-server.d.ts.map +1 -0
  23. package/dist/mcp-server.js +903 -0
  24. package/dist/mcp-server.js.map +1 -0
  25. package/dist/midi/parser.d.ts +16 -0
  26. package/dist/midi/parser.d.ts.map +1 -0
  27. package/dist/midi/parser.js +192 -0
  28. package/dist/midi/parser.js.map +1 -0
  29. package/dist/midi/types.d.ts +44 -0
  30. package/dist/midi/types.d.ts.map +1 -0
  31. package/dist/midi/types.js +8 -0
  32. package/dist/midi/types.js.map +1 -0
  33. package/dist/note-parser.d.ts +105 -0
  34. package/dist/note-parser.d.ts.map +1 -0
  35. package/dist/note-parser.js +278 -0
  36. package/dist/note-parser.js.map +1 -0
  37. package/dist/playback/controls.d.ts +124 -0
  38. package/dist/playback/controls.d.ts.map +1 -0
  39. package/dist/playback/controls.js +252 -0
  40. package/dist/playback/controls.js.map +1 -0
  41. package/dist/playback/midi-engine.d.ts +68 -0
  42. package/dist/playback/midi-engine.d.ts.map +1 -0
  43. package/dist/playback/midi-engine.js +227 -0
  44. package/dist/playback/midi-engine.js.map +1 -0
  45. package/dist/playback/position.d.ts +95 -0
  46. package/dist/playback/position.d.ts.map +1 -0
  47. package/dist/playback/position.js +223 -0
  48. package/dist/playback/position.js.map +1 -0
  49. package/dist/playback/timing.d.ts +31 -0
  50. package/dist/playback/timing.d.ts.map +1 -0
  51. package/dist/playback/timing.js +57 -0
  52. package/dist/playback/timing.js.map +1 -0
  53. package/dist/sample-engine.d.ts +17 -0
  54. package/dist/sample-engine.d.ts.map +1 -0
  55. package/dist/sample-engine.js +428 -0
  56. package/dist/sample-engine.js.map +1 -0
  57. package/dist/schemas.d.ts +40 -0
  58. package/dist/schemas.d.ts.map +1 -0
  59. package/dist/schemas.js +42 -0
  60. package/dist/schemas.js.map +1 -0
  61. package/dist/session.d.ts +106 -0
  62. package/dist/session.d.ts.map +1 -0
  63. package/dist/session.js +361 -0
  64. package/dist/session.js.map +1 -0
  65. package/dist/sfz-parser.d.ts +36 -0
  66. package/dist/sfz-parser.d.ts.map +1 -0
  67. package/dist/sfz-parser.js +95 -0
  68. package/dist/sfz-parser.js.map +1 -0
  69. package/dist/smoke.d.ts +2 -0
  70. package/dist/smoke.d.ts.map +1 -0
  71. package/dist/smoke.js +512 -0
  72. package/dist/smoke.js.map +1 -0
  73. package/dist/songs/config/loader.d.ts +14 -0
  74. package/dist/songs/config/loader.d.ts.map +1 -0
  75. package/dist/songs/config/loader.js +53 -0
  76. package/dist/songs/config/loader.js.map +1 -0
  77. package/dist/songs/config/schema.d.ts +70 -0
  78. package/dist/songs/config/schema.d.ts.map +1 -0
  79. package/dist/songs/config/schema.js +53 -0
  80. package/dist/songs/config/schema.js.map +1 -0
  81. package/dist/songs/index.d.ts +16 -0
  82. package/dist/songs/index.d.ts.map +1 -0
  83. package/dist/songs/index.js +20 -0
  84. package/dist/songs/index.js.map +1 -0
  85. package/dist/songs/jam.d.ts +48 -0
  86. package/dist/songs/jam.d.ts.map +1 -0
  87. package/dist/songs/jam.js +324 -0
  88. package/dist/songs/jam.js.map +1 -0
  89. package/dist/songs/loader.d.ts +27 -0
  90. package/dist/songs/loader.d.ts.map +1 -0
  91. package/dist/songs/loader.js +90 -0
  92. package/dist/songs/loader.js.map +1 -0
  93. package/dist/songs/midi/hands.d.ts +46 -0
  94. package/dist/songs/midi/hands.d.ts.map +1 -0
  95. package/dist/songs/midi/hands.js +134 -0
  96. package/dist/songs/midi/hands.js.map +1 -0
  97. package/dist/songs/midi/ingest.d.ts +8 -0
  98. package/dist/songs/midi/ingest.d.ts.map +1 -0
  99. package/dist/songs/midi/ingest.js +191 -0
  100. package/dist/songs/midi/ingest.js.map +1 -0
  101. package/dist/songs/midi/measures.d.ts +41 -0
  102. package/dist/songs/midi/measures.d.ts.map +1 -0
  103. package/dist/songs/midi/measures.js +64 -0
  104. package/dist/songs/midi/measures.js.map +1 -0
  105. package/dist/songs/midi/types.d.ts +25 -0
  106. package/dist/songs/midi/types.d.ts.map +1 -0
  107. package/dist/songs/midi/types.js +8 -0
  108. package/dist/songs/midi/types.js.map +1 -0
  109. package/dist/songs/registry.d.ts +37 -0
  110. package/dist/songs/registry.d.ts.map +1 -0
  111. package/dist/songs/registry.js +197 -0
  112. package/dist/songs/registry.js.map +1 -0
  113. package/dist/songs/types.d.ts +99 -0
  114. package/dist/songs/types.d.ts.map +1 -0
  115. package/dist/songs/types.js +27 -0
  116. package/dist/songs/types.js.map +1 -0
  117. package/dist/teaching/live-midi-feedback.d.ts +36 -0
  118. package/dist/teaching/live-midi-feedback.d.ts.map +1 -0
  119. package/dist/teaching/live-midi-feedback.js +259 -0
  120. package/dist/teaching/live-midi-feedback.js.map +1 -0
  121. package/dist/teaching/midi-feedback.d.ts +33 -0
  122. package/dist/teaching/midi-feedback.d.ts.map +1 -0
  123. package/dist/teaching/midi-feedback.js +208 -0
  124. package/dist/teaching/midi-feedback.js.map +1 -0
  125. package/dist/teaching/sing-on-midi.d.ts +77 -0
  126. package/dist/teaching/sing-on-midi.d.ts.map +1 -0
  127. package/dist/teaching/sing-on-midi.js +186 -0
  128. package/dist/teaching/sing-on-midi.js.map +1 -0
  129. package/dist/teaching.d.ts +148 -0
  130. package/dist/teaching.d.ts.map +1 -0
  131. package/dist/teaching.js +453 -0
  132. package/dist/teaching.js.map +1 -0
  133. package/dist/test-sound.d.ts +3 -0
  134. package/dist/test-sound.d.ts.map +1 -0
  135. package/dist/test-sound.js +41 -0
  136. package/dist/test-sound.js.map +1 -0
  137. package/dist/types.d.ts +229 -0
  138. package/dist/types.d.ts.map +1 -0
  139. package/dist/types.js +22 -0
  140. package/dist/types.js.map +1 -0
  141. package/dist/vmpk.d.ts +23 -0
  142. package/dist/vmpk.d.ts.map +1 -0
  143. package/dist/vmpk.js +236 -0
  144. package/dist/vmpk.js.map +1 -0
  145. package/logo.png +0 -0
  146. package/package.json +70 -0
  147. package/songs/builtin/a-change-is-gonna-come.json +95 -0
  148. package/songs/builtin/a-thousand-years.json +93 -0
  149. package/songs/builtin/aint-no-sunshine.json +98 -0
  150. package/songs/builtin/all-blues.json +92 -0
  151. package/songs/builtin/autumn-leaves.json +100 -0
  152. package/songs/builtin/baba-oriley.json +91 -0
  153. package/songs/builtin/bach-invention-no1.json +70 -0
  154. package/songs/builtin/bach-prelude-c-major-bwv846.json +1282 -0
  155. package/songs/builtin/basic-12-bar-blues.json +119 -0
  156. package/songs/builtin/beethoven-waldstein-mvt1.json +7766 -0
  157. package/songs/builtin/bennie-and-the-jets.json +92 -0
  158. package/songs/builtin/besame-mucho.json +93 -0
  159. package/songs/builtin/black-orpheus.json +92 -0
  160. package/songs/builtin/blue-bossa.json +94 -0
  161. package/songs/builtin/blues-in-g.json +92 -0
  162. package/songs/builtin/bohemian-rhapsody-intro.json +94 -0
  163. package/songs/builtin/boogie-woogie-basics.json +93 -0
  164. package/songs/builtin/bossa-nova-basic.json +95 -0
  165. package/songs/builtin/chopin-nocturne-op9-no2.json +70 -0
  166. package/songs/builtin/cinema-paradiso.json +94 -0
  167. package/songs/builtin/clair-de-lune.json +11511 -0
  168. package/songs/builtin/clocks.json +91 -0
  169. package/songs/builtin/crystal-stream.json +70 -0
  170. package/songs/builtin/desafinado.json +93 -0
  171. package/songs/builtin/dont-stop-believin.json +91 -0
  172. package/songs/builtin/dream-on.json +100 -0
  173. package/songs/builtin/easy-winners.json +70 -0
  174. package/songs/builtin/el-condor-pasa.json +93 -0
  175. package/songs/builtin/elite-syncopations.json +70 -0
  176. package/songs/builtin/evening-calm.json +70 -0
  177. package/songs/builtin/everyday-blues.json +93 -0
  178. package/songs/builtin/fly-me-to-the-moon.json +91 -0
  179. package/songs/builtin/forrest-gump-suite.json +93 -0
  180. package/songs/builtin/fur-elise.json +20094 -0
  181. package/songs/builtin/georgia-on-my-mind.json +93 -0
  182. package/songs/builtin/girl-from-ipanema.json +92 -0
  183. package/songs/builtin/gladiolus-rag.json +70 -0
  184. package/songs/builtin/great-balls-of-fire.json +92 -0
  185. package/songs/builtin/guantanamera.json +92 -0
  186. package/songs/builtin/hallelujah.json +92 -0
  187. package/songs/builtin/hedwigs-theme.json +93 -0
  188. package/songs/builtin/hotel-california.json +92 -0
  189. package/songs/builtin/imagine.json +92 -0
  190. package/songs/builtin/just-the-two-of-us.json +92 -0
  191. package/songs/builtin/la-bamba.json +92 -0
  192. package/songs/builtin/layla-piano-coda.json +93 -0
  193. package/songs/builtin/lean-on-me.json +91 -0
  194. package/songs/builtin/let-it-be.json +101 -0
  195. package/songs/builtin/lets-stay-together.json +93 -0
  196. package/songs/builtin/magnetic-rag.json +70 -0
  197. package/songs/builtin/maple-leaf-rag.json +99 -0
  198. package/songs/builtin/mia-and-sebastians-theme.json +93 -0
  199. package/songs/builtin/minor-blues-in-a.json +94 -0
  200. package/songs/builtin/misty.json +94 -0
  201. package/songs/builtin/moon-river.json +93 -0
  202. package/songs/builtin/moonlight-sonata-mvt1.json +101 -0
  203. package/songs/builtin/morning-light.json +70 -0
  204. package/songs/builtin/mountain-dawn.json +70 -0
  205. package/songs/builtin/mozart-k545-mvt1.json +2956 -0
  206. package/songs/builtin/my-girl.json +92 -0
  207. package/songs/builtin/night-train.json +92 -0
  208. package/songs/builtin/november-rain.json +93 -0
  209. package/songs/builtin/ocean-waves.json +70 -0
  210. package/songs/builtin/over-the-rainbow.json +93 -0
  211. package/songs/builtin/oye-como-va.json +93 -0
  212. package/songs/builtin/peacherine-rag.json +70 -0
  213. package/songs/builtin/piano-man.json +92 -0
  214. package/songs/builtin/pineapple-rag.json +70 -0
  215. package/songs/builtin/pink-panther.json +94 -0
  216. package/songs/builtin/ragtime-dance.json +70 -0
  217. package/songs/builtin/river-flows-in-you.json +102 -0
  218. package/songs/builtin/rocket-man.json +92 -0
  219. package/songs/builtin/satie-gymnopedie-no1.json +70 -0
  220. package/songs/builtin/satin-doll.json +93 -0
  221. package/songs/builtin/schindlers-list.json +96 -0
  222. package/songs/builtin/schumann-traumerei.json +70 -0
  223. package/songs/builtin/sitting-on-the-dock.json +91 -0
  224. package/songs/builtin/slow-blues-in-bb.json +98 -0
  225. package/songs/builtin/snowfall.json +70 -0
  226. package/songs/builtin/so-what.json +92 -0
  227. package/songs/builtin/solace.json +70 -0
  228. package/songs/builtin/someone-like-you.json +92 -0
  229. package/songs/builtin/spirited-away.json +94 -0
  230. package/songs/builtin/st-louis-blues.json +93 -0
  231. package/songs/builtin/stairway-intro.json +93 -0
  232. package/songs/builtin/starlight-waltz.json +70 -0
  233. package/songs/builtin/stay-with-me.json +93 -0
  234. package/songs/builtin/stormy-monday.json +94 -0
  235. package/songs/builtin/superstition.json +93 -0
  236. package/songs/builtin/sweet-home-chicago.json +93 -0
  237. package/songs/builtin/take-five.json +92 -0
  238. package/songs/builtin/take-the-a-train.json +93 -0
  239. package/songs/builtin/the-entertainer.json +98 -0
  240. package/songs/builtin/the-godfather-waltz.json +93 -0
  241. package/songs/builtin/thrill-is-gone.json +94 -0
  242. package/songs/builtin/twilight-garden.json +70 -0
  243. package/songs/builtin/watermark.json +70 -0
  244. package/songs/builtin/wave.json +93 -0
  245. package/songs/builtin/whats-going-on.json +93 -0
  246. package/songs/builtin/yesterday.json +92 -0
@@ -0,0 +1,278 @@
1
+ // ─── pianoai: Note Parser ────────────────────────────────────────────────────
2
+ //
3
+ // Converts scientific pitch notation (e.g. "C4:q", "F#5:e", "Bb3:h")
4
+ // into MIDI note numbers and durations for playback.
5
+ // ─────────────────────────────────────────────────────────────────────────────
6
+ import { NOTE_OFFSETS, DURATION_MAP } from "./types.js";
7
+ /**
8
+ * Parse a scientific pitch string into a MIDI note number.
9
+ *
10
+ * Examples:
11
+ * "C4" → 60 (middle C)
12
+ * "A4" → 69 (concert A)
13
+ * "F#5" → 78
14
+ * "Bb3" → 58
15
+ * "R" → -1 (rest)
16
+ */
17
+ export function parseNoteToMidi(noteStr) {
18
+ const trimmed = noteStr.trim();
19
+ if (trimmed === "R" || trimmed === "r")
20
+ return -1; // rest
21
+ // Match: letter + optional accidental + octave
22
+ // e.g. "C4", "F#5", "Bb3", "G#4"
23
+ const match = trimmed.match(/^([A-Ga-g])(#|b)?(\d)$/);
24
+ if (!match) {
25
+ throw new Error(`Invalid note: "${noteStr}"`);
26
+ }
27
+ const [, letter, accidental, octaveStr] = match;
28
+ const base = NOTE_OFFSETS[letter.toUpperCase()];
29
+ if (base === undefined) {
30
+ throw new Error(`Unknown note letter: "${letter}"`);
31
+ }
32
+ const octave = parseInt(octaveStr, 10);
33
+ let midi = (octave + 1) * 12 + base;
34
+ if (accidental === "#")
35
+ midi += 1;
36
+ if (accidental === "b")
37
+ midi -= 1;
38
+ if (midi < 0 || midi > 127) {
39
+ throw new Error(`MIDI note out of range: ${midi} (from "${noteStr}")`);
40
+ }
41
+ return midi;
42
+ }
43
+ /**
44
+ * Parse a duration suffix into a multiplier relative to a quarter note.
45
+ *
46
+ * ":w" → 4.0 (whole), ":h" → 2.0 (half), ":q" → 1.0 (quarter),
47
+ * ":e" → 0.5 (eighth), ":s" → 0.25 (sixteenth).
48
+ * Default (no suffix) → 1.0 (quarter).
49
+ */
50
+ export function parseDuration(suffix) {
51
+ const mult = DURATION_MAP[suffix];
52
+ if (mult === undefined) {
53
+ throw new Error(`Unknown duration suffix: "${suffix}"`);
54
+ }
55
+ return mult;
56
+ }
57
+ /**
58
+ * Convert a BPM tempo + duration multiplier → milliseconds.
59
+ *
60
+ * At 120 BPM, a quarter note = 500ms.
61
+ * A half note at 120 BPM = 1000ms.
62
+ */
63
+ export function durationToMs(multiplier, bpm) {
64
+ const quarterMs = 60_000 / bpm;
65
+ return quarterMs * multiplier;
66
+ }
67
+ /**
68
+ * Parse a single note token like "C4:q" or "R:h" into a MidiNote.
69
+ *
70
+ * @param token - e.g. "C4:q", "F#5:e", "Bb3:h", "R:q"
71
+ * @param bpm - tempo in beats per minute
72
+ * @param channel - MIDI channel (0-15)
73
+ * @param velocity - MIDI velocity (0-127)
74
+ */
75
+ export function parseNoteToken(token, bpm, channel = 0, velocity = 80) {
76
+ const trimmed = token.trim();
77
+ const parts = trimmed.split(":");
78
+ const noteStr = parts[0];
79
+ const durationSuffix = parts[1] ?? "q"; // default to quarter note
80
+ const midi = parseNoteToMidi(noteStr);
81
+ const mult = parseDuration(durationSuffix);
82
+ const durationMs = durationToMs(mult, bpm);
83
+ return {
84
+ note: midi,
85
+ velocity: midi === -1 ? 0 : velocity, // rests have 0 velocity
86
+ durationMs,
87
+ channel,
88
+ };
89
+ }
90
+ /**
91
+ * Parse a hand string like "C4:q E4:q G4:q" into an array of Beats.
92
+ *
93
+ * Each space-separated token becomes a sequential beat.
94
+ * For chords (simultaneous notes), we'd need a different notation,
95
+ * but the current ai-music-sheets format treats each token as sequential.
96
+ */
97
+ export function parseHandString(handStr, hand, bpm, channel = 0, velocity = 80) {
98
+ if (!handStr || handStr.trim() === "")
99
+ return [];
100
+ const tokens = handStr.trim().split(/\s+/);
101
+ return tokens.map((token) => ({
102
+ notes: [parseNoteToken(token, bpm, channel, velocity)],
103
+ hand,
104
+ }));
105
+ }
106
+ /**
107
+ * Parse a full Measure into a PlayableMeasure with MIDI-ready beats.
108
+ */
109
+ export function parseMeasure(measure, bpm, velocity = 80) {
110
+ return {
111
+ source: measure,
112
+ rightBeats: parseHandString(measure.rightHand, "right", bpm, 0, velocity),
113
+ leftBeats: parseHandString(measure.leftHand, "left", bpm, 0, velocity),
114
+ };
115
+ }
116
+ /**
117
+ * Safely parse a note token — returns null on error instead of throwing.
118
+ * Collects a warning in the provided array when parsing fails.
119
+ */
120
+ export function safeParseNoteToken(token, bpm, location, warnings, channel = 0, velocity = 80) {
121
+ try {
122
+ return parseNoteToken(token, bpm, channel, velocity);
123
+ }
124
+ catch (err) {
125
+ warnings.push({
126
+ location,
127
+ token,
128
+ message: err instanceof Error ? err.message : String(err),
129
+ });
130
+ return null;
131
+ }
132
+ }
133
+ /**
134
+ * Safe version of parseHandString — skips bad tokens, collects warnings.
135
+ */
136
+ export function safeParseHandString(handStr, hand, bpm, measureNumber, warnings, channel = 0, velocity = 80) {
137
+ if (!handStr || handStr.trim() === "")
138
+ return [];
139
+ const tokens = handStr.trim().split(/\s+/);
140
+ const beats = [];
141
+ for (const token of tokens) {
142
+ const note = safeParseNoteToken(token, bpm, `measure ${measureNumber} ${hand} hand`, warnings, channel, velocity);
143
+ if (note) {
144
+ beats.push({ notes: [note], hand });
145
+ }
146
+ }
147
+ return beats;
148
+ }
149
+ /**
150
+ * Safe version of parseMeasure — skips bad notes, collects warnings.
151
+ */
152
+ export function safeParseMeasure(measure, bpm, warnings, velocity = 80) {
153
+ return {
154
+ source: measure,
155
+ rightBeats: safeParseHandString(measure.rightHand, "right", bpm, measure.number, warnings, 0, velocity),
156
+ leftBeats: safeParseHandString(measure.leftHand, "left", bpm, measure.number, warnings, 0, velocity),
157
+ };
158
+ }
159
+ /**
160
+ * Convert a MIDI note number back to a note name (for display).
161
+ *
162
+ * 60 → "C4", 69 → "A4", 78 → "F#5"
163
+ */
164
+ export function midiToNoteName(midi) {
165
+ if (midi < 0)
166
+ return "R";
167
+ const noteNames = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
168
+ const octave = Math.floor(midi / 12) - 1;
169
+ const noteIndex = midi % 12;
170
+ return `${noteNames[noteIndex]}${octave}`;
171
+ }
172
+ /** Solfege mapping: note name (with optional accidental) → solfege syllable. */
173
+ const SOLFEGE_MAP = {
174
+ C: "Do", "C#": "Di", Db: "Ra",
175
+ D: "Re", "D#": "Ri", Eb: "Me",
176
+ E: "Mi",
177
+ F: "Fa", "F#": "Fi", Gb: "Se",
178
+ G: "Sol", "G#": "Si", Ab: "Le",
179
+ A: "La", "A#": "Li", Bb: "Te",
180
+ B: "Ti",
181
+ };
182
+ /**
183
+ * Convert a single note string (e.g. "C4", "F#5") to a singable syllable.
184
+ *
185
+ * - "note-names": "C", "F sharp", "B flat"
186
+ * - "solfege": "Do", "Fi", "Te"
187
+ * - "syllables": always "da"
188
+ * - "contour": falls back to letter (contour handled at measure level)
189
+ */
190
+ export function noteToSingable(noteStr, mode) {
191
+ const trimmed = noteStr.trim();
192
+ if (trimmed === "R" || trimmed === "r")
193
+ return mode === "syllables" ? "..." : "rest";
194
+ const match = trimmed.match(/^([A-Ga-g])(#|b)?(\d)?$/);
195
+ if (!match)
196
+ return trimmed; // pass through unparseable
197
+ const [, letter, accidental] = match;
198
+ const upperLetter = letter.toUpperCase();
199
+ switch (mode) {
200
+ case "note-names": {
201
+ const acc = accidental === "#" ? " sharp" : accidental === "b" ? " flat" : "";
202
+ return `${upperLetter}${acc}`;
203
+ }
204
+ case "solfege": {
205
+ const key = accidental ? `${upperLetter}${accidental}` : upperLetter;
206
+ return SOLFEGE_MAP[key] ?? upperLetter;
207
+ }
208
+ case "syllables":
209
+ return "da";
210
+ case "contour":
211
+ return upperLetter; // fallback — contour handled at measure level
212
+ }
213
+ }
214
+ /**
215
+ * Convert a hand string (e.g. "C4:q E4:q G4:q") to singable text.
216
+ *
217
+ * For "contour" mode, compares consecutive MIDI pitches and returns
218
+ * direction words: "up", "down", "same".
219
+ *
220
+ * For other modes, maps each note token through noteToSingable and
221
+ * joins with "... " (speech pause).
222
+ */
223
+ export function handToSingableText(handStr, mode) {
224
+ if (!handStr || handStr.trim() === "")
225
+ return "";
226
+ const tokens = handStr.trim().split(/\s+/);
227
+ if (mode === "contour") {
228
+ if (tokens.length <= 1)
229
+ return "hold";
230
+ const midis = tokens.map((t) => {
231
+ const noteStr = t.split(":")[0];
232
+ try {
233
+ return parseNoteToMidi(noteStr);
234
+ }
235
+ catch {
236
+ return -1;
237
+ }
238
+ });
239
+ const dirs = [];
240
+ for (let i = 1; i < midis.length; i++) {
241
+ if (midis[i] < 0 || midis[i - 1] < 0) {
242
+ dirs.push("rest");
243
+ continue;
244
+ }
245
+ const diff = midis[i] - midis[i - 1];
246
+ dirs.push(diff > 0 ? "up" : diff < 0 ? "down" : "same");
247
+ }
248
+ return dirs.join("... ");
249
+ }
250
+ // note-names, solfege, syllables
251
+ const syllables = tokens.map((t) => {
252
+ const noteStr = t.split(":")[0];
253
+ return noteToSingable(noteStr, mode);
254
+ });
255
+ return syllables.join("... ");
256
+ }
257
+ /**
258
+ * Convert a Measure to singable text for voice narration.
259
+ *
260
+ * Returns the singable syllables for the specified hand(s).
261
+ * When hand="both", right hand text comes first, then "Left hand:" prefix.
262
+ */
263
+ export function measureToSingableText(measure, options) {
264
+ const { mode, hand } = options;
265
+ if (hand === "right")
266
+ return handToSingableText(measure.rightHand, mode);
267
+ if (hand === "left")
268
+ return handToSingableText(measure.leftHand, mode);
269
+ // both
270
+ const right = handToSingableText(measure.rightHand, mode);
271
+ const left = handToSingableText(measure.leftHand, mode);
272
+ if (!left)
273
+ return right;
274
+ if (!right)
275
+ return left;
276
+ return `${right}. Left hand: ${left}`;
277
+ }
278
+ //# sourceMappingURL=note-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"note-parser.js","sourceRoot":"","sources":["../src/note-parser.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,EAAE;AACF,qEAAqE;AACrE,qDAAqD;AACrD,gFAAgF;AAEhF,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAIxD;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAAC,OAAe;IAC7C,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IAE/B,IAAI,OAAO,KAAK,GAAG,IAAI,OAAO,KAAK,GAAG;QAAE,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO;IAE1D,+CAA+C;IAC/C,iCAAiC;IACjC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,wBAAwB,CAAC,CAAC;IACtD,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,kBAAkB,OAAO,GAAG,CAAC,CAAC;IAChD,CAAC;IAED,MAAM,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,CAAC,GAAG,KAAK,CAAC;IAChD,MAAM,IAAI,GAAG,YAAY,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;IAChD,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,yBAAyB,MAAM,GAAG,CAAC,CAAC;IACtD,CAAC;IAED,MAAM,MAAM,GAAG,QAAQ,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IACvC,IAAI,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;IAEpC,IAAI,UAAU,KAAK,GAAG;QAAE,IAAI,IAAI,CAAC,CAAC;IAClC,IAAI,UAAU,KAAK,GAAG;QAAE,IAAI,IAAI,CAAC,CAAC;IAElC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,GAAG,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,2BAA2B,IAAI,WAAW,OAAO,IAAI,CAAC,CAAC;IACzE,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,MAAc;IAC1C,MAAM,IAAI,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IAClC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,6BAA6B,MAAM,GAAG,CAAC,CAAC;IAC1D,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,UAAkB,EAAE,GAAW;IAC1D,MAAM,SAAS,GAAG,MAAM,GAAG,GAAG,CAAC;IAC/B,OAAO,SAAS,GAAG,UAAU,CAAC;AAChC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAC5B,KAAa,EACb,GAAW,EACX,OAAO,GAAG,CAAC,EACX,QAAQ,GAAG,EAAE;IAEb,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAEjC,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IACzB,MAAM,cAAc,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,0BAA0B;IAElE,MAAM,IAAI,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;IACtC,MAAM,IAAI,GAAG,aAAa,CAAC,cAAc,CAAC,CAAC;IAC3C,MAAM,UAAU,GAAG,YAAY,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAE3C,OAAO;QACL,IAAI,EAAE,IAAI;QACV,QAAQ,EAAE,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,wBAAwB;QAC9D,UAAU;QACV,OAAO;KACR,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAC7B,OAAe,EACf,IAAsB,EACtB,GAAW,EACX,OAAO,GAAG,CAAC,EACX,QAAQ,GAAG,EAAE;IAEb,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,EAAE,CAAC;IAEjD,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC3C,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QAC5B,KAAK,EAAE,CAAC,cAAc,CAAC,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;QACtD,IAAI;KACL,CAAC,CAAC,CAAC;AACN,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,YAAY,CAC1B,OAAgB,EAChB,GAAW,EACX,QAAQ,GAAG,EAAE;IAEb,OAAO;QACL,MAAM,EAAE,OAAO;QACf,UAAU,EAAE,eAAe,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,QAAQ,CAAC;QACzE,SAAS,EAAE,eAAe,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,QAAQ,CAAC;KACvE,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAChC,KAAa,EACb,GAAW,EACX,QAAgB,EAChB,QAAwB,EACxB,OAAO,GAAG,CAAC,EACX,QAAQ,GAAG,EAAE;IAEb,IAAI,CAAC;QACH,OAAO,cAAc,CAAC,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;IACvD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ;YACR,KAAK;YACL,OAAO,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;SAC1D,CAAC,CAAC;QACH,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,mBAAmB,CACjC,OAAe,EACf,IAAsB,EACtB,GAAW,EACX,aAAqB,EACrB,QAAwB,EACxB,OAAO,GAAG,CAAC,EACX,QAAQ,GAAG,EAAE;IAEb,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,EAAE,CAAC;IAEjD,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC3C,MAAM,KAAK,GAAW,EAAE,CAAC;IAEzB,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,IAAI,GAAG,kBAAkB,CAC7B,KAAK,EACL,GAAG,EACH,WAAW,aAAa,IAAI,IAAI,OAAO,EACvC,QAAQ,EACR,OAAO,EACP,QAAQ,CACT,CAAC;QACF,IAAI,IAAI,EAAE,CAAC;YACT,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;QACtC,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAC9B,OAAgB,EAChB,GAAW,EACX,QAAwB,EACxB,QAAQ,GAAG,EAAE;IAEb,OAAO;QACL,MAAM,EAAE,OAAO;QACf,UAAU,EAAE,mBAAmB,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,CAAC;QACvG,SAAS,EAAE,mBAAmB,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,CAAC;KACrG,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,IAAI,IAAI,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC;IAEzB,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;IACpF,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC;IACzC,MAAM,SAAS,GAAG,IAAI,GAAG,EAAE,CAAC;IAC5B,OAAO,GAAG,SAAS,CAAC,SAAS,CAAC,GAAG,MAAM,EAAE,CAAC;AAC5C,CAAC;AAOD,gFAAgF;AAChF,MAAM,WAAW,GAA2B;IAC1C,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI;IAC7B,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI;IAC7B,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI;IAC7B,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI;IAC9B,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI;IAC7B,CAAC,EAAE,IAAI;CACR,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,OAAe,EAAE,IAAmB;IACjE,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IAC/B,IAAI,OAAO,KAAK,GAAG,IAAI,OAAO,KAAK,GAAG;QAAE,OAAO,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC;IAErF,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;IACvD,IAAI,CAAC,KAAK;QAAE,OAAO,OAAO,CAAC,CAAC,2BAA2B;IAEvD,MAAM,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,GAAG,KAAK,CAAC;IACrC,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;IAEzC,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,YAAY,CAAC,CAAC,CAAC;YAClB,MAAM,GAAG,GAAG,UAAU,KAAK,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,KAAK,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9E,OAAO,GAAG,WAAW,GAAG,GAAG,EAAE,CAAC;QAChC,CAAC;QACD,KAAK,SAAS,CAAC,CAAC,CAAC;YACf,MAAM,GAAG,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,WAAW,GAAG,UAAU,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC;YACrE,OAAO,WAAW,CAAC,GAAG,CAAC,IAAI,WAAW,CAAC;QACzC,CAAC;QACD,KAAK,WAAW;YACd,OAAO,IAAI,CAAC;QACd,KAAK,SAAS;YACZ,OAAO,WAAW,CAAC,CAAC,8CAA8C;IACtE,CAAC;AACH,CAAC;AAQD;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAChC,OAAe,EACf,IAAmB;IAEnB,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,EAAE,CAAC;IAEjD,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAE3C,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,IAAI,MAAM,CAAC,MAAM,IAAI,CAAC;YAAE,OAAO,MAAM,CAAC;QACtC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;YAC7B,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YAChC,IAAI,CAAC;gBACH,OAAO,eAAe,CAAC,OAAO,CAAC,CAAC;YAClC,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,CAAC,CAAC,CAAC;YACZ,CAAC;QACH,CAAC,CAAC,CAAC;QACH,MAAM,IAAI,GAAa,EAAE,CAAC;QAC1B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACtC,IAAI,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC;gBACrC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;gBAClB,SAAS;YACX,CAAC;YACD,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YACrC,IAAI,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;QAC1D,CAAC;QACD,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC3B,CAAC;IAED,iCAAiC;IACjC,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QACjC,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAChC,OAAO,cAAc,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;IACH,OAAO,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAChC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CACnC,OAAgD,EAChD,OAA6B;IAE7B,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC;IAE/B,IAAI,IAAI,KAAK,OAAO;QAAE,OAAO,kBAAkB,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IACzE,IAAI,IAAI,KAAK,MAAM;QAAE,OAAO,kBAAkB,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAEvE,OAAO;IACP,MAAM,KAAK,GAAG,kBAAkB,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IAC1D,MAAM,IAAI,GAAG,kBAAkB,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IACxD,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IACxB,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,OAAO,GAAG,KAAK,gBAAgB,IAAI,EAAE,CAAC;AACxC,CAAC"}
@@ -0,0 +1,124 @@
1
+ import type { ParsedMidi } from "../midi/types.js";
2
+ import type { VmpkConnector, TeachingHook, ProgressCallback } from "../types.js";
3
+ import type { MidiPlaybackState } from "./midi-engine.js";
4
+ /** Events emitted during playback. */
5
+ export type PlaybackEventType = "stateChange" | "noteOn" | "noteOff" | "speedChange" | "progress" | "error";
6
+ /** Payload for playback events. */
7
+ export interface PlaybackEvent {
8
+ type: PlaybackEventType;
9
+ /** Playback position in seconds (at speed 1.0) when this event occurred. */
10
+ positionSeconds: number;
11
+ /** Current playback state. */
12
+ state: MidiPlaybackState;
13
+ }
14
+ export interface NoteOnEvent extends PlaybackEvent {
15
+ type: "noteOn";
16
+ note: number;
17
+ noteName: string;
18
+ velocity: number;
19
+ channel: number;
20
+ duration: number;
21
+ eventIndex: number;
22
+ totalEvents: number;
23
+ }
24
+ export interface NoteOffEvent extends PlaybackEvent {
25
+ type: "noteOff";
26
+ note: number;
27
+ noteName: string;
28
+ channel: number;
29
+ }
30
+ export interface StateChangeEvent extends PlaybackEvent {
31
+ type: "stateChange";
32
+ previousState: MidiPlaybackState;
33
+ }
34
+ export interface SpeedChangeEvent extends PlaybackEvent {
35
+ type: "speedChange";
36
+ previousSpeed: number;
37
+ newSpeed: number;
38
+ }
39
+ export interface ProgressEvent extends PlaybackEvent {
40
+ type: "progress";
41
+ ratio: number;
42
+ percent: string;
43
+ eventsPlayed: number;
44
+ totalEvents: number;
45
+ elapsedMs: number;
46
+ }
47
+ export interface ErrorEvent extends PlaybackEvent {
48
+ type: "error";
49
+ error: Error;
50
+ }
51
+ /** Union of all event types. */
52
+ export type AnyPlaybackEvent = NoteOnEvent | NoteOffEvent | StateChangeEvent | SpeedChangeEvent | ProgressEvent | ErrorEvent;
53
+ /** Listener callback. */
54
+ export type PlaybackListener = (event: AnyPlaybackEvent) => void;
55
+ export interface PlaybackControlOptions {
56
+ /** Speed multiplier (0.1–4.0). Default: 1.0. */
57
+ speed?: number;
58
+ /** Teaching hook to invoke during playback. */
59
+ teachingHook?: TeachingHook;
60
+ /** Progress callback (in addition to event-based listeners). */
61
+ onProgress?: ProgressCallback;
62
+ /** AbortSignal for external cancellation. */
63
+ signal?: AbortSignal;
64
+ }
65
+ /**
66
+ * Real-time playback controller for MIDI files.
67
+ *
68
+ * Wraps MidiPlaybackEngine with:
69
+ * - Event listeners (noteOn, noteOff, stateChange, speedChange, progress)
70
+ * - Teaching hook integration (fires at note boundaries)
71
+ * - Clean pause/resume/stop with hook notification
72
+ * - Speed change during playback with listener notification
73
+ */
74
+ export declare class PlaybackController {
75
+ private readonly connector;
76
+ readonly midi: ParsedMidi;
77
+ private engine;
78
+ private listeners;
79
+ private _teachingHook;
80
+ private _lastState;
81
+ constructor(connector: VmpkConnector, midi: ParsedMidi);
82
+ get state(): MidiPlaybackState;
83
+ get speed(): number;
84
+ get durationSeconds(): number;
85
+ get positionSeconds(): number;
86
+ get eventsPlayed(): number;
87
+ get totalEvents(): number;
88
+ /** Subscribe to a specific event type or "*" for all events. */
89
+ on(type: PlaybackEventType | "*", listener: PlaybackListener): () => void;
90
+ /** Remove a listener. */
91
+ off(type: PlaybackEventType | "*", listener: PlaybackListener): void;
92
+ /** Remove all listeners. */
93
+ removeAllListeners(): void;
94
+ private emit;
95
+ private emitStateChange;
96
+ /**
97
+ * Start or resume MIDI playback with real-time event emission.
98
+ *
99
+ * Wraps the underlying engine, intercepting noteOn/noteOff events
100
+ * to fire listeners and invoke teaching hooks at note boundaries.
101
+ */
102
+ play(options?: PlaybackControlOptions): Promise<void>;
103
+ /** Pause playback. Fires stateChange event. */
104
+ pause(): void;
105
+ /** Resume playback after pause. Fires stateChange event. */
106
+ resume(options?: PlaybackControlOptions): Promise<void>;
107
+ /** Stop playback and reset. Fires stateChange event. */
108
+ stop(): void;
109
+ /** Change playback speed. Fires speedChange event. Takes effect on next note. */
110
+ setSpeed(speed: number): void;
111
+ /** Reset to beginning. */
112
+ reset(): void;
113
+ /**
114
+ * Create a connector wrapper that intercepts noteOn/noteOff to emit events
115
+ * and invoke teaching hooks.
116
+ */
117
+ private createWrappedConnector;
118
+ }
119
+ /**
120
+ * Create a PlaybackController for a parsed MIDI file.
121
+ * Shorthand for `new PlaybackController(connector, midi)`.
122
+ */
123
+ export declare function createPlaybackController(connector: VmpkConnector, midi: ParsedMidi): PlaybackController;
124
+ //# sourceMappingURL=controls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"controls.d.ts","sourceRoot":"","sources":["../../src/playback/controls.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,252 @@
1
+ // ─── Real-Time Playback Controls ────────────────────────────────────────────
2
+ //
3
+ // Wraps MidiPlaybackEngine with an event-driven control layer.
4
+ // External systems (teaching hooks, singing, UI) subscribe to playback events
5
+ // and react in real time. All state changes flow through here.
6
+ // ─────────────────────────────────────────────────────────────────────────────
7
+ import { MidiPlaybackEngine } from "./midi-engine.js";
8
+ import { midiToNoteName } from "../note-parser.js";
9
+ // ─── PlaybackController ─────────────────────────────────────────────────────
10
+ /**
11
+ * Real-time playback controller for MIDI files.
12
+ *
13
+ * Wraps MidiPlaybackEngine with:
14
+ * - Event listeners (noteOn, noteOff, stateChange, speedChange, progress)
15
+ * - Teaching hook integration (fires at note boundaries)
16
+ * - Clean pause/resume/stop with hook notification
17
+ * - Speed change during playback with listener notification
18
+ */
19
+ export class PlaybackController {
20
+ connector;
21
+ midi;
22
+ engine;
23
+ listeners = new Map();
24
+ _teachingHook = null;
25
+ _lastState = "idle";
26
+ constructor(connector, midi) {
27
+ this.connector = connector;
28
+ this.midi = midi;
29
+ this.engine = new MidiPlaybackEngine(connector, midi);
30
+ }
31
+ // ─── State Accessors ────────────────────────────────────────────────────
32
+ get state() { return this.engine.state; }
33
+ get speed() { return this.engine.speed; }
34
+ get durationSeconds() { return this.engine.durationSeconds; }
35
+ get positionSeconds() { return this.engine.positionSeconds; }
36
+ get eventsPlayed() { return this.engine.eventsPlayed; }
37
+ get totalEvents() { return this.engine.totalEvents; }
38
+ // ─── Event System ───────────────────────────────────────────────────────
39
+ /** Subscribe to a specific event type or "*" for all events. */
40
+ on(type, listener) {
41
+ if (!this.listeners.has(type)) {
42
+ this.listeners.set(type, new Set());
43
+ }
44
+ this.listeners.get(type).add(listener);
45
+ // Return unsubscribe function
46
+ return () => {
47
+ this.listeners.get(type)?.delete(listener);
48
+ };
49
+ }
50
+ /** Remove a listener. */
51
+ off(type, listener) {
52
+ this.listeners.get(type)?.delete(listener);
53
+ }
54
+ /** Remove all listeners. */
55
+ removeAllListeners() {
56
+ this.listeners.clear();
57
+ }
58
+ emit(event) {
59
+ // Fire type-specific listeners
60
+ const typeListeners = this.listeners.get(event.type);
61
+ if (typeListeners) {
62
+ for (const fn of typeListeners) {
63
+ try {
64
+ fn(event);
65
+ }
66
+ catch { /* listener errors don't break playback */ }
67
+ }
68
+ }
69
+ // Fire wildcard listeners
70
+ const allListeners = this.listeners.get("*");
71
+ if (allListeners) {
72
+ for (const fn of allListeners) {
73
+ try {
74
+ fn(event);
75
+ }
76
+ catch { /* listener errors don't break playback */ }
77
+ }
78
+ }
79
+ }
80
+ emitStateChange(previousState) {
81
+ if (this.engine.state === previousState)
82
+ return;
83
+ this.emit({
84
+ type: "stateChange",
85
+ state: this.engine.state,
86
+ previousState,
87
+ positionSeconds: this.engine.positionSeconds,
88
+ });
89
+ this._lastState = this.engine.state;
90
+ }
91
+ // ─── Playback Controls ──────────────────────────────────────────────────
92
+ /**
93
+ * Start or resume MIDI playback with real-time event emission.
94
+ *
95
+ * Wraps the underlying engine, intercepting noteOn/noteOff events
96
+ * to fire listeners and invoke teaching hooks at note boundaries.
97
+ */
98
+ async play(options = {}) {
99
+ const previousState = this.engine.state;
100
+ this._teachingHook = options.teachingHook ?? null;
101
+ // Wrap the connector to intercept note events
102
+ const wrappedConnector = this.createWrappedConnector();
103
+ // Swap the engine to use the wrapped connector (recreate)
104
+ this.engine = new MidiPlaybackEngine(wrappedConnector, this.midi);
105
+ // Emit state change
106
+ const onProgress = (p) => {
107
+ this.emit({
108
+ type: "progress",
109
+ state: this.engine.state,
110
+ positionSeconds: this.engine.positionSeconds,
111
+ ratio: p.ratio,
112
+ percent: p.percent,
113
+ eventsPlayed: p.currentMeasure,
114
+ totalEvents: p.totalMeasures,
115
+ elapsedMs: p.elapsedMs,
116
+ });
117
+ options.onProgress?.(p);
118
+ };
119
+ this.emitStateChange(previousState);
120
+ try {
121
+ await this.engine.play({
122
+ speed: options.speed,
123
+ onProgress,
124
+ signal: options.signal,
125
+ });
126
+ }
127
+ catch (err) {
128
+ this.emit({
129
+ type: "error",
130
+ state: this.engine.state,
131
+ positionSeconds: this.engine.positionSeconds,
132
+ error: err instanceof Error ? err : new Error(String(err)),
133
+ });
134
+ throw err;
135
+ }
136
+ finally {
137
+ this.emitStateChange(this._lastState);
138
+ // Notify teaching hook of completion
139
+ if (this._teachingHook && this.engine.state === "finished") {
140
+ try {
141
+ await this._teachingHook.onSongComplete(this.engine.eventsPlayed, this.midi.trackNames[0] ?? "MIDI file");
142
+ }
143
+ catch { /* hook errors don't break playback */ }
144
+ }
145
+ }
146
+ }
147
+ /** Pause playback. Fires stateChange event. */
148
+ pause() {
149
+ const prev = this.engine.state;
150
+ this.engine.pause();
151
+ this.emitStateChange(prev);
152
+ }
153
+ /** Resume playback after pause. Fires stateChange event. */
154
+ async resume(options = {}) {
155
+ if (this.engine.state !== "paused")
156
+ return;
157
+ const prev = this.engine.state;
158
+ this.emitStateChange(prev);
159
+ await this.engine.resume({
160
+ speed: options.speed,
161
+ onProgress: options.onProgress,
162
+ signal: options.signal,
163
+ });
164
+ this.emitStateChange(this._lastState);
165
+ }
166
+ /** Stop playback and reset. Fires stateChange event. */
167
+ stop() {
168
+ const prev = this.engine.state;
169
+ this.engine.stop();
170
+ this.emitStateChange(prev);
171
+ }
172
+ /** Change playback speed. Fires speedChange event. Takes effect on next note. */
173
+ setSpeed(speed) {
174
+ const prev = this.engine.speed;
175
+ this.engine.setSpeed(speed);
176
+ this.emit({
177
+ type: "speedChange",
178
+ state: this.engine.state,
179
+ positionSeconds: this.engine.positionSeconds,
180
+ previousSpeed: prev,
181
+ newSpeed: speed,
182
+ });
183
+ }
184
+ /** Reset to beginning. */
185
+ reset() {
186
+ const prev = this.engine.state;
187
+ this.engine.reset();
188
+ this.emitStateChange(prev);
189
+ }
190
+ // ─── Internal ─────────────────────────────────────────────────────────
191
+ /**
192
+ * Create a connector wrapper that intercepts noteOn/noteOff to emit events
193
+ * and invoke teaching hooks.
194
+ */
195
+ createWrappedConnector() {
196
+ const self = this;
197
+ const inner = this.connector;
198
+ let noteIndex = 0;
199
+ return {
200
+ connect: () => inner.connect(),
201
+ disconnect: () => inner.disconnect(),
202
+ status: () => inner.status(),
203
+ listPorts: () => inner.listPorts(),
204
+ noteOn(note, velocity, channel) {
205
+ inner.noteOn(note, velocity, channel);
206
+ const ch = channel ?? 0;
207
+ const event = {
208
+ type: "noteOn",
209
+ state: self.engine.state,
210
+ positionSeconds: self.engine.positionSeconds,
211
+ note,
212
+ noteName: midiToNoteName(note),
213
+ velocity,
214
+ channel: ch,
215
+ duration: 0, // filled by engine scheduling
216
+ eventIndex: noteIndex++,
217
+ totalEvents: self.midi.events.length,
218
+ };
219
+ self.emit(event);
220
+ // Fire teaching hook (non-blocking — don't await)
221
+ if (self._teachingHook) {
222
+ const noteName = midiToNoteName(note);
223
+ self._teachingHook.onMeasureStart(noteIndex, // use event index as measure proxy for MIDI files
224
+ `Note: ${noteName} (${note}) vel=${velocity}`, undefined).catch(() => { });
225
+ }
226
+ },
227
+ noteOff(note, channel) {
228
+ inner.noteOff(note, channel);
229
+ self.emit({
230
+ type: "noteOff",
231
+ state: self.engine.state,
232
+ positionSeconds: self.engine.positionSeconds,
233
+ note,
234
+ noteName: midiToNoteName(note),
235
+ channel: channel ?? 0,
236
+ });
237
+ },
238
+ allNotesOff(channel) {
239
+ inner.allNotesOff(channel);
240
+ },
241
+ playNote: (midiNote) => inner.playNote(midiNote),
242
+ };
243
+ }
244
+ }
245
+ /**
246
+ * Create a PlaybackController for a parsed MIDI file.
247
+ * Shorthand for `new PlaybackController(connector, midi)`.
248
+ */
249
+ export function createPlaybackController(connector, midi) {
250
+ return new PlaybackController(connector, midi);
251
+ }
252
+ //# sourceMappingURL=controls.js.map