diff --git a/src/components/diagrams/ChordDiagram.svelte b/src/components/diagrams/ChordDiagram.svelte new file mode 100644 index 0000000..27030e1 --- /dev/null +++ b/src/components/diagrams/ChordDiagram.svelte @@ -0,0 +1,161 @@ + + +
+ + + {#each shape.frets as f, i} + {#if f === -1} + + {:else if f === 0} + + {/if} + {/each} + + + {#if showNut} + + {:else} + {startFret} + {/if} + + + {#each Array(ROWS + 1) as _, r} + + {/each} + + {#each Array(STRINGS) as _, i} + + {/each} + + + {#each dots as d} + + {#if d.finger} + {d.finger} + {/if} + {/each} + +
+ {label ?? shape.name} + {#if positionLabel}{positionLabel}{/if} +
+
+ + diff --git a/src/components/diagrams/Fretboard.svelte b/src/components/diagrams/Fretboard.svelte new file mode 100644 index 0000000..abb0d90 --- /dev/null +++ b/src/components/diagrams/Fretboard.svelte @@ -0,0 +1,157 @@ + + +
+ + + {#each INLAYS as f} + {#if f > fromFret && f <= toFret} + + {/if} + {/each} + + + {#each Array(fretCount + 1) as _, i} + + {/each} + + + {#each Array(STRINGS) as _, s} + + {STRING_LABELS[s]} + {/each} + + + {#each Array(fretCount + 1) as _, i} + {#if (fromFret + i) % 2 === 1 || fromFret + i === 12} + {fromFret + i} + {/if} + {/each} + + + {#each visible as p (keyOf(p))} + + + {#if labels} + {pcAt(p.string, p.fret)} + {/if} + + {/each} + +
+ + diff --git a/src/components/views/CagedView.svelte b/src/components/views/CagedView.svelte new file mode 100644 index 0000000..0f77e4b --- /dev/null +++ b/src/components/views/CagedView.svelte @@ -0,0 +1,139 @@ + + + +
+ Key +
+ {#each KEYS as k} + + {/each} +
+
+ +
+ +
+ {shape.form} + {shape.positionLabel} +
+ +
+ + + +

Amber notes are the root ({key}). Step through all five shapes to cover the neck.

+
+ + diff --git a/src/components/views/ChangesView.svelte b/src/components/views/ChangesView.svelte new file mode 100644 index 0000000..73d6f09 --- /dev/null +++ b/src/components/views/ChangesView.svelte @@ -0,0 +1,199 @@ + + + +
+ {#each [pair.a, pair.b] as name} + {@const shape = getChordShape(name)} + {#if shape} + + {/if} + {/each} +
+ +
+
+ Time + {timeLeft}s +
+
+ Best + {best || '—'} +
+
+ + + + {#if beatBest} +

New best — {count} changes!

+ {/if} + +
+ {#if running} + + {:else} + + {/if} +
+ +
+
+ Chord pair + +
+
+ {#each CHANGE_PAIRS as p} + + {/each} +
+
+
+ + diff --git a/src/components/views/DiatonicView.svelte b/src/components/views/DiatonicView.svelte new file mode 100644 index 0000000..8da816f --- /dev/null +++ b/src/components/views/DiatonicView.svelte @@ -0,0 +1,119 @@ + + + +
+ Key +
+ {#each KEYS as k} + + {/each} +
+
+ +
+ {#each chords as c} + {@const shape = getChordShape(c.name)} +
+ {c.degree} + {#if shape} + + {:else} +
+ {c.name} + {c.quality} +
+ {/if} +
+ {/each} +
+
+ + diff --git a/src/components/views/IntervalsView.svelte b/src/components/views/IntervalsView.svelte new file mode 100644 index 0000000..d6ebdd2 --- /dev/null +++ b/src/components/views/IntervalsView.svelte @@ -0,0 +1,125 @@ + + + +
+ Root +
+ {#each CHROMATIC_ROOTS as r} + + {/each} +
+
+ +
+ Interval +
+ {#each INTERVALS as iv} + + {/each} +
+
+ +

+ root {root} + {interval.label} {target} +

+ + +
+ + diff --git a/src/components/views/NotePrompterView.svelte b/src/components/views/NotePrompterView.svelte new file mode 100644 index 0000000..91747d4 --- /dev/null +++ b/src/components/views/NotePrompterView.svelte @@ -0,0 +1,181 @@ + + + +
+ String +
+ {#each STRING_LABELS as label, i} + + {/each} +
+
+ +
+
+ Fret + {fret} +
+
+ Note + {revealed ? note : '·'} +
+
+ + + + + +
+ + +
+
+ + diff --git a/src/components/views/PickingView.svelte b/src/components/views/PickingView.svelte index 42faddb..3081e86 100644 --- a/src/components/views/PickingView.svelte +++ b/src/components/views/PickingView.svelte @@ -8,6 +8,8 @@ import DrillLayout from '../common/DrillLayout.svelte' import Transport from '../metronome/Transport.svelte' import PickingLane from '../patterns/PickingLane.svelte' + import ChordDiagram from '../diagrams/ChordDiagram.svelte' + import { getChordShape } from '../../lib/music/chords' interface Props { drill: PickingDrill @@ -89,10 +91,15 @@ {#if drill.chordLabels && drill.chordLabels.length}
- Chords{drill.chordLabels.length > 1 ? ' (per bar)' : ''} + Chords{drill.chordLabels.length > 1 ? ' (one per bar)' : ''}
{#each drill.chordLabels as c} - {c} + {@const shape = getChordShape(c)} + {#if shape} + + {:else} + {c} + {/if} {/each}
diff --git a/src/components/views/ReferenceToneView.svelte b/src/components/views/ReferenceToneView.svelte new file mode 100644 index 0000000..c0e7e0b --- /dev/null +++ b/src/components/views/ReferenceToneView.svelte @@ -0,0 +1,102 @@ + + + +
+ {noteName}{octave} +
+ + + +
+ Target note +
+ {#each CHROMATIC_ROOTS as n} + + {/each} +
+
+ +
+ Octave +
+ {#each OCTAVES as o} + + {/each} +
+
+ +

Play the tone, then bend up to match it. Hold your bend against the ringing reference.

+
+ + diff --git a/src/components/views/TriadsView.svelte b/src/components/views/TriadsView.svelte new file mode 100644 index 0000000..1f1df66 --- /dev/null +++ b/src/components/views/TriadsView.svelte @@ -0,0 +1,152 @@ + + + +
+
+ Root +
+ {#each CHROMATIC_ROOTS as r} + + {/each} +
+
+
+
+ Quality +
+ + +
+
+
+ String set +
+ {#each STRING_SETS as s} + + {/each} +
+
+
+
+ +
+ +
+ +
+ +
+ +
+ + +
+
+ + diff --git a/src/drills.ts b/src/drills.ts index 9cd9be4..b8767f2 100644 --- a/src/drills.ts +++ b/src/drills.ts @@ -81,8 +81,37 @@ export interface PickingDrill extends DrillBase, Meter { chordLabels?: string[] } -// Union grows as later phases add kinds (chords, fretboard, drone, info…). -export type Drill = StrumDrill | PickingDrill +/** R4 — one-minute chord changes (countdown + tap counter, no metronome). */ +export interface ChangesDrill extends DrillBase { + kind: 'changes' +} + +/** Theory + reference drills; most own their own state in the view. */ +export interface TheoryDrill extends DrillBase { + kind: + | 'note-prompter' // T1 + | 'caged' // T2 + | 'triads' // T3 + | 'diatonic' // T4 + | 'intervals' // T5 + | 'mode-drone' // T6 + | 'reference-tone' // L4 + defaultBpm?: number +} + +/** Simple description pages (E1–E3); some carry a drone. */ +export interface InfoDrill extends DrillBase { + kind: 'info' + body: string[] + hasDrone?: boolean +} + +export type Drill = + | StrumDrill + | PickingDrill + | ChangesDrill + | TheoryDrill + | InfoDrill /** Drills that run the metronome and persist a BPM. */ export type ClickDrill = StrumDrill | PickingDrill @@ -388,9 +417,77 @@ const L3: PickingDrill = { ], } +// R4 — one-minute chord changes +const R4: ChangesDrill = { + code: 'R4', + name: 'One-minute chord changes', + category: 'Rhythm', + description: 'Pick a chord pair and count clean changes in 60 seconds. Beat your best.', + kind: 'changes', +} + +// ============================================================================== +// THEORY // ============================================================================== -export const DRILLS: Drill[] = [R1, R2, R3, R5, F1, F2, F3, L1, L2, L3] +const T1: TheoryDrill = { + code: 'T1', + name: 'Name every note on one string', + category: 'Theory', + description: 'The metronome steps up a string fret by fret. Say the note before it reveals.', + kind: 'note-prompter', + defaultBpm: 60, +} +const T2: TheoryDrill = { + code: 'T2', + name: 'CAGED shapes', + category: 'Theory', + description: 'Walk the five major-chord shapes up the neck for a chosen key.', + kind: 'caged', +} +const T3: TheoryDrill = { + code: 'T3', + name: 'Triads on a 3-string set', + category: 'Theory', + description: 'Step through the three inversions of a triad on a string set.', + kind: 'triads', + defaultBpm: 60, +} +const T4: TheoryDrill = { + code: 'T4', + name: 'Diatonic chords of a key', + category: 'Theory', + description: 'The seven chords of a key with their degree, quality, and shape.', + kind: 'diatonic', +} +const T5: TheoryDrill = { + code: 'T5', + name: 'Intervals from a root', + category: 'Theory', + description: 'See a root and an interval shape on adjacent strings.', + kind: 'intervals', +} + +// ============================================================================== +// LEAD — reference +// ============================================================================== + +const L4: TheoryDrill = { + code: 'L4', + name: 'Bends and vibrato', + category: 'Lead', + description: 'Play a target pitch and match your bend to it by ear.', + kind: 'reference-tone', +} + +// ============================================================================== + +export const DRILLS: Drill[] = [ + R1, R2, R3, R4, R5, + F1, F2, F3, + T1, T2, T3, T4, T5, + L1, L2, L3, L4, +] export const DRILLS_BY_CODE: Record = Object.fromEntries( DRILLS.map((d) => [d.code, d]), diff --git a/src/lib/audio/tone.ts b/src/lib/audio/tone.ts new file mode 100644 index 0000000..abf52b4 --- /dev/null +++ b/src/lib/audio/tone.ts @@ -0,0 +1,44 @@ +// Short sustained reference tone (for L4 bend checks and T1 note reveals). +// Reuses the shared AudioContext; a plucked-ish envelope on a filtered sawtooth. + +import { audio } from './AudioEngine' + +export function midiToFreq(midi: number): number { + return 440 * Math.pow(2, (midi - 69) / 12) +} + +/** Play a reference tone at the given MIDI note for `duration` seconds. */ +export async function playTone(midi: number, duration = 1.4): Promise { + await audio.unlock() + const ctx = audio.context + const now = ctx.currentTime + + const osc = ctx.createOscillator() + const osc2 = ctx.createOscillator() + const gain = ctx.createGain() + const filter = ctx.createBiquadFilter() + + const freq = midiToFreq(midi) + osc.type = 'sawtooth' + osc2.type = 'sawtooth' + osc.frequency.value = freq + osc2.frequency.value = freq + osc2.detune.value = 6 + + filter.type = 'lowpass' + filter.frequency.value = Math.min(freq * 6, 6000) + + gain.gain.setValueAtTime(0.0001, now) + gain.gain.exponentialRampToValueAtTime(0.5, now + 0.02) + gain.gain.exponentialRampToValueAtTime(0.0001, now + duration) + + osc.connect(filter) + osc2.connect(filter) + filter.connect(gain) + gain.connect(audio.master) + + osc.start(now) + osc2.start(now) + osc.stop(now + duration + 0.05) + osc2.stop(now + duration + 0.05) +} diff --git a/src/lib/music/caged.ts b/src/lib/music/caged.ts new file mode 100644 index 0000000..0a7ee69 --- /dev/null +++ b/src/lib/music/caged.ts @@ -0,0 +1,84 @@ +// CAGED major-chord shapes across the neck for a given key. Each canonical form is +// stored relative to its barre fret; the barre fret for a key is the semitone +// distance from the form's open-chord root to the key root. This tiles the neck +// with the five familiar shapes. +// +// Frets/roots are indexed 0 = high e (1st) … 5 = low E (6th) to match Fretboard. + +import { chroma } from './theory' +import type { Pos } from './fretboard' + +interface CagedForm { + name: 'C' | 'A' | 'G' | 'E' | 'D' + openRootChroma: number + /** Fret offset from the barre per string (index 0 = high e), null = muted. */ + offsets: (number | null)[] + /** Which strings carry the root note. */ + roots: boolean[] +} + +const FORMS: CagedForm[] = [ + { + name: 'E', + openRootChroma: chroma('E'), + offsets: [0, 0, 1, 2, 2, 0], + roots: [true, false, false, true, false, true], + }, + { + name: 'D', + openRootChroma: chroma('D'), + offsets: [2, 3, 2, 0, null, null], + roots: [false, true, false, true, false, false], + }, + { + name: 'C', + openRootChroma: chroma('C'), + offsets: [0, 1, 0, 2, 3, null], + roots: [false, true, false, false, true, false], + }, + { + name: 'A', + openRootChroma: chroma('A'), + offsets: [0, 2, 2, 2, 0, null], + roots: [false, false, true, false, true, false], + }, + { + name: 'G', + openRootChroma: chroma('G'), + offsets: [3, 0, 0, 0, 2, 3], + roots: [true, false, true, false, false, true], + }, +] + +export interface CagedShape { + form: string + barreFret: number + positionLabel: string + positions: Pos[] + rootPositions: Pos[] +} + +/** The five CAGED shapes for `key`, ordered up the neck by barre fret. */ +export function cagedShapes(key: string): CagedShape[] { + const keyChroma = chroma(key) + const shapes = FORMS.map((form) => { + const barreFret = ((keyChroma - form.openRootChroma) % 12 + 12) % 12 + const positions: Pos[] = [] + const rootPositions: Pos[] = [] + form.offsets.forEach((off, string) => { + if (off === null) return + const fret = off + barreFret + const p = { string, fret } + positions.push(p) + if (form.roots[string]) rootPositions.push(p) + }) + return { + form: `${form.name} shape`, + barreFret, + positionLabel: barreFret === 0 ? 'open' : `fret ${barreFret}`, + positions, + rootPositions, + } + }) + return shapes.sort((a, b) => a.barreFret - b.barreFret) +} diff --git a/src/lib/music/chords.ts b/src/lib/music/chords.ts new file mode 100644 index 0000000..45e3a04 --- /dev/null +++ b/src/lib/music/chords.ts @@ -0,0 +1,53 @@ +// Curated chord shapes where playability matters (open + common barre forms). +// Frets are ABSOLUTE fret numbers, ordered low string -> high string +// (index 0 = low E / 6th string … index 5 = high e / 1st string), matching the +// conventional diagram layout with the low E on the left. +// -1 = muted 0 = open +// fingers (optional, same order): 0 = open/none, 1..4 = fretting fingers. + +export interface ChordShape { + name: string + frets: number[] // length 6, low -> high + fingers?: number[] +} + +const S = ( + name: string, + frets: number[], + fingers?: number[], +): ChordShape => ({ name, frets, fingers }) + +const SHAPES: ChordShape[] = [ + S('G', [3, 2, 0, 0, 0, 3], [2, 1, 0, 0, 0, 3]), + S('C', [-1, 3, 2, 0, 1, 0], [0, 3, 2, 0, 1, 0]), + S('D', [-1, -1, 0, 2, 3, 2], [0, 0, 0, 1, 3, 2]), + S('A', [-1, 0, 2, 2, 2, 0], [0, 0, 1, 2, 3, 0]), + S('E', [0, 2, 2, 1, 0, 0], [0, 2, 3, 1, 0, 0]), + S('Am', [-1, 0, 2, 2, 1, 0], [0, 0, 2, 3, 1, 0]), + S('Em', [0, 2, 2, 0, 0, 0], [0, 2, 3, 0, 0, 0]), + S('Dm', [-1, -1, 0, 2, 3, 1], [0, 0, 0, 2, 3, 1]), + S('F', [1, 3, 3, 2, 1, 1], [1, 3, 4, 2, 1, 1]), + S('Bm', [-1, 2, 4, 4, 3, 2], [0, 1, 3, 4, 2, 1]), + S('F#m', [2, 4, 4, 2, 2, 2], [1, 3, 4, 1, 1, 1]), + S('C#m', [-1, 4, 6, 6, 5, 4], [0, 1, 3, 4, 2, 1]), +] + +const BY_NAME: Record = Object.fromEntries( + SHAPES.map((s) => [s.name, s]), +) + +export function getChordShape(name: string): ChordShape | undefined { + return BY_NAME[name] +} + +/** Chord pairs for the R4 "one-minute changes" drill. */ +export const CHANGE_PAIRS: { id: string; a: string; b: string }[] = [ + { id: 'g-c', a: 'G', b: 'C' }, + { id: 'c-d', a: 'C', b: 'D' }, + { id: 'g-d', a: 'G', b: 'D' }, + { id: 'a-d', a: 'A', b: 'D' }, + { id: 'em-c', a: 'Em', b: 'C' }, + { id: 'am-f', a: 'Am', b: 'F' }, + { id: 'f-bm', a: 'F', b: 'Bm' }, + { id: 'd-a', a: 'D', b: 'A' }, +] diff --git a/src/lib/music/fretboard.ts b/src/lib/music/fretboard.ts new file mode 100644 index 0000000..e3fe60e --- /dev/null +++ b/src/lib/music/fretboard.ts @@ -0,0 +1,45 @@ +// Fretboard note mapping for standard tuning. String index 0 = high e (1st, top +// lane) … 5 = low E (6th). Uses tonal for note naming; chroma (0..11, C=0) is used +// for pitch-class comparison against scales/chords. + +import { Midi } from 'tonal' + +/** Open-string MIDI numbers, index 0 = high e (E4) … 5 = low E (E2). */ +export const OPEN_MIDI = [64, 59, 55, 50, 45, 40] as const + +export const FRET_COUNT = 15 + +export interface Pos { + string: number + fret: number +} + +export function midiAt(string: number, fret: number): number { + return OPEN_MIDI[string] + fret +} + +export function chromaAt(string: number, fret: number): number { + return midiAt(string, fret) % 12 +} + +export function noteNameAt(string: number, fret: number, sharps = true): string { + return Midi.midiToNoteName(midiAt(string, fret), { sharps, pitchClass: false }) +} + +export function pcAt(string: number, fret: number, sharps = true): string { + return Midi.midiToNoteName(midiAt(string, fret), { sharps, pitchClass: true }) +} + +/** All positions (within maxFret) whose pitch class is in the given chroma set. */ +export function positionsForChromas( + chromas: Set, + maxFret = FRET_COUNT, +): Pos[] { + const out: Pos[] = [] + for (let string = 0; string < OPEN_MIDI.length; string++) { + for (let fret = 0; fret <= maxFret; fret++) { + if (chromas.has(chromaAt(string, fret))) out.push({ string, fret }) + } + } + return out +} diff --git a/src/lib/music/theory.ts b/src/lib/music/theory.ts new file mode 100644 index 0000000..7a85b6c --- /dev/null +++ b/src/lib/music/theory.ts @@ -0,0 +1,102 @@ +// Thin wrappers over tonal for the theory drills. Keeps tonal usage in one place +// and returns plain data the components can render. + +import { Scale, Key, Note, Interval } from 'tonal' + +export const CHROMATIC_ROOTS = [ + 'C', + 'C#', + 'D', + 'D#', + 'E', + 'F', + 'F#', + 'G', + 'G#', + 'A', + 'A#', + 'B', +] as const + +export const ROMAN = ['I', 'ii', 'iii', 'IV', 'V', 'vi', 'vii°'] as const + +/** Diatonic modes, in brightness order. */ +export const MODES = [ + { id: 'ionian', label: 'Ionian (major)' }, + { id: 'dorian', label: 'Dorian' }, + { id: 'phrygian', label: 'Phrygian' }, + { id: 'lydian', label: 'Lydian' }, + { id: 'mixolydian', label: 'Mixolydian' }, + { id: 'aeolian', label: 'Aeolian (minor)' }, + { id: 'locrian', label: 'Locrian' }, +] as const + +export function chroma(note: string): number { + return Note.chroma(note) ?? 0 +} + +/** Pitch classes (names) of a scale/mode, e.g. scaleNotes('G','dorian'). */ +export function scaleNotes(tonic: string, name: string): string[] { + return Scale.get(`${tonic} ${name}`).notes +} + +export function scaleChromas(tonic: string, name: string): number[] { + return scaleNotes(tonic, name).map(chroma) +} + +export interface DiatonicChord { + degree: string + name: string + quality: string +} + +const QUALITIES = [ + 'major', + 'minor', + 'minor', + 'major', + 'major', + 'minor', + 'diminished', +] + +export function diatonicChords(key: string): DiatonicChord[] { + const triads = Key.majorKey(key).triads + return triads.map((name, i) => ({ + degree: ROMAN[i], + name, + quality: QUALITIES[i], + })) +} + +/** Notes of a triad (root + third + fifth) for the given quality. */ +export function triadNotes(root: string, quality: 'major' | 'minor'): string[] { + const third = quality === 'major' ? '3M' : '3m' + return [root, Note.transpose(root, third), Note.transpose(root, '5P')].map( + (n) => Note.pitchClass(n), + ) +} + +export interface IntervalDef { + id: string + label: string + semitones: string // tonal interval token +} + +export const INTERVALS: IntervalDef[] = [ + { id: 'm3', label: 'Minor 3rd', semitones: '3m' }, + { id: 'M3', label: 'Major 3rd', semitones: '3M' }, + { id: 'P4', label: 'Perfect 4th', semitones: '4P' }, + { id: 'P5', label: 'Perfect 5th', semitones: '5P' }, + { id: 'M6', label: 'Major 6th', semitones: '6M' }, + { id: 'm7', label: 'Minor 7th', semitones: '7m' }, + { id: 'M7', label: 'Major 7th', semitones: '7M' }, +] + +export function intervalTarget(root: string, token: string): string { + return Note.pitchClass(Note.transpose(root + '3', token)) +} + +export function intervalSemitones(token: string): number { + return Interval.semitones(token) ?? 0 +} diff --git a/src/lib/music/triads.ts b/src/lib/music/triads.ts new file mode 100644 index 0000000..b1cf18b --- /dev/null +++ b/src/lib/music/triads.ts @@ -0,0 +1,86 @@ +// Triad inversions on a three-string set, computed from chord tones + the +// fretboard map (no hand-curated tables). Returns diagram-ready shapes. + +import { chromaAt } from './fretboard' +import { triadNotes, chroma } from './theory' +import type { ChordShape } from './chords' + +export type Quality = 'major' | 'minor' + +export interface StringSet { + id: string + label: string + /** String indices, LOW pitch first (bottom of the voicing). */ + bottomToTop: number[] +} + +export const STRING_SETS: StringSet[] = [ + { id: '123', label: 'Strings 1·2·3', bottomToTop: [2, 1, 0] }, // G B e + { id: '234', label: 'Strings 2·3·4', bottomToTop: [3, 2, 1] }, // D G B +] + +export const INVERSIONS = [ + { id: 'root', label: 'Root position' }, + { id: '1st', label: '1st inversion' }, + { id: '2nd', label: '2nd inversion' }, +] as const + +// pc order (bottom→top) for each inversion, given [root, third, fifth]. +const INV_ORDER: Record = { + root: [0, 1, 2], + '1st': [1, 2, 0], + '2nd': [2, 0, 1], +} + +/** + * Build a triad-inversion shape on a string set. Chooses the frets that minimise + * the span (and prefer lower positions). `frets` is length 6, low→high, -1 muted. + */ +export function triadShape( + root: string, + quality: Quality, + set: StringSet, + inversionId: string, + maxFret = 15, +): ChordShape & { positionLabel: string } { + const tones = triadNotes(root, quality) // [R, 3, 5] pitch classes + const order = INV_ORDER[inversionId] + const targetChromas = order.map((i) => chroma(tones[i])) // bottom→top + + // Candidate frets per string that produce the required chroma. + const candidates = set.bottomToTop.map((stringIdx, i) => { + const want = targetChromas[i] + const frets: number[] = [] + for (let f = 0; f <= maxFret; f++) if (chromaAt(stringIdx, f) === want) frets.push(f) + return frets + }) + + // Search combinations for the tightest, lowest voicing. + let best: { frets: number[]; span: number; min: number } | null = null + for (const fb of candidates[0]) { + for (const fm of candidates[1]) { + for (const ft of candidates[2]) { + const min = Math.min(fb, fm, ft) + const span = Math.max(fb, fm, ft) - min + if (span > 4) continue + if (!best || span < best.span || (span === best.span && min < best.min)) { + best = { frets: [fb, fm, ft], span, min } + } + } + } + } + + const frets = new Array(6).fill(-1) + if (best) { + set.bottomToTop.forEach((stringIdx, i) => { + frets[stringIdx] = best!.frets[i] + }) + } + + const lowest = best ? best.min : 0 + return { + name: `${root}${quality === 'minor' ? 'm' : ''}`, + frets, + positionLabel: lowest > 0 ? `fret ${lowest}` : 'open', + } +} diff --git a/src/pages/DrillPage.svelte b/src/pages/DrillPage.svelte index cf8515f..86cd319 100644 --- a/src/pages/DrillPage.svelte +++ b/src/pages/DrillPage.svelte @@ -3,6 +3,13 @@ import NotFound from './NotFound.svelte' import StrumView from '../components/views/StrumView.svelte' import PickingView from '../components/views/PickingView.svelte' + import ChangesView from '../components/views/ChangesView.svelte' + import DiatonicView from '../components/views/DiatonicView.svelte' + import CagedView from '../components/views/CagedView.svelte' + import TriadsView from '../components/views/TriadsView.svelte' + import IntervalsView from '../components/views/IntervalsView.svelte' + import NotePrompterView from '../components/views/NotePrompterView.svelte' + import ReferenceToneView from '../components/views/ReferenceToneView.svelte' interface Props { code: string @@ -15,14 +22,28 @@ {#if !drill} -{:else if drill.kind === 'strum'} - {#key drill.code} - - {/key} -{:else if drill.kind === 'picking'} - {#key drill.code} - - {/key} {:else} - + {#key drill.code} + {#if drill.kind === 'strum'} + + {:else if drill.kind === 'picking'} + + {:else if drill.kind === 'changes'} + + {:else if drill.kind === 'diatonic'} + + {:else if drill.kind === 'caged'} + + {:else if drill.kind === 'triads'} + + {:else if drill.kind === 'intervals'} + + {:else if drill.kind === 'note-prompter'} + + {:else if drill.kind === 'reference-tone'} + + {:else} + + {/if} + {/key} {/if}