fix: LFO→cutoff modulation, visual knob feedback, persistent hints
- Fix LFO→Filter cutoff: add scaling Gain nodes so LFO (-1..1) maps to meaningful Hz modulation (±cutoff value). Same fix for LFO→Osc freq. Mod scale updates dynamically when user changes the base param value. - Visual modulation indicator: knobs receiving LFO/modulation show a pulsing dashed ring animation (spin + pulse) around the knob arc - Persist hint usage per level: using a hint permanently caps that level at 2 stars — survives reload/restart. No more cheating by restarting! - Hint state stored in separate localStorage key (synthquest-hints) - Admin reset also clears hint history Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -24,11 +24,16 @@ function createNode(mod) {
|
||||
case 'oscillator': {
|
||||
const osc = new Tone.Oscillator({ type: p.waveform, frequency: p.frequency, detune: p.detune });
|
||||
osc.start();
|
||||
// Modulation scaler for freq input: LFO (-1..1) × scale → added to osc.frequency
|
||||
// Scale = half the current frequency so modulation is musically meaningful
|
||||
const freqMod = new Tone.Gain(p.frequency * 0.5);
|
||||
freqMod.connect(osc.frequency);
|
||||
return {
|
||||
node: osc,
|
||||
inputs: { freq: osc.frequency, detune: osc.detune },
|
||||
_freqMod: freqMod,
|
||||
inputs: { freq: freqMod, detune: osc.detune },
|
||||
outputs: { out: osc },
|
||||
dispose: () => { osc.stop(); osc.dispose(); },
|
||||
dispose: () => { osc.stop(); freqMod.disconnect(); freqMod.dispose(); osc.dispose(); },
|
||||
};
|
||||
}
|
||||
case 'lfo': {
|
||||
@@ -53,11 +58,16 @@ function createNode(mod) {
|
||||
}
|
||||
case 'filter': {
|
||||
const filter = new Tone.Filter({ type: p.type, frequency: p.frequency, Q: p.Q });
|
||||
// Modulation scaler for cutoff input: LFO (-1..1) × scale → added to filter.frequency
|
||||
// Scale = cutoff value so full LFO sweep covers 0 to 2× the cutoff
|
||||
const cutoffMod = new Tone.Gain(p.frequency);
|
||||
cutoffMod.connect(filter.frequency);
|
||||
return {
|
||||
node: filter,
|
||||
inputs: { in: filter, cutoff: filter.frequency },
|
||||
_cutoffMod: cutoffMod,
|
||||
inputs: { in: filter, cutoff: cutoffMod },
|
||||
outputs: { out: filter },
|
||||
dispose: () => filter.dispose(),
|
||||
dispose: () => { cutoffMod.disconnect(); cutoffMod.dispose(); filter.dispose(); },
|
||||
};
|
||||
}
|
||||
case 'envelope': {
|
||||
@@ -277,7 +287,11 @@ export function updateParam(moduleId, paramName, value) {
|
||||
switch (mod.type) {
|
||||
case 'oscillator':
|
||||
if (paramName === 'waveform') entry.node.type = value;
|
||||
else if (paramName === 'frequency') entry.node.frequency.value = value;
|
||||
else if (paramName === 'frequency') {
|
||||
entry.node.frequency.value = value;
|
||||
// Update mod scaler proportionally
|
||||
if (entry._freqMod) entry._freqMod.gain.value = value * 0.5;
|
||||
}
|
||||
else if (paramName === 'detune') entry.node.detune.value = value;
|
||||
break;
|
||||
case 'lfo':
|
||||
@@ -290,7 +304,11 @@ export function updateParam(moduleId, paramName, value) {
|
||||
break;
|
||||
case 'filter':
|
||||
if (paramName === 'type') entry.node.type = value;
|
||||
else if (paramName === 'frequency') entry.node.frequency.value = value;
|
||||
else if (paramName === 'frequency') {
|
||||
entry.node.frequency.value = value;
|
||||
// Update mod scaler proportionally
|
||||
if (entry._cutoffMod) entry._cutoffMod.gain.value = value;
|
||||
}
|
||||
else if (paramName === 'Q') entry.node.Q.value = value;
|
||||
break;
|
||||
case 'envelope':
|
||||
|
||||
Reference in New Issue
Block a user