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>
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>
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>
- 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>
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>
docs/USER_GUIDE.md: a full walkthrough of every screen — Tracker (transport,
grid, note-offs, scrolling), the punch-in keyboard, Arrangement, Mix
(channels, FX routing, recording), Toolbox (tile gestures, Synth/Sampler/
SF2-XI editors, effects, presets) and Setup (projects, audio routing, themes,
input bindings). Also covers the four input methods incl. the new
hold-to-repeat, file formats, recording, and default keyboard/gamepad/MIDI
maps. 11 on-device screenshots under docs/screenshots/, linked from README.
Also refresh the README's stale "NES synth" wording to "3-oscillator synth"
(renamed in v0.5.0).
Docs only — no code change, so no version bump or rebuild.
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>
- Add GPL-3.0 LICENSE and SPDX headers (GPL-3.0-or-later) to every source file
(58 Kotlin, the C++ file, and the sample-pack HTML tool).
- README: state the GPL-3.0-or-later license (+ badge); fix stale "256 beats"
-> "256 bars"; fix build instructions (the Gradle wrapper is committed);
mention master recording and the sample-pack tool.
- DEVELOPER_HANDOVER: resync with the codebase — corrected the wrapper note,
architecture diagram, full file map, the add-instrument/effect guide, Oboe
(now default with fallback, not an opt-in toggle), moved SF2/XI to implemented,
and added recently shipped features with an honest remaining/optional list.
- Fix a pre-existing broken unit test: SngFormatTest called the old
SngFormat.import(text); the API is importInto(project, text). Tests compile
and pass again.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Denominate the canvas in bars (lengthBars, default 256) instead of a raw beat
count; canvasBeats(sig) = lengthBars * beatsPerBar, so it spans exactly 256
bars in any signature. Backing arrays sized for the widest case (5/4 -> 1280
beats). Playback still stops at lastFilledBeat, so the wider empty canvas adds
no trailing silence.
- Roll: draw with viewport culling (iterate only the beats scrolled into view)
so the ~16x-wider canvas stays cheap to redraw and scrolls smoothly.
- Persistence: store bars= (was len= in beats) and write cells only up to the
last filled beat; old files without bars= load the default 256-bar canvas with
their cells intact. .sng import grows the canvas in bars as needed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Theme: pick a light or dark Material base scheme by palette luminance and map
the palette onto the container/variant/outline/secondary roles components
actually read, so cards and secondary text stay legible on bright themes.
- Audio: remove the experimental stereo-FX rendering and the tape-delay per-head
balance (both worked poorly); each channel runs a single mono FX chain again.
Master output and recording remain stereo (dual-mono content).
- Tools: add tools/samplepack-builder.html, a standalone page for assembling
Sampler .zip preset bundles (16 note-labelled drag-n-drop pads, audition,
16-bit mono WAV re-encode, duplicate-sample detection).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stereo effects (experimental), off by default and persisted:
- When on, each mono insert effect runs a second instance for the right
channel so upstream stereo (e.g. a delay's width) carries into later
effects like the reverb; the delay stays single-instance (nativeStereo).
- Settings toggle under Audio Devices; rebuilds the FX chains on change.
Seamless system bars:
- SizzleTheme tints the system navigation bar to the palette's surface
(matching the bottom tab bar) and the status bar to the background, with
light/dark icon appearance by luminance, re-applied on theme change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- fillBlock now outputs interleaved stereo; AudioTrack and native Oboe
configured for 2 channels; MasterRecorder writes a stereo WAV.
- AudioEffect gains processStereo() (default mono-collapse); the tape delay
overrides it to pan each of its 4 heads with a level-preserving linear pan
(feedback still uses the mono sum, so loop behaviour is unchanged).
- New per-head balance param + a horizontal balance slider under each head's
division fader (center-detented, L/C/R readout), with a revision read so it
redraws.
Note: only the delay produces stereo width; other insert effects stay mono
and collapse upstream stereo, so place the delay last to keep its panning.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Recording & audio:
- Master-bus WAV recorder: arm + FX-tail + SAF output folder on the mixer
toolbar; allocation-free capture (ring buffer -> writer thread).
- Oboe is now the default backend; falls back to AudioTrack only on init
error and shows a popup. Removed the manual engine toggle.
- Arpeggiator: fixed loop-boundary slip (phase-continuous), monophonic
(no chord/voice pile-up), and a per-bar reset toggle.
- AmbienceReverb: 8-line FDN (was 16) to fit the real-time budget; per-bus
soft-clip limiter; per-channel non-finite guard on the master sum.
UI / layout:
- Responsive landscape layouts: side nav rail; tracker grid beside the
arrangement/keyboard; toolbox tiles beside a wider keyboard; two-column
mixer strips; collapsed transport toolbar.
- Two-column instrument/effect editors; reverb controls paged with dot
pagination.
- Arrangement: per-lane mute toggles; center-follow scrolling.
- Tracker toolbar: BLK dropdown, SKIP moved by OFF/KBD, glyph edit buttons.
- Sampler: normalize button, per-pad volume snap, single CLEAR.
- Import error popups; content validation on all file loaders.
- App themes: import/export/delete/dropdown library (stock themes protected).
Persistence & QoL:
- Persist keyboard MIDI channel/octave; remember loaded project in the
settings dropdown; New Project button.
Cleanup:
- Removed dead code and fixed audio-thread allocation hot spots
(per-sample iterators, Math.random on the RT thread, etc.).
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>