@greenblacksky/honmoon 0.0.0-stage → 0.4.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 (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -2
  3. package/package.json +58 -4
  4. package/src/lib/audio/context.ts +29 -0
  5. package/src/lib/audio/metronome.svelte.ts +146 -0
  6. package/src/lib/audio/mic.ts +254 -0
  7. package/src/lib/audio/pitch.ts +133 -0
  8. package/src/lib/audio/rhythmplay.ts +152 -0
  9. package/src/lib/audio/synth.ts +409 -0
  10. package/src/lib/audio/voiceplay.ts +60 -0
  11. package/src/lib/drills/defs.ts +459 -0
  12. package/src/lib/drills/questions.ts +920 -0
  13. package/src/lib/drills/registry.svelte.ts +167 -0
  14. package/src/lib/drills/runtime.svelte.ts +629 -0
  15. package/src/lib/drills/verdict.ts +241 -0
  16. package/src/lib/index.ts +117 -0
  17. package/src/lib/learn/describe.ts +72 -0
  18. package/src/lib/learn/pools.ts +44 -0
  19. package/src/lib/learn/progress.ts +100 -0
  20. package/src/lib/learn/session.ts +304 -0
  21. package/src/lib/music/bass.ts +139 -0
  22. package/src/lib/music/clefs.ts +218 -0
  23. package/src/lib/music/intervals.ts +94 -0
  24. package/src/lib/music/notes.ts +133 -0
  25. package/src/lib/music/rhythm.ts +624 -0
  26. package/src/lib/music/scales.ts +560 -0
  27. package/src/lib/music/voice.ts +227 -0
  28. package/src/lib/music/warmups.ts +318 -0
  29. package/src/lib/nav.svelte.ts +53 -0
  30. package/src/lib/notation/fretboard.ts +187 -0
  31. package/src/lib/notation/glyphdata.ts +101 -0
  32. package/src/lib/notation/glyphs.ts +71 -0
  33. package/src/lib/notation/icons.ts +12 -0
  34. package/src/lib/notation/keyboard.ts +166 -0
  35. package/src/lib/notation/rhythmsvg.ts +341 -0
  36. package/src/lib/notation/staff.ts +288 -0
  37. package/src/lib/notation/svg.ts +37 -0
  38. package/src/lib/notation/tab.ts +80 -0
  39. package/src/lib/store/honmoon.svelte.ts +329 -0
  40. package/src/lib/store/normalize.ts +23 -0
  41. package/src/lib/store/sync.ts +43 -0
  42. package/src/lib/store/types.ts +101 -0
  43. package/src/lib/styles/honmoon.css +2064 -0
  44. package/src/lib/ui/AccRow.svelte +23 -0
  45. package/src/lib/ui/BackBar.svelte +23 -0
  46. package/src/lib/ui/BassRow.svelte +30 -0
  47. package/src/lib/ui/Chips.svelte +23 -0
  48. package/src/lib/ui/DataCard.svelte +66 -0
  49. package/src/lib/ui/Fretboard.svelte +71 -0
  50. package/src/lib/ui/Keyboard.svelte +54 -0
  51. package/src/lib/ui/MetronomePanel.svelte +101 -0
  52. package/src/lib/ui/RefBar.svelte +28 -0
  53. package/src/lib/ui/RhythmBar.svelte +9 -0
  54. package/src/lib/ui/Shell.svelte +96 -0
  55. package/src/lib/ui/StaffRun.svelte +46 -0
  56. package/src/lib/ui/Stat.svelte +13 -0
  57. package/src/lib/ui/actions.ts +36 -0
  58. package/src/lib/ui/keyfollow.svelte.ts +48 -0
  59. package/src/lib/ui/overlay.ts +15 -0
  60. package/src/lib/ui/refdata.ts +355 -0
  61. package/src/lib/ui/scaleboard.ts +420 -0
  62. package/src/lib/ui/screens/DegreesRef.svelte +405 -0
  63. package/src/lib/ui/screens/DrillScreen.svelte +1155 -0
  64. package/src/lib/ui/screens/Explorer.svelte +245 -0
  65. package/src/lib/ui/screens/FamilyScreen.svelte +280 -0
  66. package/src/lib/ui/screens/Glossary.svelte +65 -0
  67. package/src/lib/ui/screens/IntervalRef.svelte +37 -0
  68. package/src/lib/ui/screens/LearnIndex.svelte +44 -0
  69. package/src/lib/ui/screens/MetronomeScreen.svelte +17 -0
  70. package/src/lib/ui/screens/PracticeHome.svelte +159 -0
  71. package/src/lib/ui/screens/RhythmRef.svelte +239 -0
  72. package/src/lib/ui/screens/ScalesScreen.svelte +210 -0
  73. package/src/lib/ui/screens/SessionDone.svelte +119 -0
  74. package/src/lib/ui/screens/SetupScreen.svelte +548 -0
  75. package/src/lib/ui/screens/StaffMap.svelte +230 -0
  76. package/src/lib/ui/screens/StatsScreen.svelte +478 -0
  77. package/src/lib/ui/screens/TapCalibration.svelte +186 -0
  78. package/src/lib/ui/tapfix.ts +93 -0
  79. package/src/lib/ui/types.ts +40 -0
  80. package/src/lib/util/list.ts +4 -0
  81. package/src/lib/util/random.ts +23 -0
  82. package/src/lib/util/text.ts +50 -0
@@ -0,0 +1,60 @@
1
+ import { ac } from './context';
2
+ import { piano, pianoChord, slidePath } from './synth';
3
+ import { vRange, type VoicePattern } from '../music/voice';
4
+
5
+ /* How a teacher does it: one note on its own so you know where you are, a
6
+ breath of silence, then the pattern from that note. Sounding the chord
7
+ underneath the pattern tells you the key but not the pitch you are about
8
+ to sing, and by the time you have found it the first note has gone. So the
9
+ chord moves into the cue, with the starting note clearly on top of it, and
10
+ the pattern starts after the pause. */
11
+ /** The chord under the cue note is held this long, then a pause. */
12
+ const CUE_CHORD_S = 1.2;
13
+ const CUE_GAP_MIN_S = 0.38;
14
+ const CUE_GAP_MAX_S = 0.9;
15
+ /** Staccato notes sound for this share of their slot; legato nearly all of it. */
16
+ const STACCATO_SHARE = 0.35;
17
+ const LEGATO_SHARE = 0.95;
18
+
19
+ export function vCueLen(tempo: number): number {
20
+ const beat = 60 / tempo;
21
+ return CUE_CHORD_S + Math.max(CUE_GAP_MIN_S, Math.min(CUE_GAP_MAX_S, beat * 0.7));
22
+ }
23
+ /** Plays one run of a pattern from `root`, optionally with the cue first.
24
+ Returns how long the cue, the pattern, and both together take. */
25
+ export function vPlayOnce(
26
+ root: number,
27
+ pat: VoicePattern,
28
+ tempo: number,
29
+ cue: boolean,
30
+ ): { cue: number; pat: number; total: number } {
31
+ const ctx = ac();
32
+ if (!ctx) {
33
+ return { cue: 0, pat: 0.8, total: 0.8 };
34
+ }
35
+ const beat = 60 / tempo;
36
+ const start = ctx.currentTime + 0.06;
37
+ let cueLength = 0;
38
+ if (cue !== false) {
39
+ cueLength = vCueLen(tempo);
40
+ // a soft major chord an octave down, with the starting pitch clearly on top
41
+ const chordRoot = root - 12;
42
+ pianoChord([chordRoot, chordRoot + 4, chordRoot + 7], start, cueLength - 0.25, 0.26);
43
+ piano(root, start + 0.03, cueLength - 0.25, 1);
44
+ }
45
+ const patternStart = start + cueLength;
46
+ const noteLength = beat * pat.beats;
47
+ let patternLength: number;
48
+ if (pat.glide) {
49
+ const top = Math.min(vRange().hi, root + pat.glide);
50
+ slidePath([root, top, root], patternStart, noteLength);
51
+ patternLength = noteLength;
52
+ } else {
53
+ const sounding = noteLength * (pat.staccato ? STACCATO_SHARE : LEGATO_SHARE);
54
+ pat.iv!.forEach((semis, i) => {
55
+ piano(root + semis, patternStart + i * noteLength, sounding, 0.9);
56
+ });
57
+ patternLength = pat.iv!.length * noteLength;
58
+ }
59
+ return { cue: cueLength, pat: patternLength, total: cueLength + patternLength };
60
+ }
@@ -0,0 +1,459 @@
1
+ import type { Sys } from '../store/types';
2
+ import type { IconName } from '../notation/icons';
3
+ import type { RhythmKind } from '../music/rhythm';
4
+ import { mdoOn } from '../music/notes';
5
+ import { enabledScales } from '../music/scales';
6
+
7
+ /** How a drill asks its question. Several drills share a kind and differ only
8
+ in a parameter — C → Do and Do → C are both 'translate'. */
9
+ export type DrillKind =
10
+ | 'fb2name'
11
+ | 'name2fb'
12
+ | 'staff2name'
13
+ | 'staffkey'
14
+ | 'translate'
15
+ | 'play'
16
+ | 'ear'
17
+ | 'earscale'
18
+ | 'mdo'
19
+ | 'rhythm'
20
+ | 'scale';
21
+
22
+ export interface DrillDef {
23
+ id: string;
24
+ /** the family it belonged to in Fretboard Fluency; apps may regroup */
25
+ g: string;
26
+ /** name, as listed */
27
+ nm: string;
28
+ /** one-line description under the name */
29
+ sb: string;
30
+ kind: DrillKind;
31
+ /** the naming system the answer is in, or 'staff' when read off a staff */
32
+ sys?: Sys | 'staff';
33
+ /** needs the bass in your hands */
34
+ bass?: boolean;
35
+ /** only offered when this returns true (e.g. movable do switched on) */
36
+ need?: () => boolean;
37
+ /** a short explanation shown under the question until hints are off */
38
+ how?: string;
39
+ ic: IconName;
40
+ /* movable-do drills: what is asked for, and what it is asked from */
41
+ mode?: 'solf' | 'deg' | 'sing';
42
+ from?: 'staff' | 'name' | 'syll' | 'fb' | 'sing';
43
+ /* rhythm drills: which kind of rhythm card */
44
+ rk?: RhythmKind;
45
+ }
46
+
47
+ /* Each drill names one direction outright, so nothing depends on a global
48
+ notation setting — C → Do and Do → C are separate exercises, not one
49
+ exercise with a switch that can quietly turn it into C → C.
50
+ The order matters: when a card fits several drills, one is drawn at random
51
+ by its position in this list, so reordering changes which drills sessions
52
+ pick (it matched the original app's order when ported). */
53
+ export const DRILLS: DrillDef[] = [
54
+ {
55
+ id: 'fb2en',
56
+ g: 'neck',
57
+ nm: 'Fret → C',
58
+ sb: 'A fret is marked. Name it in letters.',
59
+ kind: 'fb2name',
60
+ sys: 'en',
61
+ ic: 'dot',
62
+ },
63
+ {
64
+ id: 'fb2solf',
65
+ g: 'neck',
66
+ nm: 'Fret → Do',
67
+ sb: 'A fret is marked. Name it in solfège.',
68
+ kind: 'fb2name',
69
+ sys: 'solf',
70
+ ic: 'dot',
71
+ },
72
+ {
73
+ id: 'en2fb',
74
+ g: 'neck',
75
+ nm: 'C → Fret',
76
+ sb: 'Letter name. Find it on a given string.',
77
+ kind: 'name2fb',
78
+ sys: 'en',
79
+ ic: 'target',
80
+ },
81
+ {
82
+ id: 'solf2fb',
83
+ g: 'neck',
84
+ nm: 'Do → Fret',
85
+ sb: 'Solfège name. Find it on a given string.',
86
+ kind: 'name2fb',
87
+ sys: 'solf',
88
+ ic: 'target',
89
+ },
90
+ {
91
+ id: 'st2en',
92
+ g: 'read',
93
+ nm: 'Staff → C',
94
+ sb: 'Read the note, answer in letters.',
95
+ kind: 'staff2name',
96
+ sys: 'en',
97
+ ic: 'staff',
98
+ },
99
+ {
100
+ id: 'st2solf',
101
+ g: 'read',
102
+ nm: 'Staff → Do',
103
+ sb: 'Read the note, answer in solfège.',
104
+ kind: 'staff2name',
105
+ sys: 'solf',
106
+ ic: 'staff',
107
+ },
108
+ {
109
+ id: 'en2solf',
110
+ g: 'names',
111
+ nm: 'C → Do',
112
+ sb: 'Letters into solfège.',
113
+ kind: 'translate',
114
+ sys: 'solf',
115
+ ic: 'swap',
116
+ },
117
+ {
118
+ id: 'solf2en',
119
+ g: 'names',
120
+ nm: 'Do → C',
121
+ sb: 'Solfège into letters.',
122
+ kind: 'translate',
123
+ sys: 'en',
124
+ ic: 'swap',
125
+ },
126
+ {
127
+ id: 'st2play',
128
+ g: 'read',
129
+ nm: 'Staff → play',
130
+ sb: 'Read it, then play it on the bass.',
131
+ kind: 'play',
132
+ bass: true,
133
+ sys: 'staff',
134
+ ic: 'wave',
135
+ },
136
+ /* The same reading question answered on a keyboard instead of by name:
137
+ one note, one key, nothing to eliminate and nothing to guess from. */
138
+ {
139
+ id: 'st2key',
140
+ g: 'read',
141
+ nm: 'Staff → key',
142
+ sb: 'Read the note and find it on a keyboard.',
143
+ kind: 'staffkey',
144
+ ic: 'dot',
145
+ how: 'The staff and a keyboard are the same picture stood on end: step up a line or a space and you step to the next white key. Work out the letter, then find it. Which octave you pick does not matter — the board shows one, and any C is a C.',
146
+ },
147
+ {
148
+ id: 'en2play',
149
+ g: 'neck',
150
+ nm: 'C → play',
151
+ sb: 'See a letter name, play that note.',
152
+ kind: 'play',
153
+ bass: true,
154
+ sys: 'en',
155
+ ic: 'wave',
156
+ },
157
+ {
158
+ id: 'solf2play',
159
+ g: 'neck',
160
+ nm: 'Do → play',
161
+ sb: 'See a solfège name, play that note.',
162
+ kind: 'play',
163
+ bass: true,
164
+ sys: 'solf',
165
+ ic: 'wave',
166
+ },
167
+ {
168
+ id: 'ivhear',
169
+ g: 'ear',
170
+ nm: 'Hear the interval',
171
+ sb: 'Two notes played. How far apart?',
172
+ kind: 'ear',
173
+ ic: 'wave',
174
+ },
175
+ {
176
+ id: 'ivsee',
177
+ g: 'ear',
178
+ nm: 'See the interval',
179
+ sb: 'Two dots on the neck. Name the gap.',
180
+ kind: 'ear',
181
+ ic: 'dot',
182
+ },
183
+ {
184
+ id: 'ivbuild',
185
+ g: 'ear',
186
+ nm: 'Build the interval',
187
+ sb: 'A fifth above this note — find it.',
188
+ kind: 'ear',
189
+ ic: 'target',
190
+ },
191
+ {
192
+ id: 'chhear',
193
+ g: 'ear',
194
+ nm: 'Hear the chord',
195
+ sb: 'Major, minor, or something else?',
196
+ kind: 'ear',
197
+ ic: 'swap',
198
+ },
199
+ /* Naming a scale by ear is a different skill from naming its notes: you are
200
+ listening for where the half steps fall, not counting them. Which scales
201
+ turn up is whatever you have switched on in Scales, so the two stay in
202
+ step. */
203
+ {
204
+ id: 'scalehear',
205
+ g: 'ear',
206
+ nm: 'Hear the scale',
207
+ sb: 'A scale is played. Which one was it?',
208
+ kind: 'earscale',
209
+ ic: 'wave',
210
+ need: () => enabledScales().length >= 2,
211
+ how: 'The scale runs up from its root to the octave. Do not try to name the notes — you are not told the key and it does not matter. Listen for the shape: how many notes there are, whether the third sounds bright or dark, and whether the seventh leans into the octave or sits a whole step below it. The scales offered are the ones you have switched on over in Scales.',
212
+ },
213
+ {
214
+ id: 'singit',
215
+ g: 'voice',
216
+ nm: 'Sing the interval',
217
+ sb: 'It gives you a note. You sing the next.',
218
+ kind: 'ear',
219
+ ic: 'wave',
220
+ },
221
+ /* Movable do and degrees stay switched off until you turn them on in Setup:
222
+ they are a second way of hearing the same notes, and learning both at
223
+ once is how people end up confident in neither. */
224
+ {
225
+ id: 'st2mdo',
226
+ g: 'names',
227
+ nm: 'Staff → movable do',
228
+ sb: 'A key is given. Read the note as a syllable in that key.',
229
+ kind: 'mdo',
230
+ mode: 'solf',
231
+ from: 'staff',
232
+ ic: 'staff',
233
+ need: mdoOn,
234
+ how: 'In movable do the syllables belong to the <b>key</b>, not to the letters. Do is whatever note the key is built on — in C that is C, in G it is G — and the rest follow up the scale from there. That is what lets one tune keep the same syllables in every key.',
235
+ },
236
+ {
237
+ id: 'en2mdo',
238
+ g: 'names',
239
+ nm: 'C → movable do',
240
+ sb: 'A key is given. Which syllable is that note?',
241
+ kind: 'mdo',
242
+ mode: 'solf',
243
+ from: 'name',
244
+ ic: 'swap',
245
+ need: mdoOn,
246
+ how: "Find the key's Do first, then count up the scale to the note you were given: Do Re Mi Fa Sol La Ti. The chromatic notes in between raise to Di Ri Fi Si Li, or lower to Te Le Se Me Ra.",
247
+ },
248
+ {
249
+ id: 'mdo2en',
250
+ g: 'names',
251
+ nm: 'Movable do → C',
252
+ sb: 'A key is given. Which note is that syllable?',
253
+ kind: 'mdo',
254
+ mode: 'solf',
255
+ from: 'syll',
256
+ ic: 'swap',
257
+ need: mdoOn,
258
+ how: "The same trip backwards: start at the key's Do and count up the scale until you reach the syllable, then name the note you landed on.",
259
+ },
260
+ {
261
+ id: 'en2deg',
262
+ g: 'names',
263
+ nm: 'C → degree',
264
+ sb: 'A key is given. Which degree of it is that note?',
265
+ kind: 'mdo',
266
+ mode: 'deg',
267
+ from: 'name',
268
+ ic: 'target',
269
+ need: mdoOn,
270
+ how: "A scale degree is just the syllable written as a number: the key's home note is 1, the next step up is 2, and so on to 7. It is the same idea as movable do with the singing taken out, and it is how chords and progressions get talked about.",
271
+ },
272
+ {
273
+ id: 'deg2en',
274
+ g: 'names',
275
+ nm: 'Degree → C',
276
+ sb: 'A key is given. Which note is the ♭6 of it?',
277
+ kind: 'mdo',
278
+ mode: 'deg',
279
+ from: 'syll',
280
+ ic: 'target',
281
+ need: mdoOn,
282
+ how: "Count up the scale from the key's home note. A flat in front lowers that degree by a fret, a sharp raises it.",
283
+ },
284
+ {
285
+ id: 'deg2fb',
286
+ g: 'names',
287
+ nm: 'Degree → fret',
288
+ sb: 'A key is given. Find that degree on the neck.',
289
+ kind: 'mdo',
290
+ mode: 'deg',
291
+ from: 'fb',
292
+ ic: 'dot',
293
+ need: mdoOn,
294
+ how: 'Work out which note the degree names, then play it anywhere — any string, any octave. The question is the degree, not the fret.',
295
+ },
296
+ {
297
+ id: 'singdeg',
298
+ g: 'voice',
299
+ nm: 'Sing the degree',
300
+ sb: 'It plays the key, then names a degree. You sing it.',
301
+ kind: 'mdo',
302
+ mode: 'sing',
303
+ from: 'sing',
304
+ ic: 'wave',
305
+ need: mdoOn,
306
+ how: 'Play the key first and let your ear settle on home, then find the degree from there rather than from any particular pitch. Being able to do that is what makes a written line singable without hunting for a starting note.',
307
+ },
308
+ /* Rhythm: values first, then counting, then the bar, then tapping it. */
309
+ {
310
+ id: 'rvalue',
311
+ g: 'read',
312
+ nm: 'Note values',
313
+ sb: 'How long is this note or rest?',
314
+ kind: 'rhythm',
315
+ rk: 'value',
316
+ ic: 'book',
317
+ how: 'A value is how long a note lasts, measured in beats. Counting in 4/4, a quarter note is one beat, a half note two, a whole note four, and an eighth half a beat. A dot after the note adds half of it again. A rest is the same length of time, deliberately not played.',
318
+ },
319
+ {
320
+ id: 'rcount',
321
+ g: 'read',
322
+ nm: 'Count the bar',
323
+ sb: 'Read a bar and pick the counting that says it out loud.',
324
+ kind: 'rhythm',
325
+ rk: 'count',
326
+ ic: 'book',
327
+ how: 'Counting is how you say a rhythm out loud, so your hands have something to follow. Every beat gets its number. Halfway through a beat is <b>&amp;</b> (“and”), and the quarter-points are <b>e</b> and <b>a</b> — so a beat made of four sixteenths is said <i>1 e &amp; a</i>, and two eighths are <i>1 &amp;</i>.<br><br>Each answer writes out the whole bar in order, one slot per step: a syllable where a note starts, a <b>·</b> where nothing does. Count the written bar out loud, then find the line that says the same thing.',
328
+ },
329
+ {
330
+ id: 'rfill',
331
+ g: 'read',
332
+ nm: 'Complete the bar',
333
+ sb: 'One value is missing. Which one fits?',
334
+ kind: 'rhythm',
335
+ rk: 'fill',
336
+ ic: 'target',
337
+ how: 'A bar holds exactly what its time signature promises and not a fraction more: 4/4 is four beats, 3/4 is three. Add up what is written, take it from the total, and the gap is the missing value.',
338
+ },
339
+ {
340
+ id: 'rhear',
341
+ g: 'read',
342
+ nm: 'Hear the rhythm',
343
+ sb: 'A rhythm is played. Which bar was it?',
344
+ kind: 'rhythm',
345
+ rk: 'hear',
346
+ ic: 'wave',
347
+ how: 'Four clicks count you in, then the rhythm plays over the beat. Listen for which notes land on a beat and which fall between, then find the bar that puts them there.',
348
+ },
349
+ {
350
+ id: 'rtap',
351
+ g: 'read',
352
+ nm: 'Tap the rhythm',
353
+ sb: 'Read the bar and tap it with the click.',
354
+ kind: 'rhythm',
355
+ rk: 'tap',
356
+ ic: 'wave',
357
+ how: 'Read the bar first and count it through in your head. Four clicks count you in, then the beats keep going and you tap the rhythm on the pad. Each tap is matched to the note it was aiming for, and anything within about a sixth of a beat counts as on time. Miss and it eases the tempo back; land it cleanly and it nudges the tempo up.',
358
+ },
359
+ ];
360
+
361
+ /* ---------------- scale drills ---------------- */
362
+ export const SDRILLS: DrillDef[] = [
363
+ {
364
+ id: 'scbuild',
365
+ g: 'scale',
366
+ kind: 'scale',
367
+ nm: 'Build it',
368
+ sb: 'Tap the scale in order on the neck.',
369
+ bass: false,
370
+ ic: 'target',
371
+ },
372
+ {
373
+ id: 'scdeg',
374
+ g: 'scale',
375
+ kind: 'scale',
376
+ nm: 'Degrees',
377
+ sb: 'Which note is the ♭3? Which degree is this?',
378
+ bass: false,
379
+ ic: 'swap',
380
+ },
381
+ {
382
+ id: 'scwrong',
383
+ g: 'scale',
384
+ kind: 'scale',
385
+ nm: 'Wrong note',
386
+ sb: 'One note has moved a fret. Find it.',
387
+ bass: false,
388
+ ic: 'dot',
389
+ },
390
+ {
391
+ id: 'scname',
392
+ g: 'scale',
393
+ kind: 'scale',
394
+ nm: 'Name it',
395
+ sb: 'Given the shape, which scale is it?',
396
+ bass: false,
397
+ ic: 'dot',
398
+ },
399
+ /* Four reading-and-recipe drills: the same scale met as notes on a staff and
400
+ as a pattern of distances, both ways round. None of them needs the bass. */
401
+ {
402
+ id: 'scstaff',
403
+ g: 'scale',
404
+ kind: 'scale',
405
+ nm: 'Staff → scale',
406
+ sb: 'A scale is written out. Which one is it?',
407
+ bass: false,
408
+ ic: 'staff',
409
+ how: 'Read the notes up from the root, then compare them with the major scale on the same root. Every note that is a fret lower than major is a ♭ degree, and those tell you the scale: a ♭3 means minor-family, a ♭7 alone means Mixolydian, ♭3 with a plain 6 means Dorian. Count the notes first — five is a pentatonic.',
410
+ },
411
+ {
412
+ id: 'scwrite',
413
+ g: 'scale',
414
+ kind: 'scale',
415
+ nm: 'Scale → staff',
416
+ sb: 'Which staff shows D Dorian?',
417
+ bass: false,
418
+ ic: 'staff',
419
+ how: 'The options all start on the same note and usually differ by one or two accidentals. Work out the scale’s notes from its formula first — major, then flatten what the formula flattens — and look for the staff that has exactly those sharps and flats.',
420
+ },
421
+ {
422
+ id: 'scsteps',
423
+ g: 'scale',
424
+ kind: 'scale',
425
+ nm: 'Steps → scale',
426
+ sb: 'Given its steps or its formula, name the scale.',
427
+ bass: false,
428
+ ic: 'book',
429
+ how: '<b>Steps</b> are the gaps from each note to the next, in frets: major is 2 2 1 2 2 2 1. A <b>formula</b> is every note measured from the root instead: major is 1 2 3 4 5 6 7. Same scale, two ways of writing it. Look for where the 1s (half steps) fall, or which degrees carry a ♭.',
430
+ },
431
+ {
432
+ id: 'screcipe',
433
+ g: 'scale',
434
+ kind: 'scale',
435
+ nm: 'Scale → steps',
436
+ sb: 'Pick the right step pattern or formula for a scale.',
437
+ bass: false,
438
+ ic: 'book',
439
+ how: 'Build it from what you know. Start with major — steps 2 2 1 2 2 2 1, formula 1 2 3 4 5 6 7 — and change what the scale changes. Lowering a degree by one fret shortens the step into it by one and lengthens the step out of it by one, so the steps always still add up to 12.',
440
+ },
441
+ {
442
+ id: 'scplay',
443
+ g: 'scale',
444
+ kind: 'scale',
445
+ nm: 'Play it',
446
+ sb: 'Play the scale, then mark yourself.',
447
+ bass: true,
448
+ ic: 'wave',
449
+ },
450
+ ];
451
+
452
+ export const ALL_DRILLS: DrillDef[] = DRILLS.concat(SDRILLS);
453
+
454
+ export function anyDrillDef(id: string): DrillDef | null {
455
+ return ALL_DRILLS.find((drill) => drill.id === id) ?? null;
456
+ }
457
+ export function isScaleDrill(id: string): boolean {
458
+ return SDRILLS.some((drill) => drill.id === id);
459
+ }