Musializer is a desktop studio for turning music into reactive scenes, timed
lyrics, and deterministic high-quality video. It is written in C with raylib,
opens ordinary audio files directly, and keeps each song as a portable .musi
project.
This is an AI-agent-developed fork, built under human direction and review, of tsoding's original playful C repository. The fork keeps the original project's immediacy and custom C build system while growing it into a scene-based visualizer, editor, and offline rendering tool. The upstream demo below remains a lovely snapshot of where it began.
Warning
Musializer is under active development and the .musi format is currently
version 1. Keep backups of irreplaceable projects when moving between builds.
Signed binaries and a native system installer are not published yet.
- Play WAV, OGG, MP3, QOA, and FLAC files with live audio-reactive visuals.
- Switch among ten built-in scenes: Spectrum, Pulse Field, Orbital Lattice, ASCII Field, Song Atlas, Spectral Terrarium, Constellation, Cadence, Loom, and Pentagram Orbits.
- Tune every scene from a live parameter inspector. Spectrum exposes separate taper, semantic hue, and glow shaping; Spectral Terrarium exposes ecosystem speed, creature speed, population, and habitat glass; Constellation exposes star density plus event reach, duration, and hue response. Song Atlas includes broad terrain/camera ranges, 1x-3x sampling detail, manual or music-reactive hue, camera orbit, distance, and drift controls, and Filled/Wireframe surface modes; exact values and per-scene resets are saved with the track and reused by offline export, while numbered per-scene tuning presets live in a per-user library shared across every track and project.
- Turn timed lyric words into beat-choreographed kinetic geometry with Cadence, or accepted semantic energy/tension/valence into a growing whole-track textile with Loom.
- Use ASCII Field as a procedural rolling spectrogram, or import an image as a color-aware glyph canvas with live spectral density, animated glyph waves, and compression-resilient CRT scanlines; author timeline events and edit timed lyrics in the application.
- Navigate with a depth-shaded waveform, draggable capped hairline playhead, one-second buttons, and exact tenth/one/ten-second keyboard steps. The strip zooms about the pointer with the mouse wheel and pans with shift+wheel or a middle-button drag, down to a quarter-second window; a Fit button and a span readout appear while zoomed.
- Retime lyrics by hand in the cue lane: click to select, ctrl+click to add or remove, shift+click to take a range, drag the selection to move every cue in it together, and drag a block's leading or trailing edge to change one boundary. A move is applied in full or not at all.
- Generate local measured section suggestions and timed lyrics: lyrics already present in the track's metadata or a sibling text file are synchronized deterministically against Whisper word timing, and only tracks without known lyrics fall back to transcription plus a headless Codex review. Optionally ask Xiaomi MiMo V2.5 through OpenRouter for a semantic description of how the music feels.
- Stage every assisted result for review instead of silently changing a project.
- Auto-switch scenes from reviewed section markers.
- Render deterministic H.264/AAC MP4 video up to 7680x4320 with anti-aliased scenes, shared lyric captions, exact decoded-audio frame scheduling, and transactional output publication.
- Save lyrics, semantic/manual events, scene suggestions, seed, provenance, and
output intent in strict
.musiv1 projects. - Work as a normal Linux desktop application, a CLI renderer, or a hot-reloaded C development build.
Tracker modules (xm and mod) work for interactive playback and preview, but
not offline MP4 export because raylib does not expose their decoded Wave data.
The nicest way to use Musializer on Linux, including KDE Plasma, is to install the per-user launcher from a source checkout:
$ ./tools/install-linux-launcher.shThe installer bootstraps and builds the release executable when needed, then
adds Musializer to the application menu and registers .musi projects with
the desktop MIME database. It writes only below ~/.local and needs no sudo.
After installation, ordinary launches do not invoke the compiler.
You can now open audio or project files from the application menu/file manager, or run:
$ musializer path/to/song.mp3
$ musializer path/to/show.musiThe source checkout must remain where it was installed from. Re-run the
installer after moving it or when you want the launcher to use a new release
build. Diagnostics go to ~/.local/state/musializer/launcher.log.
Remove the integration with:
$ ./tools/install-linux-launcher.sh --uninstallAn unpacked Linux distribution can run the same installer without a compiler; it uses the executable included in the archive.
Musializer vendors raylib and uses nob.c, a custom build system
written in C. You need a C compiler and the X11 development headers on Linux.
FFmpeg is optional for preview but required for MP4 export.
On Debian, Ubuntu, or Kubuntu:
$ sudo apt install build-essential libx11-dev libxcursor-dev libxrandr-dev \
libxinerama-dev libxi-dev
$ sudo apt install ffmpeg # needed for video export and analysis workflowsBootstrap nob once, build, and run:
$ cc -o nob nob.c
$ ./nob build release
$ ./build/musializer./nob with no arguments remains equivalent to ./nob build release. The
build driver rebuilds itself after its sources change; if that self-rebuild is
interrupted, bootstrap it again with cc -o nob nob.c.
Useful profiles and checks:
$ ./nob build debug
$ ./nob build sanitize
$ ./nob build hotreload
$ ./nob test debug
$ ./nob test release
$ ./nob test sanitize
$ python3 -m unittest discover -s tests/adapters -v
$ ./nob distRelease, debug, and hot-reload recipes exist for Linux, macOS, OpenBSD, MinGW-w64, and MSVC. Full ASan+UBSan application builds are currently supported on Linux and macOS. See product readiness for the tested packaging boundary and cross-platform caveats.
Start with Open audio, Open project, or drag a file into the first-run workspace. Each entry under Track projects is an independent one-song document.
The normal workflow is:
-
Choose a scene from the left rail. Keys 1 through 9 switch scenes directly.
-
Select Tune beside the scene list to open its parameter inspector. The inspector initially fits inside the existing window; choose Expand when you explicitly want the application to use available monitor space. The track rail compacts before the preview falls below 30% of the window. Drag a slider for live feedback. Reset requires confirmation and exposes a one-shot Undo reset action. Save new captures a per-scene preset; Load, Update, and Delete manage the selected preset. Presets are shared across all tracks and projects: they persist in a strict-JSON per-user store (
$XDG_DATA_HOME/musializer/presets.jsonor~/.local/share/musializer/presets.jsonon Linux,%APPDATA%\Musializer\presets.jsonon Windows,~/Library/Application Support/Musializer/presets.jsonon macOS;MUSIALIZER_PRESET_STOREoverrides the path), written atomically after every change. Projects saved by older builds keep their track-local presets byte-for-byte, and opening one copies any presets you do not already have into the shared library. A store file this build cannot accept is left untouched and read-only rather than overwritten. -
Open Lyrics to write or import lyric cues and adjust their start/end times against the playhead. The text field takes typing, backspace, and Ctrl+V. A paste is applied whole or not at all, so an over-long or malformed clipboard leaves the draft untouched and says why rather than landing a truncated cue; because a cue is a single line, pasted line breaks become spaces and the panel reports that. There is no caret or selection yet: text is edited at the end of the field.
Style in the same panel governs how every cue is rendered: face (Alegreya, Space Grotesk, or a face imported from Google Fonts), backing (none, drop shadow, or the rounded plate), one of nine placements in the frame, size, maximum width, inset from the edges, and ink and plate colour. Every measurement is a fraction of the frame rather than a pixel count, so a caption typeset against the preview window exports the same at 720p and at 2160p. The settings are saved in the project; a
.musiwritten before this existed opens with the values that reproduce its original appearance, and a project that carries an explicit style will not open in an older build.Import a face... opens a browser over the Google Fonts catalogue. It searches by family and shows each one's category and which of the scripts this build can draw it covers, so a face that would render your lyrics as empty boxes is visible as such before you download it. Choosing one downloads the regular weight and the licence it is distributed under, and makes it the caption face.
This is a network feature and it asks first. Musializer contacts
fonts.google.comandfonts.gstatic.comfor the list and the face, andraw.githubusercontent.comfor the licence. Only a family name is sent: no audio, no lyrics, and no project data. Consent is asked once per run and is not remembered between runs. Nothing else in the application needs it.The face and its licence are copied into the project's
.assets/bundle and verified against their SHA-256 every time the project is opened, so sharing the project shares a face that can be checked and terms that travel with it. A face whose bytes no longer match is refused rather than quietly replaced with a substitute: opening it in a fallback and then autosaving that substitution would overwrite your choice. Only the regular weight is imported; bold and italic are not.Timing can also be edited directly in the cue lane under the waveform. A click selects a cue and binds the form to it; Ctrl+click adds or removes one cue; Shift+click takes everything between the anchor and the cue you clicked. Dragging a block's body moves the whole selection together, and dragging its leading or trailing edge moves that one boundary. A multi-cue move is applied in full or not at all, so a selection that would run off either end of the track does not move at all rather than moving partway; the blocks stop under the pointer at the limit instead of snapping back. Zoom in first when cues are close together: the lane's grab handles only appear once a block is wide enough to aim at, and a lane selection holds at most 64 cues.
-
Open Assist for timed-lyric help, measured scene planning, semantic music interpretation, or the complete pipeline. Selecting a workflow first shows its local/remote data boundary. Results are validated and staged with a lane-specific impact summary, then require an explicit Apply confirmation. A valid run with no suggestions is reported as No editor changes found and cannot be applied as a misleading no-op. Copy result, Copy log, and Copy folder keep the immutable job evidence reachable from the panel.
-
Inspect generated section markers and enable Auto scenes if they should drive both preview and export. To author a scene change manually, position the playhead, select and tune the scene, then choose + Scene. The cue captures the scene's tuning at that moment; seeking and export reload the same snapshot. Cue boundaries currently use deterministic cuts. Scene cues are create-only in the interface. Pressing + Scene again at the same instant replaces a cue, but there is no way yet to remove one, move its boundary, or point it at another scene without clearing the plan and rebuilding it; the engine supports all three and the editing surface is the next step. Choosing a base scene also turns Auto scenes off, because a running plan would otherwise override it at every cued moment. The cues are kept and come back when you re-enable it.
-
Add manual visualization events with + Feel and + Custom, or import an image into ASCII Field. Both record a single-value marker at the playhead, and Constellation is currently the only scene that reacts to them: it flares the node nearest the marker for about a second. Loom reads the semantic lane rather than manual markers, because it needs the four-value energy/tension/valence/confidence payload that only imported analysis produces. The other eight scenes ignore manual events entirely.
-
Open Export, choose resolution, frame rate, and quality, then select the destination. The action is disabled with an explanation when FFmpeg is not discoverable, before a destination picker opens. Progress reports the exact frame count and ETA; cancellation does not replace an existing video.
-
Save with Ctrl+S, or use Ctrl+Shift+S for Save As.
Canonical edits autosave after a short idle period. Unapplied lyric drafts, staged assistance, running jobs, active exports, and unsaved projects are guarded before context changes or quit, so partial work is not silently lost. Metadata-only autosaves reuse assets that this process already hash-verified and published into the project's content-addressed bundle; explicit Save and Save As continue to reverify the full asset content. Notices use distinct info/success/warning/error colors, wrap their detail text, show hidden queue depth, and let actionable Assist failures reopen the review step or copy the immutable artifact/log path.
Timed lyrics use the same caption layer in preview and export. Long cues wrap to three centered lines with a visible ellipsis. Captions are sized as a fraction of frame height, so a cue composed against the preview keeps its proportions at every export resolution. Face, backing, placement, size, width, inset, and colour are per project and saved with it, and a face can be imported from Google Fonts. The atlas covers accented Latin, Greek, Cyrillic, punctuation, currency, and common symbols -- an imported face is rasterized over the same set, so it contributes only the scripts it actually carries. CJK fallback, bidirectional text, and complex-script shaping are not yet implemented, so a cue in those scripts validates and exports as missing glyphs.
Assistance is optional and capability-based:
- Measured section planning is local and derives timing/structure from PCM.
- Timed lyrics first looks for the lyrics you already have. Use Choose
lyrics… on the Assist confirmation step to pick a text file; the panel then
names the sheet it is about to use, so you can see before starting whether
this run will time your words or transcribe its own. Without an explicit
choice it still finds a sibling
<track>.lyrics.txtbeside the audio, or unsynchronized lyrics embedded in the audio file's metadata (ID3USLT-style tags). The choice is per session and is not saved into the project: the words end up in the project as cues anyway, and a bare path to a file outside the asset bundle is not something a portable.musishould carry. When a sheet is found, a fully local deterministic aligner synchronizes those authored lines against Whisper's word timing — the authored text is displayed verbatim, section headings and stage directions are filtered out, backing lines are kept, hallucinated evidence stretches are ignored, and any line that found no timing is reported instead of guessed. Only tracks without known lyrics fall back to Whisper transcription plus an evidence-preserving headless Codex wording review, which is now bounded to short readable cues, must account for the whole track, and is followed by a deterministic splitter. Whisper discovery prefers the best installed model (large-v3-turbo>large-v3>large-v3-q5_0>medium.en, an order measured on sung material where turbo recovered the most lyric lines at a fraction of the cost;MUSIALIZER_WHISPER_MODELoverrides). - MiMo feelings uses the local measured analysis plus MiMo V2.5 through OpenRouter to produce semantic energy/tension/valence cues.
- Full assist combines those stages while retaining separate provenance.
Whisper evidence, deterministic lyric sync, Codex review, measured audio features, MiMo interpretation, and user-authored events remain distinct data lanes. MiMo/OpenRouter modes are the explicit authorization boundary for sending track audio to a remote service; every workflow asks before launch and names what leaves the computer. While a helper runs, the UI reports real elapsed time and the 40-minute job timeout rather than inventing a completion percentage. Cancellation is polled without blocking playback, and a staged lyric lane cannot replace an active authored lyric draft. Model output is validated, staged, and never mutates the project automatically.
The more experimental scenes have deliberately different jobs. ASCII Field
is a rolling spectral glyph field that can blend an imported image's aspect,
tonal structure, and source color into that live canvas. Orbital Lattice
turns band energy into damped crystalline motion and flexible links. Song
Atlas analyzes the complete decoded track into a slope-lit time-by-frequency
terrain. Cadence estimates word windows inside accepted line-level lyric
cues and assembles each word from beat-responsive character swarms. Loom
samples accepted semantic events across the full track and weaves energy,
tension, and valence into thread density, interlace, and color temperature;
without a semantic lane it freezes the measured spectrum into each column as
the working edge passes, so the finished cloth is a spectrogram-as-textile
record of the song, with live band vibration, onset ripples, and
centroid-driven color temperature at the fell.
Without lyrics or semantic analysis, Cadence and Loom retain deterministic
audio-reactive ambient/fallback behavior rather than requiring a model service.
Pentagram Orbits draws the phase portrait of the Lyness recurrence
y[k+1] = (1 + y[k])/y[k-1], whose every orbit provably closes after exactly
five steps: nested invariant-curve ovals surround the golden-ratio fixed
point while five-station orbits hop their pentagram chords in time with the
music. The nest is shaped by the sound itself: every invariant curve flexes
radially with the smoothed spectrum sampled around its circumference, each
beat sends a brightness ripple outward from the golden center, and sparks
lunge along their chords on the beat.
Set the optional credential in the process environment or in an ignored .env
at the repository root:
OPENROUTER_API_KEY=your-key-hereThe helper parses only that named value and never sources .env as shell code.
It does not write the key into arguments, logs, manifests, or cache keys. Local
children receive credential-like environment variables stripped out.
Run the non-invasive product doctor to see which workflows are ready:
$ python3 tools/musializer_doctor.py
$ python3 tools/musializer_doctor.py --json \
--require export --require local_lyrics --require remote_mimoThe doctor checks discovery and writable locations only. It does not invoke FFmpeg/models, make a network request, or reveal credential values. Setup and lower-level adapter commands are documented in Analysis adapters and Measured analysis.
Projects embed the evaluated lyrics, semantic events, manual events, scene suggestions, deterministic seed, export settings, metadata, and analysis provenance. They do not require a mutable analysis cache to replay accepted semantic cues. Saves use a durable temporary sibling and atomic replacement. Every save imports the track audio and, when present, the ASCII source image into a content-addressed sibling bundle:
show.musi
show.assets/audio/<sha256>.<ext>
show.assets/images/<sha256>.<ext>
The project stores strict relative references plus SHA-256 identities. Existing
matching objects are reused, conflicting objects are rejected, and the project
file is published only after every required asset is present. As a result,
moving show.musi together with show.assets/ keeps the project portable and
offline-replayable; source files elsewhere on the workstation are no longer a
runtime dependency after a successful save.
The v1 schema deliberately describes future composition features that this editor cannot yet preserve. This build opens only its lossless editor subset: full-track referenced or imported audio, integer-frame-rate H.264 MP4 output, one enabled opaque full-track Normal scene, Musializer's canonical slider constants, built-in audio-driven routes, scene-cue snapshots, and — for an imported caption face — a path this editor could itself have written into the sibling asset bundle. Other schema-valid composition features are rejected with an explicit unsupported-feature error instead of being silently rewritten. An imported face at an absolute or traversing path is refused for the same reason: opening it would typeset in a substitute face and then autosave that substitution over the author's choice.
An imported ASCII image is stored as a verified image asset with its derived grid dimensions. Reopening the project verifies the image identity and rebuilds the deterministic grid before exposing the track; a missing, modified, or escaped bundle path fails closed instead of silently clearing the scene.
Preview requests 4x MSAA. High and Master exports render at 2x spatial
resolution and downsample to the requested output; Balanced renders at native
resolution. Set MUSIALIZER_RENDER_SUPERSAMPLE=0 to force 1x on a GPU that
cannot allocate the larger target.
The default export is 1920x1080 at 30 fps, High quality. Balanced, High, and
Master map to H.264 High-profile CRF 20/16/12 with yuv420p, BT.709 metadata,
256 or 320 kbit/s AAC, and fast-start metadata. The UI offers 720p, 1080p,
1440p, and 2160p at 24, 30, or 60 fps; validated CLI sizes can reach 7680x4320.
The decoded raylib Wave is the canonical input for both frame analysis and
FFmpeg audio. Musializer renders
ceil(decoded_sample_frames * FPS / sample_rate) frames and pads only the final
sub-frame audio tail to align stream and container endings within the MP4 time
base. FFmpeg writes a unique sibling file; only a clean encoder exit publishes
it over the selected destination.
For very long mixes, export can currently use gigabytes because the complete decoded Wave and a float analysis copy are held in memory. GPU readback, FFmpeg pipe writes, and finalization also still occur on the UI thread, so a slow encoder can temporarily delay repaint/cancel input. These are known boundaries, not release-quality claims; see product readiness.
The complete UI workflow does not require a terminal, but the CLI is useful for repeatable renders and tests:
$ ./build/musializer --help
$ ./build/musializer --scene orbital path/to/song.mp3
$ ./build/musializer --ascii-image path/to/image.png path/to/song.mp3
$ ./build/musializer --scene constellation \
--event lyric:1.25:42:0.9 --event cue:2.0:43:1.0 path/to/song.mp3
$ ./build/musializer path/to/song.mp3 \
--analysis-bridge build/analysis/song/analysis.bridge.tsv \
--auto-scenes --render output.mp4
$ ./build/musializer path/to/song.wav --scene atlas \
--resolution 2560x1440 --fps 60 --quality master --render output.mp4
$ ./build/musializer path/to/song.mp3 --scene atlas \
--quality high --save-project path/to/show.musi
$ ./build/musializer --project path/to/show.musi --render output.mp4
$ ./build/musializer path/to/song.mp3 --scene pentagram \
--render-window 35 20 --render preview-section.mp4
$ ./build/musializer path/to/song.mp3 --scene loom \
--route loom.weight:band:2:0:0.8:0.4:2.5:smoothstep \
--route loom.glints:spectral_flux:0:0:0.15:0:2 --render routed.mp4Built-in scene selectors are spectrum, pulse, orbital, ascii, atlas,
terrarium, constellation, cadence, loom, and pentagram. Repeatable
--event type:seconds:id:value
arguments accept lyric, semantic, cue, or custom. A positional .musi
file is equivalent to --project. Command-line renders exit after FFmpeg
finishes, making them suitable for scripts and smoke tests.
Startup arguments configure the session and never edit an opened project. A
.musi file passed to --project is left byte-for-byte alone unless you save
it in the workspace or ask for a save with --save-project, so previewing a
project at a different scene, resolution, or frame rate cannot quietly replace
what it had stored. --save-project writes whatever the startup arguments
configured, so --scene atlas --quality master --save-project show.musi
persists both. --mute starts
the session with the output volume at zero — playback, analysis, and export
behave identically, the speakers just stay quiet — which keeps scripted and
test launches from being audible.
Repeatable --route arguments connect live audio measurements to any scene
setting for this session, identically in preview and export:
parameter:source:band:in_min:in_max:out_min:out_max[:curve][:noclamp]. The
parameter is a scene-settings key (the settings. prefix is optional, e.g.
loom.weight); sources are rms, peak, spectral_flux, beat_phase, and
band with a spectrum band index; curves are step, linear, smoothstep,
ease_in, and ease_out. The mapped value replaces the slider value each
frame, clamped to the setting's range; toggle settings switch at the midpoint
of their mapped range. Output endpoints must differ—flat values are ordinary
slider constants, not audio routes. Route arguments are applied after the
input audio/project is loaded, so their placement relative to --project does
not change the result. Routes are saved into .musi projects
using the format's parameter-mapping representation — a setting persists as
either its slider constant or its route — and reopened projects render
byte-identically to the session that authored them. Projects containing
routes are rejected by older builds' editors by design rather than being
silently stripped.
Routes can also be authored visually: every row in the Tune inspector has a
~ button that opens an inline route editor with source buttons, a spectrum
band stepper, a response curve, and a clamp toggle. The mapping itself is
edited as two anchors, each pairing one source level with one output value —
Quiet 0.03 → 0.00 and Loud 0.78 → 1.40 for loudness sources, Calm/Busy
for spectral flux, Beat start/Beat end for beat phase. A transfer graph
plots the full response (source level across, setting value up, the span
between the anchors shaded) with a dot riding the curve at the live audio
value, and a live readout shows the exact value the route is producing right
now — computed by the same function the frame loop uses. A routed setting's
row shows the live driven value plus a compact source · curve · range
summary in place of its slider (the underlying slider value is kept but
inactive until the route is removed). Equal output endpoints disable Apply
because .musi v1 represents that value as a slider. Route edits are drafts: they take effect on Apply,
Discard throws them away, and unapplied route changes block saving,
project/track/scene changes, rendering, and quitting the same way an unapplied
lyric draft does. Autosave also leaves the draft's track untouched. Automatic
scene switching pauses while the inline route editor is open so its host scene
cannot disappear mid-edit.
--render-window START DURATION (seconds) exports only that span of the
track. The engine first fast-forwards analysis, beat tracking, and scene
state through the preceding frames exactly as a full export would, so the
windowed frames match the same span of a full render; only the drawing and
encoding are skipped. Fractional boundaries expand to the containing video
frames so the requested interval is completely covered rather than losing its
tail; the decoded audio slice uses those same frame boundaries. The window
clamps to the end of the track, and a start at or past the track end fails
before any file is touched.
--analysis-bridge checks the bridge audio SHA-256 before importing lyric,
semantic, or scene lanes. --auto-scenes opts into its section recommendations
for both preview and export. Analysis artifacts and bounded logs live below
ignored build/analysis/.
Hot reload keeps most application logic in libplug:
$ ./nob build hotreload
$ ./build/musializerRebuild while the application is running, focus its window, and press
h to load the new plug. --reload-once with a track or scripted
render performs one automated handoff smoke.
--ui-probe opens a chosen workspace panel and parks the transport so the
interface can be photographed reproducibly on a headless display, for example
--ui-probe panel=lyrics,time=6,size=1280x720. It is a diagnostics flag: it
applies the same state transition the matching button performs, never mutates
project data, and rejects an unknown key or a panel requested without a loaded
track. tools/ui_capture.sh drives it over the catalogue in
tools/ui_states.txt to render every supported UI state to PNGs on a private
Xvfb display, leaving the operator's desktop session and audio server
untouched. tools/UI_REVIEW.md describes the review loop
and its limits. This has been exercised on Linux only.
Coding agents should follow the checkout's local AGENTS.md when present. It
records the project-format, rendering, privacy, UI-state, cross-platform, test,
and packaging invariants that a local change must preserve. The durable roadmap
and implementation log live in EXTENSION_PLAN.md.
Distribution recipes deliberately include an explicit allowlist of runtime
support files and exclude .env, Python bytecode, analysis caches, and user
media. Linux/OpenBSD produce .tar.gz, MinGW produces .zip, and macOS
produces build/Musializer.app; not all artifacts currently have equal helper
coverage. ./nob dist always rebuilds first.
Music by @nu11, from the 20:38 mark of nu11 WIP works 2016-2022:
output.mp4
Musializer is released under the MIT License. The original project and copyright belong to Alexey "tsoding" Kutepov and Musializer contributors; this fork retains that license and attribution. The upstream source remains at github.com/tsoding/musializer.
The bundled Space Grotesk interface face and Alegreya caption face are released
under the SIL Open Font License 1.1. Their copyright and license notices are in
resources/fonts.
A caption face imported from Google Fonts is not covered by either of the
above. Musializer downloads the licence that family is distributed under
(OFL 1.1, Apache 2.0, or the Ubuntu Font Licence), stores it beside the face in
the project's .assets/ bundle, and records which one it is. If you share the
project, the terms go with it. Musializer refuses to import a face whose
licence it could not retrieve, but it does not interpret those terms for you --
what they permit is between you and the licence.
