@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.
- package/LICENSE +21 -0
- package/README.md +85 -2
- package/package.json +58 -4
- package/src/lib/audio/context.ts +29 -0
- package/src/lib/audio/metronome.svelte.ts +146 -0
- package/src/lib/audio/mic.ts +254 -0
- package/src/lib/audio/pitch.ts +133 -0
- package/src/lib/audio/rhythmplay.ts +152 -0
- package/src/lib/audio/synth.ts +409 -0
- package/src/lib/audio/voiceplay.ts +60 -0
- package/src/lib/drills/defs.ts +459 -0
- package/src/lib/drills/questions.ts +920 -0
- package/src/lib/drills/registry.svelte.ts +167 -0
- package/src/lib/drills/runtime.svelte.ts +629 -0
- package/src/lib/drills/verdict.ts +241 -0
- package/src/lib/index.ts +117 -0
- package/src/lib/learn/describe.ts +72 -0
- package/src/lib/learn/pools.ts +44 -0
- package/src/lib/learn/progress.ts +100 -0
- package/src/lib/learn/session.ts +304 -0
- package/src/lib/music/bass.ts +139 -0
- package/src/lib/music/clefs.ts +218 -0
- package/src/lib/music/intervals.ts +94 -0
- package/src/lib/music/notes.ts +133 -0
- package/src/lib/music/rhythm.ts +624 -0
- package/src/lib/music/scales.ts +560 -0
- package/src/lib/music/voice.ts +227 -0
- package/src/lib/music/warmups.ts +318 -0
- package/src/lib/nav.svelte.ts +53 -0
- package/src/lib/notation/fretboard.ts +187 -0
- package/src/lib/notation/glyphdata.ts +101 -0
- package/src/lib/notation/glyphs.ts +71 -0
- package/src/lib/notation/icons.ts +12 -0
- package/src/lib/notation/keyboard.ts +166 -0
- package/src/lib/notation/rhythmsvg.ts +341 -0
- package/src/lib/notation/staff.ts +288 -0
- package/src/lib/notation/svg.ts +37 -0
- package/src/lib/notation/tab.ts +80 -0
- package/src/lib/store/honmoon.svelte.ts +329 -0
- package/src/lib/store/normalize.ts +23 -0
- package/src/lib/store/sync.ts +43 -0
- package/src/lib/store/types.ts +101 -0
- package/src/lib/styles/honmoon.css +2064 -0
- package/src/lib/ui/AccRow.svelte +23 -0
- package/src/lib/ui/BackBar.svelte +23 -0
- package/src/lib/ui/BassRow.svelte +30 -0
- package/src/lib/ui/Chips.svelte +23 -0
- package/src/lib/ui/DataCard.svelte +66 -0
- package/src/lib/ui/Fretboard.svelte +71 -0
- package/src/lib/ui/Keyboard.svelte +54 -0
- package/src/lib/ui/MetronomePanel.svelte +101 -0
- package/src/lib/ui/RefBar.svelte +28 -0
- package/src/lib/ui/RhythmBar.svelte +9 -0
- package/src/lib/ui/Shell.svelte +96 -0
- package/src/lib/ui/StaffRun.svelte +46 -0
- package/src/lib/ui/Stat.svelte +13 -0
- package/src/lib/ui/actions.ts +36 -0
- package/src/lib/ui/keyfollow.svelte.ts +48 -0
- package/src/lib/ui/overlay.ts +15 -0
- package/src/lib/ui/refdata.ts +355 -0
- package/src/lib/ui/scaleboard.ts +420 -0
- package/src/lib/ui/screens/DegreesRef.svelte +405 -0
- package/src/lib/ui/screens/DrillScreen.svelte +1155 -0
- package/src/lib/ui/screens/Explorer.svelte +245 -0
- package/src/lib/ui/screens/FamilyScreen.svelte +280 -0
- package/src/lib/ui/screens/Glossary.svelte +65 -0
- package/src/lib/ui/screens/IntervalRef.svelte +37 -0
- package/src/lib/ui/screens/LearnIndex.svelte +44 -0
- package/src/lib/ui/screens/MetronomeScreen.svelte +17 -0
- package/src/lib/ui/screens/PracticeHome.svelte +159 -0
- package/src/lib/ui/screens/RhythmRef.svelte +239 -0
- package/src/lib/ui/screens/ScalesScreen.svelte +210 -0
- package/src/lib/ui/screens/SessionDone.svelte +119 -0
- package/src/lib/ui/screens/SetupScreen.svelte +548 -0
- package/src/lib/ui/screens/StaffMap.svelte +230 -0
- package/src/lib/ui/screens/StatsScreen.svelte +478 -0
- package/src/lib/ui/screens/TapCalibration.svelte +186 -0
- package/src/lib/ui/tapfix.ts +93 -0
- package/src/lib/ui/types.ts +40 -0
- package/src/lib/util/list.ts +4 -0
- package/src/lib/util/random.ts +23 -0
- package/src/lib/util/text.ts +50 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
<!-- Any scale in any key: the notes, the steps, the degrees, the box on the neck, the staff and the keyboard. -->
|
|
2
|
+
<script lang="ts" module>
|
|
3
|
+
type BoxLabels = 'deg' | 'note' | 'fin';
|
|
4
|
+
/** What the explorer shows; kept across visits. Starts on A minor pentatonic from the E string. */
|
|
5
|
+
const XS = $state({ sid: 'minpent', root: 9, start: 3, labels: 'deg' as BoxLabels });
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
import { honmoon } from '../../store/honmoon.svelte';
|
|
10
|
+
import { STRINGS } from '../../music/bass';
|
|
11
|
+
import {
|
|
12
|
+
SCALES,
|
|
13
|
+
SGROUPS,
|
|
14
|
+
degPretty,
|
|
15
|
+
noteText,
|
|
16
|
+
rootName,
|
|
17
|
+
rootPool,
|
|
18
|
+
scaleDef,
|
|
19
|
+
scaleNotes,
|
|
20
|
+
scaleShape,
|
|
21
|
+
scaleSteps,
|
|
22
|
+
} from '../../music/scales';
|
|
23
|
+
import { kbBase } from '../../notation/keyboard';
|
|
24
|
+
import { playScale } from '../../audio/synth';
|
|
25
|
+
import { go, hasRoute } from '../../nav.svelte';
|
|
26
|
+
import { fretFrac, hscroll } from '../actions';
|
|
27
|
+
import { kbFollow } from '../keyfollow.svelte';
|
|
28
|
+
import { kbHalfStepLine, scaleBoard, stepRowHTML } from '../scaleboard';
|
|
29
|
+
import Fretboard from '../Fretboard.svelte';
|
|
30
|
+
import Keyboard from '../Keyboard.svelte';
|
|
31
|
+
import StaffRun from '../StaffRun.svelte';
|
|
32
|
+
import RefBar from '../RefBar.svelte';
|
|
33
|
+
|
|
34
|
+
const { back }: { back?: { label: string; to: string } } = $props();
|
|
35
|
+
|
|
36
|
+
const scale = $derived(scaleDef(XS.sid));
|
|
37
|
+
const notes = $derived(scaleNotes(XS.sid, XS.root));
|
|
38
|
+
const board = $derived(scaleBoard(XS.sid, XS.root, XS.start, XS.labels));
|
|
39
|
+
/** How far apart the keyboard lights the notes as the scale plays, in seconds. */
|
|
40
|
+
const FOLLOW_STEP_S = 0.33;
|
|
41
|
+
/** C3: the keyboard picture starts at the C at or below this plus the root. */
|
|
42
|
+
const KEYBOARD_BASE = 48;
|
|
43
|
+
const LABEL_CHOICES: [BoxLabels, string][] = [
|
|
44
|
+
['deg', 'Degrees'],
|
|
45
|
+
['note', 'Notes'],
|
|
46
|
+
['fin', 'Fingers'],
|
|
47
|
+
];
|
|
48
|
+
const START_CHOICES: [number, string][] = [
|
|
49
|
+
[3, 'E string'],
|
|
50
|
+
[2, 'A string'],
|
|
51
|
+
];
|
|
52
|
+
const isRootNote = (semis: number) => semis % 12 === 0;
|
|
53
|
+
const total = (steps: number[]) => steps.reduce((sum, step) => sum + step, 0);
|
|
54
|
+
const shapeMidis = (shape: { pos: { s: number; f: number }[] }) =>
|
|
55
|
+
shape.pos.map((pos) => STRINGS[pos.s].open + pos.f);
|
|
56
|
+
function hear() {
|
|
57
|
+
playScale(XS.sid, XS.root, XS.start);
|
|
58
|
+
/* follow mode: the keys light in time with the notes */
|
|
59
|
+
kbFollow(shapeMidis(scaleShape(XS.sid, XS.root, XS.start, 1)), FOLLOW_STEP_S);
|
|
60
|
+
}
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<RefBar title="Explorer" {back} resume={false} />
|
|
64
|
+
|
|
65
|
+
<div class="sec">
|
|
66
|
+
<div class="card stack">
|
|
67
|
+
<div>
|
|
68
|
+
<div style="font-family:var(--f-disp);font-size:24px;letter-spacing:.02em">
|
|
69
|
+
{rootName(XS.root) + ' ' + scale.n.toLowerCase()}
|
|
70
|
+
</div>
|
|
71
|
+
<p class="note" style="margin:4px 0 0">{scale.d}</p>
|
|
72
|
+
</div>
|
|
73
|
+
{@html stepRowHTML(XS.sid, XS.root)}
|
|
74
|
+
<p class="note" style="margin:0">
|
|
75
|
+
Big numbers are the notes, small ones their degree. The circles between them are the <b
|
|
76
|
+
>steps</b
|
|
77
|
+
>
|
|
78
|
+
— how many frets from each note to the next. They come to {total(scaleSteps(XS.sid))}, because
|
|
79
|
+
that is one octave.
|
|
80
|
+
</p>
|
|
81
|
+
<div
|
|
82
|
+
class="fbwrap"
|
|
83
|
+
use:hscroll={{
|
|
84
|
+
frac: fretFrac(board.cf, board.lo, board.hi),
|
|
85
|
+
key: XS.sid + XS.root + XS.start,
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
<Fretboard lo={board.lo} hi={board.hi} cells={board.cells} />
|
|
89
|
+
</div>
|
|
90
|
+
<div class="bar">
|
|
91
|
+
<span class="hint">Show</span>
|
|
92
|
+
{#each LABEL_CHOICES as [labels, title] (labels)}
|
|
93
|
+
<button
|
|
94
|
+
class="chip sm"
|
|
95
|
+
data-xs="labels"
|
|
96
|
+
data-val={labels}
|
|
97
|
+
aria-pressed={XS.labels === labels}
|
|
98
|
+
onclick={() => {
|
|
99
|
+
XS.labels = labels;
|
|
100
|
+
}}>{title}</button
|
|
101
|
+
>
|
|
102
|
+
{/each}
|
|
103
|
+
</div>
|
|
104
|
+
<div class="bar">
|
|
105
|
+
<span class="hint">From</span>
|
|
106
|
+
{#each START_CHOICES as [string, title] (string)}
|
|
107
|
+
<button
|
|
108
|
+
class="chip sm"
|
|
109
|
+
data-xs="start"
|
|
110
|
+
data-val={string}
|
|
111
|
+
aria-pressed={XS.start === string}
|
|
112
|
+
onclick={() => {
|
|
113
|
+
XS.start = string;
|
|
114
|
+
}}>{title}</button
|
|
115
|
+
>
|
|
116
|
+
{/each}
|
|
117
|
+
<span class="spacer"></span><button class="ghost" id="hearscale" onclick={hear}
|
|
118
|
+
>♪ Hear it</button
|
|
119
|
+
>
|
|
120
|
+
</div>
|
|
121
|
+
<p class="note">
|
|
122
|
+
Hollow rings are every other place that note occurs on the neck. The filled dots are one
|
|
123
|
+
playable box{board.shape.clean
|
|
124
|
+
? ', all inside a four-fret span.'
|
|
125
|
+
: ' — this one needs a position shift, marked by the finger numbers jumping back.'}
|
|
126
|
+
</p>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<div class="sec">
|
|
131
|
+
<h2>On the staff</h2>
|
|
132
|
+
<div class="card">
|
|
133
|
+
<div class="fbwrap">
|
|
134
|
+
<StaffRun midis={shapeMidis(board.shape)} pref={honmoon.settings.pref} tappable={false} />
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<!-- The same scale on a keyboard. On a fretboard every step looks alike, so the
|
|
140
|
+
shape of a scale is invisible; here the half steps are drawn for you —
|
|
141
|
+
they are the places where two lit white keys touch. -->
|
|
142
|
+
<div class="sec">
|
|
143
|
+
<h2>On the keyboard</h2>
|
|
144
|
+
<div class="card stack">
|
|
145
|
+
<Keyboard
|
|
146
|
+
lo={kbBase(KEYBOARD_BASE + XS.root)}
|
|
147
|
+
oct={1}
|
|
148
|
+
tap="play"
|
|
149
|
+
cells={notes.map((note) => ({
|
|
150
|
+
pc: note.pc,
|
|
151
|
+
fill: isRootNote(note.semis) ? 'var(--accent)' : 'var(--good)',
|
|
152
|
+
label: XS.labels === 'note' ? noteText(note) : degPretty(note.deg),
|
|
153
|
+
}))}
|
|
154
|
+
/>
|
|
155
|
+
<p class="note" style="margin:0">
|
|
156
|
+
Tap a key to hear it. {@html kbHalfStepLine(XS.sid, XS.root)}
|
|
157
|
+
</p>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<div class="sec">
|
|
162
|
+
<h2>Reading the steps</h2>
|
|
163
|
+
<div class="card stack">
|
|
164
|
+
<p class="note">
|
|
165
|
+
A <b>step</b> is the distance from one note of a scale to the next, counted in frets. One fret
|
|
166
|
+
is a <b>half step</b> — a semitone. Two frets is a <b>whole step</b>. That is the whole
|
|
167
|
+
vocabulary.
|
|
168
|
+
</p>
|
|
169
|
+
<p class="note">
|
|
170
|
+
Every scale is a recipe of steps from its root, and the recipe is what makes it sound the way
|
|
171
|
+
it does. Major is <b>whole whole half whole whole whole half</b>; play that from any note and
|
|
172
|
+
you have that key's major scale. On the bass the shape does not change at all when you move
|
|
173
|
+
key — it just slides up the neck. That is the one real advantage a fretted instrument has over
|
|
174
|
+
a piano, and it is why bassists think in shapes.
|
|
175
|
+
</p>
|
|
176
|
+
<p class="note">
|
|
177
|
+
Pentatonics leave notes out, so they have bigger gaps — minor pentatonic goes <b>3 2 2 3 2</b
|
|
178
|
+
>. Those three-fret jumps are exactly why it sounds open and singable rather than stepwise,
|
|
179
|
+
and why it is so hard to play a wrong note with it.
|
|
180
|
+
</p>
|
|
181
|
+
<p class="note">
|
|
182
|
+
Whatever the scale, the steps add up to <b>12</b>, because twelve semitones is an octave and
|
|
183
|
+
the scale has to arrive home.
|
|
184
|
+
</p>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<div class="sec">
|
|
189
|
+
<h2>Reading the degrees</h2>
|
|
190
|
+
<div class="card stack">
|
|
191
|
+
<p class="note">
|
|
192
|
+
The small number under each note is its <b>degree</b> — which note of the scale it is, counting
|
|
193
|
+
the root as 1.
|
|
194
|
+
</p>
|
|
195
|
+
<p class="note">
|
|
196
|
+
A ♭ or ♯ in front means it is a semitone lower or higher <i
|
|
197
|
+
>than it would be in the major scale</i
|
|
198
|
+
>. The major scale is the ruler everything else is measured against. So ♭3 means “a semitone
|
|
199
|
+
below the 3 you would get in major” — and that single semitone is the entire difference
|
|
200
|
+
between a major and a minor chord.
|
|
201
|
+
</p>
|
|
202
|
+
<p class="note">
|
|
203
|
+
This is why degrees are worth more to you than note names. <b>The ♭7</b> means the same thing
|
|
204
|
+
in every key, and a line built on 1 ♭3 4 5 works wherever you put it. <b>B♭</b> only means something
|
|
205
|
+
once somebody tells you what key you are in.
|
|
206
|
+
</p>
|
|
207
|
+
{#if hasRoute('degref')}<div class="bar">
|
|
208
|
+
<button class="ghost" data-go="degref" onclick={() => go('degref')}
|
|
209
|
+
>More on degrees, with pictures →</button
|
|
210
|
+
>
|
|
211
|
+
</div>{/if}
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<div class="sec">
|
|
216
|
+
<h2>Root</h2>
|
|
217
|
+
<div class="levels">
|
|
218
|
+
{#each rootPool() as root (root)}<button
|
|
219
|
+
class="chip sm"
|
|
220
|
+
data-xs="root"
|
|
221
|
+
data-val={root}
|
|
222
|
+
aria-pressed={XS.root === root}
|
|
223
|
+
onclick={() => {
|
|
224
|
+
XS.root = root;
|
|
225
|
+
}}>{rootName(root)}</button
|
|
226
|
+
>{/each}
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
{#each SGROUPS as group (group)}
|
|
231
|
+
<div class="sec">
|
|
232
|
+
<h2>{group}</h2>
|
|
233
|
+
<div class="levels">
|
|
234
|
+
{#each SCALES.filter((def) => def.g === group) as def (def.id)}<button
|
|
235
|
+
class="chip sm"
|
|
236
|
+
data-xs="sid"
|
|
237
|
+
data-val={def.id}
|
|
238
|
+
aria-pressed={XS.sid === def.id}
|
|
239
|
+
onclick={() => {
|
|
240
|
+
XS.sid = def.id;
|
|
241
|
+
}}>{def.n}</button
|
|
242
|
+
>{/each}
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
{/each}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
<!-- One family: what it is about, what it keeps next to its drills, and the drills. -->
|
|
2
|
+
<script lang="ts" module>
|
|
3
|
+
/* Rolled up by default, and it stays as you left it across re-draws — a
|
|
4
|
+
panel that snapped shut every time you toggled a chip would be unusable. */
|
|
5
|
+
const FOLD: Record<string, boolean> = $state({});
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
import type { Snippet } from 'svelte';
|
|
10
|
+
import { honmoon, setSetting, queueSave, saveLocal } from '../../store/honmoon.svelte';
|
|
11
|
+
import { LEVELS, accOn, lvl, positions } from '../../music/bass';
|
|
12
|
+
import { staffNotes, multiClef } from '../../music/clefs';
|
|
13
|
+
import { midiName } from '../../music/notes';
|
|
14
|
+
import { vRange } from '../../music/voice';
|
|
15
|
+
import { IC } from '../../notation/icons';
|
|
16
|
+
import { drillOn, famDrills, famHidden, famNoBass, groupDef } from '../../drills/registry.svelte';
|
|
17
|
+
import { drillMastery } from '../../learn/progress';
|
|
18
|
+
import { startDrill } from '../../drills/runtime.svelte';
|
|
19
|
+
import { go, hasRoute } from '../../nav.svelte';
|
|
20
|
+
import { toggledIn } from '../../util/list';
|
|
21
|
+
import AccRow from '../AccRow.svelte';
|
|
22
|
+
import ScalesScreen from './ScalesScreen.svelte';
|
|
23
|
+
|
|
24
|
+
interface Props {
|
|
25
|
+
fam: string;
|
|
26
|
+
/** where the voice family's "Vocal warm-up" button goes */
|
|
27
|
+
voiceWarmup?: string;
|
|
28
|
+
/** anything an app wants to add above the drill list */
|
|
29
|
+
extra?: Snippet<[string]>;
|
|
30
|
+
}
|
|
31
|
+
const { fam, voiceWarmup = 'warmhome', extra }: Props = $props();
|
|
32
|
+
|
|
33
|
+
const group = $derived(groupDef(fam));
|
|
34
|
+
const level = $derived(lvl());
|
|
35
|
+
const drills = $derived(famDrills(fam));
|
|
36
|
+
const noBassCount = $derived(famNoBass(fam));
|
|
37
|
+
const hiddenCount = $derived(famHidden(fam));
|
|
38
|
+
const offHere = $derived(drills.filter((def) => !drillOn(def.id)).length);
|
|
39
|
+
const open = $derived(FOLD[fam] || offHere > 0);
|
|
40
|
+
interface ReadingLink {
|
|
41
|
+
route: string;
|
|
42
|
+
title: string;
|
|
43
|
+
blurb: string;
|
|
44
|
+
}
|
|
45
|
+
/** The reference pages worth reading alongside this family. */
|
|
46
|
+
const refs = $derived.by(() => {
|
|
47
|
+
const links: ReadingLink[] = [];
|
|
48
|
+
if (fam === 'read') {
|
|
49
|
+
links.push({
|
|
50
|
+
route: 'ref',
|
|
51
|
+
title: 'Staff map',
|
|
52
|
+
blurb:
|
|
53
|
+
'Every note on ' +
|
|
54
|
+
(multiClef() ? 'each clef you have on' : 'the bass clef') +
|
|
55
|
+
', what it is called and where to play it.',
|
|
56
|
+
});
|
|
57
|
+
links.push({
|
|
58
|
+
route: 'rref',
|
|
59
|
+
title: 'How rhythm is written',
|
|
60
|
+
blurb: 'Values, dots, ties, rests, time signatures and how to count a bar out loud.',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (fam === 'ear') {
|
|
64
|
+
links.push({
|
|
65
|
+
route: 'ivref',
|
|
66
|
+
title: 'What an interval is',
|
|
67
|
+
blurb: 'All twelve, what they sound like, and why one shape works in every key.',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return links.filter((link) => hasRoute(link.route));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
function toggleDoff(id: string) {
|
|
74
|
+
honmoon.settings.doff = toggledIn(honmoon.settings.doff || [], id);
|
|
75
|
+
saveLocal();
|
|
76
|
+
queueSave();
|
|
77
|
+
}
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
{#if fam === 'scale'}
|
|
81
|
+
<ScalesScreen />
|
|
82
|
+
{:else if group}
|
|
83
|
+
<div class="sec">
|
|
84
|
+
<div class="bar">
|
|
85
|
+
<button class="ghost" id="back" onclick={() => go('home')}>← Practice</button>
|
|
86
|
+
<span class="spacer"></span><span class="tick">{group.need}</span>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="sec">
|
|
90
|
+
<h2>{group.n}</h2>
|
|
91
|
+
<div class="card"><p class="note" style="margin:0">{group.sb}</p></div>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
{#if fam === 'neck'}
|
|
95
|
+
<div class="sec">
|
|
96
|
+
<h2>Range</h2>
|
|
97
|
+
<div class="levels" id="lvls">
|
|
98
|
+
{#each LEVELS as range (range.id)}
|
|
99
|
+
<button
|
|
100
|
+
class="chip"
|
|
101
|
+
data-lvl={range.id}
|
|
102
|
+
aria-pressed={range.id === level.id}
|
|
103
|
+
onclick={() => setSetting('level', range.id)}>{range.name}</button
|
|
104
|
+
>
|
|
105
|
+
{/each}
|
|
106
|
+
</div>
|
|
107
|
+
<div class="card" style="margin-top:10px"><AccRow /></div>
|
|
108
|
+
<p class="note">
|
|
109
|
+
{level.desc}{accOn() ? ', sharps and flats included' : ', natural notes only'} — {positions()
|
|
110
|
+
.length} fretboard positions, {staffNotes().length} distinct notes on the staff. Both of these
|
|
111
|
+
apply everywhere, not only here: the reading drills, the daily session and the maps on Stats all
|
|
112
|
+
follow them.
|
|
113
|
+
</p>
|
|
114
|
+
</div>
|
|
115
|
+
{/if}
|
|
116
|
+
{#if fam === 'ear'}
|
|
117
|
+
<div class="sec">
|
|
118
|
+
<h2>Intervals in rotation</h2>
|
|
119
|
+
<div class="levels">
|
|
120
|
+
<button
|
|
121
|
+
class="chip sm"
|
|
122
|
+
data-set="ivset"
|
|
123
|
+
data-val="basic"
|
|
124
|
+
aria-pressed={honmoon.settings.ivset !== 'all'}
|
|
125
|
+
onclick={() => setSetting('ivset', 'basic')}>The common six</button
|
|
126
|
+
>
|
|
127
|
+
<button
|
|
128
|
+
class="chip sm"
|
|
129
|
+
data-set="ivset"
|
|
130
|
+
data-val="all"
|
|
131
|
+
aria-pressed={honmoon.settings.ivset === 'all'}
|
|
132
|
+
onclick={() => setSetting('ivset', 'all')}>All twelve</button
|
|
133
|
+
>
|
|
134
|
+
</div>
|
|
135
|
+
<p class="note">
|
|
136
|
+
{drills.some((def) => def.id === 'scalehear')
|
|
137
|
+
? 'Hear the scale draws on whatever is switched on over in Scales, not on this row. '
|
|
138
|
+
: ''}Start with 2nds, 3rds, 4ths, 5ths and the octave — they cover most of what you will
|
|
139
|
+
meet. The full set adds the tritone, 6ths and 7ths{drills.some((def) => def.id === 'chhear')
|
|
140
|
+
? ', and both chord drills widen from major/minor to seven types'
|
|
141
|
+
: ''}.
|
|
142
|
+
</p>
|
|
143
|
+
</div>
|
|
144
|
+
{/if}
|
|
145
|
+
{#if fam === 'voice'}
|
|
146
|
+
{@const voiceRange = vRange()}
|
|
147
|
+
<div class="sec">
|
|
148
|
+
<h2>Your voice</h2>
|
|
149
|
+
<div class="card stack">
|
|
150
|
+
<div class="setrow" style="border:none;padding:0">
|
|
151
|
+
<span class="lbl">Range</span><span class="tick"
|
|
152
|
+
>{voiceRange.set
|
|
153
|
+
? midiName(voiceRange.lo) + ' – ' + midiName(voiceRange.hi)
|
|
154
|
+
: 'not measured — using ' +
|
|
155
|
+
midiName(voiceRange.lo) +
|
|
156
|
+
' – ' +
|
|
157
|
+
midiName(voiceRange.hi)}</span
|
|
158
|
+
>
|
|
159
|
+
</div>
|
|
160
|
+
<p class="note">
|
|
161
|
+
Everything sung here is transposed to sit inside that range, so nothing asks you for a
|
|
162
|
+
note you do not have.
|
|
163
|
+
</p>
|
|
164
|
+
<div class="bar">
|
|
165
|
+
{#if hasRoute('range')}<button
|
|
166
|
+
class="ghost"
|
|
167
|
+
data-go="range"
|
|
168
|
+
onclick={() => go('range', { from: 'fam' })}>Find my range</button
|
|
169
|
+
>{/if}
|
|
170
|
+
<span class="spacer"></span>
|
|
171
|
+
{#if hasRoute(voiceWarmup)}<button
|
|
172
|
+
class="ghost"
|
|
173
|
+
id="tovoicewarm"
|
|
174
|
+
onclick={() => go(voiceWarmup)}>Vocal warm-up</button
|
|
175
|
+
>{/if}
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
{/if}
|
|
180
|
+
{#if noBassCount}
|
|
181
|
+
<div class="sec">
|
|
182
|
+
<div class="card stack">
|
|
183
|
+
<p class="note">
|
|
184
|
+
{noBassCount === 1 ? 'One drill here is' : noBassCount + ' drills here are'} put away because
|
|
185
|
+
you said the bass is not with you. Nothing else in this family needs it.
|
|
186
|
+
</p>
|
|
187
|
+
<div class="bar">
|
|
188
|
+
<button
|
|
189
|
+
class="ghost"
|
|
190
|
+
data-set="bass"
|
|
191
|
+
data-val="true"
|
|
192
|
+
onclick={() => setSetting('bass', true)}>I have it with me</button
|
|
193
|
+
>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
{/if}
|
|
198
|
+
{#if hiddenCount}
|
|
199
|
+
<div class="sec">
|
|
200
|
+
<div class="card stack">
|
|
201
|
+
<p class="note">
|
|
202
|
+
{hiddenCount === 1 ? 'One more drill here is' : hiddenCount + ' more drills here are'} switched
|
|
203
|
+
off: movable do and scale degrees — hearing a note as <i>the fifth of this key</i> rather
|
|
204
|
+
than as
|
|
205
|
+
<i>G</i>. It is a second language, and worth starting once what is below feels easy.
|
|
206
|
+
</p>
|
|
207
|
+
{#if hasRoute('setup')}<div class="bar">
|
|
208
|
+
<button class="ghost" data-go="setup" onclick={() => go('setup')}
|
|
209
|
+
>Turn it on in Setup</button
|
|
210
|
+
>
|
|
211
|
+
</div>{/if}
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
{/if}
|
|
215
|
+
{@render extra?.(fam)}
|
|
216
|
+
|
|
217
|
+
{#if refs.length}
|
|
218
|
+
<div class="sec">
|
|
219
|
+
<h2>Read about it</h2>
|
|
220
|
+
{#each refs as link (link.route)}
|
|
221
|
+
<button class="drill ref" data-go={link.route} onclick={() => go(link.route)}
|
|
222
|
+
><span class="ic">{@html IC.book}</span><span
|
|
223
|
+
><span class="nm">{link.title}</span><br /><span class="sb">{link.blurb}</span></span
|
|
224
|
+
><span class="mast">→</span></button
|
|
225
|
+
>
|
|
226
|
+
{/each}
|
|
227
|
+
</div>
|
|
228
|
+
{/if}
|
|
229
|
+
|
|
230
|
+
<div class="sec">
|
|
231
|
+
<h2>Drills</h2>
|
|
232
|
+
<div class="drills">
|
|
233
|
+
{#each drills as def (def.id)}
|
|
234
|
+
<button
|
|
235
|
+
class="drill"
|
|
236
|
+
class:muted={!drillOn(def.id)}
|
|
237
|
+
data-drill={def.id}
|
|
238
|
+
onclick={() => startDrill(def.id, fam)}
|
|
239
|
+
><span class="ic">{@html IC[def.ic]}</span>
|
|
240
|
+
<span
|
|
241
|
+
><span class="nm"
|
|
242
|
+
>{def.nm}{#if !drillOn(def.id)}
|
|
243
|
+
<i class="offtag">not in the session</i>{/if}</span
|
|
244
|
+
>
|
|
245
|
+
<br /><span class="sb">{def.sb}</span></span
|
|
246
|
+
><span class="mast">{drillMastery(def.id).pct}%</span></button
|
|
247
|
+
>
|
|
248
|
+
{/each}
|
|
249
|
+
</div>
|
|
250
|
+
<div class="fold" class:open>
|
|
251
|
+
<button
|
|
252
|
+
class="foldhead"
|
|
253
|
+
data-fold={fam}
|
|
254
|
+
aria-expanded={open}
|
|
255
|
+
onclick={() => {
|
|
256
|
+
FOLD[fam] = !FOLD[fam];
|
|
257
|
+
}}
|
|
258
|
+
>In the daily session{offHere ? ' · ' + offHere + ' off' : ''}<span class="caret"
|
|
259
|
+
>{open ? '▴' : '▾'}</span
|
|
260
|
+
></button
|
|
261
|
+
>
|
|
262
|
+
{#if open}
|
|
263
|
+
<div class="levels" style="margin-top:10px">
|
|
264
|
+
{#each drills as def (def.id)}
|
|
265
|
+
<button
|
|
266
|
+
class="chip sm tog"
|
|
267
|
+
data-doff={def.id}
|
|
268
|
+
aria-pressed={drillOn(def.id)}
|
|
269
|
+
onclick={() => toggleDoff(def.id)}>{def.nm}</button
|
|
270
|
+
>
|
|
271
|
+
{/each}
|
|
272
|
+
</div>
|
|
273
|
+
<p class="note">
|
|
274
|
+
Switch one off and the daily session stops asking for it. It stays in the list above, so
|
|
275
|
+
you can still open it whenever you want to give it another go.
|
|
276
|
+
</p>
|
|
277
|
+
{/if}
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
{/if}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Every word the app uses at you, defined in one line each. The point is not
|
|
3
|
+
to teach the subject — the references do that — but to stop a single
|
|
4
|
+
unfamiliar word from making a whole drill unreadable. Written so that each
|
|
5
|
+
entry stands on its own: you arrive here having met one term, not to read
|
|
6
|
+
the list end to end.
|
|
7
|
+
-->
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
import { GLOSS } from '../refdata';
|
|
10
|
+
import RefBar from '../RefBar.svelte';
|
|
11
|
+
const { back }: { back?: { label: string; to: string } } = $props();
|
|
12
|
+
|
|
13
|
+
let search = $state('');
|
|
14
|
+
/** The text of an HTML snippet, tags dropped. */
|
|
15
|
+
const plain = (html: string) => html.replace(/<[^>]+>/g, '').replace(/&/g, '&');
|
|
16
|
+
const shown = $derived.by(() => {
|
|
17
|
+
const wanted = search.trim().toLowerCase();
|
|
18
|
+
return GLOSS.map((section) => ({
|
|
19
|
+
g: section.g,
|
|
20
|
+
items: section.items.filter(
|
|
21
|
+
([term, meaning]) =>
|
|
22
|
+
!wanted || (plain(term) + plain(meaning)).toLowerCase().includes(wanted),
|
|
23
|
+
),
|
|
24
|
+
}));
|
|
25
|
+
});
|
|
26
|
+
const none = $derived(shown.every((section) => !section.items.length));
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<RefBar title="Vocabulary" {back} resume={false} />
|
|
30
|
+
<div class="sec">
|
|
31
|
+
<div class="card stack">
|
|
32
|
+
<p class="note" style="margin:0">
|
|
33
|
+
Every term the drills use at you, one line each. Type to narrow it down.
|
|
34
|
+
</p>
|
|
35
|
+
<input
|
|
36
|
+
class="gfind"
|
|
37
|
+
id="glossq"
|
|
38
|
+
type="search"
|
|
39
|
+
placeholder="Search a word…"
|
|
40
|
+
aria-label="search the vocabulary"
|
|
41
|
+
autocomplete="off"
|
|
42
|
+
bind:value={search}
|
|
43
|
+
/>
|
|
44
|
+
<p class="note" id="glossnone" hidden={!none} style="margin:0">Nothing matches that.</p>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
{#each shown as section (section.g)}
|
|
48
|
+
<div class="sec gsec" hidden={!section.items.length}>
|
|
49
|
+
<h2>{section.g}</h2>
|
|
50
|
+
<div class="card stack">
|
|
51
|
+
{#each section.items as [term, meaning] (term)}<div class="gterm">
|
|
52
|
+
<b>{@html term}</b>
|
|
53
|
+
<p>{@html meaning}</p>
|
|
54
|
+
</div>{/each}
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
{/each}
|
|
58
|
+
<div class="sec">
|
|
59
|
+
<div class="card">
|
|
60
|
+
<p class="note" style="margin:0">
|
|
61
|
+
Still stuck on a word? The references go into each of these properly — this page is only here
|
|
62
|
+
so that one unfamiliar term cannot make a whole drill unreadable.
|
|
63
|
+
</p>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<!-- What an interval is: all twelve, what they sound like, and why one shape works in every key. -->
|
|
2
|
+
<script lang="ts">
|
|
3
|
+
import { IVS } from '../../music/intervals';
|
|
4
|
+
import { playInterval } from '../../audio/synth';
|
|
5
|
+
import RefBar from '../RefBar.svelte';
|
|
6
|
+
const { back }: { back?: { label: string; to: string } } = $props();
|
|
7
|
+
/** Every interval is played up from E3. */
|
|
8
|
+
const START_NOTE = 52;
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<RefBar title="Intervals" {back} />
|
|
12
|
+
<div class="sec">
|
|
13
|
+
<div class="card">
|
|
14
|
+
<p class="note">
|
|
15
|
+
An interval is the distance between two notes, counted in semitones — on bass, in frets. It is
|
|
16
|
+
the same distance wherever you play it, which is why one shape works in every key, and it is
|
|
17
|
+
what you are actually doing when you sing: you do not sing frets, you sing distances.
|
|
18
|
+
</p>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="sec">
|
|
22
|
+
<h2>The twelve</h2>
|
|
23
|
+
<div class="card stack">
|
|
24
|
+
{#each IVS as interval (interval.s)}
|
|
25
|
+
<div class="scrow on">
|
|
26
|
+
<button
|
|
27
|
+
class="chip sm"
|
|
28
|
+
data-ivplay={interval.s}
|
|
29
|
+
onclick={() => playInterval(START_NOTE, interval.s, false)}>{interval.ab}</button
|
|
30
|
+
>
|
|
31
|
+
<span class="ivs">{interval.n} · {interval.s}{interval.s === 1 ? ' fret' : ' frets'}</span>
|
|
32
|
+
<p class="note">{interval.c}</p>
|
|
33
|
+
</div>
|
|
34
|
+
{/each}
|
|
35
|
+
</div>
|
|
36
|
+
<p class="note">Tap any of them to hear it played from the same starting note.</p>
|
|
37
|
+
</div>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Every explanation in the app, in one place. They are still linked from the
|
|
3
|
+
family that uses them, but you should not have to remember which drill owns
|
|
4
|
+
an idea in order to go and read about it.
|
|
5
|
+
-->
|
|
6
|
+
<script lang="ts">
|
|
7
|
+
import { IC } from '../../notation/icons';
|
|
8
|
+
import { go, hasRoute } from '../../nav.svelte';
|
|
9
|
+
import { LEARN } from '../refdata';
|
|
10
|
+
const items = LEARN.filter((item) => hasRoute(item.go));
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<div class="sec">
|
|
14
|
+
<div class="card">
|
|
15
|
+
<p class="note" style="margin:0">
|
|
16
|
+
{items.length} references. Nothing here is a test — read them when a drill turns up something you
|
|
17
|
+
do not recognise, or when you want the idea before the practice.
|
|
18
|
+
</p>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="sec">
|
|
22
|
+
<h2>References</h2>
|
|
23
|
+
{#each items as item (item.go)}
|
|
24
|
+
<button class="drill ref" data-go={item.go} onclick={() => go(item.go)}
|
|
25
|
+
><span class="ic">{@html IC.book}</span><span
|
|
26
|
+
><span class="nm">{item.n}</span><br /><span class="sb">{item.sb}</span></span
|
|
27
|
+
><span class="mast">→</span></button
|
|
28
|
+
>
|
|
29
|
+
{/each}
|
|
30
|
+
</div>
|
|
31
|
+
{#if hasRoute('setup')}
|
|
32
|
+
<div class="sec">
|
|
33
|
+
<div class="card">
|
|
34
|
+
<p class="note" style="margin:0">
|
|
35
|
+
How the app decides what to ask you, and why bass is written an octave high, are under <b
|
|
36
|
+
>Setup</b
|
|
37
|
+
>.
|
|
38
|
+
</p>
|
|
39
|
+
<div class="bar" style="margin-top:8px">
|
|
40
|
+
<button class="ghost" onclick={() => go('setup')}>How this works</button>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
{/if}
|