This document is the main orientation guide for future maintainers of Audio Ninja. It explains how the codebase is organized, where to look when changing a feature, how the frontend and backend communicate, and how to diagnose common setup and runtime problems.
For step-by-step environment setup, build commands, and test commands, see:
Audio Ninja is a desktop application built with:
- Tauri v2 for the native desktop shell, filesystem dialogs, application bundling, and frontend-to-backend IPC.
- SvelteKit and Svelte 5 for the frontend interface.
- Rust for audio decoding/playback, marker validation, CSV import/export, audio segment export, and application state.
- WaveSurfer.js for drawing and controlling the waveform visualization in the frontend.
- FFmpeg as a bundled Tauri sidecar used when exporting audio segments.
At a high level:
- The Svelte frontend renders the app and stores reactive UI state in
src/lib/state.svelte.ts. - Frontend event handlers in
src/lib/actions.tscall Rust commands through Tauri'sinvoke(...)API. - Rust command handlers in
src-tauri/src/commands.rsoperate on sharedAppState. - Backend modules under
src-tauri/src/perform audio playback, marker storage/validation, CSV import/export, and FFmpeg-based segment export.
Frontend state flows through Tauri into Rust services.
.
|-- README.md
|-- package.json
|-- package-lock.json
|-- vite.config.js
|-- svelte.config.js
|-- tsconfig.json
|-- vitest.config.ts
|-- scripts/
|-- docs/
|-- src/
|-- src-tauri/
`-- .github/workflows/
Generated dependency/build directories such as node_modules/, build/, src-tauri/target/, and coverage output are not source code. They can usually be deleted and regenerated if local builds get into a bad state.
The frontend lives in src/.
src/routes/+page.svelteis the main app screen. It decides whether to show the welcome screen or the full editor, wires top-level components together, registers keyboard listeners, and starts/stops polling cleanup.src/routes/+layout.tsdisables server-side rendering and enables prerendering. This matters because Tauri serves a static frontend inside a desktop webview.
src/components/WelcomeScreen.svelterenders the first screen before a file is loaded.src/components/Header.svelterenders the app title, loaded file name, open/import/export buttons, and export options.src/components/WaveformDisplay.svelteowns WaveSurfer, waveform rendering, zoom controls, timeline ticks, playhead dragging, marker overlays, and marker-position editing by dragging on the waveform.src/components/PlaybackControls.svelterenders play/pause, stepping, speed controls, looping, follow-playhead, and time display.src/components/MarkerPanel.svelterenders the editable marker list, marker type selectors, marker deletion, marker splitting, marker selection, and direct timestamp editing.src/components/SegmentPanel.svelterenders validated or preview segments and allows segment title editing.src/components/ShortcutsPanel.svelteshows and edits keyboard shortcuts.src/components/ErrorAlert.sveltedisplaysappState.error.src/components/SuccessToast.sveltedisplaysappState.successMessage.
src/lib/state.svelte.tsdefines the singletonappState. Most frontend components read and update this object.src/lib/actions.tsis the main frontend behavior layer. It handles polling playback position, requestAnimationFrame interpolation, keyboard shortcuts, seeking, file opening, marker operations, segment validation, CSV import, and export commands.src/lib/types.tsdefines frontend TypeScript interfaces that mirror serialized Rust data: file metadata, playback position, markers, marker kinds, and segments.src/lib/shortcuts.tsdefines available shortcut actions, default bindings, shortcut matching, and display formatting.src/lib/utils.tscontains formatting and parsing helpers for timestamps, marker labels, playback speeds, and zoom behavior.src/lib/validation.tsmaps backend validation error messages to affected marker IDs for UI highlighting.
src/tests/setup.tsconfigures the frontend test environment.src/tests/helpers/reset-state.tsresets shared state between tests.src/tests/unit/*.test.tscover actions, shortcuts, validation, utilities, imports, polling, playback, seeking, markers, and keyboard behavior.
See Testing for commands.
The Tauri/Rust backend lives in src-tauri/.
When trying to find backend behavior, start with src-tauri/src/lib.rs. It lists every frontend-callable command registered with Tauri. From there, jump to the matching function in src-tauri/src/commands.rs, then follow that function into the audio, marker, export, or state module it uses. This is usually the fastest way to trace how a user action reaches the Rust code.
src-tauri/src/main.rsis the executable entry point. It callsaudio_ninja_lib::run().src-tauri/src/lib.rsconfigures the Tauri app, registers plugins, manages sharedAppState, and registers all IPC commands withtauri::generate_handler![...].src-tauri/src/commands.rsis the IPC boundary. Every function marked#[tauri::command]can be called from the frontend withinvoke(...)if it is registered inlib.rs.src-tauri/src/state.rsdefines backend application state: the activeAudioEngineand the in-memoryMarkerStore, both protected byMutex.src-tauri/src/error.rsdefinesAppErrorand serializes backend errors into strings that can be shown in the frontend.
The src-tauri/src/audio/ directory handles loading, decoding, playback, seeking, speed, looping, and audio output.
audio/mod.rsexposes the audio module's public API.audio/engine.rscoordinates the decoder thread, audio output sink, playback state, control channel, metadata, and lifecycle.audio/decoder.rsprobes media files with Symphonia, decodes audio frames, handles seek/control messages, and feeds samples into a ring buffer.audio/output.rsopens the system audio output through CPAL. It also definesNullSinkfor tests that should not require real audio hardware.audio/control.rsdefines playback control messages and shared playback state.audio/resampler.rshandles playback-speed resampling with Rubato.
The src-tauri/src/markers/ directory owns marker data and marker-to-segment validation.
markers/model.rsdefinesMarkerKind,Marker, andSegment.markers/store.rsstores sorted markers and segment titles. Segment titles are keyed by the anchor marker: aStartmarker or aStartEndmarker.markers/validate.rsconverts markers into segments using a stack-based model:Startopens a segment.Endcloses the most recent open segment.StartEndcan close the previous segment and open the next one, or create a zero-length segment if no segment is open.- An unmatched
Endor unmatchedStartis a validation error.
markers/mod.rsre-exports the module's public types.
The src-tauri/src/export/ directory handles CSV and audio segment output.
export/csv.rsformats timestamps, writes segment CSV files, parses timestamps, and imports CSV rows into segments.export/segments.rssanitizes filenames, writes optional CSV output, and invokes the bundled FFmpeg sidecar once per exported segment.export/mod.rsexposes export helpers to the rest of the backend.
src-tauri/tauri.conf.jsoncontrols the app name, version, bundle targets, window size, frontend build output, asset protocol, icons, and FFmpeg sidecar configuration.src-tauri/Cargo.tomldefines Rust dependencies, features, and package metadata.src-tauri/Cargo.lockpins Rust dependency versions.src-tauri/build.rsis the Tauri build script entry point.src-tauri/capabilities/default.jsoncontrols Tauri permissions for the main window, including dialog and opener permissions.src-tauri/icons/contains application icons used by platform bundles.src-tauri/binaries/stores downloaded FFmpeg sidecar binaries. These are created byscripts/download-ffmpeg.sh.src-tauri/gen/contains generated Tauri schema files. These are generally not edited by hand.
package.jsondefines Node dependencies and scripts for frontend dev/build/check/test, Tauri commands, coverage, and FFmpeg setup.package-lock.jsonpins Node dependency versions. Usenpm ciin CI or when reproducing exact dependency installs.vite.config.jsconfigures Vite and SvelteKit.svelte.config.jsconfigures SvelteKit static output.tsconfig.jsonconfigures TypeScript.vitest.config.tsconfigures Vitest and jsdom.scripts/download-ffmpeg.shdownloads or copies platform-specific FFmpeg binaries intosrc-tauri/binaries/..github/workflows/ci.ymlruns frontend tests, FFmpeg sidecar verification, and Rust tests/checks across supported platforms..github/workflows/cd.ymlbuilds draft GitHub Releases for macOS.dmg, Windows.msi, and Linux.debwhen a version tag matchingv*is pushed.
Frontend code calls Rust commands with invoke('command_name', args). When adding or changing a command, update both sides:
- Add or modify the Rust function in
src-tauri/src/commands.rs. - Register the function in
src-tauri/src/lib.rs. - Call it from
src/lib/actions.tsor the relevant component. - Keep frontend types in
src/lib/types.tsaligned with Rust serialized structs. - Add or update tests where possible.
Main commands currently include:
- File lifecycle:
open_file,open_file_dialog - Playback:
play,pause,seek,step_forward,step_backward,set_speed,set_loop,get_playback_position - Markers:
add_marker,delete_marker,move_marker,rename_segment,list_markers,validate_markers - Import/export:
import_csv,export_audio_segments - Shortcuts:
read_shortcuts_config,write_shortcuts_config
Maintenance note: the main export UI calls export_audio_segments, which can export CSV, audio segments, or both. If shortcut-only CSV export is changed, make sure the frontend shortcut path and backend command registration stay aligned.
- Change visual layout or styling: start in the relevant
src/components/*.sveltefile, then check global layout styles insrc/routes/+page.svelte. - Change the first screen: edit
src/components/WelcomeScreen.svelte. - Change file open/import/export controls: edit
src/components/Header.svelteand the matching handlers insrc/lib/actions.ts. - Change waveform appearance, zoom, timeline labels, playhead dragging, or marker overlays: edit
src/components/WaveformDisplay.svelte. - Change playback buttons, speeds, looping, follow-playhead, or time display: edit
src/components/PlaybackControls.svelte,src/lib/utils.ts, and possibly backend playback commands. - Change marker list behavior: edit
src/components/MarkerPanel.svelte,src/lib/actions.ts, and marker commands insrc-tauri/src/commands.rs. - Change segment naming or segment preview behavior: edit
src/components/SegmentPanel.svelte,src/lib/actions.ts, andsrc-tauri/src/markers/store.rs. - Change marker validation rules: edit
src-tauri/src/markers/validate.rsfirst, then update frontend validation display insrc/lib/validation.tsif error text changes. - Change marker or segment data shape: update
src-tauri/src/markers/model.rs,src/lib/types.ts, CSV import/export code, and tests. - Change keyboard shortcuts: edit
src/lib/shortcuts.tsandsrc/components/ShortcutsPanel.svelte. - Change where shortcuts are persisted: edit
read_shortcuts_configandwrite_shortcuts_configinsrc-tauri/src/commands.rs. - Change supported input file types: update the dialog filter in
open_file_dialoginsrc-tauri/src/commands.rs, the matchingSUPPORTED_MEDIA_EXTENSIONSconstant insrc/lib/actions.ts, confirm Symphonia supports the format, and update user-facing docs. - Change drag-and-drop behavior: edit the
onDragDropEventhandler insrc/routes/+page.svelteandopenFileFromPathinsrc/lib/actions.ts. - Change audio decoding/playback behavior: start in
src-tauri/src/audio/engine.rs, then inspectdecoder.rs,output.rs,control.rs, andresampler.rs. - Change export filenames or FFmpeg arguments: edit
src-tauri/src/export/segments.rs. - Change CSV format: edit
src-tauri/src/export/csv.rs, update import/export tests, and update user documentation if the file format changes. - Change app name, version, bundle targets, icons, sidecars, or window defaults: edit
src-tauri/tauri.conf.json. - Change Tauri permissions: edit
src-tauri/capabilities/default.json. - Change release behavior: edit
.github/workflows/cd.yml. - Change CI behavior: edit
.github/workflows/ci.yml.
WelcomeScreen.svelte or Header.svelte calls openFile() in src/lib/actions.ts. That invokes open_file_dialog in src-tauri/src/commands.rs. The backend opens a native dialog, constructs an AudioEngine, stores it in AppState, clears old markers, and returns file metadata to the frontend. Files dropped on the app window are handled by src/routes/+page.svelte, which listens for Tauri's onDragDropEvent and calls openFileFromPath() in src/lib/actions.ts; that path skips the dialog and invokes open_file directly with the dropped path.
Frontend playback controls call action helpers in src/lib/actions.ts, which invoke playback commands in commands.rs. The backend delegates to AudioEngine. The frontend polls get_playback_position every 100 ms and uses requestAnimationFrame interpolation for smooth playhead movement.
WaveformDisplay.svelte creates a WaveSurfer instance when appState.metadata.filePath is available. It loads the file through Tauri's asset protocol using convertFileSrc(filePath). The component owns pointer handling so dragging the waveform can seek or move an editing marker in real time.
Marker UI actions update frontend state and invoke marker commands. The backend keeps markers sorted in MarkerStore. After marker changes, the frontend calls validate_markers; if validation fails, it stores the error and computes partial preview segments for display.
importCsv() in src/lib/actions.ts invokes import_csv. The backend opens a native CSV file picker, parses rows into segments, validates that imported timestamps fit inside the loaded media duration, clears existing markers, and replaces them with imported markers.
The header export dropdown calls exportAudioSegments(exportCsv, exportAudio). The backend validates markers into segments, asks the user for an output folder, optionally writes a CSV index, and optionally calls the bundled FFmpeg sidecar to create one audio file per segment.
Many confusing setup problems are stale generated files. From the project root:
rm -rf node_modules build src-tauri/target
npm install
npm run tauri -- devIf you only need to refresh Node dependencies, use:
npm ciLikely causes:
- Node is too old.
package-lock.jsonandpackage.jsonare out of sync.- Native dependencies need platform build tools.
Check:
node --version
npm --versionUse Node v18 or newer. If the lockfile appears inconsistent after dependency edits, regenerate it intentionally with npm install and commit both package.json and package-lock.json.
Common causes:
- SvelteKit generated files are stale.
- TypeScript types drifted after changing Rust-returned data or frontend interfaces.
- Component props changed but parent components were not updated.
Try:
npx svelte-kit sync
npm run checkIf errors mention a field name such as positionMs or durationMs, check both the Rust #[serde(rename_all = "camelCase")] structs and the TypeScript interfaces in src/lib/types.ts.
Make sure dependencies are installed from the project root:
npm install
npm run tauri -- devDo not run Tauri commands from inside src-tauri/ unless you are using cargo directly.
Symptoms may mention cargo, rustc, missing targets, or unsupported editions.
Check:
rustup --version
rustc --version
cargo --versionThen update stable Rust if needed:
rustup update stableIf a CI matrix target fails locally, install that target explicitly:
rustup target add <target-triple>Tauri depends on platform-specific native libraries and build tools.
- macOS: install Xcode Command Line Tools.
- Windows: install Microsoft Visual Studio C++ Build Tools.
- Linux: install the WebKitGTK, SSL, appindicator, SVG, and audio development packages listed in the Tauri prerequisites and mirrored in
.github/workflows/ci.yml.
If Linux errors mention webkit2gtk, libsoup, libayatana-appindicator, openssl, or xdo, this is usually a missing system package rather than an application bug.
Errors may mention ffmpeg sidecar not available, No such file, or failed to run sidecar.
Try:
npm run download-ffmpeg
ls src-tauri/binariesOn macOS, the download script downloads a target-specific static FFmpeg binary. On Linux and Windows, it downloads from BtbN FFmpeg builds and requires tools such as curl, tar, or unzip.
If adding a new platform, update:
scripts/download-ffmpeg.shsrc-tauri/tauri.conf.json.github/workflows/ci.yml.github/workflows/cd.yml
Likely causes:
- No file is loaded.
- The file format or codec cannot be decoded by Symphonia.
- The OS has no available audio output device.
- CPAL cannot open the default output device.
Check the app error message first. Backend errors are serialized from src-tauri/src/error.rs. For tests on machines without audio hardware, use the Rust test-audio feature so the backend can use NullSink.
Likely causes:
- WaveSurfer did not receive a valid file URL.
- Tauri asset protocol access is misconfigured.
- The WaveSurfer container has zero height or width.
- The media file can play but cannot be decoded by WaveSurfer in the webview.
Check:
WaveformDisplay.svelteusesconvertFileSrc(filePath).src-tauri/tauri.conf.jsonhas asset protocol enabled.- The layout in
src/routes/+page.svelteandWaveformDisplay.sveltegives the waveform a real size.
The Rust command must be registered in src-tauri/src/lib.rs. The names must also match between the frontend string and the Rust function name.
Check:
- The function has
#[tauri::command]. - It appears in
tauri::generate_handler![...]. - The frontend uses the same snake_case command string.
- Argument names match Tauri's camelCase conversion. For example, Rust
position_msis called from TypeScript aspositionMs.
Check src-tauri/capabilities/default.json. The main window must have the needed dialog permissions. Also verify the app is running under Tauri, not only as a plain browser page through Vite.
Likely causes:
- The CSV format does not match what
src-tauri/src/export/csv.rsexpects. - A timestamp cannot be parsed.
- An imported segment timestamp is beyond the loaded media duration.
- No media file is loaded before importing.
Look at the exact frontend error message; most CSV failures are converted to AppError::ValidationError or AppError::Csv.
The backend expects marker sequences that can be paired into segments. Common errors:
- An
Endmarker appears before any openStart. - A
Startmarker has no laterEnd. - Marker kinds were changed in the UI but the resulting sequence is incomplete.
The frontend highlights likely problematic markers through src/lib/validation.ts. If validation rules or error text change, update that file too.
Likely causes:
- Marker validation fails before export.
- There are no segments to export.
- The output folder cannot be written.
- FFmpeg is missing or exits with an error.
- A segment title contains filename characters that need sanitizing.
Start in src-tauri/src/export/segments.rs. The backend already sanitizes common invalid filename characters; if a platform still rejects a path, extend sanitize_filename(...).
Releases are built by .github/workflows/cd.yml only when pushing tags that match v*.
Check:
- The tag was pushed to GitHub.
- The workflow completed for the platform.
- The release may be a draft release.
- macOS signing/notarization secrets are optional, but if configured incorrectly they can break signed macOS builds.
- FFmpeg sidecar download succeeded for the target platform.
Unsigned or unnotarized macOS builds may be blocked by Gatekeeper. For professional distribution, configure Apple Developer ID signing and notarization secrets in the GitHub repository. The CD workflow documents the required secret names.
Common causes:
- Visual Studio C++ Build Tools are missing.
- The Rust MSVC target is missing.
- The build is being run from a shell without required Windows build environment support.
- The FFmpeg download script needs a bash-like shell if run manually.
Follow the Windows section of the Tauri prerequisites and compare with the Windows jobs in .github/workflows/ci.yml.
Common causes:
- Missing WebKitGTK or native system libraries.
- Missing ALSA development headers.
- Missing
patchelffor bundling. - A target architecture mismatch.
Use the Linux dependency list in .github/workflows/ci.yml and .github/workflows/cd.yml as the most concrete reference for required packages.
- Keep the frontend TypeScript types and Rust serialized structs in sync.
- When adding an IPC command, update
commands.rs,lib.rs, frontend actions, and tests together. - Prefer adding tests near the logic being changed: frontend action tests for UI behavior, Rust unit tests for backend logic.
- Keep generated files out of hand edits. Edit source files, then regenerate through normal build/test commands.
- Update
README.md, user docs, or developer docs whenever a user-visible workflow, install step, command, or supported file format changes. - When changing bundle behavior, test both
npm run tauri devandnpm run tauri build.
