Files
sheetmusictrainer/src/music/targetPool.ts
OpenClaw 9dc95e3787 Metronome always-visible controls, Giuliani patterns, pool fix
- Metronome widget: BPM/slider row and pendulum always visible (dimmed
  when off); the toggle gates sound + animation only. BPM/Timer mode
  switch restored for single-note auto-advance.
- Giuliani right-hand figures as arpeggio patterns: p-i-m, p-i-m-a,
  p-a-m-i, m-a-m-i, a-m-i — cycle [root, third, fifth, octave] per figure
  with octave register shift per repetition (120-studies feel).
- Pool fix: fretted instruments without active filters now get the full
  0-12 fretboard in ALL modes (legacy difficulty ranges no longer clamp
  the pool since the Note Set dropdown was removed).
- Tests 174/174.
2026-09-14 19:20:15 +00:00

80 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Unified target-note pool: base playable range (instrument + difficulty /
* fret window) intersected with the optional key-filter (pitch classes of a
* tonic+mode) and the optional fret window. Shared by all game modes so the
* controls behave identically everywhere.
*/
import { keyFor, type ModeId } from './scales';
import { computePlayableNotes, isFrettedInstrument } from './playableRange';
import { TUNINGS } from './Tunings';
export interface TargetPoolInput {
readonly instrumentId: string;
readonly difficulty: string;
readonly tuningId: string;
readonly customMinFret?: number;
readonly customMaxFret?: number;
/** Key filter (target-note key, independent of the displayed key signature). */
readonly keyEnabled: boolean;
readonly keyTonic: string;
readonly keyMode: string;
/** Selected string indices (0 = lowest). Empty = all strings. */
readonly strings: number[];
/** Fret window (fretted instruments only). */
readonly fretWindowEnabled: boolean;
readonly fretMin: number;
readonly fretMax: number;
}
/**
* Compute the effective target-note pool.
*
* Order of application:
* 1. base playable range (instrument difficulty / custom frets)
* 2. string selection ∩ fret window (fretted instruments only — replaces
* the range, same semantics as Phrase Mode's fret window)
* 3. key filter (pitch-class intersection, keeps spelling/degree data intact
* via the same scale machinery Phrase Mode uses)
*/
export function computeTargetPool(input: TargetPoolInput): number[] {
const tuning = TUNINGS[input.tuningId];
const fretted = isFrettedInstrument(input.instrumentId) && !!tuning;
// 1+2: base pool with string selection and optional fret window.
// For fretted instruments with NO filters the pool is the full fretboard
// (frets 0–12) — the legacy difficulty ranges no longer apply since the
// Note Set dropdown was removed.
let base: number[];
if (fretted) {
const all = tuning!.strings;
const selected = input.strings.length > 0
? all.filter((_, i) => input.strings.includes(i))
: all;
if (selected.length === 0) return [];
const lo = input.fretWindowEnabled ? Math.max(0, Math.min(24, input.fretMin)) : 0;
const hi = input.fretWindowEnabled ? Math.max(lo, Math.min(24, input.fretMax)) : 12;
base = selected.flatMap(open => {
const notes: number[] = [];
for (let fret = lo; fret <= hi; fret++) notes.push(open + fret);
return notes;
}).sort((a, b) => a - b);
} else {
base = computePlayableNotes({
instrumentId: input.instrumentId,
difficulty: input.difficulty,
tuningId: input.tuningId,
customMinFret: input.customMinFret,
customMaxFret: input.customMaxFret,
});
}
if (!input.keyEnabled) return base;
// 3: key/mode pitch-class filter
const mode: ModeId = (['major', 'minor', 'dorian', 'phrygian', 'lydian', 'mixolydian', 'locrian', 'major-pentatonic', 'minor-pentatonic'] as const).includes(input.keyMode as ModeId)
? input.keyMode as ModeId
: 'major';
const key = keyFor(input.keyTonic, mode);
const pcs = new Set(key.pitchClasses);
return base.filter(midi => pcs.has(((midi % 12) + 12) % 12));
}