@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,17 @@
|
|
|
1
|
+
<!-- The metronome on its own page. It keeps running while you move around the app. -->
|
|
2
|
+
<script lang="ts">
|
|
3
|
+
import MetronomePanel from '../MetronomePanel.svelte';
|
|
4
|
+
import BackBar from '../BackBar.svelte';
|
|
5
|
+
import { go } from '../../nav.svelte';
|
|
6
|
+
const { back = { label: 'Warm up', to: 'warmhome' } }: { back?: { label: string; to: string } } =
|
|
7
|
+
$props();
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<BackBar label={back.label} title="Metronome" sec onBack={() => go(back.to)} />
|
|
11
|
+
<div class="sec">
|
|
12
|
+
<MetronomePanel />
|
|
13
|
+
<p class="note" style="margin-top:10px">
|
|
14
|
+
It keeps running while you move around the app — the beats are scheduled on the audio clock, not
|
|
15
|
+
by the page.
|
|
16
|
+
</p>
|
|
17
|
+
</div>
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
One screen answers "what shall I do now": the session that picks for you,
|
|
3
|
+
and the families to go at by hand when you want to hammer one thing.
|
|
4
|
+
-->
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
import { honmoon, setSetting, queueSave } from '../../store/honmoon.svelte';
|
|
7
|
+
import { todayKey } from '../../util/text';
|
|
8
|
+
import { toggledIn } from '../../util/list';
|
|
9
|
+
import { micUsable } from '../../audio/mic';
|
|
10
|
+
import { bassHere } from '../../music/bass';
|
|
11
|
+
import { allCards } from '../../learn/session';
|
|
12
|
+
import { famMastery, streakDays, totals } from '../../learn/progress';
|
|
13
|
+
import {
|
|
14
|
+
appDrills,
|
|
15
|
+
appGroups,
|
|
16
|
+
famDrills,
|
|
17
|
+
famHidden,
|
|
18
|
+
famNoBass,
|
|
19
|
+
groupOff,
|
|
20
|
+
sessionGroups,
|
|
21
|
+
} from '../../drills/registry.svelte';
|
|
22
|
+
import { IC } from '../../notation/icons';
|
|
23
|
+
import { startSession } from '../../drills/runtime.svelte';
|
|
24
|
+
import { go } from '../../nav.svelte';
|
|
25
|
+
import Stat from '../Stat.svelte';
|
|
26
|
+
import BassRow from '../BassRow.svelte';
|
|
27
|
+
|
|
28
|
+
/** A rough guess at how long one card takes, for the "about N minutes" line. */
|
|
29
|
+
const SECONDS_PER_CARD = 8;
|
|
30
|
+
const SLENS = [
|
|
31
|
+
{ n: 15, label: 'Short' },
|
|
32
|
+
{ n: 30, label: 'Standard' },
|
|
33
|
+
{ n: 50, label: 'Long' },
|
|
34
|
+
];
|
|
35
|
+
const overall = $derived(totals());
|
|
36
|
+
const today = $derived(honmoon.daily[todayKey()] || { n: 0, c: 0 });
|
|
37
|
+
const sessionLength = $derived(honmoon.settings.slen || 30);
|
|
38
|
+
const poolSize = $derived(allCards().length);
|
|
39
|
+
const needsBass = $derived(appDrills().some((def) => def.bass));
|
|
40
|
+
const groups = sessionGroups();
|
|
41
|
+
const hasScales = appGroups().some((group) => group.k === 'scale');
|
|
42
|
+
|
|
43
|
+
function toggleGroup(group: string) {
|
|
44
|
+
const off = toggledIn(honmoon.settings.off || [], group);
|
|
45
|
+
if (off.length >= appGroups().length) {
|
|
46
|
+
return; // never switch everything off
|
|
47
|
+
}
|
|
48
|
+
honmoon.settings.off = off;
|
|
49
|
+
queueSave();
|
|
50
|
+
}
|
|
51
|
+
const plural = (count: number, word: string) => count + ' ' + word + (count === 1 ? '' : 's');
|
|
52
|
+
function open(group: string) {
|
|
53
|
+
if (group === 'scale') {
|
|
54
|
+
go('scales');
|
|
55
|
+
} else {
|
|
56
|
+
go('fam', { fam: group });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<div class="sec">
|
|
62
|
+
<Stat
|
|
63
|
+
items={[
|
|
64
|
+
{ v: today.n, k: 'Today' },
|
|
65
|
+
{ v: overall.n, k: 'All time' },
|
|
66
|
+
{ v: overall.n ? overall.acc + '%' : '—', k: 'Accuracy' },
|
|
67
|
+
{ v: streakDays(), k: 'Day streak' },
|
|
68
|
+
]}
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div class="sec">
|
|
73
|
+
<div class="card sesscard stack">
|
|
74
|
+
<div>
|
|
75
|
+
<div class="nm" style="font-family:var(--f-disp);font-size:22px;letter-spacing:.02em">
|
|
76
|
+
Daily session
|
|
77
|
+
</div>
|
|
78
|
+
<p class="note" style="margin:4px 0 0">
|
|
79
|
+
Your weakest cards from every family you leave switched on, mixed together. Miss one and it
|
|
80
|
+
comes back later in the same session.
|
|
81
|
+
</p>
|
|
82
|
+
</div>
|
|
83
|
+
<div class="levels">
|
|
84
|
+
{#each SLENS as length (length.n)}
|
|
85
|
+
<button
|
|
86
|
+
class="chip sm"
|
|
87
|
+
data-set="slen"
|
|
88
|
+
data-val={length.n}
|
|
89
|
+
aria-pressed={+sessionLength === length.n}
|
|
90
|
+
onclick={() => setSetting('slen', length.n)}>{length.label} · {length.n}</button
|
|
91
|
+
>
|
|
92
|
+
{/each}
|
|
93
|
+
</div>
|
|
94
|
+
{#if groups.length > 1}
|
|
95
|
+
<div>
|
|
96
|
+
<div class="hint" style="margin-bottom:6px">Include</div>
|
|
97
|
+
<div class="levels">
|
|
98
|
+
{#each groups as group (group.k)}
|
|
99
|
+
<button
|
|
100
|
+
class="chip sm tog"
|
|
101
|
+
data-grp={group.k}
|
|
102
|
+
aria-pressed={!groupOff(group.k)}
|
|
103
|
+
onclick={() => toggleGroup(group.k)}>{group.n}</button
|
|
104
|
+
>
|
|
105
|
+
{/each}
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
{/if}
|
|
109
|
+
{#if needsBass}<BassRow />{/if}
|
|
110
|
+
{#if needsBass && bassHere() && !micUsable()}<p class="note" style="margin:0">
|
|
111
|
+
With no microphone here, the three play-along drills ask you to mark yourself. They still
|
|
112
|
+
count.
|
|
113
|
+
</p>{/if}
|
|
114
|
+
<button class="start" id="startsess" onclick={() => startSession('theory')}
|
|
115
|
+
>Start session</button
|
|
116
|
+
>
|
|
117
|
+
<p class="note" style="text-align:center;margin:0">
|
|
118
|
+
{sessionLength} cards · about {Math.max(
|
|
119
|
+
1,
|
|
120
|
+
Math.round((sessionLength * SECONDS_PER_CARD) / 60),
|
|
121
|
+
)} minutes{poolSize && poolSize < sessionLength
|
|
122
|
+
? ' · only ' + poolSize + ' cards switched on, so some come round twice'
|
|
123
|
+
: ''}
|
|
124
|
+
</p>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<div class="sec">
|
|
129
|
+
<h2>Families</h2>
|
|
130
|
+
<div class="fams">
|
|
131
|
+
{#each appGroups() as group (group.k)}
|
|
132
|
+
{@const drills = famDrills(group.k)}
|
|
133
|
+
{@const progress = famMastery(group.k)}
|
|
134
|
+
{@const gated = famHidden(group.k)}
|
|
135
|
+
{@const noBass = famNoBass(group.k)}
|
|
136
|
+
<button class="fam" data-open={group.k} onclick={() => open(group.k)}>
|
|
137
|
+
<span class="ic">{@html IC[group.ic]}</span>
|
|
138
|
+
<span class="txt"
|
|
139
|
+
><span class="nm">{group.n}</span>
|
|
140
|
+
<span class="sb">{group.sb}</span>
|
|
141
|
+
<span class="meta"
|
|
142
|
+
>{plural(drills.length, 'drill')}{gated
|
|
143
|
+
? ' · ' + gated + ' more waiting in Setup'
|
|
144
|
+
: ''}{noBass ? ' · ' + noBass + ' waiting for the bass' : ''}{progress.pct
|
|
145
|
+
? ' · ' + progress.pct + '% solid'
|
|
146
|
+
: ''}</span
|
|
147
|
+
></span
|
|
148
|
+
>
|
|
149
|
+
<span class="mast">→</span></button
|
|
150
|
+
>
|
|
151
|
+
{/each}
|
|
152
|
+
</div>
|
|
153
|
+
<p class="note">
|
|
154
|
+
Switching a family off here takes it out of the daily session; it stays open to practise by
|
|
155
|
+
hand.{hasScales
|
|
156
|
+
? ' Scales also have a session of their own, on their page, which ignores this row.'
|
|
157
|
+
: ''}
|
|
158
|
+
</p>
|
|
159
|
+
</div>
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
<!-- How rhythm is written: values, dots, ties, rests, bars, time signatures, and counting out loud. -->
|
|
2
|
+
<script lang="ts">
|
|
3
|
+
import { METERS, RVALS, beatsText, type Bar } from '../../music/rhythm';
|
|
4
|
+
import { rhOneSVG, tsGlyphSVG } from '../../notation/rhythmsvg';
|
|
5
|
+
import { playMeter } from '../../audio/synth';
|
|
6
|
+
import RhythmBar from '../RhythmBar.svelte';
|
|
7
|
+
import RefBar from '../RefBar.svelte';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
back?: { label: string; to: string };
|
|
11
|
+
/** where the drop-out clock lives, for the last paragraph */
|
|
12
|
+
clockWhere?: string;
|
|
13
|
+
}
|
|
14
|
+
const { back, clockWhere = 'Mira (the bass app), under Warm up,' }: Props = $props();
|
|
15
|
+
|
|
16
|
+
/** An example bar from its events. Compound time groups in dotted quarters (6 sixteenths). */
|
|
17
|
+
const bar = (ts: [number, number], events: Bar['ev'], compound = false): Bar => ({
|
|
18
|
+
ts,
|
|
19
|
+
u: events.reduce((sum, event) => sum + event.u, 0),
|
|
20
|
+
g: compound ? 6 : 4,
|
|
21
|
+
cmp: compound,
|
|
22
|
+
ev: events,
|
|
23
|
+
});
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<RefBar title="Reading rhythm" {back} />
|
|
27
|
+
|
|
28
|
+
<div class="sec">
|
|
29
|
+
<h2>The line is time, not pitch</h2>
|
|
30
|
+
<div class="card stack">
|
|
31
|
+
<p class="note">
|
|
32
|
+
On a rhythm staff there is one line and it means nothing about how high the note is — only
|
|
33
|
+
when it happens and how long it lasts. This is worth learning on its own, because rhythm is
|
|
34
|
+
the half of reading that carries a part. A bassist who reads rhythm perfectly and pitch
|
|
35
|
+
approximately will hold a band together; the reverse will not.
|
|
36
|
+
</p>
|
|
37
|
+
<p class="note">
|
|
38
|
+
Every value below is a whole number of sixteenths, and that is the only arithmetic reading
|
|
39
|
+
ever needs.
|
|
40
|
+
</p>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div class="sec">
|
|
45
|
+
<h2>The values</h2>
|
|
46
|
+
<div class="card">
|
|
47
|
+
<table class="opens">
|
|
48
|
+
<tbody>
|
|
49
|
+
{#each RVALS as value (value.u)}
|
|
50
|
+
<tr
|
|
51
|
+
><td style="width:72px">{@html rhOneSVG(value.u, false, 62)}</td>
|
|
52
|
+
<td><b>{value.n}</b><br /><span class="hint">{beatsText(value.u)} in 4/4</span></td>
|
|
53
|
+
<td style="width:72px">{@html rhOneSVG(value.u, true, 62)}</td></tr
|
|
54
|
+
>
|
|
55
|
+
{/each}
|
|
56
|
+
</tbody>
|
|
57
|
+
</table>
|
|
58
|
+
<p class="note" style="margin-top:8px">
|
|
59
|
+
Left: the note. Right: the rest of the same length — the same amount of time, deliberately not
|
|
60
|
+
played. Rests are music, and reading them accurately is what stops a line turning to mush.
|
|
61
|
+
</p>
|
|
62
|
+
<p class="note">
|
|
63
|
+
The whole rest and the half rest are the same little block; the only difference is which side
|
|
64
|
+
of the line it sits on. The whole rest hangs below, the half rest sits on top. Everyone mixes
|
|
65
|
+
them up at first.
|
|
66
|
+
</p>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div class="sec">
|
|
71
|
+
<h2>The dot</h2>
|
|
72
|
+
<div class="card">
|
|
73
|
+
<RhythmBar bar={bar([4, 4], [{ u: 6 }, { u: 2 }, { u: 4 }, { u: 4 }])} count={true} />
|
|
74
|
+
<p class="note" style="margin-top:8px">
|
|
75
|
+
A dot after a note adds half of it again: a dotted quarter is a quarter plus an eighth, a beat
|
|
76
|
+
and a half. That is where nearly every syncopation in pop and rock comes from — the second
|
|
77
|
+
note lands on the <i>and</i>, and the phrase starts pushing.
|
|
78
|
+
</p>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div class="sec">
|
|
83
|
+
<h2>The tie</h2>
|
|
84
|
+
<div class="card">
|
|
85
|
+
<RhythmBar
|
|
86
|
+
bar={bar([4, 4], [{ u: 2 }, { u: 2 }, { u: 2, tie: true }, { u: 2 }, { u: 4 }, { u: 4 }])}
|
|
87
|
+
count={true}
|
|
88
|
+
/>
|
|
89
|
+
<p class="note" style="margin-top:8px">
|
|
90
|
+
A curved line joining two noteheads means play the first and hold through the second: one
|
|
91
|
+
sound, both lengths. It exists so a note can cross a beat or a barline while the beats stay
|
|
92
|
+
visible underneath. Counting a tie out loud, you say the number for the tied-to note but do
|
|
93
|
+
not attack it.
|
|
94
|
+
</p>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div class="sec">
|
|
99
|
+
<h2>Counting out loud</h2>
|
|
100
|
+
<div class="card stack">
|
|
101
|
+
<RhythmBar
|
|
102
|
+
bar={bar(
|
|
103
|
+
[4, 4],
|
|
104
|
+
[{ u: 1 }, { u: 1 }, { u: 1 }, { u: 1 }, { u: 2 }, { u: 2 }, { u: 4 }, { u: 2 }, { u: 2 }],
|
|
105
|
+
)}
|
|
106
|
+
count={true}
|
|
107
|
+
/>
|
|
108
|
+
<p class="note">
|
|
109
|
+
Four sixteenths in a beat are counted <b>1 e & a</b>. Eighths are <b>1 &</b>. A
|
|
110
|
+
quarter is just <b>1</b>. Say it aloud while you tap: the syllable tells you where you are
|
|
111
|
+
inside the beat without counting anything, and it is the fastest route from the page to your
|
|
112
|
+
hands.
|
|
113
|
+
</p>
|
|
114
|
+
<p class="note">
|
|
115
|
+
This is also why notes shorter than a quarter are beamed in groups of one beat. The beams are
|
|
116
|
+
not decoration — they draw the beat for you, so you can see at a glance that four sixteenths
|
|
117
|
+
belong to beat 3.
|
|
118
|
+
</p>
|
|
119
|
+
<p class="note">
|
|
120
|
+
<b>Writing a counting down.</b> The Count the bar drill writes the whole bar out in a row, one
|
|
121
|
+
slot for each step of it, in order: the syllable where a note starts and a <b>·</b> where
|
|
122
|
+
nothing does. So a bar of four quarter notes is
|
|
123
|
+
<span style="font-family:var(--f-mono)">1 2 3 4</span>, and a bar that plays only on the first
|
|
124
|
+
beat and the <i>and</i> of three is
|
|
125
|
+
<span style="font-family:var(--f-mono)">1 · · · · · & ·</span>. Listing only the syllables
|
|
126
|
+
you play would be shorter, but two different bars can produce the same short list — the dots
|
|
127
|
+
are what pin each note to its place.
|
|
128
|
+
</p>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div class="sec">
|
|
133
|
+
<h2>Bars and time signatures</h2>
|
|
134
|
+
<div class="card stack">
|
|
135
|
+
<p class="note">
|
|
136
|
+
Music is counted in equal packets called <b>bars</b>, and the vertical line at the end of one
|
|
137
|
+
is the <b>barline</b>. The first beat of every bar is the strong one — it is where a phrase
|
|
138
|
+
lands, where a chord usually changes, and where the bass usually plays the root. Nearly
|
|
139
|
+
everything about feel comes from knowing where beat 1 is.
|
|
140
|
+
</p>
|
|
141
|
+
<p class="note">
|
|
142
|
+
The pair of numbers at the start says how those packets are built. It looks like a fraction
|
|
143
|
+
but it is not one; it is two separate facts stacked up:
|
|
144
|
+
</p>
|
|
145
|
+
<div class="tsrow">
|
|
146
|
+
<div class="tsnum">{@html tsGlyphSVG([4, 4], 62)}</div>
|
|
147
|
+
<div>
|
|
148
|
+
<p class="note" style="margin:0">
|
|
149
|
+
<b>The bottom number</b> names the note value that counts as one beat. 4 means a quarter
|
|
150
|
+
note, 8 means an eighth note, 2 means a half note. It is the <i>denominator</i> in the sense
|
|
151
|
+
that 4 means “quarter”, and that is the only sense in which any of this is a fraction.
|
|
152
|
+
</p>
|
|
153
|
+
<p class="note"><b>The top number</b> says how many of those fit in a bar.</p>
|
|
154
|
+
<p class="note" style="margin-bottom:0">
|
|
155
|
+
So <b>4/4</b> is four quarter-note beats to a bar, and <b>3/4</b> is three. Same beat, different
|
|
156
|
+
sized packet — and that one difference is what separates a rock song from a waltz.
|
|
157
|
+
</p>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<div class="sec">
|
|
164
|
+
<h2>The ones you will meet</h2>
|
|
165
|
+
<div class="card stack">
|
|
166
|
+
<p class="note">
|
|
167
|
+
Tap any of them to hear two bars of the bare pulse. The accent is on beat 1 — listen for how
|
|
168
|
+
far apart the accents are, because that <i>is</i> the time signature.
|
|
169
|
+
</p>
|
|
170
|
+
{#each METERS as meter (meter.n)}
|
|
171
|
+
<div class="tsrow">
|
|
172
|
+
<div class="tsnum">{@html tsGlyphSVG(meter.ts, 54)}</div>
|
|
173
|
+
<div style="flex:1;min-width:0">
|
|
174
|
+
<div class="setrow" style="border:none;padding:0">
|
|
175
|
+
<span class="lbl">{meter.n}</span>
|
|
176
|
+
<button
|
|
177
|
+
class="chip sm"
|
|
178
|
+
data-meter={meter.ts.join('/')}
|
|
179
|
+
onclick={() => playMeter(meter.ts)}>♪ Hear it</button
|
|
180
|
+
>
|
|
181
|
+
</div>
|
|
182
|
+
<p class="note" style="margin:4px 0 0">{@html meter.d}</p>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
{/each}
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<div class="sec">
|
|
190
|
+
<h2>Counting a bar of 3/4</h2>
|
|
191
|
+
<div class="card stack">
|
|
192
|
+
<RhythmBar bar={bar([3, 4], [{ u: 4 }, { u: 2 }, { u: 2 }, { u: 4 }])} count={true} />
|
|
193
|
+
<p class="note">
|
|
194
|
+
Three beats, so the counting only ever reaches 3 before the barline comes round and it starts
|
|
195
|
+
again at 1. A bar of 3/4 that adds up to four beats is simply wrong — that is the rule the
|
|
196
|
+
Complete the bar drill is testing.
|
|
197
|
+
</p>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<div class="sec">
|
|
202
|
+
<h2>Why 6/8 is different</h2>
|
|
203
|
+
<div class="card stack">
|
|
204
|
+
<RhythmBar
|
|
205
|
+
bar={bar([6, 8], [{ u: 2 }, { u: 2 }, { u: 2 }, { u: 4 }, { u: 2 }], true)}
|
|
206
|
+
count={true}
|
|
207
|
+
/>
|
|
208
|
+
<p class="note">
|
|
209
|
+
Read literally, 6/8 is six eighth-note beats in a bar. Nobody counts it that way above a
|
|
210
|
+
crawl. In the ear it is <b>two</b> beats that each divide into <b>three</b> — the accents fall
|
|
211
|
+
on 1 and 4 — and that is what makes it roll rather than march. Time signatures that work like
|
|
212
|
+
this are called <i>compound</i>; 6/8, 9/8 and 12/8 are the common ones.
|
|
213
|
+
</p>
|
|
214
|
+
<p class="note">
|
|
215
|
+
The quick test: if the top number is 6, 9 or 12 over an 8, divide it by three to get the real
|
|
216
|
+
number of beats. Count the eighths out loud at first — 1 2 3 4 5 6 — and let the weight settle
|
|
217
|
+
on 1 and 4 until the two-beat feel takes over.
|
|
218
|
+
</p>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<div class="sec">
|
|
223
|
+
<h2>How to practise it</h2>
|
|
224
|
+
<div class="card stack">
|
|
225
|
+
<p class="note">
|
|
226
|
+
<b>Say it before you play it.</b> Count the bar out loud with the metronome until it is automatic.
|
|
227
|
+
Only then put it on the instrument — sight-reading fails far more often on rhythm than on notes.
|
|
228
|
+
</p>
|
|
229
|
+
<p class="note">
|
|
230
|
+
<b>Subdivide even when nothing is subdivided.</b> If the bar is four quarter notes, still feel the
|
|
231
|
+
eighths underneath. That is what keeps long notes from shrinking.
|
|
232
|
+
</p>
|
|
233
|
+
<p class="note">
|
|
234
|
+
<b>Practise the silence.</b> The Internal clock exercise in {clockWhere} takes the metronome away
|
|
235
|
+
for a few bars at a time. Holding tempo through silence is the skill; playing along with a click
|
|
236
|
+
is only the test.
|
|
237
|
+
</p>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
<!-- The Scales family: its own session, the drills, where you stand, and which scales are in. -->
|
|
2
|
+
<script lang="ts">
|
|
3
|
+
import { honmoon, setSetting, queueSave } from '../../store/honmoon.svelte';
|
|
4
|
+
import { bassHere } from '../../music/bass';
|
|
5
|
+
import {
|
|
6
|
+
SCALES,
|
|
7
|
+
SGROUPS,
|
|
8
|
+
degPretty,
|
|
9
|
+
enabledScales,
|
|
10
|
+
scaleCards,
|
|
11
|
+
scaleOn,
|
|
12
|
+
} from '../../music/scales';
|
|
13
|
+
import { IC } from '../../notation/icons';
|
|
14
|
+
import { scaleDrillsInApp, sdrillOn } from '../../drills/registry.svelte';
|
|
15
|
+
import { startDrill, startSession } from '../../drills/runtime.svelte';
|
|
16
|
+
import { go, hasRoute } from '../../nav.svelte';
|
|
17
|
+
import { toggledIn } from '../../util/list';
|
|
18
|
+
import { scaleMatrix, scaleStats } from '../scaleboard';
|
|
19
|
+
import Stat from '../Stat.svelte';
|
|
20
|
+
import BassRow from '../BassRow.svelte';
|
|
21
|
+
|
|
22
|
+
const scaleDrills = scaleDrillsInApp();
|
|
23
|
+
const enabled = $derived(enabledScales());
|
|
24
|
+
const pairs = $derived(scaleCards().length);
|
|
25
|
+
const summary = $derived(scaleStats());
|
|
26
|
+
const needsBass = scaleDrills.some((def) => def.bass);
|
|
27
|
+
/* which groups of scales mean anything for this app's drills: arpeggios
|
|
28
|
+
and the rest are always in play, so every group is shown */
|
|
29
|
+
function toggleSdrill(id: string) {
|
|
30
|
+
const off = toggledIn(honmoon.settings.sdoff || [], id);
|
|
31
|
+
if (scaleDrills.every((def) => off.includes(def.id))) {
|
|
32
|
+
return; // never switch every scale drill off
|
|
33
|
+
}
|
|
34
|
+
honmoon.settings.sdoff = off;
|
|
35
|
+
queueSave();
|
|
36
|
+
}
|
|
37
|
+
function toggleScale(id: string) {
|
|
38
|
+
honmoon.settings.sen = toggledIn(honmoon.settings.sen || [], id);
|
|
39
|
+
queueSave();
|
|
40
|
+
}
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<div class="sec">
|
|
44
|
+
<div class="bar">
|
|
45
|
+
<button class="ghost" id="back" onclick={() => go('home')}>← Practice</button>
|
|
46
|
+
<span class="spacer"></span><span class="tick">Scales</span>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div class="sec">
|
|
51
|
+
<div class="card sesscard stack">
|
|
52
|
+
<div>
|
|
53
|
+
<div style="font-family:var(--f-disp);font-size:22px;letter-spacing:.02em">Scale session</div>
|
|
54
|
+
<p class="note" style="margin:4px 0 0">
|
|
55
|
+
Nothing but scale-and-key pairs, for when you want a run of them. They also turn up in the
|
|
56
|
+
daily session unless you switch the family off there.
|
|
57
|
+
</p>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="levels">
|
|
60
|
+
{#each [10, 20, 30] as n (n)}
|
|
61
|
+
<button
|
|
62
|
+
class="chip sm"
|
|
63
|
+
data-set="sslen"
|
|
64
|
+
data-val={n}
|
|
65
|
+
aria-pressed={+(honmoon.settings.sslen || 20) === n}
|
|
66
|
+
onclick={() => setSetting('sslen', n)}>{n}</button
|
|
67
|
+
>
|
|
68
|
+
{/each}
|
|
69
|
+
</div>
|
|
70
|
+
<div>
|
|
71
|
+
<div class="hint" style="margin-bottom:6px">Drills</div>
|
|
72
|
+
<div class="levels">
|
|
73
|
+
{#each scaleDrills as def (def.id)}
|
|
74
|
+
{@const away = def.bass && !bassHere()}
|
|
75
|
+
<button
|
|
76
|
+
class="chip sm tog"
|
|
77
|
+
data-sdrill={def.id}
|
|
78
|
+
aria-pressed={sdrillOn(def.id) && !away}
|
|
79
|
+
disabled={away}
|
|
80
|
+
style={away ? 'opacity:.5' : ''}
|
|
81
|
+
onclick={() => toggleSdrill(def.id)}>{def.nm}{def.bass ? ' ♪' : ''}</button
|
|
82
|
+
>
|
|
83
|
+
{/each}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
{#if needsBass}<BassRow />{/if}
|
|
87
|
+
<div>
|
|
88
|
+
<div class="hint" style="margin-bottom:6px">Roots</div>
|
|
89
|
+
<div class="levels">
|
|
90
|
+
<button
|
|
91
|
+
class="chip sm"
|
|
92
|
+
data-set="sroots"
|
|
93
|
+
data-val="nat"
|
|
94
|
+
aria-pressed={honmoon.settings.sroots !== 'all'}
|
|
95
|
+
onclick={() => setSetting('sroots', 'nat')}>Natural only</button
|
|
96
|
+
>
|
|
97
|
+
<button
|
|
98
|
+
class="chip sm"
|
|
99
|
+
data-set="sroots"
|
|
100
|
+
data-val="all"
|
|
101
|
+
aria-pressed={honmoon.settings.sroots === 'all'}
|
|
102
|
+
onclick={() => setSetting('sroots', 'all')}>All twelve</button
|
|
103
|
+
>
|
|
104
|
+
{#if needsBass || scaleDrills.some((def) => def.id === 'scbuild' || def.id === 'scwrong' || def.id === 'scname')}
|
|
105
|
+
<button
|
|
106
|
+
class="chip sm"
|
|
107
|
+
data-set="sstart"
|
|
108
|
+
data-val="both"
|
|
109
|
+
aria-pressed={honmoon.settings.sstart === 'both'}
|
|
110
|
+
onclick={() => setSetting('sstart', 'both')}>Either string</button
|
|
111
|
+
>
|
|
112
|
+
<button
|
|
113
|
+
class="chip sm"
|
|
114
|
+
data-set="sstart"
|
|
115
|
+
data-val="E"
|
|
116
|
+
aria-pressed={honmoon.settings.sstart === 'E'}
|
|
117
|
+
onclick={() => setSetting('sstart', 'E')}>E only</button
|
|
118
|
+
>
|
|
119
|
+
<button
|
|
120
|
+
class="chip sm"
|
|
121
|
+
data-set="sstart"
|
|
122
|
+
data-val="A"
|
|
123
|
+
aria-pressed={honmoon.settings.sstart === 'A'}
|
|
124
|
+
onclick={() => setSetting('sstart', 'A')}>A only</button
|
|
125
|
+
>
|
|
126
|
+
{/if}
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
<button class="start" id="startscale" onclick={() => startSession('scales')}
|
|
130
|
+
>Start scale session</button
|
|
131
|
+
>
|
|
132
|
+
<p class="note" style="text-align:center;margin:0">
|
|
133
|
+
{enabled.length} scales · {pairs} scale-and-key pairs in rotation
|
|
134
|
+
</p>
|
|
135
|
+
{#if !enabled.length}<p class="err">Switch at least one scale on below.</p>{/if}
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
{#if hasRoute('sx')}
|
|
140
|
+
<div class="sec">
|
|
141
|
+
<button class="drill ref" data-go="sx" onclick={() => go('sx')}
|
|
142
|
+
><span class="ic">{@html IC.book}</span>
|
|
143
|
+
<span
|
|
144
|
+
><span class="nm">Scale explorer</span><br /><span class="sb"
|
|
145
|
+
>Every scale in every key on the neck, with degrees, fingerings and the notes spelled
|
|
146
|
+
properly.</span
|
|
147
|
+
></span
|
|
148
|
+
>
|
|
149
|
+
<span class="mast">→</span></button
|
|
150
|
+
>
|
|
151
|
+
</div>
|
|
152
|
+
{/if}
|
|
153
|
+
|
|
154
|
+
<div class="sec">
|
|
155
|
+
<h2>Where you stand</h2>
|
|
156
|
+
<Stat
|
|
157
|
+
items={[
|
|
158
|
+
{ v: summary.seen, k: 'Met' },
|
|
159
|
+
{ v: summary.solid, k: 'Solid' },
|
|
160
|
+
{ v: summary.ans, k: 'Answers' },
|
|
161
|
+
{ v: summary.ans ? summary.acc + '%' : '—', k: 'Accuracy' },
|
|
162
|
+
]}
|
|
163
|
+
/>
|
|
164
|
+
<div class="card" style="margin-top:10px">
|
|
165
|
+
{@html scaleMatrix()}
|
|
166
|
+
<div class="legend" style="margin-top:10px">
|
|
167
|
+
<span><b class="m0"></b>not met</span><span><b class="m1"></b>shaky</span>
|
|
168
|
+
<span><b class="m3"></b>getting there</span><span><b class="m5"></b>solid</span>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
<p class="note">
|
|
172
|
+
One square per scale in one key — {summary.total} of them in rotation. Squares you miss come back
|
|
173
|
+
sooner and more often, exactly as they do on the Drills side. This is the same spaced repetition,
|
|
174
|
+
just over scales.
|
|
175
|
+
</p>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<div class="sec">
|
|
179
|
+
<h2>Single drills</h2>
|
|
180
|
+
<div class="drills">
|
|
181
|
+
{#each scaleDrills as def (def.id)}
|
|
182
|
+
<button class="drill" data-drill={def.id} onclick={() => startDrill(def.id, 'scale')}
|
|
183
|
+
><span class="ic">{@html IC[def.ic]}</span>
|
|
184
|
+
<span><span class="nm">{def.nm}</span><br /><span class="sb">{def.sb}</span></span><span
|
|
185
|
+
class="mast">→</span
|
|
186
|
+
></button
|
|
187
|
+
>
|
|
188
|
+
{/each}
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
{#each SGROUPS as group (group)}
|
|
193
|
+
<div class="sec">
|
|
194
|
+
<h2>{group}</h2>
|
|
195
|
+
<div class="stack">
|
|
196
|
+
{#each SCALES.filter((scale) => scale.g === group) as scale (scale.id)}
|
|
197
|
+
<div class="scrow" class:on={scaleOn(scale.id)}>
|
|
198
|
+
<button
|
|
199
|
+
class="chip sm tog"
|
|
200
|
+
data-scale={scale.id}
|
|
201
|
+
aria-pressed={scaleOn(scale.id)}
|
|
202
|
+
onclick={() => toggleScale(scale.id)}>{scale.n}</button
|
|
203
|
+
>
|
|
204
|
+
<span class="ivs">{scale.dg.map(degPretty).join(' ')}</span>
|
|
205
|
+
<p class="note">{scale.d}</p>
|
|
206
|
+
</div>
|
|
207
|
+
{/each}
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
{/each}
|