Arpeggio sequences + auto-start on first note

- Sequence mode: bars (2/4/6/8) with one chord per bar, chosen by
  functional rules (I->IV/V/vi...), diatonic cycle, or random (no immediate
  repeats); deterministic via melody seed. Chord symbols per bar in the
  score model (chordSymbols) rendered above the staff via setSection.
- Rhythm: quarters or eighths (cycling up/down through the pattern).
- Single-chord mode unchanged (whole path, overflow bars if needed).
- Auto-start: 'Start on first note (no count-in)' — step pace begins the
  run as soon as the mic detects the first note; no start press needed.
- 'New' button re-rolls the progression (new seed).
- Tests 174/174.
This commit is contained in:
OpenClaw
2026-09-14 18:44:14 +00:00
parent d76d24ab01
commit 263b6f6b69
7 changed files with 265 additions and 27 deletions

View File

@@ -89,3 +89,50 @@ describe('generateArpeggio', () => {
expect(a).toEqual(b);
});
});
describe('generateArpeggio — sequence mode', () => {
it('generates one chord per bar with functional progression', () => {
const r = generateArpeggio({ ...base, degree: 'I', pattern: 'up', coverage: 'one-octave', bars: 4, progression: 'functional', seed: 7 }, pool);
expect(r.ok).toBe(true);
if (!r.ok) return;
expect(r.value.measures.length).toBe(4);
expect(r.value.chordSymbols?.length).toBe(4);
expect(r.value.chordSymbols?.[0]).toBe('C'); // starts on I
// bar boundaries: each bar starts on a chord root? (not enforced) — but ticks align
const ev = scoreEvents(r.value);
expect(validateScore(r.value)).toEqual([]);
// 4 beats per bar × 4 bars = 16 notes in quarters mode
expect(ev.length).toBe(16);
// deterministic
const r2 = generateArpeggio({ ...base, degree: 'I', pattern: 'up', coverage: 'one-octave', bars: 4, progression: 'functional', seed: 7 }, pool);
expect(r2.ok && JSON.stringify(scoreEvents(r2.value)) === JSON.stringify(ev)).toBe(true);
});
it('diatonic cycle walks I ii iii IV V vi vii', () => {
const r = generateArpeggio({ ...base, degree: 'I', pattern: 'up', coverage: 'one-octave', bars: 7, progression: 'diatonic-cycle', seed: 1 }, pool);
expect(r.ok).toBe(true);
if (!r.ok) return;
const syms = r.value.chordSymbols!;
expect(syms).toEqual(['C', 'Dm', 'Em', 'F', 'G', 'Am', 'Bdim']);
});
it('eighths rhythm doubles notes per bar', () => {
const r = generateArpeggio({ ...base, degree: 'I', pattern: 'up', coverage: 'one-octave', bars: 2, progression: 'functional', rhythm: 'eighths', seed: 3 }, pool);
expect(r.ok).toBe(true);
if (!r.ok) return;
const ev = scoreEvents(r.value);
expect(ev.length).toBe(16); // 2 bars × 4 beats × 2
expect(ev[0].durationTicks).toBe(240);
expect(validateScore(r.value)).toEqual([]);
});
it('random avoids repeating the same chord back-to-back', () => {
const r = generateArpeggio({ ...base, degree: 'I', pattern: 'up', coverage: 'one-octave', bars: 6, progression: 'random', seed: 11 }, pool);
expect(r.ok).toBe(true);
if (!r.ok) return;
const syms = r.value.chordSymbols!;
for (let i = 1; i < syms.length; i++) {
expect(syms[i]).not.toBe(syms[i - 1]);
}
});
});