mirror of
https://github.com/9x/sheetmusictrainer.git
synced 2026-11-03 22:03:54 +01:00
Metronome sync toggle in widget, more Giuliani figures, chord pool list
- MetronomeWidget: the Phrase Mode switch now lives IN the widget as a Free/Sync toggle. Free = independent metronome (own clock, starts when toggled, with or without clicks). Sync = silent while idle, starts with the exercise (count-in included), pendulum mirrors the run's beats. - Fixed double phraseBeatBus.emit (leftover from an earlier edit) that flipped the pendulum twice per beat — animation appeared frozen. - Click decision unified: one sound checkbox governs both the widget and (in sync mode) the run's click scheduler. 'Click 1 after count-in' is musically the last count-in click (suppressed duplicate) — now with a working pendulum the offset is visible rather than confusing. - Giuliani figures extended: p-i-m-a-m-i, p-a-p-i-m-a, a-m-i-p-i-m, m-i-a-m-i-m (six-note rolling/bass-skip/tremolo-adjacent figures). - Arpeggio chord pool: togglable degree list (empty = all 7); functional and random progressions honor the pool; first bar draws a random chord from the pool. - Tests 174/174.
This commit is contained in:
@@ -181,6 +181,7 @@ export const Controls: React.FC<ControlsProps> = ({ currentPitch }) => {
|
|||||||
rhythm={settings.rhythm}
|
rhythm={settings.rhythm}
|
||||||
onUpdate={updateRhythm}
|
onUpdate={updateRhythm}
|
||||||
showAutoAdvance={!inPhraseMode}
|
showAutoAdvance={!inPhraseMode}
|
||||||
|
syncMode={inPhraseMode ? 'option' : null}
|
||||||
gate={inPhraseMode ? phraseRunActive : null}
|
gate={inPhraseMode ? phraseRunActive : null}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
/**
|
/**
|
||||||
* Metronome widget — always-visible tempo controls + pendulum.
|
* Metronome widget — always-visible tempo controls + pendulum.
|
||||||
*
|
*
|
||||||
* One toggle ("On") arms sound + animation. One `sound` checkbox decides
|
* Modes:
|
||||||
* whether an armed metronome clicks (the pendulum always swings when armed,
|
* - Plain (single-note modes): one on/off toggle arms sound + animation.
|
||||||
* giving silent visual tempo). There is exactly ONE sound checkbox here.
|
* - Sync option (Phrase Mode): the toggle switches between
|
||||||
|
* * FREE — the metronome runs independently (own clock, starts as soon
|
||||||
|
* as toggled, with or without clicks per the sound checkbox)
|
||||||
|
* * SYNC — the metronome starts with the exercise (count-in included),
|
||||||
|
* is silent while idle, and its pendulum mirrors the run's beats.
|
||||||
*
|
*
|
||||||
* Click sources (no doubling possible):
|
* Exactly ONE sound checkbox exists (here). In sync mode it governs the
|
||||||
* - Ungated (single-note modes, phrase "at your pace"): this widget's own
|
* run's click scheduler too — clicks can never double.
|
||||||
* scheduler clicks when armed && sound.
|
|
||||||
* - Gated (phrase "sync to metronome"): the phrase run's scheduler owns the
|
|
||||||
* clock; this widget is silent and its pendulum mirrors phrase beats.
|
|
||||||
*/
|
*/
|
||||||
import { useEffect, useRef, useState, useCallback } from 'react';
|
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||||
import { useMetronome } from '../hooks/useMetronome';
|
import { useMetronome } from '../hooks/useMetronome';
|
||||||
@@ -20,14 +21,15 @@ import type { RhythmSettings } from '../types/SettingsTypes';
|
|||||||
|
|
||||||
interface MetronomeWidgetProps {
|
interface MetronomeWidgetProps {
|
||||||
rhythm: RhythmSettings;
|
rhythm: RhythmSettings;
|
||||||
/** Persist changes (bpm, active, sound). Auto-advance is single-note only. */
|
/** Persist changes. */
|
||||||
onUpdate: (updates: Partial<RhythmSettings>) => void;
|
onUpdate: (updates: Partial<RhythmSettings>) => void;
|
||||||
/** Single-note mode extra: auto-advance targets on each tick. */
|
/** Single-note mode extra: auto-advance targets on each tick. */
|
||||||
showAutoAdvance?: boolean;
|
showAutoAdvance?: boolean;
|
||||||
/** Compact layout (phrase transport row). */
|
/** Compact layout (phrase transport row). */
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
/** External gate (Phrase Mode "sync to metronome"): the widget is muted
|
/** 'option': show the sync/free toggle (Phrase Mode). null: plain on/off. */
|
||||||
* and the pendulum mirrors the run's beats. null = free-running. */
|
syncMode?: 'option' | null;
|
||||||
|
/** External gate (sync mode): true while the exercise runs. */
|
||||||
gate?: boolean | null;
|
gate?: boolean | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +38,7 @@ export const MetronomeWidget: React.FC<MetronomeWidgetProps> = ({
|
|||||||
onUpdate,
|
onUpdate,
|
||||||
showAutoAdvance = false,
|
showAutoAdvance = false,
|
||||||
compact = false,
|
compact = false,
|
||||||
|
syncMode = null,
|
||||||
gate = null,
|
gate = null,
|
||||||
}) => {
|
}) => {
|
||||||
const [beatParity, setBeatParity] = useState(0);
|
const [beatParity, setBeatParity] = useState(0);
|
||||||
@@ -47,47 +50,66 @@ export const MetronomeWidget: React.FC<MetronomeWidgetProps> = ({
|
|||||||
setBeatParity(beat % 2);
|
setBeatParity(beat % 2);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const gated = gate !== null;
|
const isSync = syncMode === 'option' && !!rhythm.syncToExercise;
|
||||||
const running = rhythm.active && (gate === null || gate);
|
const runActive = gate === true;
|
||||||
|
// Sync: pendulum/click only while the run is active (the run's scheduler
|
||||||
|
// is the click source; the widget's own scheduler stays off).
|
||||||
|
// Free/plain: the widget's own scheduler runs whenever armed.
|
||||||
|
const running = isSync ? runActive : rhythm.active;
|
||||||
|
|
||||||
// Own click scheduler — only when NOT gated.
|
|
||||||
const { restart } = useMetronome({
|
const { restart } = useMetronome({
|
||||||
bpm: rhythm.bpm,
|
bpm: rhythm.bpm,
|
||||||
volume: rhythm.sound ? rhythm.volume : 0,
|
volume: rhythm.sound ? rhythm.volume : 0,
|
||||||
playing: running && !gated,
|
playing: running && !isSync,
|
||||||
onTick: handleTick,
|
onTick: handleTick,
|
||||||
});
|
});
|
||||||
void restart;
|
void restart;
|
||||||
|
|
||||||
// Gated: pendulum mirrors the phrase run's beats (audio-time scheduled).
|
// Sync: pendulum mirrors the phrase run's beats (audio-time scheduled).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!gated || !running) return;
|
if (!isSync || !runActive) return;
|
||||||
return phraseBeatBus.subscribe((_beat, at) => {
|
return phraseBeatBus.subscribe((_beat, at) => {
|
||||||
const delay = Math.max(0, (at - audioEngine.now()) * 1000);
|
const delay = Math.max(0, (at - audioEngine.now()) * 1000);
|
||||||
window.setTimeout(() => setBeatParity(p => (p + 1) % 2), delay);
|
window.setTimeout(() => setBeatParity(p => (p + 1) % 2), delay);
|
||||||
});
|
});
|
||||||
}, [gated, running]);
|
}, [isSync, runActive]);
|
||||||
|
|
||||||
const beatMs = 60000 / Math.max(1, rhythm.bpm);
|
const beatMs = 60000 / Math.max(1, rhythm.bpm);
|
||||||
|
const armed = rhythm.active;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||||
{/* Header: label + single arm toggle */}
|
{/* Header: label + mode toggle */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
<label className="control-label" style={{ marginBottom: 0 }}>
|
<label className="control-label" style={{ marginBottom: 0 }}>
|
||||||
<span>Metronome</span>
|
<span>Metronome</span>
|
||||||
</label>
|
</label>
|
||||||
<button
|
{syncMode === 'option' ? (
|
||||||
className={`switch-button ${rhythm.active ? 'active' : ''}`}
|
<button
|
||||||
onClick={() => onUpdate({ active: !rhythm.active })}
|
className={`switch-button ${rhythm.syncToExercise ? 'active' : ''}`}
|
||||||
title={rhythm.active ? 'Turn Off' : 'Turn On (sound + pendulum)'}
|
onClick={() => onUpdate({
|
||||||
>
|
syncToExercise: !rhythm.syncToExercise,
|
||||||
<div className="switch-thumb" />
|
active: true, // armed in either mode; the mode decides behavior
|
||||||
</button>
|
})}
|
||||||
|
title={rhythm.syncToExercise
|
||||||
|
? 'Sync: starts with the exercise (count-in). Click while idle: off.'
|
||||||
|
: 'Free: runs independently — start/stop it yourself.'}
|
||||||
|
>
|
||||||
|
<div className="switch-thumb" />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className={`switch-button ${rhythm.active ? 'active' : ''}`}
|
||||||
|
onClick={() => onUpdate({ active: !rhythm.active })}
|
||||||
|
title={rhythm.active ? 'Turn Off' : 'Turn On (sound + pendulum)'}
|
||||||
|
>
|
||||||
|
<div className="switch-thumb" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Always visible body; dimmed when disarmed */}
|
{/* Always visible body; dimmed when disarmed */}
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', paddingTop: '8px', borderTop: '1px solid rgba(255,255,255,0.1)', opacity: rhythm.active ? 1 : 0.5 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', paddingTop: '8px', borderTop: '1px solid rgba(255,255,255,0.1)', opacity: armed ? 1 : 0.5 }}>
|
||||||
{/* Pendulum */}
|
{/* Pendulum */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -110,7 +132,7 @@ export const MetronomeWidget: React.FC<MetronomeWidgetProps> = ({
|
|||||||
background: 'var(--color-text-main)',
|
background: 'var(--color-text-main)',
|
||||||
transform: 'translateY(-50%)',
|
transform: 'translateY(-50%)',
|
||||||
transition: `left ${beatMs}ms linear`,
|
transition: `left ${beatMs}ms linear`,
|
||||||
display: rhythm.active ? 'block' : 'none',
|
display: armed && running ? 'block' : 'none',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -82,14 +82,16 @@ export const PhraseTrainer = forwardRef<PhraseHandle, PhraseTrainerProps>(
|
|||||||
// (single source of truth across modes; the phrase-local BPM control
|
// (single source of truth across modes; the phrase-local BPM control
|
||||||
// was replaced by the common metronome widget in Controls).
|
// was replaced by the common metronome widget in Controls).
|
||||||
phrase.bpm = settings.rhythm.bpm;
|
phrase.bpm = settings.rhythm.bpm;
|
||||||
// "Sync to metronome" (tempo): the phrase run owns the clock and the
|
// Click decision — ONE source: the metronome widget's sound checkbox.
|
||||||
// click decision comes from the METRONOME widget's sound checkbox —
|
// - Tempo + syncToExercise: the run clicks when armed && sound
|
||||||
// one place, no separate phrase click option. "At your pace" (step):
|
// (silent while idle; starts with the exercise incl. count-in).
|
||||||
// the widget clicks freely when armed (metronomeActive); a legacy
|
// - Tempo + free metronome: the widget clicks freely; the run stays
|
||||||
// phrase.clickSound=true also allows clicks without the metronome.
|
// silent (its beats are visual only) to avoid doubling.
|
||||||
|
// - Step: widget clicks freely; phrase clickSound follows the switch.
|
||||||
|
const sync = settings.rhythm.syncToExercise;
|
||||||
phrase.clickSound = phrase.pace === 'tempo'
|
phrase.clickSound = phrase.pace === 'tempo'
|
||||||
? settings.rhythm.sound
|
? (sync ? settings.rhythm.active && settings.rhythm.sound : false)
|
||||||
: settings.rhythm.active || phrase.clickSound;
|
: settings.rhythm.active && settings.rhythm.sound;
|
||||||
const setPhrase = useCallback((updates: Partial<PhraseSettings>) => {
|
const setPhrase = useCallback((updates: Partial<PhraseSettings>) => {
|
||||||
updateSettings(s => {
|
updateSettings(s => {
|
||||||
const next = { ...s, phrase: { ...getPhraseSettings(s), ...updates } };
|
const next = { ...s, phrase: { ...getPhraseSettings(s), ...updates } };
|
||||||
@@ -197,6 +199,7 @@ export const PhraseTrainer = forwardRef<PhraseHandle, PhraseTrainerProps>(
|
|||||||
meter: { numerator: phrase.meterNumerator, denominator: 4 },
|
meter: { numerator: phrase.meterNumerator, denominator: 4 },
|
||||||
bars: phrase.arpeggioBars,
|
bars: phrase.arpeggioBars,
|
||||||
progression: phrase.arpeggioProgression,
|
progression: phrase.arpeggioProgression,
|
||||||
|
chordSelection: phrase.arpeggioChordSelection as ArpeggioDegree[],
|
||||||
rhythm: phrase.arpeggioRhythm,
|
rhythm: phrase.arpeggioRhythm,
|
||||||
seed: melodySeed,
|
seed: melodySeed,
|
||||||
}, pool);
|
}, pool);
|
||||||
@@ -214,7 +217,8 @@ export const PhraseTrainer = forwardRef<PhraseHandle, PhraseTrainerProps>(
|
|||||||
}, [phrase.material, phrase.keyTonic, phrase.keyMode, phrase.bars, phrase.meterNumerator,
|
}, [phrase.material, phrase.keyTonic, phrase.keyMode, phrase.bars, phrase.meterNumerator,
|
||||||
phrase.rhythmLevel, phrase.scaleDirection, phrase.scaleCoverage, phrase.scaleRhythm, phrase.libraryId,
|
phrase.rhythmLevel, phrase.scaleDirection, phrase.scaleCoverage, phrase.scaleRhythm, phrase.libraryId,
|
||||||
phrase.arpeggioDegree, phrase.arpeggioPattern, phrase.arpeggioCoverage,
|
phrase.arpeggioDegree, phrase.arpeggioPattern, phrase.arpeggioCoverage,
|
||||||
phrase.arpeggioBars, phrase.arpeggioProgression, phrase.arpeggioRhythm, melodySeed,
|
phrase.arpeggioBars, phrase.arpeggioProgression, phrase.arpeggioRhythm,
|
||||||
|
phrase.arpeggioChordSelection, melodySeed,
|
||||||
melodySeed, pool, importedScore]);
|
melodySeed, pool, importedScore]);
|
||||||
|
|
||||||
const fullScore: Result<Score> | null = useMemo(() => {
|
const fullScore: Result<Score> | null = useMemo(() => {
|
||||||
@@ -631,6 +635,10 @@ export const PhraseTrainer = forwardRef<PhraseHandle, PhraseTrainerProps>(
|
|||||||
<option value="giuliani-pami">p-a-m-i</option>
|
<option value="giuliani-pami">p-a-m-i</option>
|
||||||
<option value="giuliani-mami">m-a-m-i</option>
|
<option value="giuliani-mami">m-a-m-i</option>
|
||||||
<option value="giuliani-ami">a-m-i</option>
|
<option value="giuliani-ami">a-m-i</option>
|
||||||
|
<option value="giuliani-pimamim">p-i-m-a-m-i</option>
|
||||||
|
<option value="giuliani-papima">p-a-p-i-m-a</option>
|
||||||
|
<option value="giuliani-amipim">a-m-i-p-i-m</option>
|
||||||
|
<option value="giuliani-miamim">m-i-a-m-i-m</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
@@ -654,6 +662,35 @@ export const PhraseTrainer = forwardRef<PhraseHandle, PhraseTrainerProps>(
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
{phrase.arpeggioBars > 1 && (
|
||||||
|
<div style={{ gridColumn: '1 / -1' }}>
|
||||||
|
<span style={{ fontSize: '11px', opacity: 0.7, display: 'block', marginBottom: '4px' }}>
|
||||||
|
Chord pool ({phrase.arpeggioChordSelection.length === 0 ? 'all 7' : `${phrase.arpeggioChordSelection.length} selected`}):
|
||||||
|
</span>
|
||||||
|
<div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap' }}>
|
||||||
|
{(Object.keys(ARPEGGIO_DEGREE_LABELS) as ArpeggioDegree[]).map(d => {
|
||||||
|
const active = phrase.arpeggioChordSelection.length === 0 || phrase.arpeggioChordSelection.includes(d);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={d}
|
||||||
|
className={`control-button small ${active ? 'active' : ''}`}
|
||||||
|
style={{ minWidth: '42px', padding: '5px 8px', fontSize: '12px' }}
|
||||||
|
onClick={() => {
|
||||||
|
// Toggle; empty selection = all back on.
|
||||||
|
const cur = phrase.arpeggioChordSelection.length === 0
|
||||||
|
? (Object.keys(ARPEGGIO_DEGREE_LABELS) as ArpeggioDegree[])
|
||||||
|
: [...phrase.arpeggioChordSelection as ArpeggioDegree[]];
|
||||||
|
const next = cur.includes(d) ? cur.filter(x => x !== d) : [...cur, d];
|
||||||
|
setPhrase({ arpeggioChordSelection: next });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ARPEGGIO_DEGREE_LABELS[d]}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<label>Rhythm
|
<label>Rhythm
|
||||||
<select value={phrase.arpeggioRhythm} onChange={e => setPhrase({ arpeggioRhythm: e.target.value as PhraseSettings['arpeggioRhythm'] })}>
|
<select value={phrase.arpeggioRhythm} onChange={e => setPhrase({ arpeggioRhythm: e.target.value as PhraseSettings['arpeggioRhythm'] })}>
|
||||||
<option value="quarters">Quarter notes</option>
|
<option value="quarters">Quarter notes</option>
|
||||||
|
|||||||
@@ -215,11 +215,6 @@ export function usePhraseTrainer(
|
|||||||
if (e.kind === 'count-in-beat' || e.kind === 'beat') {
|
if (e.kind === 'count-in-beat' || e.kind === 'beat') {
|
||||||
phraseBeatBus.emit(e.beat ?? 0, e.at);
|
phraseBeatBus.emit(e.beat ?? 0, e.at);
|
||||||
}
|
}
|
||||||
// Publish every beat to the shared bus so the metronome widget's
|
|
||||||
// pendulum stays in sync with the run (click or no click).
|
|
||||||
if (e.kind === 'count-in-beat' || e.kind === 'beat') {
|
|
||||||
phraseBeatBus.emit(e.kind === 'count-in-beat' ? (e.beat ?? 0) : ((e.beat ?? 0) + countInBeatsRef.current), e.at);
|
|
||||||
}
|
|
||||||
clickIdxRef.current++;
|
clickIdxRef.current++;
|
||||||
}
|
}
|
||||||
// Fire state boundaries only when the clock reaches them.
|
// Fire state boundaries only when the clock reaches them.
|
||||||
|
|||||||
@@ -22,7 +22,11 @@ export type ArpeggioPattern =
|
|||||||
| 'giuliani-pima' // root-third-fifth-octave (p-i-m-a)
|
| 'giuliani-pima' // root-third-fifth-octave (p-i-m-a)
|
||||||
| 'giuliani-pami' // root-octave-fifth-third (p-a-m-i)
|
| 'giuliani-pami' // root-octave-fifth-third (p-a-m-i)
|
||||||
| 'giuliani-mami' // third-fifth-octave-third (inner-voice feel)
|
| 'giuliani-mami' // third-fifth-octave-third (inner-voice feel)
|
||||||
| 'giuliani-ami'; // octave-fifth-third (descending from the top)
|
| 'giuliani-ami' // octave-fifth-third (descending from the top)
|
||||||
|
| 'giuliani-pimamim' // p-i-m-a-m-i (six-note rolling figure)
|
||||||
|
| 'giuliani-papima' // p-a-p-i-m-a (bass-skip pattern)
|
||||||
|
| 'giuliani-amipim' // a-m-i-p-i-m (tremolo-adjacent)
|
||||||
|
| 'giuliani-miamim'; // m-i-a-m-i-m (triple-string figure)
|
||||||
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';
|
||||||
@@ -63,6 +67,10 @@ export const PATTERN_LABELS: Record<ArpeggioPattern, string> = {
|
|||||||
'giuliani-pami': 'Giuliani p-a-m-i',
|
'giuliani-pami': 'Giuliani p-a-m-i',
|
||||||
'giuliani-mami': 'Giuliani m-a-m-i',
|
'giuliani-mami': 'Giuliani m-a-m-i',
|
||||||
'giuliani-ami': 'Giuliani a-m-i',
|
'giuliani-ami': 'Giuliani a-m-i',
|
||||||
|
'giuliani-pimamim': 'Giuliani p-i-m-a-m-i',
|
||||||
|
'giuliani-papima': 'Giuliani p-a-p-i-m-a',
|
||||||
|
'giuliani-amipim': 'Giuliani a-m-i-p-i-m',
|
||||||
|
'giuliani-miamim': 'Giuliani m-i-a-m-i-m',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,6 +84,10 @@ const GIULIANI_FIGURES: Record<string, number[]> = {
|
|||||||
'giuliani-pami': [0, 3, 2, 1],
|
'giuliani-pami': [0, 3, 2, 1],
|
||||||
'giuliani-mami': [1, 2, 3, 1],
|
'giuliani-mami': [1, 2, 3, 1],
|
||||||
'giuliani-ami': [3, 2, 1],
|
'giuliani-ami': [3, 2, 1],
|
||||||
|
'giuliani-pimamim': [0, 1, 2, 3, 2, 1],
|
||||||
|
'giuliani-papima': [0, 3, 0, 1, 2, 3],
|
||||||
|
'giuliani-amipim': [3, 2, 1, 0, 1, 2],
|
||||||
|
'giuliani-miamim': [2, 1, 3, 2, 1, 2],
|
||||||
};
|
};
|
||||||
|
|
||||||
/** pc of a spelled degree (step letter + alter). */
|
/** pc of a spelled degree (step letter + alter). */
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ export interface RhythmSettings {
|
|||||||
bpm: number;
|
bpm: number;
|
||||||
seconds: number;
|
seconds: number;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
|
/** Phrase Mode: sync the metronome to the exercise (starts with it,
|
||||||
|
* silent while idle) instead of running freely. */
|
||||||
|
syncToExercise: boolean;
|
||||||
autoAdvance: boolean;
|
autoAdvance: boolean;
|
||||||
sound: boolean;
|
sound: boolean;
|
||||||
volume: number;
|
volume: number;
|
||||||
@@ -164,6 +167,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
|||||||
bpm: 60,
|
bpm: 60,
|
||||||
seconds: 5,
|
seconds: 5,
|
||||||
active: false,
|
active: false,
|
||||||
|
syncToExercise: true,
|
||||||
autoAdvance: false,
|
autoAdvance: false,
|
||||||
sound: true,
|
sound: true,
|
||||||
volume: 0.5
|
volume: 0.5
|
||||||
|
|||||||
Reference in New Issue
Block a user