Prepare for open-source release under GPL-3.0-or-later; docs & test maintenance
- 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>
This commit is contained in:
@@ -30,15 +30,14 @@ Requirements: **Android Studio** (Koala / 2024.1 or newer) which bundles a
|
||||
JDK 17 and the Android SDK.
|
||||
|
||||
1. Open the project root in Android Studio. It will detect the Gradle build.
|
||||
2. If prompted, let it **create the Gradle wrapper** (or run
|
||||
`gradle wrapper --gradle-version 8.9` from a shell that has Gradle).
|
||||
3. Let it download the SDK for `compileSdk 34`.
|
||||
4. Pick a device/emulator running **Android 8.0 (API 26)** or newer and press
|
||||
2. Let it download the SDK for `compileSdk 34`.
|
||||
3. Pick a device/emulator running **Android 8.0 (API 26)** or newer and press
|
||||
**Run**.
|
||||
|
||||
> The command line here (`./gradlew assembleDebug`) needs the wrapper JAR, which
|
||||
> is generated by step 2. The wrapper JAR is intentionally not committed as a
|
||||
> binary; generating it is a one-time step.
|
||||
> The Gradle wrapper (`gradle/wrapper/gradle-wrapper.jar`) **is committed**
|
||||
> (Gradle's recommended practice), so `./gradlew assembleDebug` works on a fresh
|
||||
> clone with no extra setup. A `staging` build type (`./gradlew assembleStaging`)
|
||||
> gives release-like performance while still using the debug signing key.
|
||||
|
||||
Key versions live in **one file**: `gradle/libs.versions.toml` (the "version
|
||||
catalog"). Change a number there and the whole project follows.
|
||||
@@ -58,7 +57,7 @@ catalog"). Change a number there and the whole project follows.
|
||||
Project (the song)
|
||||
▲
|
||||
reads on audio thread │
|
||||
AudioEngine ─────────┘ ──▶ AudioTrack ──▶ speakers
|
||||
AudioEngine ─────────┘ ──▶ Oboe / AudioTrack ──▶ speakers
|
||||
(own thread)
|
||||
```
|
||||
|
||||
@@ -92,14 +91,17 @@ com.reactorcoremeltdown.sizzletracker
|
||||
├── model/ Pure data, no Android imports (unit-test friendly)
|
||||
│ ├── Music.kt Pitch, TimeSignature, Scale (incl. scale-step logic)
|
||||
│ ├── Pattern.kt Cell, CellColumn, Pattern (4 tracks x N lines)
|
||||
│ ├── Arrangement.kt Arrangement (8 lanes x 256 beats), LoopRegion A/B
|
||||
│ ├── Mixer.kt Mixer, MixerChannel (4 channels)
|
||||
│ ├── Toolbox.kt ToolboxType (all instruments+effects), ParamSpec, ToolboxSlot
|
||||
│ ├── Arrangement.kt Arrangement (8 lanes; canvas denominated in bars, up to 256), LoopRegion A/B
|
||||
│ ├── Mixer.kt Mixer, MixerChannel (4 channels; volume/mute/solo/limiter)
|
||||
│ ├── Toolbox.kt ToolboxType (all instruments+effects), ParamSpec, ToolboxSlot, SamplerPads
|
||||
│ ├── AmbiencePresets.kt Factory reverb spaces seeded into the preset library
|
||||
│ └── Project.kt The whole song
|
||||
│
|
||||
├── input/ The unified input layer
|
||||
│ ├── InputAction.kt The neutral action vocabulary
|
||||
│ ├── InputRouter.kt Flow-based message bus
|
||||
│ ├── InputActions.kt Bindable-action catalogue (for the rebind/learn UI)
|
||||
│ ├── InputRouter.kt Flow-based message bus (+ learn mode)
|
||||
│ ├── BindingStore.kt Persists gamepad/MIDI bindings
|
||||
│ ├── KeyboardInput.kt KeyEvent -> InputAction (+ a 2-octave typing piano)
|
||||
│ ├── GamepadInput.kt KeyEvent/MotionEvent -> InputAction (rebindable)
|
||||
│ └── MidiInput.kt MIDI bytes -> InputAction (USB & Bluetooth via MidiManager)
|
||||
@@ -107,23 +109,43 @@ com.reactorcoremeltdown.sizzletracker
|
||||
├── audio/ The real-time sound engine (own thread)
|
||||
│ ├── Transport.kt TransportState snapshot published to the UI
|
||||
│ ├── SynthVoice.kt One NES-style voice: pulse/triangle/noise + ADSR
|
||||
│ └── AudioEngine.kt AudioTrack + sample-accurate sequencer + voice mixing
|
||||
│ ├── SampleVoice.kt Pitched + sliced playback of a decoded sample
|
||||
│ ├── SampleStore.kt Process-wide decoded-PCM cache + WAV encode/decode
|
||||
│ ├── SampleRecorder.kt Ad-hoc mic/USB capture into a sample
|
||||
│ ├── SoundFontLoader.kt Extract PCM from an .sf2 / .xi into the SampleStore
|
||||
│ ├── Effects.kt Insert DSP: TapeDelay, Filter, Bitcrusher, GraphicEq (+ AudioEffect API)
|
||||
│ ├── AmbienceReverb.kt FDN reverb processor
|
||||
│ ├── MasterRecorder.kt Renders the master bus (with FX tail) to a WAV
|
||||
│ ├── NativeAudioBridge.kt JNI bridge to the Oboe callback (calls fillBlock)
|
||||
│ └── AudioEngine.kt Oboe/AudioTrack + sample-accurate sequencer + voice/FX mixing
|
||||
│
|
||||
├── cpp/native_audio.cpp Low-latency Oboe stream; RT callback pulls audio via JNI
|
||||
│
|
||||
├── io/ Human-readable text persistence
|
||||
│ ├── PresetIo.kt Instrument/effect preset <-> KEY=VALUE text
|
||||
│ └── ProjectIo.kt Whole song <-> .sng text
|
||||
│ ├── PresetLibrary.kt On-disk preset library (+ .zip sample bundles)
|
||||
│ ├── ProjectIo.kt Whole song <-> internal .sng text (full fidelity)
|
||||
│ ├── ProjectStore.kt Autosave / load of the current project
|
||||
│ ├── SongLibrary.kt Named project save/load browser backing
|
||||
│ ├── SngFormat.kt Desktop-compatible .sng interchange read/write
|
||||
│ ├── SettingsStore.kt App settings (theme, recording dir, keyboard) via DataStore
|
||||
│ ├── ThemeIo.kt RetroPalette <-> .szt text
|
||||
│ └── ThemeLibrary.kt On-disk theme library (import/export/delete)
|
||||
│
|
||||
├── playback/
|
||||
│ └── PlaybackService.kt Foreground service: background playback + media notification
|
||||
│ ├── EnginePlayer.kt SimpleBasePlayer wrapping the engine for MediaSession
|
||||
│ └── PlaybackService.kt MediaSessionService: background playback + media notification
|
||||
│
|
||||
└── ui/
|
||||
├── AppViewModel.kt Screen state + THE single InputAction handler
|
||||
├── App.kt Tab shell
|
||||
├── Responsive.kt Portrait/landscape layout helpers (nav rail, two-column)
|
||||
├── StackedKeyboard.kt On-screen audition / punch-in piano
|
||||
├── theme/Theme.kt RetroPalette + monospace typography + SizzleTheme
|
||||
├── components/Widgets.kt RetroButton, RetroDropdown, SectionLabel, PixelGlyph
|
||||
├── components/ Widgets (RetroButton/Dropdown…), GlyphCache, Piano
|
||||
├── tracker/ TrackerScreen, PatternGrid, ArrangementRoll
|
||||
├── mixer/MixerScreen.kt
|
||||
├── toolbox/ ToolboxScreen, ParamControl
|
||||
├── toolbox/ ToolboxScreen, ParamControl, Sampler/SoundFont/Delay/Lfo editors
|
||||
└── settings/SettingsScreen.kt
|
||||
```
|
||||
|
||||
@@ -185,10 +207,18 @@ are both **generated** from that list. See §6 to add a new device.
|
||||
|
||||
### Add a new instrument or effect
|
||||
1. Add an entry to `ToolboxType` (`model/Toolbox.kt`) with its `ParamSpec` list.
|
||||
2. Teach `AudioEngine` how to render/process it (today only `NES_SYNTH` is
|
||||
rendered; see `synthParamsForTrack`). Add a branch there.
|
||||
That's it — the Toolbox picker, parameter editor, and preset save/load all work
|
||||
automatically.
|
||||
2. Teach the engine how to sound it:
|
||||
- **Instrument**: handle its type where the engine picks a voice per channel
|
||||
(see how `NES_SYNTH`, `SAMPLER`, and `SOUNDFONT` are dispatched in
|
||||
`AudioEngine`).
|
||||
- **Signal effect**: add a processor in `audio/Effects.kt` implementing
|
||||
`AudioEffect` and register it in `AudioEffect.create(...)`; the per-channel
|
||||
insert chain picks it up automatically.
|
||||
- **MIDI effect** (arp/transpose/LFO-style): hook into the sequencer instead
|
||||
of the signal path (see `advanceArps` / `applyLfos` in `AudioEngine`).
|
||||
The Toolbox picker, parameter editor, and preset save/load all work automatically
|
||||
from the `ParamSpec` list. A device with many parameters can ship a bespoke
|
||||
editor (see `ui/toolbox/DelayEditor.kt`, `SamplerEditor.kt`).
|
||||
|
||||
### Add a new input gesture/control
|
||||
1. Add a case to `InputAction`.
|
||||
@@ -249,8 +279,9 @@ Line-oriented, sectioned (`[PATTERN ...]`, `[ARRANGEMENT ...]`, `[MIXER]`,
|
||||
## 9. Current status — implemented vs. TODO
|
||||
|
||||
This is an honest map so you know where the edges are. The **architecture and
|
||||
all four tabs are in place and interactive**; several deep features are scaffolded
|
||||
with a clear seam to finish them.
|
||||
all four tabs are in place and interactive**, and the feature set below is
|
||||
implemented and working; the short "Remaining / optional" list at the end is
|
||||
polish and interchange niceties, not functional gaps.
|
||||
|
||||
### Implemented and working
|
||||
- Full 4-tab UI, retro monospace theme (+ theme switching), tab navigation.
|
||||
@@ -307,12 +338,13 @@ with a clear seam to finish them.
|
||||
(`version`/`bpm`/`sig`, `block`…`endblock` with `roll`/`track`/steps) — covered
|
||||
by passing JVM unit tests in `app/src/test`.
|
||||
- Background playback foreground service with Play/Pause/Stop notification.
|
||||
- **Oboe / AAudio native output** ✓ (opt-in): `src/main/cpp/native_audio.cpp`
|
||||
- **Oboe / AAudio native output** ✓ (default): `src/main/cpp/native_audio.cpp`
|
||||
opens a low-latency Oboe stream whose real-time callback pulls audio via JNI
|
||||
(`audio/NativeAudioBridge.kt`) from the SAME `AudioEngine.fillBlock` the
|
||||
AudioTrack loop uses — so both backends produce identical sound, only the
|
||||
delivery path differs. Toggle it in Settings → Audio Devices ("Low-latency
|
||||
engine (Oboe)"); it falls back to AudioTrack if the native lib is absent.
|
||||
delivery path differs. Oboe is the default engine; it falls back to the
|
||||
AudioTrack loop only if it fails to initialise (or the native lib is absent),
|
||||
surfacing a popup with the reason (there is no user toggle).
|
||||
Built via **NDK r27** (`ndkVersion` in `app/build.gradle.kts`) + CMake + the
|
||||
`com.google.oboe:oboe:1.10.0` prefab AAR. All packaged `.so` files are **16 KB
|
||||
page-aligned** (Android 15 / Google Play requirement): NDK r27 aligns ELF load
|
||||
@@ -320,17 +352,34 @@ with a clear seam to finish them.
|
||||
lib is aligned, our `CMakeLists.txt` also passes `-Wl,-z,max-page-size=16384`,
|
||||
and AGP stores each `.so` at a 16 KB-aligned offset in the APK. (Verify with
|
||||
`llvm-readelf -l <lib>.so` → LOAD `Align 0x4000`.)
|
||||
- **SF2 / XI loader instrument** ✓: `audio/SoundFontLoader.kt` extracts PCM from
|
||||
an `.sf2` / `.xi` into the `SampleStore`, played back through the same
|
||||
`SampleVoice` path as the Sampler; edited via `ui/toolbox/SoundFontEditor.kt`
|
||||
(root note, volume, ADSR).
|
||||
- **Master recording** ✓: `audio/MasterRecorder.kt` renders the stereo master bus
|
||||
(plus a configurable FX tail) to a WAV in a user-chosen folder (SAF).
|
||||
- **Stereo output & recording** ✓: `fillBlock` renders interleaved stereo and the
|
||||
recorder captures two channels (voices/inserts are mono, so both channels carry
|
||||
the same sum).
|
||||
- **Responsive landscape layout** ✓ (`ui/Responsive.kt`): nav rail, side-by-side
|
||||
screens, two-column mixer/editors, wider on-screen keyboards for tablets/handhelds.
|
||||
- **Arrangement niceties** ✓: 256-bar canvas with viewport-culled drawing,
|
||||
per-lane mutes, and centre-follow scrolling.
|
||||
- **Per-bus soft-clip limiter** ✓, sampler QoL (fire-on-tap, clear, normalize),
|
||||
theme import/export, and preset `.zip` bundle import/export.
|
||||
- **Sample-pack builder** ✓: `tools/samplepack-builder.html`, a standalone offline
|
||||
page that assembles importable Sampler `.zip` bundles.
|
||||
|
||||
### Scaffolded — remaining
|
||||
1. **SF2 / XI loader instrument.** The Sampler (WAV) is done; the SoundFont/XI
|
||||
loader still falls back to the synth. Parse `.sf2`/`.xi`, feed the extracted
|
||||
PCM through the existing `SampleStore`/`SampleVoice` path. (This was not in the
|
||||
original milestone list; noted for completeness.)
|
||||
### Remaining / optional
|
||||
|
||||
All spec milestones are implemented. Next steps are polish/hardening: routing the
|
||||
Oboe backend through the chosen output device (Oboe `setDeviceId`), a full C++
|
||||
port of the synth/mixer to remove JVM/GC from the native audio callback, and the
|
||||
SF2/XI loader above.
|
||||
- **Byte-exact desktop `.sng` compatibility.** `io/SngFormat.kt` reads/writes the
|
||||
reference line format and round-trips musically; a field-for-field byte match
|
||||
with the CLI project layout is the remaining interchange nicety.
|
||||
- **Full C++ audio port.** The DSP currently runs in Kotlin behind the Oboe
|
||||
callback (via JNI). Porting the hot path to C++ would remove JVM/GC from the
|
||||
real-time thread — a performance hardening step, not a functional gap.
|
||||
- **Route the Oboe backend through the chosen output device** (Oboe
|
||||
`setDeviceId`), matching the AudioTrack path's device selection.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user