Flyback is a patchable synthesiser for .NET 10. One graph can generate both a picture and a sound. The visual path and the audio path share the same module graph and are compiled down to the same flat instruction stream.
The website has screenshots and tutorials, and docs/plugin-guide.md is how to write a plugin. See CHANGELOG.md for what changed in each release, and the releases page for downloads.
Flyback is mostly written by a large language model (LLM). A human directs the work, decides what ships and writes some of it by hand. That is how the project happens to be made, not a requirement on anyone who works on it.
The project holds to the same standards any modern codebase would: a CI gate that restores, compiles and runs the whole test suite on every push and pull request, signed releases built from the same image the gate stops inside, architecture decisions recorded in docs/adr/, and a changelog per release. Nothing merges that the gate has not passed.
Tokens are the one thing the project spends money on, and a donation goes on that budget. Flyback is strictly non-profit: nothing in it is sold, nothing is behind a payment, and nobody is paid out of it. The address is in About and in the site's footer.
CONTRIBUTING.md is how to work in it.
dotnet run --project src/Flyback.Editor.Desktop -c Release- Visual patch editor for building synth graphs by wiring modules together
- One patch generates both a picture and a sound from the same graph
- Per-pixel signal synthesis driven by coordinates and time
- Normalled sockets so common sources such as time and coordinates are present by default
- Feedback and iterative image generation via feedback modules and previous-frame sampling
- Sources, oscillators, patterns, forms, geometry, color, maths, pitch, timing, shaping, time effects, feedback and measurement modules, with presets for each
- Live preview, output settings and live recording in the app shell; deterministic export through the CLI
- MP4, WebM, MOV, MP3, M4A and FLAC through ffmpeg where it is installed, found on
PATHor picked by hand; Motion JPEG AVI and WAV are written by Flyback itself and need nothing - MIDI input support through platform backends (Windows, macOS and Linux)
- CLI tools for rendering, checking, inspecting and bundling patches
- A text language a patch can be written in, saved as and read back from — the same instrument, as source
- A viewer that opens a patch and plays it at once, picture and sound, with no editor and nothing written
- A web viewer that plays a patch in a browser, on the same engine compiled to WebAssembly
- Plugin-based architecture for platform-specific audio/video backends and extensions
- Patch bundles that package the patch with its referenced sample and image files
- Agentic patch authoring through a model-backed assistant that can listen, propose changes and work inside the same patch graph — over any chat-completions endpoint, over Gemini, whose models hear the patch themselves, or through the Claude Code or Codex you are signed in to, with no API key
- A decision model, run here or hosted, that answers small questions about words with a probability: the module list finds a module by what a phrase means (Settings → Decisions), and does without one exactly as before
- Cross-platform publish targets for Windows, macOS and Linux
- Updates itself from signed GitHub releases: downloads a new release in the background and installs it at the next start (Settings → Privacy to turn off)
- Counts how it is used — version, operating system, plugins and sound backend, the rough size of the machine, the kinds of module in a patch that plays, which assistant is asked, how long a run lasts and what it did, and where it crashed — anonymously, in coarse bands, and with nothing about any patch in it (Settings → Privacy to turn off)
The solution builds the web editor, which needs the wasm-tools workload:
dotnet workload install wasm-toolsdotnet publish src/Flyback.Editor.Desktop -c Release -r win-x64 -o artifacts/win-x64
dotnet publish src/Flyback.Cli -c Release -r win-x64 -o artifacts/win-x64
dotnet publish src/Flyback.Viewer.Desktop -c Release -r win-x64 -o artifacts/win-x64This produces a self-contained folder with the app, the CLI and the viewer, plus the shared runtime and plugin folders:
Flyback.exe the app
flyback-cli.exe the command line tool
flyback-viewer.exe the viewer: opens a patch and plays it, and writes nothing
Flyback.Core.dll the patch model and the module API, which plugins are built against
Flyback.Engine.dll the compiler, the language and the renderers
Flyback.Plugins.dll shared plugin host
plugins/ platform backends, and the Picture, Voice, Effects and Mastering modules
Supported publish targets include:
win-x64win-arm64osx-x64osx-arm64linux-x64
macOS bundles are published as Flyback.app beside the output folder, whose Info.plist declares .fbk, .fbkb and .fbks. On Windows and Linux, Settings → Files registers the editor or the viewer to open them, for the current user.
docker build --output artifacts .This restores dependencies, compiles the solution, runs the tests, and publishes the self-contained outputs for the default target set.
To choose a specific set of runtimes:
docker build --build-arg RIDS="win-x64 win-arm64 osx-arm64 osx-x64 linux-x64" --output artifacts .To restore, compile and run the whole test suite without publishing anything, which is what the Build workflow (.github/workflows/ci.yml) does on every push and pull request:
docker build --target gate .To measure how much of the code the tests run, into coverage/ with a table per assembly in coverage/summary.md and the specs' figure beside it rather than in the sum, the same way the weekly Coverage workflow does:
./scripts/coverage.shTo find out whether a change made anything faster or slower, measured against the commit before it on this machine, benchmarks and the web viewer's sound alike, with a change called only where the difference is real:
./scripts/bench-compare.sh HEAD~1 HEAD --filter '*AudioBenchmarks*' --web "Whole band"The Release workflow (.github/workflows/release.yml) builds every platform, zips each one, and publishes them with a SHA256SUMS file and its signature, SHA256SUMS.sig. The app only installs an update when that signature checks out against the public key in src/Flyback.Editor/Updates/release-key.pem. The private key is kept in the repository secret RELEASE_SIGNING_KEY, and the workflow fails before it builds anything if the secret is missing or doesn't match the committed public key. It also fails before building if CHANGELOG.md has no ## X.Y.Z heading for the version being released, since that heading is what the "What's new" window reads. ADR-0088 explains the design. The same key signs the packages of the plugins the preset site starts with and hands out, Figures, Fractals, Easy and Drawings among them, which no release carries (ADR-0141).
Everything but the publishing is release.sh, which runs the same here:
./scripts/release.sh 1.4.0It builds, tests, packs and signs into dist/, with each platform as a folder to run rather than a zip, and publishes nothing. Off GitHub, RELEASE_SIGNING_KEY holds a local test key rather than the release key: release-key.sh takes it from the environment, or makes one and keeps it in the user environment, and only GitHub stops at a key that does not pair with the committed public key or at a missing changelog heading. The preset site's build and a Release run of the site sign with the same variable, and a Debug build checks no keys at all. A build on this machine, release.sh included, embeds the public half of the local key in place of release-key.pem, so a local release installs over local builds, and its runs count as debug in the usage statistics; on GitHub the committed key is embedded.
To make the key pair, with the openssl that comes with Git Bash:
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out flyback-release.keyopenssl pkey -in flyback-release.key -pubout -out src/Flyback.Editor/Updates/release-key.pemCommit the public key. Paste the whole contents of flyback-release.key into the RELEASE_SIGNING_KEY secret, keep a copy somewhere safe such as a password manager, and then delete the file. Never commit it. Installed copies only accept releases signed with the key they were built with, so a lost key means everyone installs the next release by hand.
The CLI runs the same engine without the Avalonia shell, so it is useful for rendering, checking and batch jobs.
flyback-cli render nebula.fbk -o nebula.png --size 1920x1080 --at 2.5
flyback-cli render drone.fbk -o drone.mp4 --seconds 30 --fps 30
flyback-cli render drone.fbk -o drone.avi --seconds 30 --fps 30
flyback-cli render drone.fbk -o drone.mp3 --seconds 30
flyback-cli render drone.fbk -o drone.wav --seconds 30
flyback-cli render drone.fbk -o drone.wav --seconds 30 --loudness
flyback-cli render drone.fbk -o drone.wav --from 30 --seconds 10
flyback-cli render drone.fbk -o drone.mkv --seconds 30 --format mp4 --ffmpeg /opt/bin/ffmpeg
flyback-cli check nebula.fbk
flyback-cli check nebula.fbk --strict
flyback-cli check --preset "Mycelium" --json
flyback-cli compare nebula.fbk nebula-ported.fbk --seconds 30
flyback-cli info nebula.fbk
flyback-cli info --preset "Plasma"
flyback-cli measure nebula.fbk LFO Mixer.out --seconds 10
flyback-cli modules
flyback-cli modules adsr
flyback-cli pack nebula.fbk -o nebula.fbkb
flyback-cli pack --preset "Mycelium" -o mycelium.fbkb
flyback-cli pack-plugin Flyback.Plugins.Ripple.csproj -o ripple.fbkp --key ripple.key
flyback-cli plugin-key -o ripple.key
flyback-cli print nebula.fbk -o nebula.fbks
flyback-cli print nebula.fbk --check
flyback-cli print --preset "Whole band"
flyback-cli render nebula.fbks -o nebula.png
flyback-cli save --preset "Acid" -o acid.fbk
flyback-cli save nebula.fbk -o nebula.fbks
flyback-cli probe --keys
flyback-cli shot --preset "Flyback Theme" -o theme.png --at 30.3 --select "Picture: Scope"
flyback-cli probe --provider all
flyback-cli ask drone.fbk "slower, and warmer"
flyback-cli decide "make the bass slower" --yes-no "Does this ask for an edit?" --json
flyback-cli decide --status
flyback-cli modules --find "a mirror maze of shards"
flyback-cli ask --preset "Plasma" -o plasma.fbkb --model gpt-4.1 "make it blue"
flyback-cli viewer nebula.fbkrender, check, info, measure, print, pack, save and shot read a patch file, or a shipped or plugin preset named with --preset in its place; --presets lists the names, as a JSON array under --json, and a name nobody shipped is refused with the list.
render: renders a still, a clip or a sound file from a patch. The extension picks the format —.png,.avi,.mp4,.webm,.mov,.wav,.mp3,.m4a,.flac— and everything but.png,.aviand.wavis encoded by ffmpeg, taken fromPATHunless--ffmpegnames one.--formatoverrides the extension,--fromstarts a clip or a sound at that second, playing the patch up to it unrecorded so feedback and sequencers are where they would be, and--loudnessprints how loud the sound came out: integrated loudness in LUFS and true peak in dBTP, measured as ITU-R BS.1770 does. The picture is drawn on the GPU through a headless OpenGL context (EGL on Linux, WGL on Windows) where there is one, and on the processor where there is not;--processorasks for the processor, whose picture is exact to the bit, and--gpufails rather than fall back. On the processor the patch runs compiled;--interpretedkeeps it on the interpreter, which writes the same bytes more slowly. A still of a patch whose picture reads the sound, through a Scope, an Analyzer, a Beam or a Meter, plays the sound from the start up to--atfirst.check: compiles the patch and reports issues; for a text patch,--jsongives each complaint's line, column and a stablecode;--triageputs the complaints likeliest to be why the patch is silent or dark first, each with how likely, as the decision model judgesinfo: shows module and wire counts and compile cost;--presetdescribes a shipped preset by name, and--presetslists themmeasure: runs the patch offline for--secondsfrom--fromwith nothing played in, and says what each output carried, to the speakers and to the screen apart: its value when it holds still, otherwise its range, mean and how fast it moves (the hertz it repeats at, the steps a second it jumps at, or its steepest slope), and whether it varies across the picture. Every output of every module, wired or not, unless modules ormodule.outputs are named; two modules sharing a title are numbered in patch order,Oscillator 2pack: packs a patch together with the files it references;--presetpacks a shipped preset with the files it carriespack-plugin: builds a plugin into a.fbkp, signed with the key--keynamesplugin-key: makes the key a plugin's packages are signed with, which every update must be signed with tooshot: draws the editor's window with the patch open into a PNG, with no screen and no sound:--atis the second the picture is of, run up to from a second and a half before;--sizeis the window's, 1440x900 unless given;--selectselects a box or module by name, so the inspector shows it;--canvasshows the canvas for a patch whose text is the document;--cropwrites only the canvas around the modules, with 24 pixels of room;--measuremeasures the outputs from--at, the selected module's or every one's, and pins them before the picture is taken;--assistantopens the assistant's column on the conversation the patch carries, under the provider and model this machine's settings choose;--editoris the program to draw with when none is beside this one, as in a dev build.Flyback --shot, beside it, does the drawing (ADR-0166)viewer: startsflyback-viewerwith everything after the word, soflyback-cli viewer --helpis the viewer's own helpprint: writes the patch out as text in the language, and can check that the text builds back to the same program;--presetprints a shipped preset by name, and--presetslists themsave: saves the patch as the file--out's extension names:.fbk,.fbksor.fbkb;--presetsaves a shipped preset as a patch to open and change. A preset that plays recordings it carries saved as.fbkor.fbksis written, and exits1saying.fbkbwould take them alongcompare: plays two patches side by side for--secondsat--sizeand says whether they are the same instrument, sample for sample and pixel for pixel, and where they first part when they are not; it exits1when they differmodules: lists the modules this build has, and which plugin defines each; given one by type id or name, it describes that module: each socket's default and range, where|>lands, what it carries besides its sockets and what it does;--find "<phrase>"lists the modules a phrase describes, likeliest first: those whose name or words spell it, then the rest as the decision model ranks themprobe: asks an assistant which models it has and what each one acceptsask: asks the assistant the editor is set to about a patch, and writes its answer back into the file with the conversation, where the nextaskand the editor carry it on: inside a.fbkb, beside a.fbkor.fbks. A file that does not exist yet starts empty, and--outwrites elsewhere. With no message it reads one from standard input, or asks line by line at a terminal.--provider,--modeland--set key=valuechange the settings for one run,--freshstarts a new conversation,--seenkeeps every picture it looked at and sound it heard,--briefingprints what it is handed,--expandprints the message written out in full, as the editor's Expand does, and builds and writes nothing, and--jsonwrites one object a line, each tool call and its arguments included. Each turn ends with what it cost: its requests, the tokens they sent, had cached and wrote, and any time spent waiting out a rate limit. It exits2when a turn failsdecide: asks the decision model the settings choose typed questions about some text, given as an argument or-for standard input:--yes-no,--choicewith--option label=description,--scorewith--level, or a file of them in the System One format with--ask. Each answer comes with its probability, and--jsonwrites the format's own answer.--modelasks another,--statuslists them and what each lacks, and--preparedownloads what one needs after a yes, which--yesgives for a script.--set key=valuesets the model up for one run,--for modules,issuesorturnsfor that use alone (the module search, the complaints' order, reading an assistant turn), and--savekeeps it and--modelas the chosen onestills: draws a still of every preset into--out, with theindex.jsonthe galleries show them by in place of drawing them (ADR-0163);./scripts/stills.sh <folder>lays out the plugins a build ships first, and every release and site build runs it
check exits with:
0: no errors1: patch errors2: the job could not run
--strict makes a warning fail as well. check, compare, info, measure, pack, modules,
probe, ask and decide each take --json, which writes the same answer as a document instead of
as prose.
info says what a patch requires and modules says what is installed to meet
it, which is the pair to reach for when a patch reports that it did not load
completely.
probe and ask are the commands that reach off the machine, and the provider bills both. probe asks a provider's endpoint what it
offers and records the answer in the settings file both programs read, so the app's model box
fills itself in without being told. It takes minutes and the provider bills for it, so it says
what that means and waits for a yes; --yes answers for a script, which has nobody to ask.
--keys says where each key would come from and asks nothing of anybody, and --dry-run prints
what was found and leaves the settings alone.
Settings → Assistant runs the same survey from a button, about the one model the form is set to. It goes out with the provider, the form and the key as they stand on the settings window rather than with what was last saved, which is how a model, a key or an endpoint can be tried before any of them is kept. What it finds is written over that model's line in the list this command left; filling the list is the command's job, since only it asks about every model.
The CLI answers the [suggest] directive, which is the protocol dotnet-suggest
speaks:
flyback-cli "[suggest:13]" "flyback-cli pr" # print, probeSo installing that tool and adding its shim to your shell gives completion of commands, options and file arguments.
Flyback uses three file types:
.fbk: a patch document.fbkb: a patch bundle containing the patch plus all referenced files.fbks: the patch written as text, in the language
flyback-cli pack nebula.fbk -o nebula.fbkbA bundle is a zip archive with the patch and any sample/picture files it uses. It is readable by standard tools and does not require unpacking to render.
The app saves and opens all three. A .fbks is a copy rather than a document: it keeps the instrument exactly — the text builds back to the same program, op for op — but not the groups or the canvas layout, so saving one leaves the open document as it was.
flyback-viewer opens a patch and plays it, picture and sound, with no editor around it. It writes nothing — no settings, layout, recovery file or statistics — and takes its defaults from the same settings.json the editor saves, every one of them overridable on the command line.
flyback-viewer nebula.fbk
flyback-viewer --preset "No Sense Dub" --size 1080p --mute
flyback-viewer drone.fbk --from 30 --cpu --for 10
flyback-viewer --preset "Whole band" --hidden --for 10
flyback-viewer --helpHover the top middle of the window for the transport: pause, rewind, the seek bar, loop and sound, with the knobs at the bottom (--transport bottom swaps them); click the picture to play or pause, double-click it for full screen. A patch with no picture, or a run with --no-video, opens as just the transport and its knobs. A patch that does not say its length has no seek bar or loop, and plays on. Space, or Ctrl+P, pauses. --background opens the window without taking focus, and --hidden opens none at all.
A patch made to be played is played here too: the computer's keys are notes wherever the patch reads them, a MIDI In hears the device it names, and a panel knob bound to a MIDI controller follows it. There is no knob panel, so a knob with no controller stays where the patch left it.
src/Flyback.Viewer.Web plays a patch in a browser: the engine and the module plugins compiled to WebAssembly, the picture drawn on WebGL 2 by the desktop's own GPU renderer. It opens a shipped preset or a .fbk, .fbkb or .fbks dropped on it, and plays it; nothing else. The sound runs as JavaScript written from the patch, the heaviest showcase presets with a fifth of real time to spare; a patch whose sound still cannot keep up plays its picture alone and says how slow (ADR-0160).
dotnet publish src/Flyback.Viewer.Web -c Release -p:RunAOTCompilation=true -o artifacts/webThe preset site serves it at /viewer/, and each preset's page has a Play in your browser button that opens it there. On its own, serve artifacts/web/wwwroot from any static server and open /viewer/; ?preset=Nebula, ?file=<url> (with &name= where the URL does not end in the file's name, &title= for the name shown and &back= for the page to return to), ?size=1280x720, ?loop and ?mute pick what opens and how. It offers no presets of its own: the presets page, on the preset site and on GitHub Pages, lists the shipped ones from the build's stills (ADR-0163) and sends one here, and the header leads back to &back=, or to the presets page. Space plays and pauses, left and right seek five seconds, up and down set the volume, M mutes and F goes full screen. A patch's panel knobs stand beside the picture, dragged up or right to turn and double-clicked to put back; on a phone they sit in a sheet under it, with keys on the screen for a patch played on the keyboard. A patch played on the computer keyboard takes its keys as the editor does, as a piano or in the patch's scale, with PageUp and PageDown moving the octave; its note keys win over the shortcuts. The AOT switch needs dotnet workload install wasm-tools; without it the page around the sound runs interpreted, at under half the speed. window.flyback drives the page from a script: presets(), open, play, pause, seek, size(width, height), volume(level), knobs(), turn(knob, value), strike(note, down), release(), panel(pixels), the phone's sheet's height, tab(name), its tab, status(), still(seconds), the frame as a PNG at the patch's size, and edit(), Edit it: back to the web editor that sent the patch, or the patch opened in the web editor.
The sound runs without a page under Node, which the workload brings; --knob fog=0 turns a panel knob first, --note 60:0.1:0.6 holds a note on the computer keyboard from one second to another, and --presets lists the presets:
node artifacts/web/hear.mjs --preset "Sidebands" --seconds 2 --out sidebands.f32src/Flyback.Editor.Web is the editor itself in a browser: the same window under Avalonia.Browser, the picture drawn on a canvas of its own by the desktop's GPU renderer (ADR-0162). It keeps nothing between visits but the two settings its toolbar's gear offers, drag to pan and compact modules, which the browser's local storage holds; it carries every plugin that makes modules, as the viewer does, and plays its sound in the web viewer's worker. The Flyback mark first on its toolbar goes back to the site, and the browser asks before the page is left with an edit on it. The presets page's Edit opens a preset in it, a shipped one by ?preset=<name> and a shared one by ?file=<url> (with &name= and &title= as the viewer takes them). The preset site's Worker serves it at /editor/; worker/build-assets.sh --no-editor builds the site without it, and worker/dev.sh serves what it built:
worker/dev.shThe gallery shows the build's stills from /stills/ where the site has them (ADR-0163), and draws each preset on the page's one thread where it does not; worker/build-assets.sh draws them.
window.flyback drives the page from a script: state() says which preset is open, how many modules and wires it has, which renderer draws the picture and at what rate, and the last thing the editor said; preset(name) opens a shipped preset, as /editor/?preset=<name> does on load; openUrl(url, fileName, title) fetches a shared one and opens it, as ?file= does, answering null or why it could not; text() reads the open patch in the language; apply(text) applies text as the text view's Apply does, one edit that one undo takes back, and answers null or what is wrong with it; sound() says how the sound is going; view() presses View it, which opens the patch as it stands in the web viewer in a tab of its own, and answers the viewer's address, or null where the browser refused the tab.
worker/build-assets.sh builds the website as the Worker serves it, the viewer, the editor and the stills built in, and worker/dev.sh serves it; --no-aot makes it quicker.
src/Flyback.Editor.Android is the editor as an Android app, with the module plugins linked in and its files in the app's private folder (ADR-0184). It plays and listens through the device's own speaker and microphone; MIDI is not there yet. It needs the android workload on a .NET SDK from Microsoft, a JDK and the Android SDK, so Flyback.slnx lists it without building it, the gate never needs them, and it is built by path:
dotnet build src/Flyback.Editor.Android -t:RunA patch is a graph, but during rendering it is compiled into a flat straight-line program over registers. Unused sections are not compiled, and the inner loop is designed to be cheap and predictable.
The project is split roughly as:
src/
Flyback.Editor.Desktop the editor on the desktop
Flyback.Editor the editor itself: its window, canvas and regions
Flyback.Cli command line tool
Flyback.Core patch model, module API and the built-in modules
Flyback.Engine compiler, text language, renderers and file formats
Flyback.Plugins plugin host and built-in plugin logic
Flyback.Ui the preview, sound device and look the app and the viewer share
Flyback.Viewer.Desktop the viewer: opens a patch and plays it
Flyback.Viewer.Web the web viewer: the same, in a browser
Flyback.Editor.Web the web editor: the editor in a browser
Flyback.Site flyback-site: reads what is submitted to the preset site, for its workflows
worker/ the preset site as a Cloudflare Worker (deploy/cloudflare/README.md)
tests/
Flyback.Core.Tests core engine tests
Flyback.Specs feature requirements as Gherkin scenarios
Flyback.Core.Benchmarks engine benchmarks
Flyback.Editor.Tests editor tests, headless Avalonia
Flyback.Viewer.Desktop.Tests viewer window tests
Flyback.Editor.Desktop.Tests desktop shell tests
Flyback.Ui.Tests shared control, audio and MIDI tests
Flyback.Ui.Testing the headless test harness (not a test project)
Flyback.Cli.Tests command line tests
Flyback.Site.Tests flyback-site's checks and its talk with the site
Flyback.Plugins.Tests plugin and runtime behavior tests
Flyback.Plugins.OpenAi.Tests chat-completions session tests
Flyback.Plugins.Gemini.Tests generateContent session tests
Flyback.Plugins.ClaudeCode.Tests Claude Code session tests
Flyback.Plugins.Codex.Tests Codex command line and output tests
Flyback.Plugins.Programs.Tests what the program-backed assistants share
docs/engineering-guide.md is how the codebase is put together, how its code is written and how its tests are written. See the docs/adr folder for the architecture decisions behind it.
docs/language.md is the reference for the text language — a
second way to author a patch, decided in
ADR-0065. The app
saves and opens it, the CLI reads it wherever it reads a patch and writes one with print, and the
assistant writes a whole patch in one call with it.