The single Strength control becomes four faders: the master Strength plus a
per-stage trim for EQ, Compressor and Limiter. Each stage's effective amount
is master × its own fader, so you can keep the tone shaping but ease off the
compression, push only the limiter for loudness, etc. Backward compatible —
presets still set the master `strength`; the new eqAmt/compAmt/limAmt default
to 1 (full).
* DSP (MixTightener): EQ gains scale by strength×eqAmt; compressor gain
reduction + makeup by strength×compAmt (compActive skips the whole comp
stage at compAmt 0); limiter ceiling + output make-up by strength×limAmt.
* Editor: four labelled vertical faders (Strength / EQ / Comp / Limit). Fixed
a refresh bug where a fader child composable didn't observe vm.revision, so
its value didn't visibly update after a drag (the DSP did update); it now
subscribes like ParamControl.
Verified on-device: dragging EQ to 19% left Strength/Comp/Limit at 100%
(independent control, UI refreshes). Test instance added on a spare slot for
verification, then cleared — project untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mix Tightener — a preset-driven "channel strip in a box" (Neutron-style):
parametric EQ → 3-band multiband compressor → peak limiter, all behind ONE
visible control, Strength, which morphs the whole chain from transparent (0)
to the full preset (1). Every per-band value is a hidden slot param the
presets set; the editor shows only a vertical Strength fader.
* DSP (Effects.kt): reusable RBJ biquad; LR-style 3-way crossover; per-band
peak-follower compressors; fast peak limiter + output make-up. Performance:
the expensive per-band gain computation (log/pow) runs at control rate while
the envelope follower runs per sample; Strength≈0 bypasses the whole effect,
and flat EQ bands / ratio≈1 comp bands are skipped. Output is clamped and
NaN-guarded so a channel FX can never poison the master.
* 30 factory presets (MixTightenerPresets) across drums (808 Crisp, Industrial
Drums, Trap Hats, Techno Kick, DnB Breaks, …), bass (EBM Bass, Reese, Acid
303, Sub Bass Tame, Goth Bass, Synthwave Bass, …), synths/leads/pads
(Synthwave Lead, Cutting Lead, Darkwave Pad, Arp Sparkle, …) and bus glue
(Mix Glue, Loudness Max), seeded via PresetLibrary.
Global: RetroDropdown now lays long option lists (>12) into balanced columns
so preset menus grow sideways instead of into a tall vertical scroll — applies
to every dropdown in the app.
Master loudness: raise MASTER_GAIN 0.35 → 0.5 (~+3 dB) so playback sits closer
to other media apps; the soft-knee limiter still catches peaks.
Builds clean (staging + release). On-device performance profiling and install
are pending — the device dropped off wireless adb mid-session.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Navigation and value-step actions (Nav Up/Down/Left/Right, Next/Prev
Column, Increment/Decrement) now auto-repeat while their control is held —
fire once on press, then accelerate from 320ms to 45ms per step — and stop
the instant it's released. Implemented ONCE as a shared repeater in
InputRouter (pressRepeat/releaseRepeat, keyed by the physical control id),
so all three hardware inputs get identical behaviour:
* Keyboard: the OS's own key auto-repeat is now ignored (held notes sustain
instead of retriggering) and holds are driven by the shared repeater;
key-up ends it.
* Gamepad: buttons, D-pad hat AND analog sticks repeat (they all funnel
through controlDown/controlUp); releasing the main control stops it,
releasing a chord modifier does not.
* MIDI: a momentary nav CC repeats between its press (value>=64) and
release (value<64).
One-shots (transport, tab switch, notes, clear) are unaffected — they fire
once even when held. InputRouter takes an injectable dispatcher so the new
InputRouterTest drives the accelerating schedule on a virtual clock
(verifies repeat-while-held, stop-on-release, one-shot-fires-once, and that
an unrelated release doesn't stop the active repeat). All unit tests pass;
on-device smoke confirms single-press nav + transport still work, no crash.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Single tap now acts immediately instead of just selecting: an empty tile
opens the device picker, a filled tile opens its settings editor (this was
the double-tap behaviour; the highlight-only single-tap and the double-tap
are gone). Long-press clears the tile — but now via a confirmation dialog
("Clear tile") rather than wiping instantly, since clearing discards the
device and its settings with no undo (matches the preset DELETE confirm).
Long-pressing an empty tile does nothing.
Removes the now-unused per-tile `selected` state/highlight and onDoubleTap.
Verified on-device: filled→editor, empty→picker, long-press→confirm(Cancel
keeps the tile).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two changes that cut playback-time UI jank (measured on-device):
- Expose a deduped isPlaying StateFlow (maps the transport flow through
distinctUntilChanged) and collect THAT in the transport toolbar, so the
PLAY/PAUSE button recomposes on play↔stop instead of ~12×/second as the
playhead advances (the full transport emits every line).
- Precompute the pattern grid's fixed-width text (gutter hex, note names,
velocity/channel) into class-load lookup tables. The grid redraws on
every playhead line and scroll pixel, and the old toString/padStart
formatting allocated a String per cell per frame — steady draw-phase
garbage. The draw is now allocation-free (also improves GlyphCache hit
rate via stable string instances).
gfxinfo over ~12s of playback on the busy track (moto g84):
janky frames 47.2% → 32.0%; 99th-pct frame 129ms → 53ms;
missed vsync 40 → 10; 50th 18ms → 14ms.
No engine/audio change. Remaining jank is the auto-follow scroll
redrawing the whole grid each line (everything shifts), not addressed here.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Rename the 3-osc synth's display name "NES Synth" → "Synth" (the enum
NES_SYNTH stays as the serialized id, so saved projects/presets are
unaffected).
- Knobs: replace absolute finger-position control (full range mapped onto
the 44dp knob → very touchy) with relative drag over ~280dp of travel,
so fine tweaks are easy and the control isn't sensitive.
- Add a darkwave/industrial preset family: Pluck Bass, Acid Pluck
(punchy plucky basses), Razor Lead, Cold Lead (cutting leads), and
Grit Sub, Detune Wall (harsh, detuned deep basses for layering under
bass pads).
- Preset seeding now records seeded names in the per-type marker instead
of a single flag, so factory presets added in a later version appear on
upgrade without resurrecting presets the user deleted (legacy "1"
markers upgrade cleanly).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Optimization B, superseding v0.4.6's per-bus skip. Instead of iterating
each active bus's whole POLYPHONY pool per sample (calling render() on
idle slots, which merely early-out), fillBlock now builds compact lists
of the voices that are actually sounding — once per block — and the
per-sample mix loop touches only those.
Correctness for mid-block starts: the sequencer can trigger a note part
way through a block, so the allocators queue the chosen voice into this
block's list (queueSeq/queueSmp), keeping note timing sample-accurate.
A per-voice generation stamp dedups the append so a voice that finishes
and is re-allocated within one block is never rendered twice (which would
double its amplitude). Live/bus-audition voices are snapshotted at block
start (a manual press mid-block sounds at the next block, ≤2 ms later —
imperceptible, and the pre-existing ~30 ms output buffer dwarfs it).
On-device (moto g84, 98% battery, real busy track, ~13 voices, 2.0 ms
budget), avg render callback:
baseline 2.00 ms (xruns +250..+515) → A 1.44 ms (+0) → B ~1.00 ms (+0)
B removes ~51 inactive-slot render() calls per sample; ART's virtual
dispatch makes each ~90 ns, so this is a real ~30% gain over A (not the
~1% first estimated from the sample track — measurement corrected it).
Fast-tapping audition during playback stays ~1.0-1.3 ms, xruns +0,
voices drain cleanly (no stuck notes, no double-render).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fillBlock rendered EVERY pooled voice every sample: 32+32 sequencer +
32+32 bus-audition + 16 live/audition voices, render() called ~144×
per frame × 96 frames per callback — even at idle. The bus-audition
pool (64 voices) is only used by the Toolbox MIDI piano yet ran every
sample during normal playback. This fixed cost, not active-voice count,
dominated the render budget.
Now the pools are scanned once per block into per-bus active flags, and
the per-sample mix loop skips whole idle buses and the entire bus-
audition pool when nothing is being auditioned. The sequencer sets the
flag the instant it triggers a note mid-block, so newly-started notes
still sound sample-accurately within the same block.
On-device profiling (moto g84, 98% battery, real busy track, ~11 sample
voices, 96-frame/2.0ms callback):
before: avg 2.00 ms, xruns +250..+515 per ~500 callbacks (audible drops)
after: avg 1.44 ms, xruns +0 (steady)
Fast-tapping the audition keyboard during playback: avg 1.1-1.4 ms,
xruns +0, voices peaked at 16 then drained cleanly — no stuck notes.
With this, under-load render time equals idle, so voice rendering is no
longer the bottleneck; the remaining ~1.4 ms is fixed JNI + per-channel
FX overhead. A block-based per-voice refactor was evaluated and dropped
as unnecessary (it would not reduce that fixed cost).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The subtractive synth's amplitude-envelope release decremented at
`(dt / r) * s` — the rate was multiplied by the sustain level, floored
at 0.001. For any percussive patch (sustain≈0: Square Bass, Pulse Pluck,
Noise Perc), release ran ~1000× too slow, so a 0.2 s tail took minutes.
Since a voice's isActive follows its amp envelope, the voice never freed
and kept rendering (3 osc + filter) indefinitely.
That one bug produced all three reported symptoms:
* missed note-offs / stuck notes needing panic — the note-off fires but
the voice takes minutes to go silent, reading as ignored;
* fast-tapping dropouts on every instrument — released-mid-envelope
voices pile up (whole pool + 8 live audition voices) and CPU climbs
until the audio callback overruns → xruns;
* audition-then-play dropouts — audition notes are already stuck on the
live pool before the sequencer stacks track voices on top.
Release now ramps to zero at a constant 1/r rate, reaching silence
within r seconds from any level (matching SampleVoice's behaviour). The
Sampler/SoundFont path already captured its release rate at note-off, so
it was less affected — but the shared synth voice pinned the CPU budget,
which is why the stutter showed across all instruments.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On-device profiling (worst case: 4 channels x 3 oscillators + cutoff & pitch
modulation, dense pattern at 200 BPM) showed the render callback averaging 2.8ms
against a 2.0ms budget with continuous xruns — the hiccups. The cost was the
per-sample Math.pow used for pitch modulation across 3 oscillators x many voices.
Recompute modulated oscillator pitch at control rate (every 16 samples, like the
filter cutoff) instead of per sample; LFO/envelopes move slowly enough that this
is inaudible. Render dropped to ~1.27ms avg with zero new xruns. No crashes under
load (the ConcurrentHashMap + stable SVF filter fixes from 0.4.3 hold).
Patch bump to 0.4.4 (versionCode 11).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address three issues introduced with the 3-oscillator synth:
- Crashes: ToolboxSlot.values was a plain HashMap read on the audio thread while
the UI wrote it (knob drags vs. the synth's ~40-param-per-note reads). Made it a
ConcurrentHashMap so get/put are atomic and reads are lock-free (RT-safe).
- Filter cutoff not affecting the sound: (a) the cheap ladder went unstable at high
cutoff (f > 1 -> NaN -> silent voice) — replaced with a stable Cytomic TPT
state-variable low-pass (with NaN recovery); (b) a voice captured its patch only
at note-on, so live knob turns were inaudible — the engine now refreshes active
voices' patches every block, so cutoff/level/mod changes affect held & playing
notes.
- Hiccups: every note allocated a SynthPatch (+arrays) causing GC dropouts — the
patch is now cached per bus and reparsed only on param/tempo change, so steady
playback allocates nothing on the audio thread.
Patch bump to 0.4.3 (versionCode 10).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add fling/drag vertical scrolling to the pattern grid via Modifier.scrollable
(fractional pixel offset for smooth, non-snapping motion). The view still follows
the playhead during playback (and the cursor when stopped) by default; a finger
flick detaches it into free inertial scrolling, and switching to another block
re-attaches the follow (the scroll state is keyed on the active pattern).
Tap-to-focus is unchanged. Patch bump to 0.4.2 (versionCode 9).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two changes that landed together (the note-off work was uncommitted when the synth
rewrite began):
Synth rewrite — replace the simple NES tone generator with a classic Minimoog-style
voice: 3 oscillators (+ noise) → mixer → resonant 4-pole ladder low-pass (own
contour envelope + key tracking) → amplifier (own envelope), plus a tempo-synced
LFO and a 4-slot modulation matrix.
- DSP: SynthVoice rewritten (PolyBLEP oscillators, ladder filter, two ADSRs, LFO,
mod matrix); new SynthPatch parses a slot into an immutable snapshot per note.
PolyBLEP + control-rate cutoff + precomputed osc tuning keep the RT cost down.
- Model: SynthDefs (shared option lists/defaults), rewritten NES_SYNTH params,
NesSynthPresets (8 chiptune factory presets) seeded into the preset library.
- Editor: NesSynthEditor — swipeable cards (Oscillators / Filter / Amp+LFO / Mod
Matrix) with dot pagination; new rotary Knob control (absolute, fader-like
vertical drag: bottom = empty, top = full); enums via RetroDropdown. The shared
slot-audition keyboard (renamed SoundFontKeyboard -> SlotAuditionKeyboard) is
pinned at the editor bottom.
- Engine builds a SynthPatch per note. The enum id stays NES_SYNTH so existing
projects still route; the old single `wave` param is dropped (amp envelope keys
carry over).
Track-tied note-offs — a note-off ("===") is now tied to its track, not a channel:
it has no velocity/channel of its own (shown as ".." and non-editable) and simply
releases whatever that track last played, on the bus(es) that note routed to
(AudioEngine.trackLastChannel). `.sng` export writes "OFF .. ..".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each transport-toolbar row now distributes its elements across the full width
(Arrangement.SpaceBetween) instead of clustering at the left: dropped the
per-row horizontalScroll (which gave the row infinite width and defeated any
distribution) and the manual landscape spacer. Rows no longer scroll — elements
fill the bar edge to edge. Patch bump to 0.3.2 (versionCode 5).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Configure the release variant's output filename (via the legacy applicationVariants
API) so the unsigned release APK is stamped with its versionCode — e.g.
space.rcmd.android.sizzle_4.apk — to simplify uploading to the private F-Droid
repo. Staging keeps its app-staging.apk name.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lay out and place the SF2/XI loader's audition keyboard like the toolbox and
tracker keyboards: extract it into SoundFontKeyboard (an octave selector above two
weighted octaves) and pin it at the bottom of the device editor, with the controls
scrolling in a weighted region above (1.7 : 1, matching the toolbox tab) — so it's
the same height and always visible instead of scrolling away. Patch bump to 0.3.1
(versionCode 4).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- SF2/XI loader: add a Loop XFade control (ms, 0-500) next to the loop toggle.
SampleVoice crossfades the loop tail into its head for a click-free seam even
when the file's loop points don't match; 0 ms keeps the previous hard wrap.
Threaded through VoiceSample and all noteOn call sites.
- Add a reusable RepeatButton: fires on press then auto-repeats while held with
an exponentially shrinking interval (stops on release/scroll-off). Applied to
every +/- stepper (tempo, MIDI channel, arrangement repeats, audition/keyboard
octave, and the new crossfade).
- Bump version to 0.3.0 (versionCode 3) per the minor-per-change policy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Global rename of the app/package identifier from
com.reactorcoremeltdown.sizzletracker to space.rcmd.android.sizzle:
- build.gradle.kts: applicationId + namespace.
- Move the Kotlin source tree (main + test) and rewrite every package/import.
- native_audio.cpp: JNI symbol names (Java_space_rcmd_android_sizzle_...) so the
Oboe callback still links against the relocated NativeAudioBridge.
- docs: package/file-map header.
The desktop-project GitHub URLs (github.com/reactorcoremeltdown/sizzletracker)
and the Reactorcoremeltdown copyright/SPDX author identity are intentionally left
unchanged. Note: the new applicationId installs as a separate package (existing
installs won't upgrade in place).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A retro, grid-based music tracker for Android (Kotlin + Jetpack Compose,
single Activity) with four equally-capable input methods (touch, keyboard,
gamepad, MIDI) and four tabs: tracker, mixer, toolbox, settings.
Highlights:
- Tracker: Canvas-drawn 4-track pattern grid over an 8-lane arrangement roll,
with a glyph cache and draw-phase state reads so the playhead and edits
redraw without per-frame recomposition.
- Audio: sample-accurate sequencer feeding a shared AudioEngine, driven by
either a Kotlin AudioTrack loop or native Oboe/AAudio via JNI (16 KB-aligned
native libs). media3 MediaSession for lock-screen/headset transport.
- Toolbox: 16 instrument/effect slots with a 2-octave audition keyboard;
single-tap select, double-tap edit, long-press clear.
- Note entry: long-press cell popups (piano keyboard / value steppers) plus
keyboard/gamepad stepping that resumes from the last note/channel entered.
Velocity capped at 0x7F, channel at 16.
- Selection/clipboard (cut/copy/paste/delete) and .sng import/export
compatible with the reference desktop tool.
- A `profile` build type (non-debuggable, debug-signed) for realistic
on-device performance testing.
- Developer handover documentation under docs/.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>