Skip to content

Releases: AmMoPy/FIZX

Release list

Audited Slop

Choose a tag to compare

@AmMoPy AmMoPy released this 15 May 00:07

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.
    aeneas forces 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 with AudioContext /
    AudioBufferSourceNode pipeline. Audio.currentTime drifts relative to
    AudioContext.currentTime. Beat scheduling was running off a clock that disagreed
    with the sound card. AudioBufferSourceNode locks timing to the driver clock.
    sourceNode.onended fires exactly at buffer end with no polling required.
  • Removed scheduleAll / clearScheduled setTimeout forest — was spawning
    one setTimeout per 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-particle divs — each had its own CSS animation and
    its own compositor layer. 20 independent layers on a legacy GPU is not free.
    Replaced with a single Float32Array(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 offsetWidth reflow triggers — were used to restart CSS
    animations by forcing a synchronous layout calculation. Replaced with a
    requestAnimationFrame defer: 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.json schema{ LYRICS_RAW: [...], LYRICS: [...] }. Edit LYRICS_RAW
    by hand. Run --lyrics to populate LYRICS. Studio exports to same format.
  • Font injection — Base64-encoded subsetted woff2 fonts injected by compile.py.
    No Google Fonts request. Works offline. Subset generated via pyftsubset.
  • 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() and build_presets_block()
    into extract_preset_data(). build_fonts(['merri', 'deja']) now called once in main().
    Added inline_blocks() function for replacing all /* @@TAG@@ */ blocks in one regex pass.

Python extraction (ex_beats.py)

  • Extractor class — single-pass extraction. Spleeter runs at most once even
    when --both is used. Previously called separate scripts sequentially, paying file
    I/O cost twice.
  • --beats, --lyrics, --both, --force, --onnx flags — granular control over
    what gets re-extracted. Staleness check: beats.json is stale if source filename
    changed; lyrics.json is stale if LYRICS array 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.
    Separate PARAMS (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, initParamPanels all 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) follows inherits chain (one level). syncParam looks 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 as type:'waveform' Transferable Float32Array. Zero additional
    processing. Received on main thread, stored in state.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. originEl prevents 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/timeToX are 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 timestampsend field removed from state.results. Template
    renderer uses LYRICS[i+1].start ?? Infinity as 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-dimmed CSS 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 call player.play() / player.pause().

Memory & Performance

  • Unified rAF schedulerrequestRedraw() + _rafTick(). Loop reschedules only
    when !player.paused || _draggingIdx >= 0. One-shot redraws via requestRedraw()
    for seek-while-paused, drag end, zoom, param changes. Tab hidden: visibilitychange
    pauses loop.
  • AbortController for drag listeners — replaces document.addEventListener +
    manual removeEventListener pairs. abort() removes all drag listeners atomically.
    No orphaned mousemove handlers on preview-close-during-drag.
  • Module-scope _seekThrottle — was local to renderPreviewEditor, creat...
Read more