// SPDX-FileCopyrightText: 2026 Reactorcoremeltdown // SPDX-License-Identifier: GPL-3.0-or-later package space.rcmd.android.sizzle import android.app.Application import space.rcmd.android.sizzle.audio.AudioEngine import space.rcmd.android.sizzle.input.BindingStore import space.rcmd.android.sizzle.input.GamepadInput import space.rcmd.android.sizzle.input.InputRouter import space.rcmd.android.sizzle.input.KeyboardInput import space.rcmd.android.sizzle.input.MidiInput import space.rcmd.android.sizzle.io.PresetLibrary import space.rcmd.android.sizzle.io.ProjectStore import space.rcmd.android.sizzle.io.SettingsStore import space.rcmd.android.sizzle.io.SongLibrary import space.rcmd.android.sizzle.io.ThemeLibrary import space.rcmd.android.sizzle.model.Project import java.io.File import kotlinx.coroutines.runBlocking /** * The Application object is our (very small) dependency container. Instead of a * DI framework — overkill for a learning-friendly codebase — the handful of * app-wide singletons live here and are reached via `application as SizzleApp`. * * These objects outlive any single screen so playback and input keep working * across rotation and tab switches. */ class SizzleApp : Application() { /** The single in-memory song. Everything edits this one object. */ val project: Project by lazy { Project() } /** Neutral input bus shared by touch / keyboard / gamepad / MIDI. */ val inputRouter: InputRouter by lazy { InputRouter() } /** The real-time sound engine (started/stopped by the PlaybackService). */ val audioEngine: AudioEngine by lazy { AudioEngine() } // Input source handlers are singletons so their (re)bindable maps are shared // between MainActivity (which feeds them hardware events) and the Settings // screen (which edits their bindings via MIDI-learn / gamepad rebind). val keyboardInput: KeyboardInput by lazy { KeyboardInput(inputRouter) } val gamepadInput: GamepadInput by lazy { GamepadInput(inputRouter) } val midiInput: MidiInput by lazy { MidiInput(this, inputRouter) } /** MIDI clock sync (send/receive) over USB + Bluetooth MIDI. */ val midiClock: space.rcmd.android.sizzle.input.MidiClock by lazy { space.rcmd.android.sizzle.input.MidiClock(this) } /** Persists the input binding maps across restarts (DataStore-backed). */ val bindingStore: BindingStore by lazy { BindingStore(this) } /** On-disk instrument/effect preset library in the app's scoped storage. */ val presetLibrary: PresetLibrary by lazy { PresetLibrary(File(filesDir, "presets")) } /** On-disk `.sng` song library in the app's scoped storage. */ val songLibrary: SongLibrary by lazy { SongLibrary(File(filesDir, "songs")) } /** On-disk user colour-theme library (`.szt`) in the app's scoped storage. */ val themeLibrary: ThemeLibrary by lazy { ThemeLibrary(File(filesDir, "themes")) } /** Autosave of the whole project (crash recovery / resume). */ val projectStore: ProjectStore by lazy { ProjectStore(File(filesDir, "autosave.sng")) } /** App-level settings persistence (theme, audio backend). */ val settingsStore: SettingsStore by lazy { SettingsStore(this) } override fun onCreate() { super.onCreate() // Crash recovery: restore the last autosaved project before the engine or // UI read it, so a crash / process-death resumes where the user left off. projectStore.restore(project) // Reload the sample audio for any preset-backed Sampler/SoundFont slots — // the autosave keeps file references, not the decoded PCM, so this makes a // selected preset's samples sound again on a fresh start. presetLibrary.rehydrate(project) // Write the built-in factory presets (e.g. the Ambience reverb spaces) to the // on-disk library on first launch so they appear in the standard browser. presetLibrary.seedFactoryPresets() audioEngine.setProject(project) // Restore saved bindings before any hardware event can arrive. This is a // single small read; blocking briefly at cold start keeps the input maps // authoritative from the first keypress rather than racing an async load. runBlocking { bindingStore.loadInto(keyboardInput, gamepadInput, midiInput) } // Feed incoming MIDI real-time (clock / start / stop) to the clock sync module. midiInput.onRealtime = midiClock::handleRealtime installCrashAutosave() } /** Flush an autosave of the current project on any uncaught exception, then let * the default handler crash/report as usual — so state survives a crash. */ private fun installCrashAutosave() { val previous = Thread.getDefaultUncaughtExceptionHandler() Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> runCatching { projectStore.save(project) } previous?.uncaughtException(thread, throwable) } } }