Commit Graph

38 Commits

Author SHA1 Message Date
Reactorcoremeltdown
b305f0a2b5 Arpeggiate the whole held chord, not just the last note (v0.15.1)
Holding a chord on an arpeggiated channel used to collapse to the last (rightmost)
note and retrigger only that — the other notes were killed by the per-note
releaseBus + single-note arp.start. ChannelArp now holds the whole chord (sorted,
de-duped) and cycles all its notes across the octave range in the chosen mode.

Notes entered on the same line accumulate into one chord via a per-line generation
stamp; a new line starts a fresh chord, and a looping chord keeps its running phase
so it doesn't glitch at the loop point. ChannelArp is made internal to cover the
sequencing with ChannelArpTest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 09:07:28 +02:00
Reactorcoremeltdown
79a1844512 Tap tempo by tapping the BPM readout (v0.15.0)
Tapping the "### BPM" label sets the tempo from the average interval of recent
taps (up to 8, refining as you go); a pause over 2 s starts a fresh count. The
−/+ nudge buttons are unchanged. Purely UI-side — a small remembered list of tap
timestamps, no model/engine changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 09:07:06 +02:00
Reactorcoremeltdown
8c44ed5151 Add full backup & restore to Settings (v0.14.0)
A new Backup & Restore card saves everything the app persists — songs, presets
(with their sample WAVs), themes, the autosaved project, and the settings +
input-binding DataStore blobs — into one .zip via the system file picker, and
restores from one.

Restore is defensive: it extracts to a staging folder and only swaps it into
place once the archive validates, so a corrupt/wrong file can't destroy current
data; it's zip-slip guarded and runs off the main thread. Since DataStore caches
in memory, restore prompts to reopen the app to finish applying. Covered by
BackupIoTest (round-trip replace, bad-file safety, empty-zip rejection).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 22:28:24 +02:00
Reactorcoremeltdown
3488bac1ca Group and enlarge the toolbox device picker (v0.13.4)
Split the SELECT DEVICE list into Instruments / MIDI effects / Audio effects,
each with a gap and a small header, and bump the device labels 13→18 sp with more
tap padding. The old [INS]/[EFF] prefix is dropped since the grouping conveys it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 18:11:30 +02:00
Reactorcoremeltdown
5dfbda7c15 Tape bobbin holes: rounded trapezoids, narrow side outward (v0.13.3)
Replace the inward-pointing triangular reel cutouts with rounded trapezoids —
wide toward the hub, narrow toward the rim. Generalize the corner-rounding helper
(roundedTri → roundedPoly) to handle the four-corner shape.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 15:54:14 +02:00
Reactorcoremeltdown
d43112f296 Redraw Tape Engine as a real tape transport (v0.13.2)
Reels are now NAB-style: a metal flange with three rotating rounded-triangular
bobbin holes and a hub, instead of chariot-wheel spokes. Tape runs under tension
as straight lines from each reel down to a rounded-rectangle playhead between and
below them (the top tape line is gone). The four controls are vintage round knobs
in a row — wow, flutter, hiss, wow-sync — replacing the sliders/stepper.

Each knob is weighted so all four are visible (the shared Knob's internal
fillMaxWidth text had let the first one swallow the row), and Knob gains an
optional knobSize (default 44 dp, so other editors are unchanged) which the tape
knobs set to 58 dp.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 15:48:31 +02:00
Reactorcoremeltdown
71bb50c775 Add Tape Engine and Granular Glitch visual effects (v0.13.0)
Tape Engine: a varispeed reel-to-reel. Audio runs through a ring buffer read at a
glided 'speed'; the editor draws two spinning reels you press-and-hold to brake,
diving the pitch like a finger on a slowing tape. Wow (slow, tempo-synced via
division) and flutter (fast) wobble the rate; hiss adds tape noise.

Granular Glitch: records the signal and, driven by Intensity, jumps the read head
back to replay short grains (stutter/repeat/octave glitch). A new VizEffect
interface lets the processor publish a decimated scope + playhead; the engine
exposes it per slot (effectScope/effectPlayhead), and the editor draws a dotted
oscilloscope with the jumping playhead over it plus a horizontal Intensity slider.
The scope reads in the Canvas draw phase (redraw only, no recomposition), mirroring
the VU-meter path.

Also make the toolbox device picker scrollable so all types stay reachable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 15:20:36 +02:00
Reactorcoremeltdown
ef13a46026 Widen arrangement mute toggles and block headers ~1.5× (v0.12.1)
Bigger, easier thumb targets on the arrangement roll's left column: the mute
toggle 34→51 dp and the block-number header 34→51 dp (laneColWidth 68→102). The
scrollable ruler/roll to the right is unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 15:20:11 +02:00
Reactorcoremeltdown
abdcf77ce5 Stacked keyboards go chromatic when routed to a Sampler (v0.12.0)
A Sampler maps a pad to each MIDI note, so scale filtering left out-of-scale
pads greyed-out and unreachable. When the punch-in / audition keyboard's channel
routes to a bus with a Sampler instrument, bypass the scale filter and enable all
twelve keys; otherwise scale filtering is unchanged. Non-destructive - the
project's scale/root setting is untouched, and switching the channel back to a
synth/SF2 restores filtering.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 23:59:36 +02:00
Reactorcoremeltdown
8eee588780 Point Help user-guide link at the guide's heading anchor (v0.11.2)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 23:37:41 +02:00
Reactorcoremeltdown
40a946a894 Add Help section to Settings (about, source, guide, support) (v0.11.1)
A new Help card under Project shows the app name + live version (read from the
package, so it never drifts from the build) and links to the source repo, the
user guide, and the support page. Each link is a full-width ~64 dp row with a
large label, the destination URL beneath, and a chevron affordance — a
comfortable thumb target that opens in the system browser (missing-browser safe).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 23:25:37 +02:00
Reactorcoremeltdown
3508777ba9 Mono-per-track: a new note releases the track's previous one (v0.10.0)
A sustaining note held with no explicit note-off used to pile up until
voice-stealing killed it. Now, when a note appears under the playhead on a
track, that track's still-held note is released automatically, so a line moves
note-to-note like a real monophonic voice.

Each sequencer voice carries an ownerTrack tag (the lane+track that started it),
and triggerCell calls releaseTrackVoices(trackKey) before playing a new note. It
releases only voices this track owns and only gated (still-held) ones, so chords
built across several tracks on the same bus keep sounding and release tails ring
out. Uses note-off (not kill) for a click-free legato hand-off. Arp voices are
unowned and keep self-managing as mono-per-bus.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 22:55:51 +02:00
Reactorcoremeltdown
7a0f2e4426 Make mixer VU meters optional via a Settings toggle (v0.9.1)
Add a persisted "Mixer VU meters" switch (Settings -> Interface, on by default),
backed by a DataStore boolean. When off, the mixer skips the per-frame poll loop
entirely (the LaunchedEffect is keyed on the flag) and doesn't draw the meter, so
disabling it removes the metering cost, not just the visuals; the volume fill and
thumb render exactly as before.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 21:54:07 +02:00
Reactorcoremeltdown
b07d758609 Add per-channel VU meters inside the mixer faders (v0.9.0)
The render loop now tracks each bus's post-fader/post-FX peak per block and folds
it into a decaying meter value (instant attack, ~300 ms release) plus a clip
latch (~0.8 s when the bus crosses 0 dBFS). Exposed via channelMeter /
channelClipped and surfaced on the ViewModel.

Each mixer fader paints, in one drawBehind, a VU meter inside its volume fill:
rising from the bottom, coloured green -> amber by absolute level with a red tip,
and solid red while the bus clips. A per-frame ticker writes into stable state
objects read only in the fader's draw phase, so meter movement repaints just the
four faders (no recomposition); an unchanged write is a no-op, so a silent mixer
costs nothing but the frame tick.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 21:53:35 +02:00
Reactorcoremeltdown
a5b9837bbc Fix stuck audition notes under rapid / chord tapping (v0.8.1)
The synth/SF2 audition note-off matched a voice purely by pitch, and isActive
stays true through a voice's release tail. So tapping a key, then immediately
holding it again, landed the note-off on the still-releasing first voice (a
no-op) and left the held one stuck, needing a MIDI panic. Two fingers on one
key hit the same trap.

Give voices an isGated flag (held = ADSR not in release/idle) and make
auditionSlotOff / liveNoteOff release a gated voice of that pitch in preference
to one already ringing out. Add SynthVoiceGateTest covering the gate/release
distinction and the exact stuck-note scenario.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 21:51:41 +02:00
Reactorcoremeltdown
5943040a2b Louder master via makeup gain + look-ahead limiter (v0.8.0)
The master stage was a flat 0.5x attenuation into a static tanh soft-clip,
leaving ~9 dB of unused headroom: even the loudest single preset with its mix
bus maxed peaked around -7 dBFS, well below other media playback.

Replace it with a generous makeup gain (1.5x, ~+9.5 dB) feeding a feed-forward,
look-ahead brickwall limiter. A short delay line lets the peak detector pull the
gain down before a loud sample reaches the output and hold it across the
look-ahead window, so the output is mathematically guaranteed to stay under the
ceiling (0.97) - loudness without clipping, and none of the tanh grit a static
clipper adds to a hot single voice. Allocation-free (results go to scratch
fields, not a Pair) and NaN-guarded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 21:50:39 +02:00
Reactorcoremeltdown
b7ae8cfbff Glissando: slide across stacked keyboards to play (v0.7.3)
Input moves from per-key gestures to one octave-level multi-touch handler in
PianoOctave, so a finger can slide across keys and each note retriggers as it
crosses a key boundary (black keys win in their upper region via geometry-aware
hit-testing); multiple fingers still play independently for chords. Out-of-scale
keys are skipped mid-slide. onNoteOn carries a `slid` flag so the tracker
punch-in keyboard records only the initial press (a slide auditions a glide
without spraying a run of notes into the pattern), while the toolbox / editor
audition keyboards play every key crossed. StackedKeyboard and SlotAuditionKeyboard
both migrate to the new callback API; PianoOctave now renders the keys itself.

Verified on-device: a slow slide lights each in-scale key as the finger reaches
it and skips out-of-scale keys.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 20:19:00 +02:00
Reactorcoremeltdown
3c0b93855b Scale-filter toolbox keyboards; pin sampler pads at keyboard height (v0.7.2)
- Scale/root filtering on the Toolbox audition keyboards, matching the tracker
  punch-in keyboard: out-of-scale keys are dimmed and inert (Chromatic = all
  playable). StackedKeyboard now filters in both punch-in and audition mode,
  and SlotAuditionKeyboard (synth / SF2 editors) gained the same filter.
- Sampler editor restructured so the 16-pad grid is pinned at the bottom at the
  same height the audition keyboards use, consistent across every instrument
  editor. The per-pad volume faders moved to a second swipe page (waveform on
  page 1, volumes on page 2, with dot pagination), and pad cells now fill the
  pinned region instead of a fixed 46dp. Selected-pad state is hoisted into
  ParamEditor so the detail area and the pinned grid stay in sync.

Verified on-device: audition keyboard dims to F harmonic minor; sampler pads
fill the pinned bottom region with volumes on the swipe page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 20:05:53 +02:00
Reactorcoremeltdown
c4f4cf34c0 Mix Tightener: per-component Strength faders (EQ / Comp / Limiter) (v0.7.1)
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>
2026-07-18 18:35:58 +02:00
Reactorcoremeltdown
58639d07cf Add Mix Tightener effect, multi-column preset menus, louder master (v0.7.0)
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>
2026-07-18 18:11:56 +02:00
Reactorcoremeltdown
750d928ce1 Hold-to-repeat for navigation keys across keyboard, gamepad and MIDI (v0.6.0)
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>
2026-07-18 15:14:34 +02:00
Reactorcoremeltdown
e0fe89d62c Rework toolbox tile gestures: tap to pick/edit, long-press to clear (v0.5.2)
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>
2026-07-18 15:00:17 +02:00
Reactorcoremeltdown
2f671ed372 UI smoothness: distinct isPlaying flow + allocation-free grid draw (v0.5.1)
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>
2026-07-18 14:49:08 +02:00
Reactorcoremeltdown
cf306d25e0 Generic synth name, calmer knobs, darkwave/industrial presets (v0.5.0)
- 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>
2026-07-18 10:56:52 +02:00
Reactorcoremeltdown
12c2092843 Render only active voices via per-block active-voice lists (v0.4.7)
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>
2026-07-18 10:39:57 +02:00
Reactorcoremeltdown
d54fd27c9e Skip idle voice pools per block — fix playback dropouts (v0.4.6)
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>
2026-07-18 10:28:35 +02:00
Reactorcoremeltdown
3155caf3cc Fix stuck synth voices: release scaled by sustain level (v0.4.5)
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>
2026-07-18 02:04:14 +02:00
Reactorcoremeltdown
00f52c3434 Move synth pitch modulation to control rate — fix playback hiccups (v0.4.4)
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>
2026-07-18 01:48:39 +02:00
Reactorcoremeltdown
78ebd88548 Fix synth crashes, dead filter, and playback hiccups (v0.4.3)
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>
2026-07-18 01:36:53 +02:00
Reactorcoremeltdown
74d60993f0 Inertial touch scrolling for the tracker grid (v0.4.2)
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>
2026-07-18 01:15:02 +02:00
Reactorcoremeltdown
63cad6897f Rewrite the NES synth as a 3-oscillator subtractive synth; track-tied note-offs (v0.4.1)
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>
2026-07-18 01:06:47 +02:00
Reactorcoremeltdown
9ab7660012 Justify the tracker toolbar rows edge to edge (v0.3.2)
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>
2026-07-18 00:08:42 +02:00
Reactorcoremeltdown
26931dd501 Name the release APK space.rcmd.android.sizzle_<versionCode>.apk
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>
2026-07-17 23:21:05 +02:00
Reactorcoremeltdown
b5620b429d Pin the SF2/XI audition keyboard at the editor bottom (v0.3.1)
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>
2026-07-17 20:45:41 +02:00
Reactorcoremeltdown
ba56b5ab9e SF2/XI loop crossfade + hold-to-repeat steppers (v0.3.0)
- 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>
2026-07-17 20:38:10 +02:00
Reactorcoremeltdown
4bd8aac4d5 Rename application identifier to space.rcmd.android.sizzle
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>
2026-07-17 18:59:51 +02:00
Reactorcoremeltdown
a4c8ba2080 Big batch update: the tracker is mostly usable 2026-07-15 16:22:04 +02:00
Reactorcoremeltdown
3cef1b4e85 Initial commit: Sizzletracker Android music tracker
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>
2026-07-13 22:15:13 +02:00