@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,259 @@
1
+ // ─── Live MIDI Teaching Feedback (Position-Aware) ───────────────────────────
2
+ //
3
+ // Position-aware teaching hook that uses the PositionTracker to provide
4
+ // measure-level feedback during MIDI playback. Unlike the basic
5
+ // createMidiFeedbackHook (which reacts to individual note events), this
6
+ // hook operates at the measure level — detecting patterns, phrasing,
7
+ // section transitions, and difficulty ahead of time.
8
+ //
9
+ // This is what makes the teacher feel like they're sitting next to you:
10
+ // they see what's coming and comment on it before you get there.
11
+ // ─────────────────────────────────────────────────────────────────────────────
12
+ import { PositionTracker } from "../playback/position.js";
13
+ import { midiToNoteName } from "../note-parser.js";
14
+ // ─── Pattern Detection ──────────────────────────────────────────────────────
15
+ /** Detect if a measure has a large range (hands spread wide). */
16
+ function detectWideRange(minNote, maxNote) {
17
+ const range = maxNote - minNote;
18
+ if (range >= 24)
19
+ return `Wide range — ${midiToNoteName(minNote)} to ${midiToNoteName(maxNote)} (${range} semitones). Stretch those hands.`;
20
+ if (range >= 18)
21
+ return `Spread out: ${midiToNoteName(minNote)} to ${midiToNoteName(maxNote)}. Keep your wrists relaxed.`;
22
+ return null;
23
+ }
24
+ /** Detect sudden velocity shift compared to previous measure. */
25
+ function detectVelocityShift(prevAvg, currAvg) {
26
+ const diff = currAvg - prevAvg;
27
+ if (diff > 30)
28
+ return "Sudden jump in volume — lean into it with confidence.";
29
+ if (diff < -30)
30
+ return "Dropping to a whisper — lighten your touch gradually.";
31
+ return null;
32
+ }
33
+ /** Detect dense measure (lots of notes). */
34
+ function detectDensity(noteCount) {
35
+ if (noteCount >= 12)
36
+ return `Busy passage ahead — ${noteCount} notes in this measure. Stay steady.`;
37
+ if (noteCount >= 8)
38
+ return `Active measure — ${noteCount} notes. Keep the tempo even.`;
39
+ return null;
40
+ }
41
+ /** Detect sparse measure (breathing room). */
42
+ function detectSparse(noteCount, prevCount) {
43
+ if (noteCount <= 2 && prevCount >= 6)
44
+ return "Space to breathe — let the notes ring.";
45
+ return null;
46
+ }
47
+ /** Detect if we're approaching a section boundary (long gap between measures). */
48
+ function detectSectionBoundary(tracker, measure) {
49
+ if (measure >= tracker.totalMeasures)
50
+ return null;
51
+ const currEvents = tracker.eventsInMeasure(measure);
52
+ const nextEvents = tracker.eventsInMeasure(measure + 1);
53
+ if (currEvents.length === 0 || nextEvents.length === 0)
54
+ return null;
55
+ const lastNoteEnd = Math.max(...currEvents.map((e) => e.time + e.duration));
56
+ const nextNoteStart = Math.min(...nextEvents.map((e) => e.time));
57
+ const gap = nextNoteStart - lastNoteEnd;
58
+ // A full measure's rest or more suggests a section boundary
59
+ if (gap > 2.0)
60
+ return "Natural break coming up — good place to breathe.";
61
+ return null;
62
+ }
63
+ // ─── Encouragement Phrases ──────────────────────────────────────────────────
64
+ const MEASURE_ENCOURAGEMENTS = [
65
+ "Great flow!",
66
+ "Keep that steady pulse!",
67
+ "Nice touch!",
68
+ "Solid rhythm!",
69
+ "Beautiful!",
70
+ "You're in the groove!",
71
+ "Smooth!",
72
+ "That's the way!",
73
+ ];
74
+ const MILESTONE_MESSAGES = {
75
+ 25: "Quarter of the way through — you're doing great!",
76
+ 50: "Halfway there — keep the energy up!",
77
+ 75: "Three quarters done — bring it home!",
78
+ };
79
+ // ─── Hook ───────────────────────────────────────────────────────────────────
80
+ /**
81
+ * Create a position-aware live feedback hook for MIDI playback.
82
+ *
83
+ * Uses PositionTracker to analyse measure context before each measure plays.
84
+ * Provides: range warnings, velocity shift detection, density alerts,
85
+ * section boundary markers, milestone announcements, periodic encouragement.
86
+ *
87
+ * Designed to be composed with createSingOnMidiHook for the full experience.
88
+ */
89
+ export function createLiveMidiFeedbackHook(voiceSink, asideSink, midi, options = {}) {
90
+ const { voiceInterval = 8, warnOnWideRange = true, warnOnVelocityShift = true, warnOnDensity = true, detectSections = true, announceMilestones = true, voice, speechSpeed = 1.0, } = options;
91
+ const tracker = new PositionTracker(midi);
92
+ const voiceDirectives = [];
93
+ const asideDirectives = [];
94
+ // Pre-compute measure summaries for lookahead
95
+ const measureSummaries = new Map();
96
+ for (let m = 1; m <= tracker.totalMeasures; m++) {
97
+ measureSummaries.set(m, tracker.measureSummary(m));
98
+ }
99
+ let lastMeasureAnnounced = 0;
100
+ let measureCounter = 0;
101
+ let lastAnnouncedMilestone = 0;
102
+ let prevMeasureSummary = null;
103
+ async function emitVoice(directive) {
104
+ voiceDirectives.push(directive);
105
+ await voiceSink(directive);
106
+ }
107
+ async function emitAside(directive) {
108
+ asideDirectives.push(directive);
109
+ await asideSink(directive);
110
+ }
111
+ return {
112
+ voiceDirectives,
113
+ asideDirectives,
114
+ tracker,
115
+ async onMeasureStart(eventIndex, _teachingNote, _dynamics) {
116
+ // Map event index to measure number via position tracker
117
+ const event = midi.events[eventIndex - 1];
118
+ if (!event)
119
+ return;
120
+ const snapshot = tracker.snapshotAt(event.time);
121
+ const currentMeasure = snapshot.measure;
122
+ // Only process each measure once
123
+ if (currentMeasure === lastMeasureAnnounced)
124
+ return;
125
+ lastMeasureAnnounced = currentMeasure;
126
+ measureCounter++;
127
+ const summary = measureSummaries.get(currentMeasure);
128
+ if (!summary || summary.noteCount === 0)
129
+ return;
130
+ // ── Wide range warning ──
131
+ if (warnOnWideRange) {
132
+ const rangeWarning = detectWideRange(summary.minNote, summary.maxNote);
133
+ if (rangeWarning) {
134
+ await emitAside({
135
+ text: `Measure ${currentMeasure}: ${rangeWarning}`,
136
+ priority: "med",
137
+ reason: "difficulty-warning",
138
+ source: `measure-${currentMeasure}`,
139
+ tags: ["piano-teacher", "live-feedback", "range"],
140
+ });
141
+ }
142
+ }
143
+ // ── Velocity shift ──
144
+ if (warnOnVelocityShift && prevMeasureSummary && prevMeasureSummary.noteCount > 0) {
145
+ const shift = detectVelocityShift(prevMeasureSummary.avgVelocity, summary.avgVelocity);
146
+ if (shift) {
147
+ await emitAside({
148
+ text: `Measure ${currentMeasure}: ${shift}`,
149
+ priority: "low",
150
+ reason: "dynamics-change",
151
+ source: `measure-${currentMeasure}`,
152
+ tags: ["piano-teacher", "live-feedback", "dynamics"],
153
+ });
154
+ }
155
+ }
156
+ // ── Density warning ──
157
+ if (warnOnDensity) {
158
+ const densityWarning = detectDensity(summary.noteCount);
159
+ if (densityWarning) {
160
+ await emitAside({
161
+ text: `Measure ${currentMeasure}: ${densityWarning}`,
162
+ priority: "low",
163
+ reason: "difficulty-warning",
164
+ source: `measure-${currentMeasure}`,
165
+ tags: ["piano-teacher", "live-feedback", "density"],
166
+ });
167
+ }
168
+ if (prevMeasureSummary) {
169
+ const sparseMsg = detectSparse(summary.noteCount, prevMeasureSummary.noteCount);
170
+ if (sparseMsg) {
171
+ await emitAside({
172
+ text: `Measure ${currentMeasure}: ${sparseMsg}`,
173
+ priority: "low",
174
+ reason: "style-tip",
175
+ source: `measure-${currentMeasure}`,
176
+ tags: ["piano-teacher", "live-feedback", "phrasing"],
177
+ });
178
+ }
179
+ }
180
+ }
181
+ // ── Section boundary ──
182
+ if (detectSections) {
183
+ const boundary = detectSectionBoundary(tracker, currentMeasure);
184
+ if (boundary) {
185
+ await emitAside({
186
+ text: `Measure ${currentMeasure}: ${boundary}`,
187
+ priority: "low",
188
+ reason: "style-tip",
189
+ source: `measure-${currentMeasure}`,
190
+ tags: ["piano-teacher", "live-feedback", "section"],
191
+ });
192
+ }
193
+ }
194
+ // ── Milestone announcements ──
195
+ if (announceMilestones && tracker.totalMeasures > 0) {
196
+ const percent = Math.round((currentMeasure / tracker.totalMeasures) * 100);
197
+ for (const [milestone, msg] of Object.entries(MILESTONE_MESSAGES)) {
198
+ const ms = Number(milestone);
199
+ if (percent >= ms && lastAnnouncedMilestone < ms) {
200
+ lastAnnouncedMilestone = ms;
201
+ await emitVoice({
202
+ text: msg,
203
+ voice,
204
+ speed: speechSpeed,
205
+ blocking: false,
206
+ });
207
+ }
208
+ }
209
+ }
210
+ // ── Periodic encouragement ──
211
+ if (voiceInterval > 0 && measureCounter % voiceInterval === 0) {
212
+ const phrase = MEASURE_ENCOURAGEMENTS[measureCounter % MEASURE_ENCOURAGEMENTS.length];
213
+ await emitVoice({
214
+ text: phrase,
215
+ voice,
216
+ speed: speechSpeed,
217
+ blocking: false,
218
+ });
219
+ }
220
+ prevMeasureSummary = summary;
221
+ },
222
+ async onKeyMoment(moment) {
223
+ await emitVoice({
224
+ text: moment,
225
+ voice,
226
+ speed: speechSpeed,
227
+ blocking: false,
228
+ });
229
+ },
230
+ async onSongComplete(eventsPlayed, songTitle) {
231
+ const durationMin = Math.round(midi.durationSeconds / 60);
232
+ const timeStr = durationMin > 0
233
+ ? `${durationMin} minute${durationMin !== 1 ? "s" : ""}`
234
+ : "less than a minute";
235
+ await emitVoice({
236
+ text: `Excellent work on ${songTitle}! ${eventsPlayed} notes across ${tracker.totalMeasures} measures in ${timeStr}.`,
237
+ voice,
238
+ speed: speechSpeed,
239
+ blocking: false,
240
+ });
241
+ await emitAside({
242
+ text: `Session complete: ${songTitle} — ${tracker.totalMeasures} measures, ${eventsPlayed} notes at ${midi.bpm} BPM.`,
243
+ priority: "low",
244
+ reason: "session-complete",
245
+ tags: ["piano-teacher", "live-feedback", "completion"],
246
+ });
247
+ },
248
+ async push(interjection) {
249
+ await emitAside({
250
+ text: interjection.text,
251
+ priority: interjection.priority,
252
+ reason: interjection.reason,
253
+ source: interjection.source,
254
+ tags: ["piano-teacher", "live-feedback", interjection.reason],
255
+ });
256
+ },
257
+ };
258
+ }
259
+ //# sourceMappingURL=live-midi-feedback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live-midi-feedback.js","sourceRoot":"","sources":["../../src/teaching/live-midi-feedback.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import type { ParsedMidi } from "../midi/types.js";
2
+ import type { TeachingHook, VoiceDirective, VoiceSink, AsideDirective, AsideSink, LiveFeedbackHookOptions } from "../types.js";
3
+ export interface MidiFeedbackOptions extends LiveFeedbackHookOptions {
4
+ /**
5
+ * Warn about large leaps (interval > this many semitones).
6
+ * Default: 12 (one octave).
7
+ */
8
+ leapWarnSemitones?: number;
9
+ /**
10
+ * Warn about dense chords (more notes than this at once).
11
+ * Default: 4.
12
+ */
13
+ denseChordThreshold?: number;
14
+ }
15
+ /**
16
+ * Create a live feedback hook for MIDI file playback.
17
+ *
18
+ * Analyses the raw MIDI events to provide:
19
+ * - Voice encouragement every N notes
20
+ * - Aside tips on dynamics changes (velocity zones)
21
+ * - Aside warnings for wide leaps and dense chords
22
+ *
23
+ * @param voiceSink - Voice output callback.
24
+ * @param asideSink - Aside output callback.
25
+ * @param midi - Parsed MIDI file data.
26
+ * @param options - Configuration.
27
+ * @returns Teaching hook with recorded directives for inspection.
28
+ */
29
+ export declare function createMidiFeedbackHook(voiceSink: VoiceSink, asideSink: AsideSink, midi: ParsedMidi, options?: MidiFeedbackOptions): TeachingHook & {
30
+ voiceDirectives: VoiceDirective[];
31
+ asideDirectives: AsideDirective[];
32
+ };
33
+ //# sourceMappingURL=midi-feedback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"midi-feedback.d.ts","sourceRoot":"","sources":["../../src/teaching/midi-feedback.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,UAAU,EAAiB,MAAM,kBAAkB,CAAC;AAClE,OAAO,KAAK,EACV,YAAY,EACZ,cAAc,EACd,SAAS,EACT,cAAc,EACd,SAAS,EACT,uBAAuB,EACxB,MAAM,aAAa,CAAC;AAuCrB,MAAM,WAAW,mBAAoB,SAAQ,uBAAuB;IAClE;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAID;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,SAAS,EACpB,SAAS,EAAE,SAAS,EACpB,IAAI,EAAE,UAAU,EAChB,OAAO,GAAE,mBAAwB,GAChC,YAAY,GAAG;IAAE,eAAe,EAAE,cAAc,EAAE,CAAC;IAAC,eAAe,EAAE,cAAc,EAAE,CAAA;CAAE,CAqKzF"}
@@ -0,0 +1,208 @@
1
+ // ─── Live Feedback for MIDI Files ───────────────────────────────────────────
2
+ //
3
+ // Teaching hook that provides real-time encouragement and context-aware
4
+ // asides during MIDI file playback. Unlike the library feedback hook (which
5
+ // reads SongEntry teaching notes and dynamics), this analyses the raw MIDI
6
+ // event stream: velocity shifts, wide leaps, chord density, tempo changes.
7
+ //
8
+ // Designed to work with PlaybackController's event system.
9
+ // ─────────────────────────────────────────────────────────────────────────────
10
+ import { midiToNoteName } from "../note-parser.js";
11
+ // ─── Analysis Helpers ───────────────────────────────────────────────────────
12
+ /** Detect velocity zone: pp, p, mp, mf, f, ff based on MIDI velocity. */
13
+ function velocityToDynamic(velocity) {
14
+ if (velocity < 20)
15
+ return "pp";
16
+ if (velocity < 45)
17
+ return "p";
18
+ if (velocity < 65)
19
+ return "mp";
20
+ if (velocity < 85)
21
+ return "mf";
22
+ if (velocity < 105)
23
+ return "f";
24
+ return "ff";
25
+ }
26
+ /** Dynamics-aware tips (same as library hook). */
27
+ const DYNAMICS_TIPS = {
28
+ pp: "Very soft passage — light touch, barely pressing the keys.",
29
+ p: "Soft passage — gentle, controlled touch.",
30
+ mp: "Moderately soft — warm, balanced tone.",
31
+ mf: "Moderately loud — confident, full sound.",
32
+ f: "Loud passage — strong, powerful playing.",
33
+ ff: "Very loud — give it full energy!",
34
+ };
35
+ /** Encouragement phrases. */
36
+ const ENCOURAGEMENTS = [
37
+ "Keep it up!",
38
+ "Sounding great!",
39
+ "Nice rhythm!",
40
+ "Beautiful phrasing!",
41
+ "You've got this!",
42
+ "Smooth playing!",
43
+ "Lovely tone!",
44
+ "Great hands!",
45
+ ];
46
+ // ─── MIDI Feedback Hook ─────────────────────────────────────────────────────
47
+ /**
48
+ * Create a live feedback hook for MIDI file playback.
49
+ *
50
+ * Analyses the raw MIDI events to provide:
51
+ * - Voice encouragement every N notes
52
+ * - Aside tips on dynamics changes (velocity zones)
53
+ * - Aside warnings for wide leaps and dense chords
54
+ *
55
+ * @param voiceSink - Voice output callback.
56
+ * @param asideSink - Aside output callback.
57
+ * @param midi - Parsed MIDI file data.
58
+ * @param options - Configuration.
59
+ * @returns Teaching hook with recorded directives for inspection.
60
+ */
61
+ export function createMidiFeedbackHook(voiceSink, asideSink, midi, options = {}) {
62
+ const { voiceInterval = 16, encourageOnDynamics = true, warnOnDifficult = true, voice, speechSpeed = 1.0, leapWarnSemitones = 12, denseChordThreshold = 4, } = options;
63
+ const voiceDirectives = [];
64
+ const asideDirectives = [];
65
+ let noteCount = 0;
66
+ let lastDynamic = "";
67
+ let lastNote = -1;
68
+ // Pre-compute chord density map (count notes at each time cluster)
69
+ const chordDensityMap = new Map();
70
+ const events = midi.events;
71
+ if (events.length > 0) {
72
+ let clusterStart = events[0].time;
73
+ let clusterCount = 1;
74
+ for (let i = 1; i < events.length; i++) {
75
+ if (events[i].time - clusterStart < 0.01) { // 10ms threshold
76
+ clusterCount++;
77
+ }
78
+ else {
79
+ if (clusterCount >= denseChordThreshold) {
80
+ // Mark all events in this cluster
81
+ for (let j = i - clusterCount; j < i; j++) {
82
+ chordDensityMap.set(j, clusterCount);
83
+ }
84
+ }
85
+ clusterStart = events[i].time;
86
+ clusterCount = 1;
87
+ }
88
+ }
89
+ // Handle last cluster
90
+ if (clusterCount >= denseChordThreshold) {
91
+ for (let j = events.length - clusterCount; j < events.length; j++) {
92
+ chordDensityMap.set(j, clusterCount);
93
+ }
94
+ }
95
+ }
96
+ async function emitVoice(directive) {
97
+ voiceDirectives.push(directive);
98
+ await voiceSink(directive);
99
+ }
100
+ async function emitAside(directive) {
101
+ asideDirectives.push(directive);
102
+ await asideSink(directive);
103
+ }
104
+ return {
105
+ voiceDirectives,
106
+ asideDirectives,
107
+ async onMeasureStart(eventIndex, _teachingNote, _dynamics) {
108
+ noteCount++;
109
+ const midiEvent = events[eventIndex - 1]; // 1-based to 0-based
110
+ if (!midiEvent)
111
+ return;
112
+ // ── Dynamics change detection (velocity zones) ──
113
+ if (encourageOnDynamics) {
114
+ const dynamic = velocityToDynamic(midiEvent.velocity);
115
+ if (dynamic !== lastDynamic && lastDynamic !== "") {
116
+ const tip = DYNAMICS_TIPS[dynamic];
117
+ if (tip) {
118
+ await emitAside({
119
+ text: `${tip}`,
120
+ priority: "low",
121
+ reason: "dynamics-change",
122
+ source: `note-${eventIndex}`,
123
+ tags: ["piano-teacher", "midi-feedback", "dynamics"],
124
+ });
125
+ }
126
+ }
127
+ lastDynamic = dynamic;
128
+ }
129
+ // ── Wide leap warning ──
130
+ if (warnOnDifficult && lastNote >= 0) {
131
+ const interval = Math.abs(midiEvent.note - lastNote);
132
+ if (interval >= leapWarnSemitones) {
133
+ const fromName = midiToNoteName(lastNote);
134
+ const toName = midiToNoteName(midiEvent.note);
135
+ await emitAside({
136
+ text: `Big leap: ${fromName} → ${toName} (${interval} semitones). Prepare your hand position.`,
137
+ priority: "med",
138
+ reason: "difficulty-warning",
139
+ source: `note-${eventIndex}`,
140
+ tags: ["piano-teacher", "midi-feedback", "leap"],
141
+ });
142
+ }
143
+ }
144
+ lastNote = midiEvent.note;
145
+ // ── Dense chord warning (first note of a dense cluster only) ──
146
+ if (warnOnDifficult && chordDensityMap.has(eventIndex - 1)) {
147
+ const density = chordDensityMap.get(eventIndex - 1);
148
+ // Only warn on the first note of each dense cluster
149
+ const prevInSameCluster = eventIndex >= 2 &&
150
+ events[eventIndex - 2] &&
151
+ Math.abs(events[eventIndex - 2].time - midiEvent.time) < 0.01;
152
+ if (!prevInSameCluster) {
153
+ await emitAside({
154
+ text: `Dense chord ahead: ${density} notes at once. Spread your fingers.`,
155
+ priority: "med",
156
+ reason: "difficulty-warning",
157
+ source: `note-${eventIndex}`,
158
+ tags: ["piano-teacher", "midi-feedback", "chord"],
159
+ });
160
+ }
161
+ }
162
+ // ── Periodic voice encouragement ──
163
+ if (voiceInterval > 0 && noteCount % voiceInterval === 0) {
164
+ const phrase = ENCOURAGEMENTS[noteCount % ENCOURAGEMENTS.length];
165
+ await emitVoice({
166
+ text: phrase,
167
+ voice,
168
+ speed: speechSpeed,
169
+ blocking: false,
170
+ });
171
+ }
172
+ },
173
+ async onKeyMoment(moment) {
174
+ // MIDI files don't have key moments, but support the interface
175
+ await emitVoice({
176
+ text: moment,
177
+ voice,
178
+ speed: speechSpeed,
179
+ blocking: false,
180
+ });
181
+ },
182
+ async onSongComplete(eventsPlayed, songTitle) {
183
+ const durationMin = Math.round(midi.durationSeconds / 60);
184
+ await emitVoice({
185
+ text: `Great work on ${songTitle}! ${eventsPlayed} notes over ${durationMin > 0 ? `${durationMin} minute${durationMin !== 1 ? "s" : ""}` : "less than a minute"}.`,
186
+ voice,
187
+ speed: speechSpeed,
188
+ blocking: false,
189
+ });
190
+ await emitAside({
191
+ text: `Session complete: ${songTitle} — ${eventsPlayed} notes played at ${midi.bpm} BPM.`,
192
+ priority: "low",
193
+ reason: "session-complete",
194
+ tags: ["piano-teacher", "midi-feedback", "completion"],
195
+ });
196
+ },
197
+ async push(interjection) {
198
+ await emitAside({
199
+ text: interjection.text,
200
+ priority: interjection.priority,
201
+ reason: interjection.reason,
202
+ source: interjection.source,
203
+ tags: ["piano-teacher", "midi-feedback", interjection.reason],
204
+ });
205
+ },
206
+ };
207
+ }
208
+ //# sourceMappingURL=midi-feedback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"midi-feedback.js","sourceRoot":"","sources":["../../src/teaching/midi-feedback.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,77 @@
1
+ import type { ParsedMidi, MidiNoteEvent } from "../midi/types.js";
2
+ import type { TeachingHook, VoiceDirective, VoiceSink } from "../types.js";
3
+ import type { SingAlongMode } from "../note-parser.js";
4
+ /**
5
+ * Convert a MIDI note number to a singable syllable.
6
+ *
7
+ * - "note-names": "C4", "F#5", "Bb3"
8
+ * - "solfege": "Do", "Re", "Mi" (based on pitch class, not key)
9
+ * - "syllables": always "da"
10
+ * - "contour": not applicable for individual notes (returns note name)
11
+ */
12
+ export declare function midiNoteToSingable(note: number, mode: SingAlongMode): string;
13
+ /**
14
+ * Convert a cluster (chord or single note) to singable text.
15
+ *
16
+ * - Single note: just the syllable
17
+ * - Chord: syllables joined with " and " (e.g. "Do and Mi and Sol")
18
+ * - Contour mode between clusters: "up", "down", "same"
19
+ */
20
+ export declare function clusterToSingable(events: readonly MidiNoteEvent[], mode: SingAlongMode): string;
21
+ /**
22
+ * Generate contour direction between two clusters.
23
+ * Compares the highest note of each cluster.
24
+ */
25
+ export declare function contourDirection(prev: readonly MidiNoteEvent[], curr: readonly MidiNoteEvent[]): string;
26
+ /**
27
+ * Which notes to sing from each cluster:
28
+ * - "all" — sing every note (chords become "C and E and G")
29
+ * - "melody-only" — highest note per cluster (typical RH melody)
30
+ * - "harmony" — lowest note per cluster (bass line / LH)
31
+ */
32
+ export type SingVoiceFilter = "all" | "melody-only" | "harmony";
33
+ /**
34
+ * Filter a cluster of events based on the voice filter.
35
+ * Returns a subset of events to sing.
36
+ */
37
+ export declare function filterClusterForVoice(events: readonly MidiNoteEvent[], filter: SingVoiceFilter): MidiNoteEvent[];
38
+ export interface SingOnMidiOptions {
39
+ /** Singable mode. Default: "note-names". */
40
+ mode?: SingAlongMode;
41
+ /** Voice filter: which notes to sing per cluster. Default: "all". */
42
+ voiceFilter?: SingVoiceFilter;
43
+ /** Voice preset for speech synthesis. */
44
+ voice?: string;
45
+ /** Speech speed multiplier. Default: 1.0. */
46
+ speechSpeed?: number;
47
+ /** Whether to announce cluster/beat numbers. Default: false. */
48
+ announcePosition?: boolean;
49
+ /** Max clusters to announce per batch (to avoid overwhelming). Default: 8. */
50
+ batchSize?: number;
51
+ /**
52
+ * Threshold in ms for grouping simultaneous notes into a chord.
53
+ * Default: 10ms.
54
+ */
55
+ chordThresholdMs?: number;
56
+ /** Whether the voice should block before piano plays. Default: true. */
57
+ blocking?: boolean;
58
+ }
59
+ /**
60
+ * Create a teaching hook that sings along with MIDI file playback.
61
+ *
62
+ * Pre-processes the ParsedMidi events into time clusters (chords/single notes),
63
+ * then emits singable text as each cluster is reached during playback.
64
+ *
65
+ * The hook implements TeachingHook so it can be composed with other hooks
66
+ * via composeTeachingHooks.
67
+ *
68
+ * @param sink - Voice sink to emit speech directives.
69
+ * @param midi - Parsed MIDI file data.
70
+ * @param options - Configuration.
71
+ * @returns Teaching hook with `.directives` for inspection.
72
+ */
73
+ export declare function createSingOnMidiHook(sink: VoiceSink, midi: ParsedMidi, options?: SingOnMidiOptions): TeachingHook & {
74
+ directives: VoiceDirective[];
75
+ clusters: MidiNoteEvent[][];
76
+ };
77
+ //# sourceMappingURL=sing-on-midi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sing-on-midi.d.ts","sourceRoot":"","sources":["../../src/teaching/sing-on-midi.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAClE,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAWvD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,GAAG,MAAM,CAW5E;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,SAAS,aAAa,EAAE,EAChC,IAAI,EAAE,aAAa,GAClB,MAAM,CAYR;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,SAAS,aAAa,EAAE,EAC9B,IAAI,EAAE,SAAS,aAAa,EAAE,GAC7B,MAAM,CAOR;AAID;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,aAAa,GAAG,SAAS,CAAC;AAEhE;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,SAAS,aAAa,EAAE,EAChC,MAAM,EAAE,eAAe,GACtB,aAAa,EAAE,CAmBjB;AAID,MAAM,WAAW,iBAAiB;IAChC,4CAA4C;IAC5C,IAAI,CAAC,EAAE,aAAa,CAAC;IAErB,qEAAqE;IACrE,WAAW,CAAC,EAAE,eAAe,CAAC;IAE9B,yCAAyC;IACzC,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,6CAA6C;IAC7C,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,gEAAgE;IAChE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B,wEAAwE;IACxE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAID;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,SAAS,EACf,IAAI,EAAE,UAAU,EAChB,OAAO,GAAE,iBAAsB,GAC9B,YAAY,GAAG;IAAE,UAAU,EAAE,cAAc,EAAE,CAAC;IAAC,QAAQ,EAAE,aAAa,EAAE,EAAE,CAAA;CAAE,CA2F9E"}