@greenblacksky/honmoon 0.0.0-stage → 0.4.1

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 +85 -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,167 @@
1
+ import { honmoon } from '../store/honmoon.svelte';
2
+ import { bassHere } from '../music/bass';
3
+ import { ALL_DRILLS, SDRILLS, type DrillDef } from './defs';
4
+ import { IC, type IconName } from '../notation/icons';
5
+
6
+ /* ---------------- what this app contains ----------------
7
+ The engine knows every drill; each app says which families it shows and
8
+ which drills sit in each. Everything that used to read the global lists —
9
+ sessions, mastery, the Practice screen — reads through here, so a drill the
10
+ app does not include simply does not exist for it. */
11
+
12
+ export interface GroupDef {
13
+ /** keep the original keys (neck, read, names, ear, voice, scale): the
14
+ session and stats logic refer to them */
15
+ k: string;
16
+ n: string;
17
+ sb: string;
18
+ /** what the family needs: "Screen, and sound for the rhythm drills" */
19
+ need: string;
20
+ ic: IconName;
21
+ /** drill ids, in the order they are listed */
22
+ drills: string[];
23
+ /** left out of the daily-session "Include" row (the scales family has its own session) */
24
+ sep?: boolean;
25
+ }
26
+
27
+ /** The six families of the original app, as defaults an app can reuse or reword. */
28
+ export const DEFAULT_GROUPS: Record<string, Omit<GroupDef, 'drills'>> = {
29
+ neck: {
30
+ k: 'neck',
31
+ n: 'The neck',
32
+ sb: 'Where a note lives on the bass, and what the thing under your finger is called.',
33
+ need: 'Screen — two of them want the bass in your hands',
34
+ ic: 'dot',
35
+ },
36
+ read: {
37
+ k: 'read',
38
+ n: 'Reading',
39
+ sb: 'Getting music off the page: pitch on the staff, and rhythm in time.',
40
+ need: 'Screen, and sound for the rhythm drills',
41
+ ic: 'staff',
42
+ },
43
+ names: {
44
+ k: 'names',
45
+ n: 'Names & keys',
46
+ sb: 'The same note under different names, and what it is doing inside a key.',
47
+ need: 'Nothing but the screen',
48
+ ic: 'swap',
49
+ },
50
+ ear: {
51
+ k: 'ear',
52
+ n: 'Ear',
53
+ sb: 'Intervals and chords by sound alone.',
54
+ need: 'Sound on',
55
+ ic: 'wave',
56
+ },
57
+ voice: {
58
+ k: 'voice',
59
+ n: 'Voice',
60
+ sb: 'Singing what you read, and finding a note from the key rather than from a reference pitch.',
61
+ need: 'Your voice, and sound on',
62
+ ic: 'voice',
63
+ },
64
+ scale: {
65
+ k: 'scale',
66
+ n: 'Scales',
67
+ sb: 'Shapes, degrees and the keys they live in.',
68
+ need: 'Screen; one drill wants the bass',
69
+ ic: 'target',
70
+ },
71
+ };
72
+
73
+ const app = $state({ groups: [] as GroupDef[] });
74
+
75
+ /** Called once by each app at start-up. */
76
+ export function configureGroups(groups: GroupDef[]): void {
77
+ app.groups = groups;
78
+ }
79
+ export function appGroups(): GroupDef[] {
80
+ return app.groups;
81
+ }
82
+ export function groupDef(key: string): GroupDef | null {
83
+ return app.groups.find((group) => group.k === key) ?? null;
84
+ }
85
+ export function groupName(key: string): string {
86
+ return groupDef(key)?.n ?? key;
87
+ }
88
+ export function groupIcon(group: GroupDef): string {
89
+ return IC[group.ic];
90
+ }
91
+ /** The family a drill sits in, in this app. */
92
+ export function groupOfDrill(id: string): string | null {
93
+ return app.groups.find((group) => group.drills.includes(id))?.k ?? null;
94
+ }
95
+ /** Every drill this app includes, in the engine's canonical order. */
96
+ export function appDrills(): DrillDef[] {
97
+ return ALL_DRILLS.filter((drill) => groupOfDrill(drill.id) != null);
98
+ }
99
+ export function appHasDrill(id: string): boolean {
100
+ return groupOfDrill(id) != null;
101
+ }
102
+ export function appHasGroup(key: string): boolean {
103
+ return groupDef(key) != null;
104
+ }
105
+ /** A drill this app includes, or null. */
106
+ export function drillDef(id: string): DrillDef | null {
107
+ return appHasDrill(id) ? (ALL_DRILLS.find((drill) => drill.id === id) ?? null) : null;
108
+ }
109
+ export function scaleDrillsInApp(): DrillDef[] {
110
+ return SDRILLS.filter((drill) => appHasDrill(drill.id));
111
+ }
112
+ export function sessionGroups(): GroupDef[] {
113
+ return app.groups.filter((group) => !group.sep);
114
+ }
115
+
116
+ /* ---------------- what is switched on ---------------- */
117
+ export function groupOff(key: string): boolean {
118
+ return (honmoon.settings.off || []).includes(key);
119
+ }
120
+ /** In this app, and not switched off for the daily session. */
121
+ export function groupUsable(key: string): boolean {
122
+ return appHasGroup(key) && !groupOff(key);
123
+ }
124
+ /** A single drill you would rather not meet. It stays on its family screen so
125
+ you can still open it by hand — switching it off only keeps it out of the
126
+ daily session, which is where an exercise you dislike does the most damage:
127
+ it turns up unasked, between things you did want. */
128
+ export function drillOn(id: string): boolean {
129
+ return !(honmoon.settings.doff || []).includes(id);
130
+ }
131
+ export function sdrillOn(id: string): boolean {
132
+ return !(honmoon.settings.sdoff || []).includes(id);
133
+ }
134
+ /** Whether a drill can run right now: its settings need is met, and if it
135
+ wants the bass in your hands, it is. */
136
+ export function drillAvailable(drill: DrillDef): boolean {
137
+ if (drill.need && !drill.need()) {
138
+ return false;
139
+ }
140
+ return !(drill.bass && !bassHere());
141
+ }
142
+ /** A family's drills that this app includes, in the app's order. */
143
+ function familyDrillDefs(key: string): DrillDef[] {
144
+ const group = groupDef(key);
145
+ if (!group) {
146
+ return [];
147
+ }
148
+ return group.drills.map((id) => drillDef(id)).filter((drill): drill is DrillDef => !!drill);
149
+ }
150
+ /** The drills a family shows, as the app lists them. */
151
+ export function famDrills(key: string): DrillDef[] {
152
+ const drills = familyDrillDefs(key);
153
+ if (groupDef(key)?.k === 'scale') {
154
+ return drills.filter((drill) => !drill.bass || bassHere());
155
+ }
156
+ return drills.filter(drillAvailable);
157
+ }
158
+ /** Drills a family owns but is not showing because of a setting. */
159
+ export function famHidden(key: string): number {
160
+ return familyDrillDefs(key).filter((drill) => drill.need && !drill.need()).length;
161
+ }
162
+ /** Drills a family is not showing only because the bass is not to hand. */
163
+ export function famNoBass(key: string): number {
164
+ return familyDrillDefs(key).filter(
165
+ (drill) => drill.bass && !bassHere() && (key === 'scale' || !drill.need || drill.need()),
166
+ ).length;
167
+ }