083744203b
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>
68 lines
2.4 KiB
TypeScript
68 lines
2.4 KiB
TypeScript
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)
|
|
}
|
|
})
|
|
})
|