The periodic audio glitches were caused by main thread starvation:
~840 events/sec during playback starved the audio buffer.
Changes:
- Master clock 480→120 Hz (still 6x headroom for 300 BPM sixteenths)
- Connection cache: replace O(n) reduce hash with dirty flag (zero work
on cache hit, flag set only when connections actually change)
- Tone.js lookAhead: 100ms→50ms for tighter scheduling
- ModuleNode LFO visualization RAF: 60fps→15fps (every 4th frame)
- ScopeDisplay RAF: 60fps→30fps (every 2nd frame)
Net effect: ~840 events/sec → ~200 events/sec during playback.
Audio processing gets 4x more main thread headroom.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The _masterTicks++ counter fell behind when Tone.Clock callbacks were
delayed by GC pauses, UI interactions, or tab throttling. The counter
never recovered, causing cumulative drift between sequencers.
Now ticks are derived from the callback's time parameter (which comes
from AudioContext.currentTime — hardware clock, always precise):
ticks = Math.round((time - startTime) * MASTER_TICK_RATE)
If a callback is delayed by 50ms, the time is still correct and ticks
jump ahead to the right value. No accumulation, no drift, self-healing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: floor(elapsed * rateA) vs floor(elapsed * rateB) where
rateB = 2*rateA doesn't maintain exact 2:1 ratio due to floating-point
multiplication errors. This creates a beat/aliasing pattern where
sequencers at 80 and 160 BPM periodically go in and out of phase.
Fix: Master clock now uses an integer tick counter (_masterTicks++)
instead of floating-point elapsed time. Sequencers derive steps via:
stepIdx = floor(ticks / ticksPerStep) % numSteps
where ticks is an integer — no floating-point accumulation possible.
Also bumped master clock to 480 Hz for cleaner division at common BPMs:
80 BPM: 480*60/320 = 90 ticks/step (exact)
120 BPM: 480*60/480 = 60 ticks/step (exact)
160 BPM: 480*60/640 = 45 ticks/step (exact)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The _masterTime was captured from Tone.now() BEFORE the clock started,
but the time parameter in Tone.Clock callbacks comes from a different
scheduler timeline. This caused elapsed to drift systematically.
Now _masterTime is set from the first callback's own time parameter,
guaranteeing both are on the exact same clock source. Zero drift.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Master clock 960→240 Hz: reduces CPU/GC pressure by 4x while still
providing 12x headroom for 300 BPM sixteenths
- Remove Tone.getContext().setTimeout() for note-off scheduling —
these accumulated over time causing periodic hiccups
- Note-off now happens at step boundary: previous gate turned off
at the start of each new step (cleaner, zero accumulation)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace independent Tone.Clock per sequencer/pianoroll with a single
shared master clock running at 960 Hz in audioEngine.
Architecture:
- Master clock starts/stops with audio engine (startAudio/stopAudio)
- Widgets subscribe via subscribeTick(id, callback) receiving
(audioTime, elapsed) on every tick
- Each widget derives its own step/position from elapsed time and
its own BPM, so different BPMs stay perfectly in sync
- BPM/steps/bars changes are read from refs (no clock restart needed)
Benefits:
- All timing derived from one clock source = zero relative drift
- No clock recreation on param changes = no glitches
- 960 Hz tick rate ≈ 1ms precision (plenty for musical timing)
- Sequencer at 80 BPM and 160 BPM maintain perfect 1:2 ratio forever
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Problem: two sequencers at different BPMs (e.g. 80 and 160) would
drift apart over time because each used an independent step counter
(step++) that accumulated floating-point rounding errors.
Fix: derive step/position from audio clock time (Tone.now()), not
from an incrementing counter. Step = floor(elapsed * rate) % numSteps.
This makes timing mathematically exact regardless of how long it runs.
Also:
- Sequencer note-off uses Tone.getContext().setTimeout() (audio-thread)
instead of Tone.Transport.scheduleOnce() which needs Transport running
- Clock runs at 2x rate for tighter step edge detection
- PianoRoll uses same time-based position calculation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
UI sounds weren't playing until the user hit Play because Tone.js
AudioContext was suspended. Now Tone.start() is called on the first
pointerdown or keydown event, so UI sounds work immediately.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Call handleCenterView after level load with a short delay to let
the DOM settle, so modules are centered on screen on both mobile
and desktop.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Web app manifest with name, icons, theme color, standalone display
- Service worker with stale-while-revalidate caching strategy
- 192px and 512px PNG icons generated from favicon.svg
- Apple-specific meta tags for iOS home screen support
- Register service worker on page load
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The fullscreen piano/drumpad was rendering inside ModuleNode which has
CSS transform: scale(zoom). This breaks position: fixed (fixed elements
inside a transformed parent position relative to the transform, not the
viewport). Using createPortal to document.body fixes this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace double-tap trigger with ⤢ expand button in module header
for keyboard and drumpad modules (more reliable, no text selection)
- Disable user-select globally (except inputs/textareas)
- Fullscreen state managed in ModuleNode, passed to widgets as props
- Remove unused imports (useIsMobile, useRef) from widgets
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fullscreen piano redesign:
- 1 octave with 7 large white keys filling the entire screen
- Gradient-lit keys with cyan press highlight
- Octave navigation buttons (◀ ▶) to shift up/down
- Note labels on each key (C4, D4, etc.)
- Black keys proportionally sized at 58% height
- touch-action: none to prevent any browser interference
Block native browser zoom:
- viewport meta: maximum-scale=1.0, user-scalable=no
- html touch-action: manipulation (prevents double-tap zoom on Safari)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Keyboard fullscreen:
- Double-tap keyboard widget to enter fullscreen piano mode
- 2-octave touch-friendly piano with labeled keys
- Active key highlights cyan, close button to exit
Drum Pad module (🥁):
- New module type with 4x4 colored pad grid
- Each pad triggers a unique frequency (C2-D4 range)
- Outputs freq + gate signals (same as keyboard)
- Double-tap for fullscreen pad mode with large touch targets
- Color-coded pads with hit animation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Bottom sheet starts collapsed (handle bar only), swipe up to expand
- Tabs visible when collapsed in puzzle view, content hidden
- Swipe down or tap handle to collapse
- Add usePinchZoom hook: two-finger pinch gesture controls canvas zoom
- Pinch zoom wired into both Sandbox and Puzzle View canvases
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add touch-action: none on canvas to prevent browser scroll hijack
- Single-finger touch on empty canvas now triggers pan (pointerType check)
- Fix page bounce on mobile with position: fixed and 100dvh height
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The zoom panel was offset 220px from the right (legacy offset for a
non-existent right sidebar). Now sits flush at right:12px matching
the puzzle view layout.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add ⌂ button to zoom bar (sandbox + puzzle) that centers camera on
all modules
- Fix sequencer _steps array not growing when step count param increases
(e.g. 8→32 now properly adds new empty steps)
- Make piano roll width dynamic based on bar count (BEAT_PX constant
density instead of fixed ROLL_W)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Module width now adapts to step/bar count so extra steps are never
hidden. Sequencer width scales with numSteps, piano roll width scales
with bar count using a fixed BEAT_PX density.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace shared global Tone.Transport with per-instance Tone.Clock so
multiple sequencers and pianorolls run independently without interfering
with each other's timing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix pianoroll/sequencer Transport not resetting on stop/restart (notes
were scheduled in the past and never fired)
- Stop and cancel Transport in stopAudio() to prevent stale events
- Add zoom +/- buttons to scope widget (6 levels, 64–2048 samples)
- Increase scope analyser buffer from 256 to 2048 for wider time view
- Add vertical grid lines to scope display
- Add "Limpiar" clear canvas button to PuzzleView
- Skip audio-graph connection for keyboard/seq/pianoroll freq→osc freq
(direct frequency setting prevents inaudible ultrasonic values)
- Auto-trigger envelopes without gate connections for noise/ambient levels
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Enhance targetAudio.js with envelope (ADSR), LFO modulation, effects
(delay/reverb/distortion), and retrigger patterns for rhythmic sounds
- Fill in target audio configs for 87 levels (worlds 3-12) that had empty
build arrays, making the "Objetivo" preview button functional everywhere
- Increase base sizes for modules, sidebar, ports, knobs, and typography
so the UI feels less empty at 100% zoom
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the bypass auto-solve (which just set passed: true on all checks)
with a legitimate solver that loads actual module configurations and
connections via deserialize(), then validates through handleCheck().
Each solution defines the exact modules, parameters, and wiring needed
to pass all 3-star checks for every level across all 12 worlds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Search by level name, subtitle, ID, or world name. Shows filtered
results as a flat grid with world.level numbering and world color.
Escape key clears search, clear button resets and refocuses input.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Flex children with default flex-shrink:1 were compressing to fit,
hiding the Mision panel content. Force flex-shrink:0 on all sidebar
children so they keep their natural size and the sidebar scrolls.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add adminMode toggle in AdminPanel (green "Admin ON/OFF" button)
- Pass adminMode through GameApp → PuzzleView
- Show purple "🛠 Resolver" button in puzzle toolbar when admin is active
- Auto-solve gives 3 stars instantly and shows completion overlay
- Lets admin skip through all 96 levels for rapid testing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add min-height: 0 to .gm-puzzle-content and .gm-puzzle-sidebar so
flexbox allows shrinking below content size, enabling overflow-y scroll.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add procedural UI sound effects (connect/disconnect, engine start/stop,
level complete/fail, star earned, hint, navigation) via Tone.js
- Live LFO modulation visualization: knobs animate in real-time showing
modulated value, ghost dot shows base value, number glows cyan
- Fix wire recalculation on zoom/pan/level re-entry (post-layout refresh)
- Fix retry button to keep current patch instead of reloading level
- Fix default param detection: newly added modules now populate all
default params so level checkers work without manual param changes
- Add worlds 7-12: Secuencias y Ritmos, Texturas de Ruido, Síntesis
Sustractiva, Espacio y Stereo, Técnicas Avanzadas, Gran Final
(48 new levels, 144 new possible stars, 288 total stars)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 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>
- World 2 (Filtros): 8 levels teaching filters, resonance, LFO modulation, acid bass
- World 3 (Envelopes): 8 levels teaching VCA, ADSR, pluck, tremolo, full synth lead
- Star-based world unlock system (12 stars for W2, 24 for W3)
- Level patch persistence: auto-saves player patches, restores on revisit
- Google Maps-style zoom controls (+/−/reset) in both puzzle and sandbox views
- Multi-world navigation in GameApp and WorldMap
- Target audio now supports filter chain for World 2 levels
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Double-clicking a knob opens an inline text input for precise value
entry. Enter confirms, Escape cancels, blur auto-commits. Value is
clamped to the knob's min/max range. Styled to match the synth theme.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace elementFromPoint with bounding rect distance search for port
detection. The SVG wire overlay was intercepting pointer events,
requiring users to wait for animations before connecting. Now finds
the closest port-dot within 18px radius regardless of z-index.
- Add hint system: concept text hidden behind "Mostrar Pista" button.
Using the hint permanently caps the level at 2 stars max. Reiniciar
resets the penalty. Visual feedback in objectives and completion screen.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Gamified synth learning inspired by Turing Complete. Progressive puzzle
system teaches oscillators, waveforms, frequency, and mixing through
hands-on module patching. Includes world map, level progression with
3-star rating, target audio playback, solution validation, and
localStorage persistence. Sandbox mode still accessible via button.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Part events were pre-converted to seconds via Tone.Time().toSeconds(),
making the BPM setting ineffective. Now uses bars:quarters:sixteenths
notation so the Transport BPM actually controls playback speed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix mouse coordinate scaling in piano roll when canvas is zoomed
(getBoundingClientRect returns visual size, now dividing by scale ratio)
- Replace Megaman melody with Super Mario Bros overworld theme
- Tune chiptune preset: faster BPM (200), snappier envelopes, brighter
filter, less delay/distortion for cleaner NES sound
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix wires not recalculating positions on zoom until panning
- Add MIDI file import button to Piano Roll (parses .mid files)
- Allow envelope release to go to 0 (was clamped at 0.001)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add step sequencer (16-step with note/gate editing) and piano roll
(canvas-based MIDI editor with draw/erase tools). Includes a Megaman-style
melody in C minor. Chiptune preset now uses piano roll instead of keyboard.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace fragile DOM position matching in finishConnection with
data-module-id/data-port-name/data-port-direction attributes
- Add nearby port detection (8px radius) for easier connections
- Wire glow effects with drop-shadow filters
- Port dots z-index above wires for reliable click targeting
- Chiptune demo preset: 2x square osc, envelopes, VCAs, mixer,
filter, delay, distortion, scope — full 8-bit signal chain
- "Chiptune Demo" toolbar button to load the example
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>