mirror of
https://github.com/9x/sheetmusictrainer.git
synced 2026-11-03 22:03:54 +01:00
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.
This commit is contained in:
@@ -87,76 +87,87 @@ export const MetronomeWidget: React.FC<MetronomeWidgetProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{rhythm.active && (
|
{/* Always visible: pendulum + BPM; the toggle gates sound + animation */}
|
||||||
<div className="rhythm-details" style={{ display: 'flex', flexDirection: 'column', gap: '6px', paddingTop: '8px', borderTop: '1px solid rgba(255,255,255,0.1)' }}>
|
<div className="rhythm-details" style={{ display: 'flex', flexDirection: 'column', gap: '6px', paddingTop: '8px', borderTop: '1px solid rgba(255,255,255,0.1)', opacity: rhythm.active ? 1 : 0.5 }}>
|
||||||
{/* Linear pendulum — same element, left transitions each beat */}
|
{/* Linear pendulum — same element, left transitions each beat */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'relative',
|
||||||
|
height: compact ? 14 : 18,
|
||||||
|
borderRadius: 9,
|
||||||
|
background: 'color-mix(in srgb, var(--color-text-main) 8%, transparent)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
aria-hidden
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: 'absolute',
|
||||||
height: compact ? 14 : 18,
|
top: '50%',
|
||||||
borderRadius: 9,
|
left: beatParity === 0 ? '6%' : '88%',
|
||||||
background: 'color-mix(in srgb, var(--color-text-main) 8%, transparent)',
|
width: compact ? 10 : 12,
|
||||||
overflow: 'hidden',
|
height: compact ? 10 : 12,
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: 'var(--color-text-main)',
|
||||||
|
transform: 'translateY(-50%)',
|
||||||
|
transition: `left ${beatMs}ms linear`,
|
||||||
|
display: rhythm.active ? 'block' : 'none',
|
||||||
}}
|
}}
|
||||||
aria-hidden
|
/>
|
||||||
>
|
</div>
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: '50%',
|
|
||||||
left: beatParity === 0 ? '6%' : '88%',
|
|
||||||
width: compact ? 10 : 12,
|
|
||||||
height: compact ? 10 : 12,
|
|
||||||
borderRadius: '50%',
|
|
||||||
background: 'var(--color-text-main)',
|
|
||||||
transform: 'translateY(-50%)',
|
|
||||||
transition: `left ${beatMs}ms linear`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{rhythm.mode === 'bpm' || compact ? (
|
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
{!compact && (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
<div style={{ display: 'flex', gap: '4px', flexShrink: 0 }}>
|
||||||
<button
|
<button
|
||||||
className="control-button small"
|
className={`control-button small ${rhythm.mode === 'bpm' ? 'active' : ''}`}
|
||||||
onClick={() => onUpdate({ bpm: Math.max(30, rhythm.bpm - 1) })}
|
onClick={() => onUpdate({ mode: 'bpm' })}
|
||||||
style={{ width: '24px', padding: '0', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
style={{ fontSize: '10px' }}
|
||||||
>
|
>BPM</button>
|
||||||
−
|
<button
|
||||||
</button>
|
className={`control-button small ${rhythm.mode === 'seconds' ? 'active' : ''}`}
|
||||||
<input
|
onClick={() => onUpdate({ mode: 'seconds' })}
|
||||||
type="number"
|
style={{ fontSize: '10px' }}
|
||||||
min="30"
|
>Timer</button>
|
||||||
max="300"
|
|
||||||
value={rhythm.bpm}
|
|
||||||
onChange={(e) => onUpdate({ bpm: Math.max(1, parseInt(e.target.value) || 60) })}
|
|
||||||
className="control-input"
|
|
||||||
style={{ width: '48px', textAlign: 'center', padding: '2px' }}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
className="control-button small"
|
|
||||||
onClick={() => onUpdate({ bpm: Math.min(300, rhythm.bpm + 1) })}
|
|
||||||
style={{ width: '24px', padding: '0', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="30"
|
|
||||||
max="240"
|
|
||||||
step="1"
|
|
||||||
value={rhythm.bpm}
|
|
||||||
onChange={(e) => onUpdate({ bpm: Number(e.target.value) })}
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: '11px', opacity: 0.7, textAlign: 'center' }}>
|
|
||||||
{getTempoMarking(rhythm.bpm)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{rhythm.mode === 'bpm' ? (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className="control-button small"
|
||||||
|
onClick={() => onUpdate({ bpm: Math.max(30, rhythm.bpm - 1) })}
|
||||||
|
style={{ width: '24px', padding: '0', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||||
|
>
|
||||||
|
−
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="30"
|
||||||
|
max="300"
|
||||||
|
value={rhythm.bpm}
|
||||||
|
onChange={(e) => onUpdate({ bpm: Math.max(1, parseInt(e.target.value) || 60) })}
|
||||||
|
className="control-input"
|
||||||
|
style={{ width: '48px', textAlign: 'center', padding: '2px' }}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="control-button small"
|
||||||
|
onClick={() => onUpdate({ bpm: Math.min(300, rhythm.bpm + 1) })}
|
||||||
|
style={{ width: '24px', padding: '0', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="30"
|
||||||
|
max="240"
|
||||||
|
step="1"
|
||||||
|
value={rhythm.bpm}
|
||||||
|
onChange={(e) => onUpdate({ bpm: Number(e.target.value) })}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
<>
|
||||||
<span style={{ fontSize: '12px', minWidth: '32px' }}>{rhythm.seconds}s</span>
|
<span style={{ fontSize: '12px', minWidth: '32px' }}>{rhythm.seconds}s</span>
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
@@ -167,8 +178,36 @@ export const MetronomeWidget: React.FC<MetronomeWidgetProps> = ({
|
|||||||
onChange={(e) => onUpdate({ seconds: Number(e.target.value) })}
|
onChange={(e) => onUpdate({ seconds: Number(e.target.value) })}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
{rhythm.mode === 'bpm' && (
|
||||||
|
<div style={{ fontSize: '11px', opacity: 0.7, textAlign: 'center' }}>
|
||||||
|
{getTempoMarking(rhythm.bpm)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center' }}>
|
||||||
|
{showAutoAdvance ? (
|
||||||
|
<label style={{ fontSize: '10px', display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={rhythm.autoAdvance}
|
||||||
|
onChange={(e) => onUpdate({ autoAdvance: e.target.checked })}
|
||||||
|
/>
|
||||||
|
Auto
|
||||||
|
</label>
|
||||||
|
) : <span />}
|
||||||
|
<label style={{ fontSize: '10px', display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={rhythm.sound}
|
||||||
|
onChange={(e) => onUpdate({ sound: e.target.checked })}
|
||||||
|
/>
|
||||||
|
Sound
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
{showAutoAdvance ? (
|
{showAutoAdvance ? (
|
||||||
@@ -189,9 +228,7 @@ export const MetronomeWidget: React.FC<MetronomeWidgetProps> = ({
|
|||||||
/>
|
/>
|
||||||
Sound
|
Sound
|
||||||
</label>
|
</label>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { usePhraseTrainer, type PhraseTrainerApi, type PhrasePhase } from '../ho
|
|||||||
import { getPhraseSettings, getPracticeFilter, type PhraseSettings, type PracticeFilter } from '../types/SettingsTypes';
|
import { getPhraseSettings, getPracticeFilter, type PhraseSettings, type PracticeFilter } from '../types/SettingsTypes';
|
||||||
import { rawFrameCount } from '../hooks/rawFrameBus';
|
import { rawFrameCount } from '../hooks/rawFrameBus';
|
||||||
import { audioEngine } from '../audio/AudioEngine';
|
import { audioEngine } from '../audio/AudioEngine';
|
||||||
import { computePlayableNotes, isFrettedInstrument, positionsWithinWindow, fretWindowNotes } from '../music/playableRange';
|
import { computePlayableNotes, isFrettedInstrument, positionsWithinWindow } from '../music/playableRange';
|
||||||
import { generateMelody } from '../music/melodyGenerator';
|
import { generateMelody } from '../music/melodyGenerator';
|
||||||
import { generateScaleDrill } from '../music/scaleDrills';
|
import { generateScaleDrill } from '../music/scaleDrills';
|
||||||
import { generateArpeggio, type ArpeggioDegree } from '../music/arpeggioGenerator';
|
import { generateArpeggio, type ArpeggioDegree } from '../music/arpeggioGenerator';
|
||||||
@@ -109,39 +109,22 @@ export const PhraseTrainer = forwardRef<PhraseHandle, PhraseTrainerProps>(
|
|||||||
|
|
||||||
// ---- Playable pool ----------------------------------------------------
|
// ---- Playable pool ----------------------------------------------------
|
||||||
const pool = useMemo(() => {
|
const pool = useMemo(() => {
|
||||||
// Unified filter: strings ∩ fret window take precedence for fretted
|
// Unified filter: strings ∩ fret window, computed identically to
|
||||||
// instruments (same semantics as the single-note modes).
|
// the single-note modes (full 0–12 fretboard when no filter is
|
||||||
if (fretted && currentTuning && (pf.strings.length > 0 || pf.fretWindowEnabled)) {
|
// active — the legacy difficulty ranges no longer apply).
|
||||||
|
if (fretted && currentTuning) {
|
||||||
const all = currentTuning.strings;
|
const all = currentTuning.strings;
|
||||||
const selected = pf.strings.length > 0
|
const selected = pf.strings.length > 0
|
||||||
? all.filter((_, i) => pf.strings.includes(i))
|
? all.filter((_, i) => pf.strings.includes(i))
|
||||||
: all;
|
: all;
|
||||||
if (selected.length > 0) {
|
if (selected.length === 0) return [];
|
||||||
if (pf.fretWindowEnabled) {
|
const lo = pf.fretWindowEnabled ? Math.max(0, Math.min(24, pf.fretMin)) : 0;
|
||||||
const lo = Math.max(0, Math.min(24, pf.fretMin));
|
const hi = pf.fretWindowEnabled ? Math.max(lo, Math.min(24, pf.fretMax)) : 12;
|
||||||
const hi = Math.max(lo, Math.min(24, pf.fretMax));
|
return selected.flatMap(open => {
|
||||||
return selected.flatMap(open => {
|
const notes: number[] = [];
|
||||||
const notes: number[] = [];
|
for (let fret = lo; fret <= hi; fret++) notes.push(open + fret);
|
||||||
for (let fret = lo; fret <= hi; fret++) notes.push(open + fret);
|
return notes;
|
||||||
return notes;
|
}).sort((a, b) => a - b);
|
||||||
}).sort((a, b) => a - b);
|
|
||||||
}
|
|
||||||
// Strings only: use their full range
|
|
||||||
const maxFret = Math.max(...all.map(open => {
|
|
||||||
let f = 0;
|
|
||||||
while (open + f <= 127) f++;
|
|
||||||
return Math.min(f - 1, 24);
|
|
||||||
}));
|
|
||||||
return selected.flatMap(open => {
|
|
||||||
const notes: number[] = [];
|
|
||||||
for (let fret = 0; fret <= maxFret; fret++) notes.push(open + fret);
|
|
||||||
return notes;
|
|
||||||
}).sort((a, b) => a - b);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
if (phrase.fretWindowEnabled && fretted && currentTuning) {
|
|
||||||
return fretWindowNotes(currentTuning, phrase.fretMin, phrase.fretMax);
|
|
||||||
}
|
}
|
||||||
return computePlayableNotes({
|
return computePlayableNotes({
|
||||||
instrumentId: settings.instrument,
|
instrumentId: settings.instrument,
|
||||||
@@ -640,6 +623,13 @@ export const PhraseTrainer = forwardRef<PhraseHandle, PhraseTrainerProps>(
|
|||||||
<option value="down">Down</option>
|
<option value="down">Down</option>
|
||||||
<option value="updown">Up & down</option>
|
<option value="updown">Up & down</option>
|
||||||
<option value="1235">1-2-3-5</option>
|
<option value="1235">1-2-3-5</option>
|
||||||
|
<optgroup label="Giuliani studies">
|
||||||
|
<option value="giuliani-pim">p-i-m</option>
|
||||||
|
<option value="giuliani-pima">p-i-m-a</option>
|
||||||
|
<option value="giuliani-pami">p-a-m-i</option>
|
||||||
|
<option value="giuliani-mami">m-a-m-i</option>
|
||||||
|
<option value="giuliani-ami">a-m-i</option>
|
||||||
|
</optgroup>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>Coverage
|
<label>Coverage
|
||||||
|
|||||||
@@ -14,7 +14,15 @@ import { STEP_LETTERS, LETTER_PC, type StepLetter } from '../score/model';
|
|||||||
import { makeRng } from './melodyGenerator';
|
import { makeRng } from './melodyGenerator';
|
||||||
|
|
||||||
export type ArpeggioDegree = 'I' | 'ii' | 'iii' | 'IV' | 'V' | 'vi' | 'vii0';
|
export type ArpeggioDegree = 'I' | 'ii' | 'iii' | 'IV' | 'V' | 'vi' | 'vii0';
|
||||||
export type ArpeggioPattern = 'up' | 'down' | 'updown' | '1235';
|
export type ArpeggioPattern =
|
||||||
|
| 'up' | 'down' | 'updown' | '1235'
|
||||||
|
// Giuliani-style right-hand figures over one chord (indices into the
|
||||||
|
// chord cycle per note; 1=root 2=third 3=fifth 4=octave):
|
||||||
|
| 'giuliani-pim' // root-third-fifth (p-i-m)
|
||||||
|
| 'giuliani-pima' // root-third-fifth-octave (p-i-m-a)
|
||||||
|
| 'giuliani-pami' // root-octave-fifth-third (p-a-m-i)
|
||||||
|
| 'giuliani-mami' // third-fifth-octave-third (inner-voice feel)
|
||||||
|
| 'giuliani-ami'; // octave-fifth-third (descending from the top)
|
||||||
export type ArpeggioCoverage = 'one-octave' | 'two-octave';
|
export type ArpeggioCoverage = 'one-octave' | 'two-octave';
|
||||||
/** How the chord per bar is chosen in sequence mode. */
|
/** How the chord per bar is chosen in sequence mode. */
|
||||||
export type ArpeggioProgression = 'random' | 'functional' | 'diatonic-cycle';
|
export type ArpeggioProgression = 'random' | 'functional' | 'diatonic-cycle';
|
||||||
@@ -45,6 +53,28 @@ export const ARPEGGIO_DEGREE_LABELS: Record<ArpeggioDegree, string> = {
|
|||||||
I: 'I', ii: 'ii', iii: 'iii', IV: 'IV', V: 'V', vi: 'vi', 'vii0': 'vii°',
|
I: 'I', ii: 'ii', iii: 'iii', IV: 'IV', V: 'V', vi: 'vi', 'vii0': 'vii°',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PATTERN_LABELS: Record<ArpeggioPattern, string> = {
|
||||||
|
up: 'Up', down: 'Down', updown: 'Up & down', '1235': '1-2-3-5',
|
||||||
|
'giuliani-pim': 'Giuliani p-i-m',
|
||||||
|
'giuliani-pima': 'Giuliani p-i-m-a',
|
||||||
|
'giuliani-pami': 'Giuliani p-a-m-i',
|
||||||
|
'giuliani-mami': 'Giuliani m-a-m-i',
|
||||||
|
'giuliani-ami': 'Giuliani a-m-i',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Giuliani figures: chord-cycle index per note (0=root 1=third 2=fifth
|
||||||
|
* 3=octave). Each figure repeats with an octave register shift per
|
||||||
|
* repetition, mirroring the p-i-m-a feel of the 120 right-hand studies.
|
||||||
|
*/
|
||||||
|
const GIULIANI_FIGURES: Record<string, number[]> = {
|
||||||
|
'giuliani-pim': [0, 1, 2],
|
||||||
|
'giuliani-pima': [0, 1, 2, 3],
|
||||||
|
'giuliani-pami': [0, 3, 2, 1],
|
||||||
|
'giuliani-mami': [1, 2, 3, 1],
|
||||||
|
'giuliani-ami': [3, 2, 1],
|
||||||
|
};
|
||||||
|
|
||||||
/** pc of a spelled degree (step letter + alter). */
|
/** pc of a spelled degree (step letter + alter). */
|
||||||
const degreePc = (d: { step: StepLetter; alter: number }): number =>
|
const degreePc = (d: { step: StepLetter; alter: number }): number =>
|
||||||
(LETTER_PC[STEP_LETTERS.indexOf(d.step)] + d.alter + 144) % 12;
|
(LETTER_PC[STEP_LETTERS.indexOf(d.step)] + d.alter + 144) % 12;
|
||||||
@@ -146,6 +176,26 @@ function buildPath(key: KeyContext, degreeIndex: number, pool: number[], coverag
|
|||||||
}
|
}
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
default: {
|
||||||
|
// Giuliani figure: cycle [root, third, fifth, octave] with the
|
||||||
|
// figure selecting cycle positions per note; the octave wraps so
|
||||||
|
// e.g. p-i-m over C-E-G becomes C E G | C' E' G' ... (ascending
|
||||||
|
// octave register shift every figure repetition).
|
||||||
|
const figure = GIULIANI_FIGURES[pattern];
|
||||||
|
// 4-note cycle per octave: root, third, fifth, octave
|
||||||
|
const cycleOct = (oct: number): number[] => [
|
||||||
|
base[0] + 12 * oct, base[1] + 12 * oct, base[2] + 12 * oct, base[0] + 12 * (oct + 1),
|
||||||
|
];
|
||||||
|
const path: number[] = [];
|
||||||
|
for (let oct = 0; oct < octavesWanted; oct++) {
|
||||||
|
const c = cycleOct(oct);
|
||||||
|
for (const idx of figure) {
|
||||||
|
const note = c[idx];
|
||||||
|
if (note !== undefined && (oct === 0 || pool.includes(note))) path.push(note);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +322,7 @@ export function generateArpeggio(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const chordName = `${key.tonic} ${ARPEGGIO_DEGREE_LABELS[config.degree]}`;
|
const chordName = `${key.tonic} ${ARPEGGIO_DEGREE_LABELS[config.degree]}`;
|
||||||
const patternLabel = { up: 'up', down: 'down', updown: 'up & down', '1235': '1-2-3-5' }[config.pattern];
|
const patternLabel = PATTERN_LABELS[config.pattern];
|
||||||
// Single-bar paths can overflow into extra bars (updown/two-octave);
|
// Single-bar paths can overflow into extra bars (updown/two-octave);
|
||||||
// sequence mode always has exactly `bars` measures.
|
// sequence mode always has exactly `bars` measures.
|
||||||
const totalMeasures = bars === 1 ? Math.max(1, Math.ceil((cursor) / full)) : bars;
|
const totalMeasures = bars === 1 ? Math.max(1, Math.ceil((cursor) / full)) : bars;
|
||||||
|
|||||||
@@ -40,7 +40,10 @@ export function computeTargetPool(input: TargetPoolInput): number[] {
|
|||||||
const tuning = TUNINGS[input.tuningId];
|
const tuning = TUNINGS[input.tuningId];
|
||||||
const fretted = isFrettedInstrument(input.instrumentId) && !!tuning;
|
const fretted = isFrettedInstrument(input.instrumentId) && !!tuning;
|
||||||
|
|
||||||
// 1+2: base pool with string selection and optional fret window
|
// 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[];
|
let base: number[];
|
||||||
if (fretted) {
|
if (fretted) {
|
||||||
const all = tuning!.strings;
|
const all = tuning!.strings;
|
||||||
@@ -49,7 +52,7 @@ export function computeTargetPool(input: TargetPoolInput): number[] {
|
|||||||
: all;
|
: all;
|
||||||
if (selected.length === 0) return [];
|
if (selected.length === 0) return [];
|
||||||
const lo = input.fretWindowEnabled ? Math.max(0, Math.min(24, input.fretMin)) : 0;
|
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)) : 24;
|
const hi = input.fretWindowEnabled ? Math.max(lo, Math.min(24, input.fretMax)) : 12;
|
||||||
base = selected.flatMap(open => {
|
base = selected.flatMap(open => {
|
||||||
const notes: number[] = [];
|
const notes: number[] = [];
|
||||||
for (let fret = lo; fret <= hi; fret++) notes.push(open + fret);
|
for (let fret = lo; fret <= hi; fret++) notes.push(open + fret);
|
||||||
|
|||||||
Reference in New Issue
Block a user