Phase 4: drone + T6/E-series + polish + deploy + README

Add drone synth + DronePlayer, wire T6 (mode over drone) and E1-E3 info pages
(E3 carries a drone). Screen Wake Lock while any click/drone plays. Vitest suite
for scheduler timing (no drift over 5 min) and music-layer correctness (triads,
CAGED, diatonic, fretboard). Deploy artifacts: public/_redirects, multi-stage
Dockerfile + nginx.conf (SPA fallback, immutable asset caching, gzip),
.dockerignore. README covering build, Cloudflare Pages / nginx / Docker deploy,
and the deep-link URL format. prefers-reduced-motion honored throughout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-22 00:36:34 +02:00
parent 365aac58a0
commit 083744203b
15 changed files with 706 additions and 1 deletions
+67
View File
@@ -0,0 +1,67 @@
import { describe, it, expect } from 'vitest'
import { chromaAt, pcAt } from './fretboard'
import { triadNotes, diatonicChords, scaleNotes, intervalTarget } from './theory'
import { triadShape, STRING_SETS } from './triads'
import { cagedShapes } from './caged'
import { deriveRoles } from '../../drills'
describe('fretboard mapping', () => {
it('maps open strings and frets correctly (index 0 = high e)', () => {
expect(pcAt(0, 0)).toBe('E') // high e open
expect(pcAt(5, 0)).toBe('E') // low E open
expect(pcAt(5, 3)).toBe('G') // low E, 3rd fret
expect(pcAt(4, 2)).toBe('B') // A string, 2nd fret
expect(chromaAt(0, 0)).toBe(4) // E
})
})
describe('theory helpers', () => {
it('builds triads and diatonic chords', () => {
expect(triadNotes('C', 'major')).toEqual(['C', 'E', 'G'])
expect(triadNotes('A', 'minor')).toEqual(['A', 'C', 'E'])
expect(diatonicChords('G').map((c) => c.name)).toEqual([
'G', 'Am', 'Bm', 'C', 'D', 'Em', 'F#dim',
])
expect(scaleNotes('G', 'dorian')).toEqual(['G', 'A', 'Bb', 'C', 'D', 'E', 'F'])
expect(intervalTarget('A', '5P')).toBe('E')
})
})
describe('deriveRoles', () => {
it('accents, downbeats, and subdivisions land correctly', () => {
// 4/4 sixteenths, accent on beat 1.
const roles = deriveRoles(16, 4, [0])
expect(roles[0]).toBe('accent')
expect(roles[4]).toBe('normal') // beat 2 downbeat
expect(roles[1]).toBe('subdivision')
})
})
describe('triad inversions', () => {
it('produces C major tones on the 1-2-3 string set', () => {
const set = STRING_SETS.find((s) => s.id === '123')!
for (const inv of ['root', '1st', '2nd']) {
const shape = triadShape('C', 'major', set, inv)
const chromas = shape.frets
.map((f, string) => (f >= 0 ? chromaAt(string, f) : null))
.filter((c): c is number => c !== null)
.sort((a, b) => a - b)
// C(0) E(4) G(7)
expect(chromas).toEqual([0, 4, 7])
}
})
})
describe('CAGED shapes', () => {
it('returns 5 shapes with the E-shape barre at fret 3 for key G', () => {
const shapes = cagedShapes('G')
expect(shapes).toHaveLength(5)
const eShape = shapes.find((s) => s.form === 'E shape')!
expect(eShape.barreFret).toBe(3)
// Every marked position should be a chord tone of G major (G B D).
const gTones = new Set([7, 11, 2])
for (const p of shapes[0].positions) {
expect(gTones.has(chromaAt(p.string, p.fret))).toBe(true)
}
})
})