Releases: AmMoPy/FIZX
Releases · AmMoPy/FIZX
Release list
Audited Slop
CHANGELOG
[1.0.0] — First Release
The Big Removals
- Removed Essentia WASM entirely — 5.2MB -> ~400KB studio. The binary alone was
3.5MB (4.7MB Base64-encoded). The algorithm it provided (BeatTrackerDegara) returns
a beat grid, not onsets. It quantizes to a steady tempo and fails on syncopated songs.
The browser FFT worker replaced it at zero size cost with measurably better results.
Essentia integration code preserved in git history as a monument to trying. - Removed Whisper + Spleeter (Python path) — replaced with
aeneas+ ONNX CTC.
Whisper transcribes freely, then maps segments to your lyrics. Alignment accuracy
depends on Whisper getting the words right. On rap slang for example this is optimistic.
aeneasforces lyrics onto the audio via DTW. It cannot get the words
wrong because it already knows them. - Removed inter-onset interval median for BPM — was returning ~2× correct BPM
on rap. Hi-hats fire on every 8th note. Median of all inter-onset intervals finds
the most frequent gap, which is the hi-hat interval, not the beat. Replaced with
autocorrelation of the onset envelope + octave correction. - Removed beat masking from vocal onset detection — attempted to remove drum bleed
by discarding vocal onsets within 30ms of a full-mix beat. Rap vocals for example land
on the beat intentionally. The mask was removing correct detections. - Removed L-R center-channel subtraction — L-R = 0 on any track with vocal widening
or reverb, which is most tracks. Was attenuating the vocal itself. Replaced with
mono downmix + bandpass. The bandpass does the actual work. - Removed HTML5
new Audio()playback — replaced withAudioContext/
AudioBufferSourceNodepipeline.Audio.currentTimedrifts relative to
AudioContext.currentTime. Beat scheduling was running off a clock that disagreed
with the sound card.AudioBufferSourceNodelocks timing to the driver clock.
sourceNode.onendedfires exactly at buffer end with no polling required. - Removed
scheduleAll/clearScheduledsetTimeout forest — was spawning
onesetTimeoutper beat at play time, then cancelling and re-spawning the entire
set on every pause, resume, and seek. On a 200-beat track that's 200 allocations
per play, 200 cancellations per pause, repeat. Replaced with a single integer
pointer (lastFiredBeatIdx) that advances inside the rAF loop. O(1) amortized
per frame. On seek: reset and fast-forward in a tight while loop. Zero allocations
per beat, ever. - Removed 20 DOM
.dust-particledivs — each had its own CSS animation and
its own compositor layer. 20 independent layers on a legacy GPU is not free.
Replaced with a singleFloat32Array(140)allocated once at boot and a single
beginPath()/fill()call per frame. One GPU draw call for all 20 particles.
The typed array is never reallocated. GC has nothing to do here. - Removed
void offsetWidthreflow triggers — were used to restart CSS
animations by forcing a synchronous layout calculation. Replaced with a
requestAnimationFramedefer: remove class on frame N, re-add on frame N+1.
The browser sees a genuine remove+add cycle without blocking the main thread
on a layout pass. - Removed the recording feature entirely — three implementation attempts,
all documented below in the Archaeology section. The correct conclusion is that
studio-grade video output from a browser on constrained hardware requires the
OS compositor, not a canvas. That is not a solvable problem from JS.
Added
Compiler (compile.py)
lyrics.jsonschema —{ LYRICS_RAW: [...], LYRICS: [...] }. EditLYRICS_RAW
by hand. Run--lyricsto populateLYRICS. Studio exports to same format.- Font injection — Base64-encoded subsetted woff2 fonts injected by
compile.py.
No Google Fonts request. Works offline. Subset generated viapyftsubset. - URL encoding for FIZX_TEMPLATE — replaced backtick escaping. The template contains
backticks, backslashes, and${sequences. Reliably escaping all of them across all
content variations is harder than it sounds.urllib.parse.quote+decodeURIComponent
handles it correctly every time. - Single-pass preset processing — Merged
build_styles_block()andbuild_presets_block()
intoextract_preset_data().build_fonts(['merri', 'deja'])now called once inmain().
Addedinline_blocks()function for replacing all/* @@TAG@@ */blocks in one regex pass.
Python extraction (ex_beats.py)
Extractorclass — single-pass extraction. Spleeter runs at most once even
when--bothis used. Previously called separate scripts sequentially, paying file
I/O cost twice.--beats,--lyrics,--both,--force,--onnxflags — granular control over
what gets re-extracted. Staleness check:beats.jsonis stale ifsourcefilename
changed;lyrics.jsonis stale ifLYRICSarray is empty.- aeneas forced alignment — DTW on MFCC features. Aligns lyrics to audio.
Runs on full mix directly (no vocal separation needed).is_audio_file_detect_head_max
handles tracks with long instrumental intros without drifting. - ONNX CTC path — Sherpa-ONNX Spleeter (INT8) -> vocal stem -> wav2vec2 CTC forced
aligner. Offline after first model download. soxr_hq anti-aliasing on 44100->16kHz
resample. RMS normalization before alignment to prevent amplitude non-uniformity from
causing CTC to compress low-energy sections.
Studio (template_studio.html)
Audio Analysis
- Dual-engine analysis — Spectral Flux (FFT-based, timbral change) and Energy RMS
(amplitude-based, loud attacks). Both share the same peak-pick / backtrack / de-dupe
pipeline. Engine selection in UI. - Vocal stem proxy — separate analysis path for lyric snapping: mono downmix ->
500–8kHz bandpass + 3kHz peaking EQ (+6dB) -> spectral flux -> vocal-specific params.
SeparatePARAMS(vocalDelta, vocalWait, vocalMinGap) so tuning vocal detection
doesn't affect beat detection config. - Auto-tune grid search — 150-combination exhaustive search over delta/wait/minGap
space. Envelope pre-computed once; only peak-pick reruns (~1ms each). Total: ~150ms.
Proxy scoring: count score (0.6) + coverage score (0.2) + gap sanity score (0.2).
Discovered params written back to UI sliders. Toggle in vocal params panel. ENGINE_REGISTRY— declarative engine config. Adding a new engine: one object
literal.updateParamVisibility,showOverlay,initParamPanelsall driven from
registry. Zero hardcoded engine names in logic code.PARAM_SPECS— declarative parameter spec. Renders UI rows from config objects.
renderParamPanel(container, specs, idSuffix)builds DOM from spec.
resolveParams(engineId)followsinheritschain (one level).syncParamlooks up
spec type from flattened PARAM_SPECS — no engine context needed at call site.
Preview & Editing
- Waveform canvas preview — replaced sandboxed iframe. Single
<canvas>element:
waveform bars (top-peaked per column from normalised spectral/energy envelope),
onset dots (full-mix beats + vocal beats, toggleable), numbered draggable lyric lines.
No blob URL, no sandbox, no communication barrier. - Waveform data from Worker — computed during beat extraction pass (envelope already
normalised), posted astype:'waveform'TransferableFloat32Array. Zero additional
processing. Received on main thread, stored instate.waveformData. - Bidirectional drag sync — canvas lyric line drag and slider drag both route through
setResultStart(i, v, originEl). Single mutation point. Structural impossibility of
divergence.originElprevents feedback loops between inputs. - Zoom/pan on waveform — scroll wheel zoom centered on cursor. Right/middle drag pan.
Pinch zoom on touch. Shift+drag: 10× fine mode (1px ≈ 1ms instead of 15ms on 60s/400px).
Double-click reset. View window:_viewStart/_viewDur.xToTime/timeToXare the
only coordinate conversion functions — used everywhere. - Populate editor — three-mode beat-window distribution using vocal onsets when
available. Mode A (enough beats): first beat of each equal-size window. Mode B (sparse):
nearest-beat snap with collision avoidance. Mode C (no beats): even time distribution
with 5% end margin. Despite being called "AUTO-ASSIGN" it makes no accuracy claim. - Implicit end timestamps —
endfield removed fromstate.results. Template
renderer usesLYRICS[i+1].start ?? Infinityas implicit end.end[i] = start[i+1]
in compiled output. Eliminates drift when lines are reordered or deleted. - In-page editor rows — numbered lyric index, text input, start number input, delete,
timeline slider. Grid layout:[num | text | start | del]row 1,[slider]row 2.
row-active/row-dimmedCSS during drag. Scroll active row into view on drag start. - Playback controls in session — play/pause/stop with
<canvas>seek bar. Player
state drives rAF loops via event listeners (play,pause,ended,seeked).
Button handlers only callplayer.play()/player.pause().
Memory & Performance
- Unified rAF scheduler —
requestRedraw()+_rafTick(). Loop reschedules only
when!player.paused || _draggingIdx >= 0. One-shot redraws viarequestRedraw()
for seek-while-paused, drag end, zoom, param changes. Tab hidden:visibilitychange
pauses loop. AbortControllerfor drag listeners — replacesdocument.addEventListener+
manualremoveEventListenerpairs.abort()removes all drag listeners atomically.
No orphanedmousemovehandlers on preview-close-during-drag.- Module-scope
_seekThrottle— was local torenderPreviewEditor, creat...