Skip to content

Repository files navigation

psnd — a polyglot editor for music programming languages

psnd is a self-contained modal editor, REPL, and playback environment aimed at music programming languages. The project is a polyglot platform for composing, live-coding, and rendering music DSLs from one binary.

Five languages are currently supported:

  • Alda - Declarative music notation language
  • Joy - Concatenative (stack-based) functional language for music
  • TR7 - R7RS-small Scheme with music extensions
  • Bog - C implementation of dogalog, a prolog-based beats-oriented language for music
  • MHS - Micro Haskell with MIDI support for functional music programming

All are practical for daily live-coding, REPL sketches, and headless playback. The Alda and Joy MIDI cores are from the midi-langs project. Languages register themselves via a modular dispatch system, allowing additional DSLs to be integrated without modifying core dispatch logic. Audio output is handled by the built-in TinySoundFont synthesizer or, optionally, a Csound backend for advanced synthesis. macOS, Linux, and Windows are supported.

Features

  • Vim-style editor with INSERT/NORMAL modes, live evaluation shortcuts, and opt-in Lua scripting (built on loki, a fork of kilo)
  • Split window support with vim-style Ctrl-W commands for horizontal/vertical splits, pane navigation, and per-pane independent view state
  • TOML configuration via .psnd/config.toml for themes, keybindings, audio settings, and editor options
  • Tree-sitter syntax highlighting in REPLs with 17 TOML themes (monokai, dracula, nord, gruvbox, solarized, catppuccin, tokyo-night, kanagawa, and more) plus custom theme support via .psnd/themes/
  • MIDI tracker/step sequencer with terminal UI, plugin-based cell notation, and pattern looping
  • Native webview mode for a self-contained GUI window without requiring a browser (optional)
  • Web-based editor accessible via browser using xterm.js terminal emulator (optional)
  • Language-aware REPLs for interactive composition (Alda, Joy, TR7 Scheme, Bog, MHS)
  • Headless play mode for batch jobs and automation
  • Non-blocking async playback through libuv - REPLs remain responsive during playback
  • Integrated MIDI routing powered by libremidi
  • MIDI file export using midifile
  • TinySoundFont synthesizer built on miniaudio
  • Optional Csound backend for deeper sound design workflows
  • Ableton Link support for networked tempo sync (playback matches Link session tempo)
  • OSC (Open Sound Control) support for remote control and inter-application communication (optional)
  • Parameter binding for MIDI CC and OSC control of named parameters from physical controllers
  • Scala .scl import support for microtuning
  • Lua APIs (opt-in) for editor automation, playback control, and extensibility

Status

psnd is in active development. All five languages register through the same uniform integration layer (lang_dispatch.h plus the LokiLangOps editor bridge), demonstrating the polyglot architecture and letting new DSLs be added without modifying core dispatch logic. They differ in maturity: Alda and Joy are mature, fully bespoke implementations; Bog is a bespoke implementation in beta; TR7 Scheme and MHS are adapters over their upstream interpreters. Additional mini MIDI languages from midi-langs can reuse the same editor, REPL, and audio stack. Expect iteration and occasional breaking changes as polyglot support expands.

Prerequisites

macOS:

brew install cmake flex bison

Linux (Debian/Ubuntu):

sudo apt install cmake flex bison

Linux (Fedora/RHEL):

sudo dnf install cmake flex bison

Linux (Arch):

sudo pacman -S cmake flex bison

Windows:

# Using Visual Studio 2022 with C++ workload# Install CMake from https://cmake.org/download/# Install winflexbison from https://github.com/lexxmark/winflexbison/releases# Add winflexbison to PATH

Or with vcpkg:

vcpkg install winflexbison

flex and bison are required for building the MHS (Micro Haskell) language component.

Building

Build presets select the synthesizer backend and optional features:

TargetAliasDescription
make psnd-tsfmake, make defaultTinySoundFont only (smallest)
make psnd-tsf-csoundmake csoundTinySoundFont + Csound
make psnd-fluidFluidSynth only (higher quality)
make psnd-fluid-csoundFluidSynth + Csound
make psnd-tsf-webmake webTinySoundFont + Web UI
make psnd-fluid-webFluidSynth + Web UI
make psnd-fluid-csound-webmake fullEverything

MHS (Micro Haskell) build variants:

TargetBinary SizeDescription
make~5.7MBFull MHS with fast startup (~2s) and compilation support
make mhs-small~4.5MBMHS without compilation to executable
make mhs-src~4.1MBMHS with source embedding (~17s startup)
make mhs-src-small~2.9MBSmallest binary with MHS
make no-mhs~2.1MBMHS disabled entirely

Synth backends (mutually exclusive at compile time):

  • TinySoundFont - Lightweight SoundFont synthesizer, fast compilation
  • FluidSynth - Higher quality synthesis, more SoundFont features

CMake options (for custom builds):

cmake -B build -DBUILD_FLUID_BACKEND=ON # Use FluidSynth instead of TSF
cmake -B build -DBUILD_CSOUND_BACKEND=ON # Enable Csound synthesis
cmake -B build -DBUILD_WEB_HOST=ON # Enable web server mode
cmake -B build -DBUILD_WEBVIEW_HOST=ON # Enable native webview mode
cmake -B build -DBUILD_OSC=ON # Enable OSC (Open Sound Control) support
cmake -B build -DBUILD_PLUGIN_SQLITE=ON # Enable SQLite FTS5 search index
cmake -B build -DLOKI_EMBED_XTERM=ON # Embed xterm.js in binary (no CDN)

Usage

psnd exposes three complementary workflows: REPL mode for interactive sketching, editor mode for live-coding within files, and play mode for headless rendering. Running psnd with no arguments displays help. Flags (soundfont, csound instruments, etc.) are shared between modes.

REPL Mode

Alda REPL:

psnd alda # Start Alda REPL
psnd alda -sf gm.sf2 # REPL with built-in synth

Type Alda notation directly:

alda> piano: c d e f g
alda> violin: o5 a b > c d e
alda> :stop
alda> :q

Joy REPL:

psnd joy # Start Joy REPL
psnd joy --virtual out # Joy REPL with named virtual port
psnd joy -p 0 # Joy REPL using MIDI port 0

Type Joy code directly:

joy> :virtual
joy> 120 tempo
joy> [c d e f g] play
joy> c major chord
joy> [c e g] [d f a] [e g b] each chord
joy> :q

TR7 Scheme REPL:

psnd tr7 # Start TR7 Scheme REPL
psnd scheme # Alias for tr7
psnd tr7 -sf gm.sf2 # REPL with built-in synth
psnd tr7 --virtual out # TR7 REPL with named virtual port
psnd tr7 song.scm # Run Scheme file

Type Scheme code directly:

tr7> (midi-virtual "TR7Out")
tr7> (set-tempo 120)
tr7> (play-note 60 80 500) ; note 60, velocity 80, 500ms
tr7> (play-chord '(60 64 67) 80 500) ; C major chord
tr7> (set-octave 5)
tr7> :q

Bog REPL:

psnd bog # Start Bog REPL
psnd bog --virtual out # Bog REPL with named virtual port
psnd bog -sf gm.sf2 # Bog REPL with built-in synth

Type Bog rules directly:

bog> :def kick event(kick, 36, 0.9, T) :- every(T, 1.0).
Slot 'kick' defined (new)
bog> :def hat event(hat, 42, 0.5, T) :- every(T, 0.25).
Slot 'hat' defined (new)
bog> :slots
Slots:
kick: event(kick, 36, 0.9, T) :- every(T, 1.0).
hat: event(hat, 42, 0.5, T) :- every(T, 0.25).
bog> :mute hat
bog> :solo kick
bog> :q

MHS REPL (Micro Haskell):

psnd mhs # Start MHS REPL
psnd mhs -r file.hs # Run a Haskell file
psnd mhs -oMyProg file.hs # Compile to executable

Type Haskell code directly:

mhs> import Midi
mhs> midiInit
mhs> midiOpenVirtual "MHS-MIDI"
mhs> midiNoteOn 0 60 100
mhs> midiSleep 500
mhs> midiNoteOff 0 60
mhs> :quit

MHS-specific commands (MicroHs built-in commands also work):

CommandAction
:type EXPRShow type of expression
:kind TYPEShow kind of type
:browse MODULEList exports from module
:quitExit MicroHs REPL

Shared REPL Commands (work in Alda, Joy, TR7, Bog, and MHS, with or without :):

CommandAction
:q:quit:exitExit REPL
:h:help:?Show help
:l:listList MIDI ports
:s:stopStop playback
:p:panicAll notes off
:play PATHPlay file (dispatches by extension)
:sf PATHLoad soundfont and enable built-in synth
:presetsList soundfont presets
:midiSwitch to MIDI output
:synth:builtinSwitch to built-in synth
:virtual [NAME]Create virtual MIDI port
:link [on|off]Enable/disable Ableton Link
:link-tempo BPMSet Link tempo
:link-statusShow Link status
:cs PATHLoad CSD file and enable Csound
:csoundEnable Csound backend
:cs-disableDisable Csound
:cs-statusShow Csound status
:themeList available color themes
:theme NAMESwitch to a color theme

Alda-specific commands:

CommandAction
:sequentialWait for each input to complete
:concurrentEnable polyphonic playback (default)
:export FILEExport to MIDI file

Joy-specific commands:

CommandAction
.Print stack

Bog-specific commands:

CommandAction
:def NAME RULEDefine a named slot
:undef NAMERemove a named slot
:slots:lsShow all defined slots
:clearRemove all slots
:mute NAMEMute a slot
:unmute NAMEUnmute a slot
:solo NAMESolo a slot (mute all others)
:unsoloUnmute all slots
:tempo BPMSet tempo
:swing AMOUNTSet swing (0.0-1.0)

Editor Mode

psnd song.alda # Open Alda file in editor
psnd song.joy # Open Joy file in editor
psnd song.scm # Open Scheme file in editor
psnd song.bog # Open Bog file in editor
psnd song.csd # Open Csound file in editor
psnd -sf gm.sf2 song.alda # Editor with TinySoundFont synth
psnd -cs instruments.csd song.alda # Editor with Csound synthesis

Keybindings:

KeyAction
Ctrl-EPlay current part/line (or selection)
Ctrl-PPlay entire file
Ctrl-GStop playback
Ctrl-SSave
Ctrl-QQuit
Ctrl-FFind
Ctrl-LLua console
iEnter INSERT mode
ESCReturn to NORMAL mode

Split Window Commands (in NORMAL mode):

KeyAction
Ctrl-W sSplit horizontally (top/bottom)
Ctrl-W vSplit vertically (left/right)
Ctrl-W hMove to left pane
Ctrl-W jMove to pane below
Ctrl-W kMove to pane above
Ctrl-W lMove to right pane
Ctrl-W wCycle to next pane
Ctrl-W cClose current pane
Ctrl-W =Equalize split ratios
Ctrl-W nNew buffer in current pane
Ctrl-W bCycle buffers in current pane

Ex Commands (press : in NORMAL mode):

CommandAction
:wSave file
:qQuit (warns if unsaved)
:wqSave and quit
:q!Quit without saving
:e FILEOpen file
:123Go to line 123
:goto 123Go to line 123
:s/old/new/Replace first occurrence on line
:s/old/new/gReplace all occurrences on line
:helpShow help
:linkToggle Ableton Link
:csdToggle Csound synthesis
:export FILEExport to MIDI file
:themeOpen theme picker (full-screen selection)
:theme NAMESwitch to named color theme
:plugin presetsOpen preset picker (for loaded plugins)

Picker Keybindings (when picker is open):

KeyAction
j / DOWNMove selection down
k / UPMove selection up
g / GGo to top / bottom
CTRL-D / CTRL-UPage down / up
ENTERSelect item
ESC / qCancel

Play Mode

psnd play song.alda # Play Alda file and exit
psnd play song.joy # Play Joy file and exit
psnd play song.scm # Play Scheme file and exit
psnd play song.bog # Play Bog file and exit
psnd play song.csd # Play Csound file and exit
psnd play -sf gm.sf2 song.alda # Play Alda with built-in synth
psnd play -v song.csd # Play with verbose output

Web Mode

Run psnd as a web server and access the editor through a browser using xterm.js terminal emulation.

psnd --web # Start web server on port 8080
psnd --web --web-port 3000 # Use custom port
psnd --web song.alda # Open file in web editor
psnd --web -sf gm.sf2 song.joy # Web editor with soundfont

Then open http://localhost:8080 in your browser.

Features:

  • Full terminal emulation via xterm.js
  • Mouse click-to-position support
  • Language switching with :alda, :joy, :langs commands
  • First-line directives (#alda, #joy) for automatic language detection
  • All editor keybindings work as in terminal mode

Build requirement: Web mode requires building with -DBUILD_WEB_HOST=ON.

Embedded mode: Build with -DLOKI_EMBED_XTERM=ON to embed xterm.js in the binary, eliminating CDN dependency for offline use.

Native Webview Mode

Run psnd in a native window using the system's webview (WebKit on macOS, WebKitGTK on Linux). This provides the same xterm.js-based UI as web mode but in a self-contained native application - no browser required.

psnd --native song.alda # Open file in native window
psnd --native -sf gm.sf2 song.joy # Native window with soundfont

Features:

  • Same UI as web mode (xterm.js terminal emulation)
  • Play/Stop/Eval buttons in toolbar
  • All editor keybindings work as in terminal mode
  • Works completely offline
  • Clean window close handling

Build requirement: Native webview mode requires building with -DBUILD_WEBVIEW_HOST=ON.

Platform dependencies:

  • macOS: WebKit framework (always available)
  • Linux: GTK3 and WebKitGTK (libgtk-3-dev libwebkit2gtk-4.0-dev)

Piped Input

All REPLs support non-interactive piped input for scripting and automation:

# Alda REPLecho'piano: c d e f g'| psnd alda
echo -e 'piano: c d e\n:q'| psnd alda
# Joy REPLecho'[c d e] play'| psnd joy
printf':cs synth.csd\n:cs-status\n:q\n'| psnd joy
# TR7 Scheme REPLecho'(play-note 60 80 500)'| psnd tr7
# Bog REPLecho':def kick event(kick, 36, 0.9, T) :- every(T, 1.0).'| psnd bog

This is useful for testing, CI/CD pipelines, and batch processing.

Configuration

psnd uses TOML for configuration. Settings are loaded from .psnd/config.toml (project-local) or ~/.psnd/config.toml (home directory).

Example config.toml

[editor]
theme = "nord"# Color theme (see .psnd/themes/)line_numbers = true# Show line numberstab_width = 4# Tab width for indentation
[editor.lua]
enabled = false# Opt-in Lua scripting (disabled by default)
[audio]
backend = "tsf"# "tsf", "fluid", "csound", or "midi"soundfont = ""# Path to SoundFont (empty = default GM)
[link]
enabled = false# Ableton Link synctempo = 120# Default tempo# Available commands: save, find, stop, lua_repl, new_buffer, copy, word_wrap# Note: quit, eval_line, play_file have special built-in handling# Note: ctrl-w is reserved for split window commands
[keybindings]
"ctrl-s" = "save""ctrl-g" = "stop""ctrl-f" = "find""ctrl-l" = "lua_repl"# Only works when Lua is enabled"ctrl-t" = "new_buffer""ctrl-c" = "copy"

Lua Scripting (Opt-in)

Lua scripting is disabled by default for security. To enable it, set editor.lua.enabled = true in your config.toml. When enabled:

  • .psnd/init.lua is loaded at startup
  • Ctrl-L opens the Lua console
  • Custom commands and keybindings via Lua are available
  • Full loki.* Lua API is accessible

When Lua is disabled, core functionality (Alda, Joy, Bog, themes, keybindings, audio) works normally.

Lua Scripting

Press Ctrl-L in the editor to access the Lua console (requires editor.lua.enabled = true):

-- Play Alda codeloki.alda.eval_sync("piano: c d e f g a b > c")
-- Async playback with callbackloki.alda.eval("piano: c d e f g", "on_done")
-- Stop playbackloki.alda.stop_all()
-- Load soundfont for built-in synthloki.alda.load_soundfont("path/to/soundfont.sf2")
loki.alda.set_synth(true)

Joy Language

Joy is a concatenative (stack-based) language for music composition. It provides a different paradigm from Alda's notation-based approach.

Quick Start

psnd joy # Start Joy REPL
psnd song.joy # Edit Joy file
psnd play song.joy # Play Joy file headlessly

Basic Syntax

Joy uses postfix notation where operations follow their arguments. Playback is non-blocking - the REPL remains responsive while notes play in the background:

\ Comments start with backslash
:virtual \ Create virtual MIDI port
120 tempo \ Set tempo to 120 BPM
80 vol \ Set volume to 80
\ Play notes (non-blocking)
c play \ Play middle C
[c d e f g] play \ Play a melody
[c d e] play [f g a] play \ Layer multiple phrases
\ Chords
[c e g] chord \ Play C major chord
c major chord \ Same thing using music theory
a minor chord \ A minor chord
g dom7 chord \ G dominant 7th
\ Direct MIDI control
60 80 500 midi-note \ Note 60, velocity 80, 500ms duration

Music Theory Primitives

Joy includes music theory primitives for building chords:

PrimitiveDescriptionExample
majorMajor triadc major chord
minorMinor triada minor chord
dom7Dominant 7thg dom7 chord
maj7Major 7thc maj7 chord
min7Minor 7tha min7 chord
dimDiminished triadb dim chord
augAugmented triadc aug chord

Stack Operations

Joy is stack-based, so values are pushed onto a stack and operations consume them:

60 dup \ Duplicate: [60 60]
60 70 swap \ Swap: [70 60]
60 70 pop \ Pop: [60]
[1 2 3] [dup *] map \ Map: [1 4 9]

Lua API

-- Initialize Joyloki.joy.init()
-- Evaluate Joy codeloki.joy.eval(":virtual 120 tempo [c d e] play")
-- Define a custom wordloki.joy.define("cmaj", "[c e g] chord")
-- Stop playbackloki.joy.stop()

TR7 Scheme Language

TR7 is an R7RS-small Scheme interpreter with music extensions. It provides a Lisp-based approach to music composition.

Quick Start

psnd tr7 # Start TR7 REPL
psnd tr7 song.scm # Run Scheme file
psnd song.scm # Edit Scheme file
psnd play song.scm # Play Scheme file headlessly

Music Primitives

TR7 extends R7RS-small Scheme with music-specific procedures:

ProcedureDescription
(play-note pitch velocity duration-ms)Play a single note (non-blocking)
(play-chord '(p1 p2 ...) velocity duration-ms)Play a chord (non-blocking)
(play-seq '(p1 p2 ...) velocity duration-ms)Play notes in sequence (non-blocking)
(note-on pitch velocity)Send note-on message
(note-off pitch)Send note-off message
(set-tempo bpm)Set tempo
(set-octave n)Set octave (0-9)
(set-velocity v)Set velocity (0-127)
(set-channel ch)Set MIDI channel (0-15)
(program-change prog)Change instrument
(control-change cc value)Send CC message
(note name [octave])Convert note name to MIDI number

MIDI Control

ProcedureDescription
(midi-list)List available MIDI ports
(midi-open port)Open MIDI port by index
(midi-virtual name)Create virtual MIDI port
(midi-panic)All notes off
(tsf-load path)Load soundfont for built-in synth
(sleep-ms ms)Sleep for milliseconds

Example

; TR7 music composition example
(midi-virtual "TR7Out")
(set-tempo 120)
(set-velocity 80)
; Play a C major scale
(define (play-scale)
(for-each (lambda (n)
(play-note n 80250))
'(6062646567697172)))
; Play a chord progression
(play-chord '(606467) 80500) ; C major
(play-chord '(656972) 80500) ; F major
(play-chord '(677174) 80500) ; G major
(play-chord '(606467) 801000) ; C major

Bog Language

Bog is a Prolog-based live coding language for music, inspired by dogalog. Musical events emerge from declarative logic rules rather than imperative sequences.

Quick Start

psnd bog # Start Bog REPL
psnd bog song.bog # Run Bog file
psnd song.bog # Edit Bog file
psnd play song.bog # Play Bog file headlessly

Core Concept

All Bog patterns produce event/4 facts:

event(Voice, Pitch, Velocity, Time)
  • Voice: Sound source (kick, snare, hat, clap, noise, sine, square, triangle)
  • Pitch: MIDI note number (0-127) or ignored for drums
  • Velocity: Intensity (0.0-1.0)
  • Time: Beat time (bound by timing predicates)

Timing Predicates

PredicateDescriptionExample
every(T, N)Fire every N beatsevery(T, 0.5) - 8th notes
beat(T, N)Fire on beat N of barbeat(T, 1) - beat 1
euc(T, K, N, B, R)Euclidean rhythmeuc(T, 5, 16, 4, 0) - 5 hits over 16 steps

Named Slots

The REPL uses named slots to manage multiple concurrent patterns:

bog> :defkick event(kick, 36, 0.9, T) :- every(T, 1.0).
bog> :defsnare event(snare, 38, 0.8, T) :- every(T, 2.0).
bog> :defhat event(hat, 42, 0.5, T) :- every(T, 0.25).
bog> :slots % Listallpatternsbog> :mutehat % Mutehi-hatbog> :solokick % Solokickdrumbog> :undefsnare % Removesnarepatternbog> :clear % Removeallpatterns

Example Patterns

% Basic four-on-the-floorevent(kick, 36, 0.9, T) :- every(T, 1.0).
event(snare, 38, 0.8, T) :- beat(T, 2), beat(T, 4).event(hat, 42, 0.5, T) :- every(T, 0.5).
% Euclidean breakbeatevent(kick, 36, 0.9, T) :- euc(T, 5, 16, 4, 0).event(snare, 38, 0.85, T) :- euc(T, 3, 8, 4, 2).% Random variationevent(kick, 36, Vel, T) :- every(T, 1.0), choose(Vel, [0.7, 0.8, 0.9, 1.0]).
event(hat, 42, 0.5, T) :- every(T, 0.25), chance(0.7, true).

Lua API

-- Initialize Bogloki.bog.init()
-- Evaluate Bog codeloki.bog.eval("event(kick, 36, 0.9, T) :- every(T, 1.0).")
-- Stop playbackloki.bog.stop()
-- Set tempo and swingloki.bog.set_tempo(140)
loki.bog.set_swing(0.3)

MHS Language

MHS (Micro Haskell) is a lightweight Haskell implementation with MIDI support, providing functional programming for music composition.

Quick Start

psnd mhs # Start MHS REPL
psnd mhs --virtual MHS-Out # REPL with virtual MIDI port
psnd mhs -sf gm.sf2 # REPL with built-in synth
psnd mhs -r file.hs # Run a Haskell file
psnd mhs -oMyProg file.hs # Compile to standalone executable
psnd mhs -oMyProg.c file.hs # Output C code only

REPL Features

The MHS REPL provides full feature parity with other psnd languages:

  • Syntax highlighting for Haskell keywords, types, and MIDI primitives
  • Tab completion for 80+ Haskell keywords and MIDI functions
  • History persistence (~/.psnd/mhs_history)
  • All shared commands (:help, :stop, :panic, :list, :sf, :link, etc.)
  • Ableton Link integration for tempo sync

Architecture: MicroHs runs in a forked child process with a pseudo-terminal (PTY). The parent process handles psnd's syntax-highlighted input and forwards Haskell code to MicroHs via the PTY. MIDI is initialized in the child process after fork.

Available Modules

ModuleDescription
MidiLow-level MIDI I/O (ports, note on/off, control change)
MusicHigh-level music notation (notes, chords, sequences)
MusicPerformMusic performance/playback
MidiPerformMIDI event scheduling
AsyncAsynchronous operations

Example

importMidimain::IO()
main =do
midiInit
midiOpenVirtual "MHS-MIDI"-- Play a C major chord
midiNoteOn 060100-- C
midiNoteOn 064100-- E
midiNoteOn 067100-- G
midiSleep 1000
midiNoteOff 060
midiNoteOff 064
midiNoteOff 067
midiCleanup

Build Variants

MHS can be built with different configurations to trade off binary size vs features:

TargetSizeStartupFeatures
make5.7MB~2sFull: packages + compilation
make mhs-small4.5MB~2sPackages, no compilation
make mhs-src4.1MB~17sSource embedding + compilation
make mhs-src-small2.9MB~17sSource only, no compilation
make no-mhs2.1MBN/AMHS disabled

See source/langs/mhs/README.md for detailed documentation on VFS embedding, compilation, and standalone builds.

Tracker Sequencer

psnd includes a MIDI tracker/step sequencer with a terminal-based UI, inspired by classic trackers like FastTracker and Renoise.

Quick Start

Run the interactive demo:

./build/tests/tracker/tracker_demo ~/Music/sf2/FluidR3_GM.sf2

Controls

KeyAction
h/j/k/l or arrowsNavigate cells
Enter or iEdit cell
EscapeExit edit mode / Quit
SpacePlay/Stop
q or QQuit

Cell Notation (Notes Plugin)

The tracker uses a simple note notation language:

SyntaxDescription
C4Middle C
D#5D sharp, octave 5
Bb3B flat, octave 3
C4@80C4 with velocity 80
C4~2C4 held for 2 rows
C4 E4 G4C major chord
r or -Rest
x or offNote off

Architecture

The tracker is built with a modular plugin system:

  • tracker_model - Data structures (songs, patterns, tracks, cells)
  • tracker_plugin - Plugin system for notation languages
  • tracker_engine - Playback engine with event scheduling
  • tracker_view - View layer (theme, undo, clipboard, JSON)
  • tracker_audio - Audio integration with SharedContext

API Example

// Create a song and patternTrackerSong*song=tracker_song_new("My Song");
TrackerPattern*pattern=tracker_pattern_new(16, 4, "Pattern 1");
tracker_song_add_pattern(song, pattern);
// Add notes to cellsTrackerCell*cell=tracker_pattern_get_cell(pattern, 0, 0);
tracker_cell_set_expression(cell, "C4@80", "notes");
// Create engine and connect to audioTrackerEngine*engine=tracker_audio_engine_new(&audio_ctx);
tracker_engine_load_song(engine, song);
// Start playbacktracker_engine_play(engine);

Ableton Link

psnd supports Ableton Link for tempo synchronization with other musicians and applications on the same network.

Quick Start

In the editor, use the :link command:

:link on # Enable Link
:link off # Disable Link
:link # Toggle Link

In REPLs, use the same commands:

alda> :link on
[Link] Peers: 1
[Link] Tempo: 120.0 BPM
alda> piano: c d e f g # Plays at Link session tempo

When Link is enabled:

  • Status bar shows "ALDA LINK" instead of "ALDA NORMAL"
  • Playback tempo automatically syncs with the Link session for all languages (Alda, Joy, TR7)
  • REPLs print notifications when tempo, peers, or transport state changes
  • Other Link-enabled apps (Ableton Live, etc.) share the same tempo

Lua API

-- Initialize and enable Linkloki.link.init(120) -- Initialize with 120 BPMloki.link.enable(true) -- Start networking-- Tempo controlloki.link.tempo() -- Get session tempoloki.link.set_tempo(140) -- Set tempo (syncs to all peers)-- Session infoloki.link.peers() -- Number of connected peersloki.link.beat(4) -- Current beat (4 beats per bar)loki.link.phase(4) -- Phase within bar [0, 4)-- Transport sync (optional)loki.link.start_stop_sync(true)
loki.link.play() -- Start transportloki.link.stop() -- Stop transportloki.link.is_playing() -- Check transport state-- Callbacks (called when values change)loki.link.on_tempo("my_tempo_handler")
loki.link.on_peers("my_peers_handler")
loki.link.on_start_stop("my_transport_handler")
-- Cleanuploki.link.cleanup()

OSC (Open Sound Control)

psnd supports OSC for remote control and communication with other music software like SuperCollider, Max/MSP, Pure Data, and hardware controllers.

Building with OSC

cmake -B build -DBUILD_OSC=ON && make # Build with OSC support

Quick Start

# Start editor with OSC server on default port (7770)
psnd --osc song.alda
# Use custom port
psnd --osc-port 8000 song.alda
# Also broadcast events to another application
psnd --osc --osc-send 127.0.0.1:9000 song.alda

Incoming Messages

Control psnd from external applications:

AddressArgumentsDescription
/psnd/pingnoneConnection test (replies /psnd/pong)
/psnd/tempofloat bpmSet tempo
/psnd/noteint ch, int pitch, int velPlay note (note on + scheduled off)
/psnd/noteonint ch, int pitch, int velNote on
/psnd/noteoffint ch, int pitchNote off
/psnd/ccint ch, int cc, int valControl change
/psnd/pcint ch, int progProgram change
/psnd/bendint ch, int valPitch bend (-8192 to 8191)
/psnd/panicnoneAll notes off
/psnd/playnonePlay entire file
/psnd/stopnoneStop all playback
/psnd/evalstring codeEvaluate code string

Outgoing Messages

When a broadcast target is set (--osc-send), psnd automatically sends messages for state changes and MIDI events, regardless of what triggered them (keyboard, Lua API, or incoming OSC):

AddressArgumentsDescription
/psnd/pongnoneReply to ping
/psnd/status/playingint playingPlayback state (1=playing, 0=stopped)
/psnd/status/tempofloat bpmTempo changes
/psnd/midi/noteint ch, int pitch, int velNote on (vel>0) or off (vel=0)

Example: SuperCollider

// Send notes to psnd
n = NetAddr("127.0.0.1", 7770);
n.sendMsg("/psnd/note", 0, 60, 100); // C4
n.sendMsg("/psnd/note", 0, 64, 100); // E4
n.sendMsg("/psnd/note", 0, 67, 100); // G4
n.sendMsg("/psnd/tempo", 140.0); // Set tempo
n.sendMsg("/psnd/panic"); // Stop all notes// Playback control
n.sendMsg("/psnd/play"); // Play entire file
n.sendMsg("/psnd/stop"); // Stop playback
n.sendMsg("/psnd/eval", "piano: c d e f g"); // Evaluate code

Example: Pure Data / Max

Send messages to udp 127.0.0.1 7770:

  • /psnd/note 0 60 100 - Play middle C
  • /psnd/tempo 120 - Set tempo
  • /psnd/panic - Stop all notes

Lua API

Control OSC from Lua scripts and init.lua:

-- Initialize and start OSC serverosc.init(7770) -- Initialize on port 7770 (default)osc.start() -- Start the server-- Check statusosc.enabled() -- Returns true if runningosc.port() -- Returns current port number-- Set broadcast target for outgoing messagesosc.broadcast("localhost", 8000)
-- Send messagesosc.send("/my/path", 1, 2.5, "hello") -- To broadcast targetosc.send_to("192.168.1.100", 9000, "/custom", 42) -- To specific address-- Register callbacks (callback is called by function name)osc.on("/my/handler", "my_callback_function")
osc.off("/my/handler") -- Remove handler-- Stop serverosc.stop()

Type auto-detection for osc.send() and osc.send_to():

  • Lua integers become OSC int32 (i)
  • Lua floats become OSC float (f)
  • Lua strings become OSC string (s)
  • Lua booleans become OSC true/false (T/F)
  • Lua nil becomes OSC nil (N)

Design Document

See docs/PSND_OSC.md for the complete OSC address namespace specification and implementation details.

Parameter Binding System

psnd supports binding named parameters to MIDI CC and OSC addresses, enabling physical controllers (knobs, faders) to modify variables that affect music generation in real-time.

Quick Start

-- In Lua console (Ctrl-L) or init.lua-- Define a parameter with range and defaultparam.define("cutoff", { min=20, max=20000, default=1000 })
param.define("resonance", { min=0, max=1, default=0.5 })
-- Bind to MIDI CC (channel 1, CC 74)midi.in_open_virtual("PSND_MIDI_IN") -- Create virtual MIDI inputparam.bind_midi("cutoff", 1, 74) -- Moving CC 74 updates cutoff-- Bind to OSC addressparam.bind_osc("resonance", "/fader/2") -- OSC messages update resonance-- Read values from your music codelocalval=param.get("cutoff") -- Returns current value

Joy Usage

\ Define parameter (from Lua first)
\ Then read in Joy code:
"cutoff" param \ Push parameter value onto stack
5000 "cutoff" param! \ Set parameter value
param-list \ Print all parameters

MIDI Input

Open a MIDI input port to receive CC messages:

-- List available input portsmidi.in_list_ports()
-- Open by index (1-based)midi.in_open_port(1)
-- Or create a virtual input portmidi.in_open_virtual("MyController")
-- Check if openmidi.in_is_open() -- true/false-- Close when donemidi.in_close()

When MIDI CC messages arrive on a bound channel/CC, the parameter value is automatically updated with scaling from 0-127 to the parameter's min/max range.

OSC Control

When OSC is enabled (--osc), parameters can be controlled via OSC messages:

# Set parameter value
oscsend localhost 7770 /psnd/param/set sf "cutoff" 5000.0
# Query parameter (replies with /psnd/param/value)
oscsend localhost 7770 /psnd/param/get s "cutoff"# List all parameters
oscsend localhost 7770 /psnd/param/list

Parameters bound to custom OSC paths also respond to those paths:

# If bound: param.bind_osc("resonance", "/fader/2")
oscsend localhost 7770 /fader/2 f 0.75

Lua API Reference

FunctionDescription
param.define(name, opts)Define parameter (opts: min, max, default, type)
param.undefine(name)Remove parameter definition
param.get(name)Get current parameter value
param.set(name, value)Set parameter value
param.bind_osc(name, path)Bind parameter to OSC path
param.unbind_osc(name)Remove OSC binding
param.bind_midi(name, ch, cc)Bind to MIDI CC (channel 1-16, CC 0-127)
param.unbind_midi(name)Remove MIDI binding
param.list()Get table of all parameters
param.info(name)Get detailed info (value, range, bindings)
param.count()Number of defined parameters

MIDI Input:

FunctionDescription
midi.in_list_ports()Print available input ports
midi.in_port_count()Get number of input ports
midi.in_port_name(idx)Get port name (1-based index)
midi.in_open_port(idx)Open input port by index
midi.in_open_virtual(name)Create virtual input port
midi.in_close()Close current input port
midi.in_is_open()Check if input is open

Example: Filter Control

-- init.lua: Set up filter parameter with MIDI control-- Define parameterparam.define("filter_cutoff", {
type="float",
min=100,
max=10000,
default=1000
})
-- Open MIDI input and bind to CC 74 (filter cutoff is often CC 74)midi.in_open_virtual("PSND_Controller")
param.bind_midi("filter_cutoff", 1, 74)
-- Also allow OSC controlparam.bind_osc("filter_cutoff", "/synth/filter")

Then in Joy:

\ Apply filter with parameter value
"filter_cutoff" param \ Get current cutoff value
\ ... use value in synthesis/playback ...

Thread Safety

Parameter values use atomic floats, making them safe to read from any thread (main, audio, MIDI callback, OSC handler) without locks. This is essential for real-time audio applications where mutex locks could cause audio glitches.

Full-Text Search (SQLite FTS5)

psnd includes an optional SQLite FTS5 plugin for fast full-text search across .psnd/ configuration files, modules, themes, and scales.

Building with FTS

cmake -B build -DBUILD_PLUGIN_SQLITE=ON && make

Requires system SQLite3 with FTS5 support (standard on macOS 10.12+, most Linux distributions).

Usage

Ex-commands (in editor):

:search chord major # Full-text search in file contents
:find *.alda # Find files by path pattern
:reindex # Update index (incremental)
:rebuild-index # Full reindex from scratch
:index-stats # Show index statistics

Lua API:

-- Search contentlocalresults=loki.fts.search("chord major", 20)
for_, rinipairs(results) doprint(r.path, r.snippet)
end-- Search file pathslocalfiles=loki.fts.find("*.lua")
-- Index managementloki.fts.index() -- Incremental index of ~/.psndloki.fts.index(path, false) -- Full reindex of specific pathloki.fts.rebuild() -- Full reindex of default path-- Statisticslocalstats=loki.fts.stats()
print(stats.file_count, stats.total_bytes)

FTS5 Query Syntax

QueryDescription
chordFiles containing "chord"
"major chord"Exact phrase match
chord AND majorBoth terms
chord OR minorEither term
cho*Prefix match (chord, chorus, etc.)
path:aldaSearch in file paths only

MIDI Export

Export Alda compositions to Standard MIDI Files (.mid) for use in DAWs and other music software.

Quick Start

  1. Play some Alda code to generate events (Ctrl-E or Ctrl-P)
  2. Run :export song.mid to export

Lua API

-- Export to MIDI filelocalok, err=loki.midi.export("song.mid")
ifnotokthenloki.status("Export failed: " ..err)
end

Notes

  • Single-channel compositions export as Type 0 MIDI (single track)
  • Multi-channel compositions export as Type 1 MIDI (multiple tracks)
  • All events (notes, program changes, tempo, pan) are preserved

Csound Synthesis

psnd optionally supports Csound as an advanced synthesis backend, providing full synthesis capabilities beyond TinySoundFont's sample playback.

Building with Csound

make csound # Build with Csound backend (~4.4MB binary)

Quick Start

Option 1: Command-line (recommended)

psnd -cs .psnd/csound/default.csd song.alda

This loads the Csound instruments and enables Csound synthesis automatically when opening the file.

Option 2: Ex commands in editor

In the editor, use the :csd command after loading a .csd file:

:csd on # Enable Csound synthesis
:csd off # Disable Csound, switch to TinySoundFont
:csd # Toggle between Csound and TSF

Or via Lua:

-- Check if Csound is availableifloki.alda.csound_available() then-- Load Csound instrumentsloki.alda.csound_load(".psnd/csound/default.csd")
-- Switch to Csound backendloki.alda.set_backend("csound")
end

Standalone CSD Playback

You can also open and play .csd files directly without using them as MIDI instrument definitions:

psnd song.csd # Edit CSD file, Ctrl-P to play
psnd play song.csd # Headless playback

This plays the CSD file's embedded score section using Csound's native playback, not the MIDI-driven synthesis mode.

Lua API

-- Check availabilityloki.alda.csound_available() -- true if compiled with Csound-- Load instruments from .csd file (for MIDI-driven synthesis)loki.alda.csound_load("instruments.csd")
-- Enable/disable Csound (for MIDI-driven synthesis)loki.alda.set_csound(true) -- Enable Csound (disables TSF)loki.alda.set_csound(false) -- Disable Csound-- Unified backend selectionloki.alda.set_backend("csound") -- Use Csound synthesisloki.alda.set_backend("tsf") -- Use TinySoundFont (SoundFont)loki.alda.set_backend("midi") -- Use external MIDI only-- Standalone CSD playback (plays score section)loki.alda.csound_play("song.csd") -- Play CSD file asynchronouslyloki.alda.csound_playing() -- Check if playback is activeloki.alda.csound_stop() -- Stop playback

Default Instruments

The included .psnd/csound/default.csd provides 16 instruments mapped to MIDI channels, including subtractive synth, FM piano, pad, pluck, organ, bass, strings, brass, and drums.

Architecture Notes

Csound and TinySoundFont each have independent miniaudio audio devices. When you switch backends, the appropriate audio device is started/stopped. They do not share audio resources, allowing clean separation of concerns.

Syntax Highlighting

psnd provides built-in syntax highlighting for music programming languages with language-aware features.

Supported Languages

ExtensionLanguageFeatures
.aldaAldaInstruments, attributes, note names, octave markers, comments
.joyJoyStack ops, combinators, music primitives, note names, comments
.scm.ss.schemeTR7 SchemeKeywords, special forms, music primitives, comments
.bogBogPredicates, variables, operators, comments
.hs.mhsMHS (Haskell)Keywords, types, operators, strings, comments
.csdCsound CSDSection-aware (orchestra/score/options), opcodes, control flow
.orcCsound OrchestraFull orchestra syntax
.scoCsound ScoreScore statements, parameters
.sclScala ScaleComments, numbers (for microtuning definitions)

Csound CSD Section Awareness

CSD files contain multiple sections with different syntax. psnd detects these sections and applies appropriate highlighting:

  • <CsInstruments> - Full Csound orchestra highlighting:

    • Control flow (if, then, else, endif, while, do, od, goto)
    • Structure (instr, endin, opcode, endop)
    • Header variables (sr, kr, ksmps, nchnls, 0dbfs, A4)
    • Common opcodes (oscili, vco2, moogladder, pluck, reverb, etc.)
    • Comments (; single-line, /* */ block)
    • Strings and numbers
  • <CsScore> - Score statement highlighting:

    • Statement letters (i, f, e, s, t, etc.)
    • Numeric parameters
    • Comments
  • <CsOptions> - Command-line flag highlighting

Section tags themselves are highlighted as keywords, and section state is tracked across lines.

Color Themes

psnd supports 17 color themes with true RGB colors. Use :theme to list available themes and :theme NAME to switch.

Built-in themes: ayu-dark, basic16, catppuccin, dracula, everforest, github-light, gruvbox-dark, kanagawa, monokai, nord, norse, one-dark, palenight, rose-pine, solarized-dark, solarized-light, tokyo-night

Custom TOML themes (recommended): Create .psnd/themes/<name>.toml:

# Example: .psnd/themes/my-theme.toml
[meta]
name = "My Theme"description = "A custom color theme"
[colors]
# Base typesnormal = [200, 200, 200]
comment = [128, 128, 128]
keyword1 = [220, 100, 220]
keyword2 = [100, 200, 200]
string = [150, 200, 100]
number = [200, 150, 100]
# Extended types (51 total for full tree-sitter support)function = [100, 150, 220]
function_builtin = [100, 200, 200]
variable = [200, 200, 200]
variable_builtin = [220, 100, 100]
type = [200, 180, 100]
operator = [220, 100, 220]
# ... see existing themes for all 51 types

Lua themes (requires editor.lua.enabled = true): Create .psnd/themes/<name>.lua for dynamic themes with Lua logic.

The :theme command loads themes in order: TOML > Lua > C built-ins.

Scala Scale Files (Microtuning)

psnd supports Scala scale files (.scl) for microtuning and alternative temperaments.

Loading Scales

-- Load a scale fileloki.scala.load(".psnd/scales/just.scl")
-- Check if loadedifloki.scala.loaded() thenprint(loki.scala.description()) -- "5-limit just intonation major"print(loki.scala.length()) -- 7end

Converting MIDI to Frequencies

-- Convert MIDI note to frequency using loaded scale-- Arguments: midi_note, root_note (default 60), root_freq (default 261.63)localfreq=loki.scala.midi_to_freq(60) -- C4 in the scalelocalfreq=loki.scala.midi_to_freq(67, 60, 261.63) -- G4 with explicit root-- Get ratio for a specific degreelocalratio=loki.scala.ratio(4) -- 4th degree ratio (e.g., 3/2 for perfect fifth)

Generating Csound Pitch Tables

-- Generate Csound f-table statement for use in .csd fileslocalftable=loki.scala.csound_ftable(261.63, 1)
-- Returns: "f1 0 8 -2 261.630000 294.328125 327.031250 ..."

Sample Scales

The .psnd/scales/ directory includes example scales:

FileDescription
12tet.scl12-tone equal temperament (standard Western tuning)
just.scl5-limit just intonation major scale
pythagorean.sclPythagorean 12-tone chromatic scale

Lua API Reference

FunctionDescription
loki.scala.load(path)Load scale file, returns true or nil+error
loki.scala.load_string(content)Load from string
loki.scala.unload()Unload current scale
loki.scala.loaded()Check if scale is loaded
loki.scala.description()Get scale description
loki.scala.length()Number of degrees (excluding 1/1)
loki.scala.ratio(degree)Get frequency ratio for degree
loki.scala.frequency(degree, base)Get frequency in Hz
loki.scala.midi_to_freq(note, [root], [freq])MIDI note to Hz
loki.scala.degrees()Get all degrees as table
loki.scala.csound_ftable([base], [fnum])Generate Csound f-table
loki.scala.cents_to_ratio(cents)Convert cents to ratio
loki.scala.ratio_to_cents(ratio)Convert ratio to cents

Roadmap

Recent additions:

  • Windows/MSVC native support with full cross-platform compatibility
  • Playback visualization with source line highlighting during playback
  • Link transport sync mode for synchronized playback with Link peers
  • Tempo tap and metronome commands for beat reference
  • Web-based editor using xterm.js terminal emulation
  • Mouse click-to-position support in web mode
  • Language switching commands in web REPL

Planned:

  • Multi-client support for web mode (currently single connection)
  • Session persistence across server restarts
  • Beat-aligned playback with Ableton Link
  • Integrate additional MIDI DSLs from midi-langs

Feedback and experiments are welcome - polyglot support will be guided by real-world usage.

Project Architecture

PSND Architecture

Project Structure

source/
core/
loki/ # Editor components (core, modal, syntax, lua, hosts)
host_terminal.c # Terminal-based host
host_web.c # Web server host (mongoose + xterm.js)
host_webview.cpp # Native webview host (WebKit/WebKitGTK)
host_headless.c # Headless playback host
tracker/ # MIDI tracker/step sequencer
tracker_model.c # Data structures (song, pattern, track, cell)
tracker_plugin.c # Plugin system for notation languages
tracker_engine.c # Playback engine with event scheduling
tracker_audio.c # Audio integration with SharedContext
tracker_view.c # View layer (theme, undo, clipboard)
tracker_view_terminal.c # Terminal UI with VT100 rendering
shared/ # Language-agnostic backend (audio, MIDI, Link)
include/ # Public headers
langs/
alda/ # Alda music language (parser, interpreter, backends)
joy/ # Joy language runtime (parser, primitives, MIDI)
tr7/ # TR7 Scheme (R7RS-small + music extensions)
bog/ # Bog language (Prolog-based live coding)
mhs/ # MHS (Micro Haskell with MIDI support)
main.c # Entry point and CLI dispatch
thirdparty/ # External dependencies (lua, libremidi, TinySoundFont, mongoose, xterm.js)
tests/
loki/ # Editor unit tests
alda/ # Alda parser tests
joy/ # Joy parser and MIDI tests
bog/ # Bog parser and runtime tests
tracker/ # Tracker unit tests
shared/ # Shared backend tests

Documentation

See the docs folder for full technical documentation.

Credits

  • Alda - music programming language by Dave Yarwood
  • Joy - concatenative music language from midi-langs
  • TR7 - R7RS-small Scheme interpreter
  • dogalog - Prolog-based live coding inspiration for Bog
  • MicroHs - Small Haskell implementation by Lennart Augustsson
  • kilo by Salvatore Sanfilippo (antirez) - original editor
  • loki - Lua-enhanced fork
  • Csound - sound synthesis system (optional)
  • link - Ableton Link
  • midifile - C++ library for reading/writing Standard MIDI Files
  • libremidi - Modern C++ MIDI 1 / MIDI 2 real-time & file I/O library
  • TinySoundFont - SoundFont2 synthesizer library in a single C/C++ file
  • miniaudio - Audio playback and capture library written in C, in a single source file
  • mongoose - Embedded web server/networking library (optional, for web mode)
  • xterm.js - Terminal emulator for the browser (optional, for web mode)
  • webview - Cross-platform webview library (optional, for native webview mode)
  • liblo - Lightweight OSC implementation (optional, for OSC support)

License

GPL-3

See docs/licenses for dependent licenses.

About

A polyglot editor & REPL for music programming languages

Topics

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Contributors

Languages