Changelog

All notable changes to Freally Oscillate are recorded here. The format follows Keep a Changelog, and the version numbers follow the ladder published on the docs site.

⚠ There are no releases yet, and no downloads before v1.0.0. Rung tags before then exist so the ladder is verifiable; their artifacts are built and verified but not published.

[1.0.0-rc] — 2026-09-25

The release-candidate review of 24–25 September 2026 is at the top: one new feature, the defects the review found, and what now sounds different in a saved project. Below it come the 19 September fixes, then the beta's machinery (Phase 11), then faces, voices, turns and credits (Phase 9.5), then the room (Phase 9), then the writing tools (Phase 8), Oscillate's own devices (Phase 7), plugin hosting (Phase 6), the editors (Phase 5) and the arrangement view and the project underneath it (Phases 3 and 4, the whole v0.2.0 rung).

Added — a hardware MIDI keyboard plays and records

⛔⛔ Nothing in Oscillate had ever opened a MIDI port. The driver code was built and tested and nothing called it, while the README, the roadmap and the first-run story all said "play the MIDI keyboard → sound" and Settings ▸ MIDI said the feature was not there yet. It is there now.

Two on-screen keyboard defects, fixed on the way because both kinds of keyboard now share one path:

Fixed — the release-candidate review

▶ /simplify, /code-review and /security-review over the whole tree once more, then about a dozen fix passes. Hundreds of defects were fixed, each with a test that was watched failing first wherever the tooling allowed. The worst are first. Changed, further down, lists what now sounds or behaves differently in a project you already have.

The worst of it

Security

Playback and the engine

Export

Recording

Plugins

The editors

Devices and instruments

Rooms and sessions

Windows and settings

The updater and release tooling

Found by reviewing the fixes themselves

⚠ Every fix above was reviewed again after it landed. Those reviews found real regressions in the fixes, and several older defects the first pass had missed. All of them are fixed here.

Changed — what sounds different in a saved project

⚠ A project saved before this build can sound different when you open it. Each of these is a correction, and several move levels by a few decibels, so check a mix you care about.

Still open

Added — a pattern can finally reach the timeline

⛔⛔ **ClipContent::Midi has been a live reference since TASK-037A — "editing the source changes every placement of it, which is what a pattern is" — and nothing on the page could make a second placement of anything.** ArrangementEdit::PlacePattern carried no source and its handler called MidiSource::empty unconditionally, so every placement was a brand new empty pattern. A producer could build a pattern in the rack and had no way at all to put it in the song. The model could do it; a producer could not, which is the distinction this project has already paid for once at the operation log.

Fixed — New pattern looked like it did nothing

⛔ Owner, 2026-09-19. The edit landed every time. refreshPatternRack keeps the open pattern when it still exists, and after an add the previous one always does — so the selection never moved, the body of the panel never changed, and the only evidence of success was one more entry in a closed dropdown.

Changed — the synth crate caught up with its upstream

▶ synth/ is imported from Freally MIDI Master, never edited here (TASK-077), and tests/the_synth_is_not_forked.rs compares the two byte for byte on any machine that has both checked out. It had gone red: this copy was one commit behind.

Thirteen gates the upstream did not have. The tone control shipped with no test in either product, and "every field at its default is exactly bypass" is load-bearing for 372,600 sounds there and 1,648 here — it was resting on a comment. Each was proven to fail on the defect it names. The ones that earn their place:

⚠ The tone control is not reachable from Oscillate, and that is the honest state rather than an oversight. Nothing here writes patch.fx.eq and the instrument device exposes no control for it — it is Freally MIDI Master's front-panel feature, which arrived because the crate arrives whole. So it is flat for ever here, which is to say bypassed for ever. The gates above exist so that the day Oscillate does expose it, the promise it was imported on is already being held.

⛔ And two defects found by writing them, both fixed upstream and imported back. SynthEngine::set_sample_rate rebuilt the voices, the delay and the reverb but not the tone control — the rebuild was guarded on the parameters changing and a sample rate is not one of them, so a host moving 48 kHz → 96 kHz kept coefficients built for 48 kHz and put every band an octave low, a 6 kHz shelf acting at 3 kHz. ▶ Live in the other product, not theoretical: its plugin calls set_sample_rate on initialize, so a project opened at another rate would have shipped exactly that.

⚠ Looking at the guard for that fix turned up a second one: it recorded the clamped parameters and compared them against the unclamped patch, so any patch carrying one out-of-range number never matched and rebuilt every block — three transcendentals per band per channel, on the audio thread, for the life of the note. That is the cost the guard exists to avoid, paid in full.

▶ Both are fixed in ff99972c and this crate carries the fix. ⛔ Neither was patched here — TASK-077 is exactly that rule, and a defect in an imported crate is reported to its upstream and comes back through a re-import.

⚠ Oscillate could not reach either: prepare reconstructs the engine rather than mutating its rate, nothing here calls set_sample_rate on a synth at all, and nothing writes patch.fx.eq, so the stage is skipped outright. The test is kept anyway — the day this product exposes the control, nobody will remember that the crate once got this wrong.

Fixed — a gate that measured the machine

Fixed — the two reviews, and the thirty defects they found

▶ /code-review and /security-review over the whole tree, then a wiring review of every control. The security review found nothing exploitable. The other two found thirty, and the pattern in them is one thing said three ways: a mechanism that works, and nothing calling it.

Audio

Controls that reached nothing

Things that would have failed on somebody else's machine

Honest about what is not built

Changed

Removed

Added — five gates, each proven to fail on the defect it was written for

▶ The reviews found the defects; these are what find the next ones.

Added — nine things the piano roll could not do

▶ The gap between Oscillate's roll and the one a producer already knows. Nine of them, and none is a setting: each is a gesture that either exists or does not.

Added — three more the owner asked for while these were being built

Changed — the fourteen instruments say what they are

▶ Owner, 2026-09-16: "change all the names for my audio/midi effects plugins to regular names, just not the same as Ableton/FL Studio, the same with anything else across my app that has to do with 'Kiln' or anything like that."

The sixty-one effects were renamed last rung; the instruments were left as forge-themed codenames and the open question was whether those should follow. They have:

was is now
Crucible Modelled Synth
Flint One-Shot Sampler
Armory Multisampler
Bench Pad Grid
Hammer Percussion Sampler
Skin Modelled Drums
Bronze Modelled Cymbals
Sideband FM Percussion
Quiver Layer Rack
Outpost Hardware Instrument
Ember Track Replay
Filings Granular Player
Muster Section Player
Salvage Folder Kit

⚠ Three of those are still greyed and say why, and they say it under their new names: Hardware Instrument plays external hardware over a MIDI port and Oscillate has no MIDI port yet; Track Replay needs this track's own recorded output; Folder Kit needs the producer's sample library, which is the File Explorer's job.

⚠ One effect moved too. Strum is now Chord Strummer, because Live 12 ships a MIDI transformation called exactly Strum and the instruction was explicit that no name may be one of theirs.

⛔ Nothing a project file stores changed. Only the displayed name moved; every type_id — crucible, flint, hammer and the rest — is exactly what it was, because that string is the key a saved .oscillate carries. A producer opening last week's project finds every device where they left it, with its settings and its automation. ⚠ A codename surviving in a type_id is therefore not an oversight to tidy up later; tidying it up is the defect, and every_device_name_says_what_it_is.rs pins all fifteen of them.

⚠ "Kiln" was deliberately left alone, and it is worth saying where it is: not a device at all, but "Kiln Gong", one preset among the 1,648 in the imported factory library. That library is compared byte for byte against Freally MIDI Master's copy, so renaming inside it forks an import and breaks the promise that one PresetId renders the same sound in both products.

Added — crash recovery reaches a session that was never saved

⛔⛔ A journal is named after the project it belongs to, and a session that was never saved has no project file to reopen — so the next launch minted a new project with a new id and never looked for it. The work was on the disk the whole time and nothing offered it back, which is exactly the producer who has been working since lunch without saving once.

✅ Oscillate now snapshots an unsaved session's starting point into its journal on the first edit, and a launch that has not yet started work of its own is offered the newest recoverable orphan. ⚠ The dialog says which kind of offer it is: an orphan has no saved file, so the ordinary reassurance — "your saved project has not been changed" — would be describing a file that does not exist.

⛔ A journal without a snapshot of its own is never offered, because there is no saved project to replay it onto and the result would be a project that never existed. An offer that has already been accepted or discarded does not come back.

Known limit — a long session makes a large project file

⚠ The operation log lives inside the .oscillate, on purpose: it is what lets undo survive a save and a reload, and both Session Rewind and the Session Timelapse are derived from it. Nothing trims it, so the file grows with the session — measured at 29 MB after 100,000 operations and 58 MB after 200,000, roughly an eight-hour session, written in 67 ms.

▶ The growth is linear, not compounding, and the ceiling is documented rather than capped: trimming the log would remove undo-after-reload, Session Rewind and the Timelapse together. ⛔ Owner's decision, 2026-09-15.

Changed — the browser narrows at the source

⚠ Opening a shelf in the browser no longer builds the whole roster first. Each of the five shelves — Instruments and the four Factory Expansions — was listed by cloning all 1,648 instruments and then discarding the four fifths that belong to the other shelves, on every listing. It now walks only the shelf asked for: 0.140 ms → 0.001 ms measured in release for Instruments, and at least 1,298 string allocations that no longer happen each time.

⛔ This is the same defect Freally MIDI Master fixed upstream the same day, and its reasoning came across with the import: a list narrowed after the fact is built in full on "the thread a DAW draws its window from".

Fixed — what the Phase 11 reviews found

⛔ The macOS splash held the application back for seven seconds on every launch. The native window has no page in it, so nothing ever called splash_finished — and the 4-second watchdog written for "a page that threw" had silently become macOS's normal path. The native splash now hands over on its own animation's length, and a gate reads the macOS branch to make sure the page watchdog never returns there.

⛔ And it was playing a different jump from Windows and Linux. The Rust version had a symmetric parabola where the stylesheet has 51 − 17u − 78·sin(πu), no scaling at all, the sign of the vertical offset inverted, and a window 62 % of the display instead of all of it — so the figure dipped instead of leaping, never came toward you, and was cropped against an invisible box in the middle of the desktop. All four are corrected, and splash_macos.rs now parses window.css's own key frames and fails if the two implementations disagree at any of the twenty-one samples.

⛔ The docs site stayed in the previous language when you chose English again. Every translatable node ships its English in the markup, and the switcher read that as "there is nothing to do for English" — so picking Français and then English left a French page under lang="en". From Arabic the layout un-mirrored while the text stayed Arabic. English is now a catalogue like any other, read off the page once before anything overwrites it.

⛔ A failed update download threw away the offer you had just reviewed, so retrying meant a second network request. ⚠ The Updates panel also announced "Nothing was contacted." as a live result on every fresh install, underneath a switch that already said "Off." — a sentence about something nobody had done.

⚠ Also: --verify without --assets reported the § F-32 catastrophe over a sound manifest; a deny.toml exception could silently waive the GPL denial; a | inside a code span added a column to a published table; and three gates were reading main.rs with a search a comment could answer, an indented method could satisfy, and — in one case — an anti-vacuity control that never reached the branch it was guarding.

⛔ Feature freeze — 11 September 2026

The feature list is closed until v1.0.0 stable. From here only defects, hardening, optimisation, documentation and localisation land. An idea that arrives during the beta gets a version number after 1.0 rather than a place in it — a beta whose feature set moves is not a beta, and its bug reports stop meaning anything.

⚠ Three things specified before the line are still being finished and are carried rather than cut: Session Timelapse, MixSir's stem delivery, and dead-peer reporting in the room. Carrying an unfinished feature is not the same as adding a new one.

Added — it can update itself, and there is a manual

Oscillate can update itself, and it asks first. Settings ▸ Updates shows the version you are running, a Check for updates button, the exact address that would be contacted, and the last time one actually was. ⛔ Nothing happens until you press something. Checking at launch is a switch and it is off; with it off nothing is contacted, the panel shows no result at all, and Last checked stays Never. ⛔ It never draws "you are up to date" over a check that did not happen — those are different facts and only one of them would be true.

⚠ What the check sends: nothing. It asks for one static file and carries no information about you, this machine or your projects. When an update is found you are shown the version, the date, the changelog and the size, and you choose. ⛔ Every update is signed, and one whose signature does not verify is refused with an error rather than installed — there is no path through that check.

⛔ Two sentences were corrected because of it, and both were already shipping. Settings said "no account, no telemetry, no automatic connections of any kind" in eighteen languages, and the agreement said the same. A launch check you switched on is an automatic connection, so both now say Oscillate makes no connection you did not ask for and name the three occasions on which it uses the network at all.

A documentation site, in eighteen languages. Home, the full manual, the changelog, and the version ladder — with no CDN, no analytics, no tracker, no embedded font and no third-party script anywhere on it, which is the same promise the application makes and is enforced by a build gate rather than left as a claim. ⛔ No downloads on it, and none until v1.0.0. The download page is built, says plainly that there is nothing there yet, and is not linked.

Every documented procedure is illustrated by a real screenshot of the real build, with the thing you are told to press boxed in red and numbered — taken by a headless pass against the built front end, never drawn by hand, and checked on every run so a picture of a build that no longer exists cannot survive.

A native splash on macOS. It is the same figure and the same jump, drawn by AppKit instead of a second web view — which let the one private Apple API this product used go. ⚠ A private call is a call Apple can remove in a point release; had that happened during the beta, every Mac would have painted an opaque rectangle over the desktop. Windows and Linux are unchanged.

Fixed

⛔ **The in-app Open a pre-filled GitHub issue button went nowhere.** It pointed at the repository's previous owner, so every producer who used it — the one path the whole crash reporter exists to reach — got a 404, after writing the report. The plugin host introduced itself to CLAP plugins with the same dead address.

⚠ v0.4.5 was missing from the published version ladder, and from the private one, although the phase behind it has been built since 2026-09-04. The two ladders are now generated from one file and compared on every build.

Added — you can see them, hear them, hand them a track, and print who wrote what

⛔⛔ Voice, camera and screen share, in the room panel. Press Talk and the others hear you; press Camera and they see you. One person at a time can share a screen. ⚠ Your microphone and camera are off until you press something, and Oscillate refuses them by default — the application holds its own consent and the webview asks the operating system only after you have said yes. Closing the room or turning a control off stops the track, which is what turns the hardware light out.

⚠ Nothing about a camera or a microphone is recorded, buffered to disk, or sent anywhere but to the peers in your room. There is no server in the path. ⚠ A shared screen carries video only — whatever is playing on your desktop, including music you did not write, stays on your machine.

⛔ Screen share is Windows and Linux. macOS has no display-capture route out of an embedded webview that does not need private API in a notarised binary, so the button is absent there and says why rather than failing when pressed.

⛔⛔ Takeover — hand one track to somebody and take it back with one key. While they hold it they are the only writer for it, including you, whose own edits to that track are refused until you take it back. ⚠ And the hold is confining as well as exclusive: they may write that track and nothing else. Handing over a bassline is not handing over the session.

⚠ Taking it back is a stamp, not a switch. An edit they made before they could have known is still theirs and still lands; one made after does not. Refusing everything in flight would punish somebody for the speed of light.

⛔ A hold binds only the machine that gave it, and lasts exactly as long as the connection. Somebody who disconnects holds nothing — otherwise leaving the room would lock you out of your own track for the rest of the session.

⛔⛔ Kumite — a beat battle where each turn owns its own bars. Every turn gets a disjoint stretch of the timeline, fixed when the battle starts, so whose turn it is and inside their range are the same question. ⚠ The countdown on screen gates nothing, and that is deliberate: a check that consulted a wall clock would let two computers reach opposite answers about the same edit and let the two projects drift apart silently. Stalling the clock buys nothing, and there is no turn to claim or refuse to yield.

⚠ Bar windows cover clip placement. Patterns, tracks, automation, CC and drum lanes span the whole timeline by construction and stay under last-writer-wins; the panel says so rather than letting you discover it.

⛔⛔ The Split Sheet is derived, and work you took back is not counted. An undo is a normal operation in the log, so a naive count would credit a producer for eight notes they undid — and credit them twice, once for the work and once for the undo. Redo is resolved too: work undone and then redone is credited again. ⚠ Per author: operations, notes, clips, devices and the tracks they touched, with the shares summing to the total they are fractions of.

⚠ A derived fact cannot be typed in. Contribution carries no text at all — only ids, integers and timestamps — and it has no deserialiser, so it can never arrive from a file, a peer or the page. It can only ever be computed. Names are resolved at the edge, from ids, and an author this machine has no channel for keeps their id rather than borrowing somebody else's handle.

⚠ Export to Markdown, CSV and PDF is not built yet, and neither is the editable percentage column. What ships is the derivation, on screen.

Changed — a peer cannot speak as somebody else

⛔⛔ Identity is the connection, not a field in the message. Until this release an operation's author was a string the sender chose: the old check refused only an operation claiming to be you, and did nothing about a peer stamping a third party's identity on one. That would have made a handed-over track, a battle turn and a credit line forgeable in one line each.

▶ A room has no accounts and nothing could ever sign a message, so Oscillate uses the one thing it can know: the connection the bytes physically arrived on. The first message down a connection introduces who is speaking, and after that neither half of that pair may change — a connection cannot rename itself, and a fresh connection cannot claim somebody Oscillate is already hearing from. ⚠ An identity never seen before is believed the first time it speaks; with no accounts there is nothing better available, and pretending otherwise would be worse than the limit.

⛔ A refusal is no longer a strike. Three refusals disconnect a peer, and every permission refusal used to count — so somebody whose countdown said the buzzer had not gone yet, or whose grant was revoked a moment ago, collected strikes for behaving exactly as designed. Only a malformed or forged message counts now, and the distinction is a compile error rather than a comment.

Fixed — five dead ends in the room panel

⛔⛔ A collapsed region can be opened again. The room panel hides itself below 1180 CSS pixels and its titlebar button was disabled — but a 1280-pixel laptop at the display scaling Windows ships with reports about 853, so on an ordinary machine there was no way to open a room at all. Collapsing is now a default you can overrule. Cramped and usable beats correct and unreachable.

⛔ There is a way back from a half-made room. A producer who pressed Create and then wanted to join a friend's room instead was stranded: no join field, no Leave, and nothing that returned the panel to the start. Restarting the application was the only recovery.

⛔ A named failure now has a button under it. When a connection gathered fine and then never opened, the panel drew an accurate explanation of a permanent failure with nothing to press — a spinner with extra steps.

⛔ An empty code is a refusal, not a code. With no backend the panel drew an empty box with a live Copy button and a treat this like a password warning beside it.

⚠ "Receiving the project…" no longer sits over an idle panel, and the second and third joiner get an empty paste box instead of the last one's text.

Added — one key turns the session into something you can post

⛔⛔ Session cards. Press Ctrl/Cmd+Shift+C, or File ▸ Session card…, and Oscillate draws a 1200×630 image of the session — tempo, key, how many tracks and clips, how long it is, the shape of the arrangement and the Oscillate mark. Save it or copy it and post it.

⛔ The room code is off by default, and its toggle carries the warning next to it rather than in a tooltip: treat it like a password — anybody who sees it can join. With no room open the toggle says so instead of sitting there greyed out with no explanation.

⛔ A card cannot leak your machine, and that is a property of the design rather than a filter. The facts the card is built from have nowhere to put text — no project name, no track name, no sample name, no path. There is no sanitiser to get wrong because there is nothing to sanitise.

⚠ Save is the button that always works. Whether this platform lets Oscillate put an image on the clipboard is not something we can promise, so Copy says plainly when it cannot rather than looking like it worked.

⚠ The shape is drawn from the whole arrangement, not the part on screen — so the same session makes the same card twice.

Added — your session opens without Oscillate

⛔⛔ File ▸ Export companion folder… turns the session into a folder of ordinary files: a full-length stem per track, a MIDI file per part, all the parts in one type-1 .mid, a tempo map and a README that explains the lot. Project files are hostage-taking everywhere else. A collaborator without Oscillate drops the stems at bar 1 and they line up.

▶ Every stem is the whole song long, so nothing has to be nudged into place.

⛔ A muted part is rendered and named, never quietly dropped. Handing somebody a correctly-named, correctly-lengthed silent file is the worst way to omit a track, because it looks right — so Oscillate renders it anyway and lists it under MUTED PARTS in the README, for you to mute again if you want what was playing.

⛔ The README says what is NOT in the folder, too. Oscillate renders these stems without any VST 3 or CLAP that was on the session — Oscillate's own devices are included, plug-ins are not — and a collaborator who gets a part that does not sound right deserves to be told why rather than left guessing. That limit is written into the file every export produces.

⚠ A session longer than ten minutes cannot be exported this way yet, and says so rather than producing something truncated.

Added — the master tells you what each service will do to it

⛔⛔ Honest master metering. Press Loudness in the mixer and Oscillate shows the master's integrated, short-term and momentary LUFS, its loudness range, and its true peak in dBTP — then, beside every service's published target, what that service would actually do to this master. A master at −9.16 LUFS against Spotify's −14 is turned down 4.84 dB, and the panel says so in those words.

▶ Everybody has LUFS. Nobody tells you what it means for where the record is going. That table is the point of the feature.

⛔ True peak is not sample peak, and until now Oscillate only had the second one. A converter reconstructs a waveform through the samples, and that waveform can rise above the highest one — so a limiter set by sample peak clips on playback while the meter reads −0.1. Oscillate now measures at 4× oversampling, as ITU-R BS.1770-4 requires, and reports it in dBTP so the unit itself tells you which number you are looking at.

⛔ A target is not a rule, and every row says which it is. Most of these are playback normalisation a listener can switch off; two — EBU R 128 and ATSC A/85 — are real delivery specifications. The note is part of the row, not a tooltip you have to go looking for. ⚠ The table lives in data/loudness-targets.json with the date it was last checked, because these change.

⛔ Two decimals, always, and a figure nobody measured is a dash rather than a number. A meter that rounds in a way that flatters is a meter that lies politely, and "−200.00 LUFS" is not a measurement of anything.

⚠ Off by default, because it costs processor time, and the button says so.

Added — the mixer can tell you which two tracks are fighting

⛔⛔ The Masking Radar. Press Radar in the mixer and Oscillate lists the pairs of tracks that occupy the same part of the spectrum, worst first, naming the band they meet in. Click a row and it takes you to one of them.

▶ It is arithmetic, and the panel says so in as many words. Every track's audio is reduced to thirty-one third-octave band energies; two tracks score high in a band when their energies are alike and both are actually present, and a collision is a pair that has held that for a second and a half. iZotope charges for a masking meter and leans on machine learning; this is an overlap calculation that ships in the box, and the tooltip tells you that rather than implying otherwise.

⛔ Off by default, because it costs processor time, and the button says so before you press it. Switching it off frees the analysers and stops the audio thread capturing — off really is off.

⛔ **A pair Oscillate has not heard is drawn as not heard yet, never as clear.** Two tracks that have produced no audio are not two tracks that came back clean, and a panel that reported the second when it meant the first would be making a claim nobody measured.

⚠ What is not there yet, said plainly. There is no Suggest: the radar tells you where the overlap is and does not propose an EQ move, and clicking a row selects one of the two tracks rather than opening both. And a session with more than thirty-two tracks is watched only that far — the panel says so rather than drawing the rest as clear.

Added — the build now refuses a feature the app cannot reach

⛔⛔ check-commands.mjs: every #[tauri::command] must be in generate_handler!. This is a gate against the defect this project has shipped more often than any other — a mechanism that outlived its only caller, seventeen recorded instances. A command missing from that roster compiles, passes clippy, passes cargo test, and does not exist to the page: no error, no warning, no failing test, just a feature that silently is not there in the shipped app. An entire phase was once unreachable this way.

▸ It fails in both directions — a command nobody registered, and a name left in the roster after its function was renamed away. ▸ It cannot match its own text. The obvious version of this check greps for the command's name and so matches the fn declaration itself, passing over the very thing it was written to catch; this compares two sets, so there is no substring search to fool. ▸ And it refuses to report a clean sweep of nothing: if either side parses to fewer than a hundred entries the parse has broken, and the gate fails rather than saying "clean". A gate that passes when it found nothing is worse than no gate, because it is believed.

⚠ It runs in npm run ci:local and in CI's supply-chain job. It was watched going red in both directions before it was committed, and the tree it guards is currently clean at 163 declared, 163 registered.

Added — your words are on the timeline, and the take remembers which ones

⛔⛔ The Lyric Lane. Press the lyrics button in the arrangement toolbar and a strip opens under the ruler with your words laid out along the song. The line you are on is drawn in the accent colour and the syllable the playhead is over lights up as it passes — read from the same tick the playhead is drawn from, sixty times a second, never from a timer. A prompter that lags the transport is the one thing this feature must never do.

▸ Import .txt or .lrc. A timed .lrc lands on the bars it was sung on — its [mm:ss.xx] stamps are converted through your project's tempo map, so a song that slows down at bar nine still lines up. Untimed text spreads across the section and you nudge each line into place, a bar at a time, one press of undo each. ⚠ The file's words are copied into your project; the file itself is never referenced again, so a sheet you dragged out of a downloads folder does not empty itself next week.

▸ A take remembers the words it was sung to. When a recording lands it records which sheet was on screen, so a comp built from four takes carries the right verse in every region. ⛔ And rewriting a line does not rewrite history: the first edit after a take has landed keeps the old words for that take and gives you a fresh sheet to work in. Deleting a sheet that takes point at asks twice and tells you how many.

⚠ Not built yet: dragging a line with the pointer (the nudge buttons are what moves one today) and splitting a line into syllables by hand.

Added — a prompter you can read from across the room

⛔⛔ Three ways to see the words, and none of them needs a second screen. The lyrics glyph in the arrangement toolbar still opens them inline in the lane. Beside Record there are now two more: Prompter, which raises a large floating prompter over the session, and ⇱, which moves it into a window of its own for a second display. All three draw the same words from the same tick — the one the playhead is drawn from — so a lane and a prompter can never disagree about which line you are on.

▸ Type sized for a microphone stand, not a desk. The smallest the prompter goes is more than three times the size of the application's body text, and the slider only goes up from there. That floor is enforced twice, in the code and in the stylesheet, and it is measured in a real browser rather than declared — because a size that is written down and never applied looks exactly like one that works.

▸ Mirror, for teleprompter glass. The words come out backwards on the screen so the glass turns them the right way round for the singer. ⚠ The controls are never mirrored: a slider you cannot aim is worse than no slider.

▸ High contrast — white on black in both themes, 21:1, which is well past the strictest accessibility bar there is. The syllable you are on keeps its accent colour and its underline.

▸ Scroll speed, and it is honest about what it does: the transport decides when a line becomes current, so the control changes only how quickly the column travels to it — from an instant cut to a slow glide. Nothing here is driven by a clock.

Esc puts the floating prompter away. The detached window's ⇲ hides it, so it comes back exactly where and how you left it.

Changed — the credits page stopped overstating what ships

⛔⛔ It said "926 components". 360 of those were build tools that are not in Oscillate at all — the compilers and test runners the project is built with, which never reach your machine. Settings → Credits now says the two numbers plainly: 567 components ship inside Freally Oscillate, and a further 360 only ever ran on the machine that built it. This is the page that exists so you can check the "free, and no AI" claim for yourself, so a number that flattered us was the worst thing it could carry.

▸ The list is grouped, and you can see what each thing is. Plug-in SDKs, audio codecs, Rust crates, icon sets, npm packages, and the artwork Oscillate's owner drew — six headings, because "which of these is the codec?" was not answerable from a flat list of nine hundred rows.

▸ Every component that names an author now shows one, which is what the promise of a credits page actually means. ⚠ npm packages do not name theirs anywhere Oscillate can read without asking a server, and Oscillate does not ask servers, so those rows show no author rather than a guess.

▸ The VST 3 Plug-In SDK is listed, with its copyright notice — it is not in any lockfile, so nothing had ever put it there, and the licence asks for exactly that one thing.

Added — the take you just recorded reaches the other producer

⛔⛔ A recording travels; a sample pack does not. A take is the one thing you unambiguously own, so when somebody joins your room the takes you recorded go with the project and their clips play. Everything you bought stays where it is, and the list of what to go and get is unchanged.

▶ Oscillate knows a take is yours because it wrote the file, at the moment it wrote it — not by guessing from a path. And it only ever sends a file that is actually sitting in its own Takes folder, so a project somebody emails you cannot turn your machine into a way to send their files on.

▸ A take that arrives is not yours to pass along. Each machine vouches only for what it recorded itself. ⚠ This was written down as a rule before it was true: the code that was supposed to clear the marker did not exist, so a take from one collaborator would have been offered onward to a third as though you had recorded it. It is enforced now, in both directions, with a gate over it.

▸ The panel says how many takes have landed, beside the list of samples that have not. A clip that starts playing a few seconds after you join is the feature working, and there was nothing on screen to say so.

Fixed — an imported sample was marked as your own recording

⛔⛔ Every audio file you imported through the arrangement was recorded in the project as a take Oscillate made. It shared one code path with the recorder and the path set the marker unconditionally, with a comment claiming only the recorder ever reached it. Two things followed: a collaborator was never told which pack one of your imported samples came from, so they could not go and buy it; and once takes began to travel, a bought sample would have travelled with them. Importing and recording are now told apart by the type system rather than by a comment.

Added — your sample packs stay yours

⛔⛔ Oscillate is not a way to move sample packs between computers, and it now enforces that rather than promising it. When you work with somebody, what crosses is the project's reference to a sound — never the audio, and never the path it lives at on your disk.

▶ If you both own the pack, it simply plays. Oscillate recognises your collaborator's sound in your own library by its contents, so the same pack works no matter where you keep it or what you renamed the folder to. You do not have to locate anything.

▸ If you do not own it, Oscillate tells you what it is — the pack and the file — and the clip stays silent until you get it, from your collaborator over Discord or by buying it, the way you would have anyway. ⚠ A sample of yours with the same name but different audio is reported as different rather than quietly put in its place, because that would change the record without telling you.

⚠ This holds while you are working, not just when you join. A sample dragged onto a track mid-session goes out with its path removed, the same as everything else.

Added — a mark on the packs you both own

▸ Oscillate can now tell you which of your sample packs the people you are working with also have — byte for byte, not by name. A folder you both hold gets a mark beside it in the browser, so you know at a glance that a sound you reach for is one they can actually hear.

⚠ It compares in two steps so it stays quick: file names and sizes first, which reads nothing off your disk, and the full byte-for-byte check only on the packs somebody else turned out to have too. A pack you both have under different folder names still matches.

⛔ It marks, and that is all it does. Nothing is sent — a match tells you the audio is identical, and it can never tell anybody how either of you came by it.

Fixed — the room panel was not on screen at all

⛔⛔ The room region drew its title, its detach button and nothing else. Everything behind it was built and none of it could be reached: no button to press, no code to copy. ⚠ Found by a test that looks at the built page rather than at a component.

▸ ⚠ And the virtual keyboard's show/hide strip was too small to be a comfortable target — 21 pixels tall where anything pressable should be at least 24. It only became visible everywhere in the last release, which is when it started mattering.

Fixed — undo did nothing after opening a saved project

⛔⛔ OPEN A PROJECT, MAKE A CHANGE, PRESS Ctrl+Z — AND NOTHING HAPPENED. Every launch gave this computer a new identity, while a saved project remembered whoever wrote it, so after opening a file your changes were filed under somebody else's name and your undo could not find them. ⚠ There was no error: the menu item was simply grey and the shortcut did nothing.

▸ This computer now keeps one identity, so reopening yesterday's project also brings back the undo history you left in it — which is what the project file was always designed to carry.

Added — turn a recording into notes, and a pattern into a recording

⛔⛔ RIGHT-CLICK A CLIP AND IT BECOMES THE OTHER KIND. An audio clip becomes a pattern beside it; a pattern becomes audio beside it. ⚠ The clip you started from is never replaced — you asked for the other kind, not for the first one to stop existing — and the whole conversion is one press of undo, including the track it makes to put the result on.

▸ A recording becomes notes by listening for where each note starts and what pitch it is. ⚠ It will be imperfect and it says so. A drum loop converts to nothing at all, and tells you how many hits it could not read a pitch from — a melody your recording does not contain is worse than an honest refusal.

▸ A pattern becomes audio through whatever is on that track: the instrument in its slot, then every audio effect after it, in order, each one named in the line that appears when it finishes. ⚠ A device you switched off with its purple dot, or a whole group you switched off, is not in the render. ⚠ The track's fader and pan are deliberately not baked in — the new clip plays through them, so printing them would apply them twice.

▸ If the instrument slot is empty it says which sound it used instead, and a sampler with nothing loaded is refused by name rather than handed back as two seconds of silence.

▸ The progress is a number, not a spinner.

Added — the room opens, and nobody runs a server

⛔⛔ FOUR PRODUCERS, ONE SESSION, NO SERVER. Press Create a room code, send the code to somebody, paste the code they send back, and you are connected. No account, no login, nothing to keep running and nothing anybody can switch off.

▸ The joiner's half exists now. Paste a code into the room panel and Oscillate answers with one of its own to send back — the piece that was missing when the last version could create a room nobody could enter.

▸ ⛔ A third and fourth person cost one exchange each, not six. Once two people are connected, that connection carries the introductions for everybody else: a four-person room is three exchanges rather than the six a mesh would otherwise need, and peers three and four never paste anything at each other.

▸ You can see who is here. A roster with a colour per person — the same colour in every place that person appears — and their pointer moving on your arrangement as they work. ⚠ Names are typed by whoever is using them and the roster says so: there is no account, so there is nothing that could check one.

▸ Chat, for as long as the room lasts. ⛔ It is written nowhere — not to disk, not into the project, not into your settings.

▸ ⛔ Your undo is yours. Ctrl+Z takes back your last change and never somebody else's, even when their edits are landing between yours. Two people editing the same thing at the same moment end up seeing the same result, without either machine asking anything.

▸ ⚠ When it does not connect, it tells you why. Roughly one pair in ten cannot reach each other directly, and Oscillate names the actual reason — including measuring the case where your network gives every server a different address, which is the one where nothing but a relay can help. ⛔ There is no spinner that goes on for ever.

▸ ⛔ You are told what it will contact, and what it did. Opening a room asks a public STUN server for your address — the servers it will try are listed on the panel before you press anything, the ones that actually answered are in Settings ▸ Collaboration afterwards, the list is yours to replace, and Contact no servers switches it off entirely.

▸ ⛔⛔ A room code contains your computer's public address, which is what makes a serverless room possible at all. "Treat this like a password" is beside every button that copies one.

⚠ Nothing is started until it is needed — no room, no process, and no firewall prompt on a machine where nobody has ever opened one.

⚠ What is not here yet: a person joining does not receive your project. Until that lands, both machines have to open the same .oscillate file before the edits mean the same thing.

Fixed — nothing ever read a folder's waveforms

⛔⛔ ADDING A FOLDER NOW READS ITS WAVEFORMS. It was supposed to already: there was a pass, a progress bar and a cache, and nothing in the program ever started one. Every waveform was drawn the first time you happened to click that file, which is the moment you least want to wait. ⚠ The bar beside the folder list now moves, with a file count on it.

Added — files land where you drop them, and every device has its own face

⛔⛔ DRAG A SAMPLE OUT OF EXPLORER AND ONTO THE ARRANGEMENT. Audio lands on audio lanes and MIDI on MIDI ones; the wrong kind is refused rather than silently given a new track, and the target says so before you let go. Empty ground below the last track makes a lane of the right kind, named after the file. ⚠ Several files at once land end to end and count as one undo press, and a .mid arrives as notes on the project's own grid rather than at the tempo it was written at.

▶ This needed no new permission. The drag rides the same channel the meters do; the file paths never reach the page at all.

⛔⛔ A LIMITER NO LONGER LOOKS LIKE AN EQ. Every one of the seventy-five devices declares its own face — what it draws above its controls, how wide it wants to be, and which of its controls are the ones you reach for first. An equaliser draws its response; a compressor draws what it is taking off, with the number beside the bar. ⚠ A face whose measurement this build does not take yet draws nothing rather than an invented shape.

⛔⛔ GROUP DEVICES, AND SWITCH A WHOLE GROUP OFF WITH ONE DOT. Tick two or more cards in the rack, press Group, and they move together under one header with a name, a fold arrow and a single purple dot — filled when the group is on, hollow when it is off. ⚠ Switching a group off and on again gives back exactly what you had, including the one device inside it you had bypassed on purpose. ⛔ A project with groups in it still opens on a build that has never heard of them.

▸ Every device card has that dot too, on Oscillate's own devices and on hosted VST 3 and CLAP plugins alike.

▸ Devices can be dragged out of the browser into the rack, onto the gap you choose — the gaps that will not take it say why before you release.

▸ Windows exclusive audio, event-driven. The device goes to Oscillate alone and the mix path is bypassed; the settings screen reports what actually opened rather than what was asked for. ⚠ Anything the machine refuses falls back to a working shared stream instead of silence.

Fixed — a plugin's own notes had nowhere to go

⛔ Notes emitted by a hosted plugin now have a gate on every hop. They were drained from the plugin, carried across the pipe and pushed into the block — and the one test over that wire round-tripped an empty event list, so the twenty bytes an event occupies were never checked by anything.

Added — plugin editors open, and the browser knows which lane you are on

⛔⛔ A HOSTED PLUGIN'S OWN WINDOW OPENS. The sandbox owns a real top-level window with a message pump, so a VST 3 or CLAP editor appears, moves, closes and reopens — closing an editor no longer means deleting the card to get it back. ⚠ A plugin that refuses to show one is now told apart from a plugin that crashed: a refusal greys nothing out.

▸ The Plugins category lists what the scanner found, and the plugins arrive as they are found rather than all at the end. ⚠ Scanning no longer opens a console window per plugin — a first scan of a real library used to flash dozens of them.

▸ A track header says whether it is MIDI or Audio, in a word. The badge was a small glyph behind aria-hidden, which told a screen reader nothing and a new producer very little; the question it has to answer is which effects can I put here.

▸ The browser follows the selected lane. An audio lane does not list instrument or note-effect plugins, because it has no notes to give them; a stock category that does not fit says so rather than emptying. Oscillate's own audio effects are never hidden — they belong on both kinds of lane.

▸ A second instrument replaces the first, the way every other DAW does, in one gesture so one press of undo puts the old one back with its settings.

Fixed — tempo-synced plugins were told the wrong time

⛔⛔ THE PLAYHEAD MATCHED; THE TIMELINE AROUND IT DID NOT. A hosted plugin is handed a transport every block. Its beat position was always real — five fields beside it were constants, and both plugin formats declared those fields valid:

▸ All five now come from the transport itself, and the sample position is derived from the playhead rather than counted separately — two clocks diverge, one cannot.

Added — Windows opens a low-latency stream, and the panel stops guessing

⛔⛔ THE LADDER COULD GRANT A RUNG NOTHING WAS ABLE TO OPEN. Oscillate has asked Windows what period its audio engine runs at since Phase 7; what it could not do was open a stream at that period. cpal does not make the IAudioClient3::InitializeSharedAudioStream call and cannot be asked to, so ModeReason::LowLatencyNotBuilt existed to say so honestly rather than promise a latency the product could not deliver.

▸ There is a second audio backend now. MMDeviceEnumerator ▸ IAudioClient3 ▸ GetSharedModeEnginePeriod ▸ InitializeSharedAudioStream, event-driven, with its own render thread. On the development machine both endpoints answer 480 frames at 48 kHz. ⚠ Every failure falls back to the ordinary shared stream and still makes a sound: an older Windows, a driver that will not take the period, a mix format that is not 32-bit float.

⛔ The reason string changed meaning, so it changed wording in all 18 languages. lowLatencyNotBuilt used to mean Oscillate cannot do this. It now means this machine would not, which is where the blame belongs.

Fixed — the audio panel reported a prediction instead of a fact

⛔⛔ SETTINGS ▸ AUDIO CALLED THE DEVICE “SHARED” WHILE A LOW-LATENCY STREAM WAS PLAYING THROUGH IT. The ladder decides from what a driver advertises: a minimum period of 128 frames or better at 48 kHz. The development machine's endpoints advertise 480 — and InitializeSharedAudioStream opens them anyway.

▶ The panel no longer predicts about a device that is open. The engine records the rung it actually took and that wins outright; the ladder keeps one honest job, which is saying what a device that is not open would probably give. ⚠ An earlier fix corrected the prediction from the engine instead, and a correction can only ever subtract from a prediction it did not make — so it caught one direction and was blind to the other.

⛔ And the period on screen is now the measured one. It was min_period_frames, which is what the driver claims, shown beside a stream running at whatever InitializeSharedAudioStream granted. For a device that is not open the advertised figure remains, where it is a prediction rather than a claim about something running.

⛔ A refusal keeps its reason. A driver that will not run that fast, a Windows too old for IAudioClient3 and a device that vanished were collapsed into one blank answer, and the panel labelled all three "this machine would not open a low-latency stream" — which sends somebody to update Windows over a driver setting.

Fixed — the audio panel reported a decision instead of a fact

⛔⛔ Building the backend re-opened the same lie one storey up. With the low-latency stream real, the ladder grants that rung to any driver fast enough — and when the open then fails, the engine quietly falls back while the panel still reads "Low latency, shared". ▸ Settings ▸ Audio now reports what the running engine actually took, not what was decided for it.

Fixed — a 10 ms block scheduled like a text editor

⛔ The render thread was not registered with MMCSS. Asking a driver for 480 frames buys nothing if the thread filling them can be preempted by a window drag. It joins the Pro Audio task now, and a gate reads the render thread's own priority from inside the callback — 0 → 15 — rather than taking it on trust.

Added — a hosted plugin makes a sound, for the first time

⛔⛔ NO VST 3 OR CLAP HAD EVER PROCESSED A BLOCK OF AUDIO IN THIS PRODUCT. For the whole of Phase 6 and Phase 7 a plugin would scan, load, activate, report its latency, write that latency back into the project and open its own editor window — every step of it gated and green — and the signal went straight past it. What a producer heard was the track without their plugin on it, with the plugin's window open in front of them.

▶ Four things had to be true at once: DeviceRack held Vec<Box<dyn StockDevice>>; GraphBuilder's only door was registry::build; Channel::chain was keyed by a registry type_id a plugin does not have; and oscillate_app::mixer opened its projection loop with let DeviceKind::Stock { .. } else { continue }. ⚠ The eighth time this project has found a complete mechanism with nothing calling it.

⚠ oscillate-audio still does not depend on oscillate-host, and must not. The engine declares a shape — rack::ForeignDevice — and src-tauri, which already depends on both, supplies one. The engine still cannot open a shared library, and now it can run what something else opened.

Added — a track can play another track's plugin output

What Live spells "MIDI From → track → Plug-in Output", and InputRouting gains notes_from. ⛔ This was the signature defect in its purest form: a note-emitting plugin's output was drained by CLAP, drained by VST 3, carried back across the pipe by AudioMessage::events and pushed into the caller's list — five correct stages — and then dropped, because nothing in the engine had anywhere to put it.

⚠ A route is an ordering edge in the same topological pass that orders audio routing, so the notes are this block's rather than the previous one's, and two tracks pointing at each other are refused rather than rendered one block late for ever.

Added — recording MIDI, which Oscillate has never done

capture::NoteCapture — lock-free, allocation-free, sixteen thousand notes, and each one carries where it was in subticks, because a ring of bare messages would put every note of a take on beat one. ⛔ Captured after the routed notes join and before the track's own note chain, which is what every DAW records: the input, not the post-effect result. Capturing after the chain would bake the arpeggiator in, and playing the clip back through it would arpeggiate the arpeggio.

⚠ A take lands as one Operation carrying two edits — the pattern and the clip that places it — so undo does not leave a pattern behind with nothing playing it.

Added — Settings ▸ Audio, and the ladder decides on the truth

⛔ That tab drew an empty body for eight phases, while device::enumerate, device::resolve, negotiate, Granted and ModeReason sat in the engine with no caller anywhere outside device.rs — the ninth instance of the same defect. The panel lists the outputs, chooses one, chooses how much of the device to take, and names what was granted with its reason, because the trade between latency and sharing the device is the producer's to make.

engineperiod::smallest_period asks IAudioClient3::GetSharedModeEnginePeriod through windows-rs, so the ladder decides on the real engine period rather than the driver's advertised buffer floor. ⛔ That found two real defects: this machine's onboard advertises a floor of zero, which negotiate read as 0 <= 128 and granted the low-latency rung over; and the panel would have reported "Low latency, shared" over an ordinary shared stream, because InitializeSharedAudioStream is not built. Both are fixed, and the second is now ModeReason::LowLatencyNotBuilt — an honest answer rather than a lie on the producer's own settings screen.

Added — the explorer, five export formats, and the browser's preview

The File Explorer with its preview transport, MIDI clip previews, instrument tags on two axes, waveform caching and indexing; export to WAV, AIFF, FLAC, Ogg and MP3, master and stems and MIDI, with a Settings panel and honest progress.

Measured — the web app go/no-go, four rungs early

⛔ Moved forward from v2.0.0 on the owner's argument, which was correct: finding out at v2.0.0 means finding out after every feature has been designed twice. Both questions are answered in web/spike/.

⚠ Phases 6 and 7 both landed on main on 2026-08-29, as merge commits 43785ad and 7bb1ea1. Together they are the v0.3.0 rung — a DAW that hosts your plugins and has its own — and the rung is not tagged yet: it wants REL-0.3.0, the end-to-end release build, first. v0.2.0 and v0.2.5 are still untagged behind it.

Added — all 248 instruments, the drum packs, and Freally MixSir

⛔⛔ THE ROSTER WAS 197 AND THE REASON IS WORTH KNOWING. It walked each engine's own Kind::ALL, so it could only ever see instruments a modelled engine names. SubBass, SawLead, WarmPad, Riser, VoxChop and forty-six more are defined by a patch rather than a kind index — no walk could reach one. That is the whole gap between 197 and 248.

⛔ And a kind index is not a sound. Pinning (Bowed, 2) gave the bowed engine's default patch with Cello selected — not the Cello a producer hears in Freally MIDI Master, which is generator::generate → articulation::apply → tone::apply. Two products, same name, different sound, nothing failing.

So factory/ is the catalogue, imported: seven files byte for byte out of the plugin's own preset module, plus a preset.rs derived from the slice of its mod.rs that carries no module declarations. A fork gate re-derives that slice and fails naming the line. style_sound, palette and user stayed behind — the artists/producers half — and a fourth test fails if one ever appears.

⚠ Two local duplicates were deleted rather than kept beside the import. natural_note and the one-shot baker's per-family shape were both coarser copies of audition_note() and render_time(), which the taxonomy already carries per instrument.

Pitch modulation, the keyboard and the sound rack. 91 of the 248 can have their pitch modulated and 157 cannot, and the split cuts straight through the families — Percussion alone is 11 with and 82 without — so the browser filters with a pill and never folders. Both halves of the question are answered: the pill for hunting, an inline-SVG mark on every row for browsing. (Not ∿, U+223F, which is absent from Noto Sans and draws an empty box.)

The virtual keyboard's keys are labelled with a real piano's contrast — white keys take dark labels, black keys light — through --color-text rather than a literal, which is what makes it survive both themes. It is persisted and open by default.

The sound rack above it carries the plugin's own words — Room, Heat, Echo, Gain, Pan, Pitch Mod — lifted out of its catalogues in all eighteen languages, because "a DAW that calls it 'Drive' while the plugin calls it 'Heat' is two names for one control". ⛔ It is not drawn at all on an instrument whose pitch cannot be modulated. ⚠ Gain and Pan are the track's, not a second pair.

The drum packs. Eleven kits and 407 CC0 one-shots, bundled and browsable. ⛔ The audio was copied and the generator was not: kitgen draws every sample from a ChaCha8 stream and Oscillate's rng is SplitMix64, so a ported generator would ship a different drum library under the same eleven names.

Freally MixSir — pair a phone by QR, send it the arranged project as stems, balance it with drawn knobs, and download the mix or any track as WAV or MP3. ⛔ Everything after the stems arrive is local: the mixer and the export work with the network off, which is the whole point — "a friend's house is a different network. A live stream dies at the front door. A file does not."

⛔ The session secret is not optional. One per pairing, nothing persisted, five failures and the pairing is dead, and a single refusal that says nothing about which part was wrong — because a message distinguishing "no secret" from "wrong secret" tells an attacker which of the two they achieved.

Fixed

Changed

Added — you can hear a device before you use it

⛔⛔ NOTHING IN OSCILLATE COULD SOUND A DEVICE THAT WAS NOT ALREADY ON A TRACK. The browser listed seventy-five of them by an evocative name and a line of plain English, and choosing between Crucible and Muster meant adding one to a track, drawing a note, pressing play, listening, deleting it, and doing the same for the other.

▶ A preview player, below the File Explorer. Click one of Oscillate's own devices and it plays its own demonstration; choose a folder and click a sample and it plays that. One player rather than a transport per row: only one sound can be auditioning at a time, and the waveform has somewhere to live.

⚠ Five of the eleven instruments say why they cannot. Flint, Armory, Bench, Hammer and Filings are samplers and are silent until a file is loaded into them, so they show that sentence instead of a button that plays nothing. ⛔ A gate holds the line: every built instrument either declares an audition or declares sample slots.

⚠ The player is a voice mixed AFTER the graph, following click.rs. It sounds with the transport stopped, needs no graph rebuild, and never allocates — the buffer is rendered up in the app and handed down, and the one it replaces is parked for the app thread rather than dropped, because freeing is allocating's twin. The audio-thread gate covers it, and was watched failing with deallocations: 1.

Added — Freally Secretarium's instruments, and previews that are file reads

⛔⛔ THE BROWSER LISTED ELEVEN INVENTED INSTRUMENTS AND NONE OF THE REAL ONES. Owner instruction, said four times: "i was supposed to have all the same instruments as Freally MIDI Master", "and Freally Secretarium", "those instruments, not made up ones".

▶ **synth/ in this workspace is Secretarium's havoc_dsp — module for module — and all 197 of its instruments were already compiled into Oscillate. Nothing listed them. Quiver, Muster and Crucible were showing where Violin, Tuba, Grand Piano, Rhodes EP, Accordion and the 808/909/707/606 kits should have been. ⚠ The sixth instance in this project of a mechanism that is correct with nothing calling it**, and by far the largest.

⚠ The catalogue hard-codes no name. oscillate_dsp::secretarium walks each engine's own Kind::ALL and asks it its name(), so it is Secretarium's list by construction and cannot drift.

Fixed — clicking ▶ froze the application for nine seconds

⛔⛔ MEASURED, IN RELEASE, AT 48 kHz:

device main-thread block per click
Muster 8,886 ms
Quiver 5,238 ms
Crucible 1,006 ms

▶ Every #[tauri::command] in this application is synchronous, and a synchronous command runs on the UI thread. preview_device built a whole modelled instrument and rendered seconds of audio there, on every click — so clicking Muster froze the app for nine seconds and clicking again queued another nine. That is one cause for two owner reports: "the instruments load really slow" and "i keep clicking the button to play it and it eventually crashes".

⛔ The fix is the owner's own design: "it should just load an audio clip for the one shot, not the whole instrument unless it's added to the arrangement view". bake-oneshots renders all 197 at build time — each at the note that instrument actually sounds in, from its real tessitura: Tuba at C2, Piccolo at C6, Violin at G4, Double Bass at E2. Unpitched percussion is marked as such rather than given a fake key.

⛔ And the clips very nearly shipped unreachable. Declaring them as ../assets/oneshots put a literal _up_ directory in the installed tree — Freally Oscillate\_up_\assets\oneshots\ — while the application resolved assets/oneshots. Every dev run worked, because a workspace fallback answered; the installed build would have found nothing and played silence for all 197. ⚠ Found by extracting the MSI and running the extracted layout with the source copy hidden, which is the only arrangement in which the fallback cannot rescue the bug. The baker now writes inside src-tauri/, so no ../ is involved.

Fixed — hammering Add spawned a plugin process per click

⛔ Owner report: "if i add a plugin over and over again really fast it crashes the app, it shouldn't try to load it 15 times really fast into the same audio/midi track and then crash". Every add starts a oscillate-hostproc sandbox and waits for it; fifteen clicks was fifteen process spawns racing on the UI thread. ⚠ Dropped, not queued — a producer leaning on the button wants one more device, not fifteen.

Added — select an instrument and play it with ← and →

⚠ The list is a listbox: clicking a name selects it, → plays it forwards and ← backwards. ⛔ The direction is set before the clip loads — the other order plays the first press forwards and only reverses on the second, which reads as a dropped keypress rather than a bug.

Added — the File Explorer is a browser now, set up the way Ableton's is

⛔⛔ IT WAS A FLAT LIST, AND A FLAT LIST OF SEVENTY-FIVE DEVICES IS A WALL. Owner instruction: "it should have a list like Plugins, Instruments, then a button to Add Folder … just like Ableton's browser is set up".

▶ A home screen of places, each with a ›: Instruments, Audio Effects, MIDI Effects, Plugins. Click one and it lists what is in it. Under them Choose folder, and under that the folders you have added, by name only — click a name and you get that folder's tree: root, subfolders, samples and MIDI.

Fixed — three defects the browser rewrite introduced, all found by driving the build

⛔ A sampler was told it had no voice. previewDevice picked its refusal from a list the old panel fetched on mount; the rewritten panel did not fetch it, so the list stayed empty and every sampler fell through to "this device has no voice of its own". ▶ The fourth time in this project that a mechanism outlived its only caller, and the third inside this one feature. The row already carries the answer, so the refusal comes from the row now and there is no second list to fall out of step with.

⛔ The walkthrough photographed a refusal under a caption promising a waveform. It clicked the first ▶ in the list, which is Armory — a sampler. Six of the seventy-five sound unaided; the ▶ now says which it is in data-can-hear, and the walkthrough asks for one that does.

⛔ Nothing gated the owner's own rule about length. "Not just like a 1 second one shot, but like a 2-4 second one shot so that you can hear the ensemble or the violin playing" — and the only check was a 400..=6_000 band on a number in the registry, which happily passes a violin chopped at 400 ms, because a declared duration says nothing about whether the sound had finished. Two gates now listen instead:

Fixed — a path from the page reached the disk, and the gate could not see it

⛔⛔ preview_file TOOK A PATH FROM THE WEBVIEW AND OPENED IT, and no_command_opens_a_path_from_the_page stayed green over it. Source::open( was in that gate's marker list; the preview decodes at the session rate and so calls Source::open_at, which the entry did not match.

▶ That is precisely what the list's own comment predicts happens when a call gets a new name and the list is not told — and it happened twice in one change, because preview_browse reaches read_dir through a private listing_for and contained no marker either. The same shape as place_recording before it.

⚠ The gate now knows Source::open_at(, read_dir( and listing_for(, and asserts it selects five path-taking commands rather than three. Watched failing: removing the check makes it say "the list is not the gate; the call is."

⛔ The check itself is oscillate_app::preview::Roots — folders the producer picked in a native dialog, which a page can neither open nor choose for somebody. The same argument Session::knows makes about a project. It canonicalises before comparing, so ../ out of the tree, a symlink out of the tree, and a sibling folder sharing a name prefix are all refused, with a test each.

⚠ Found, not fixed — the modelled percussion voices are not level-calibrated

⛔ Rendering a demonstration was the first thing in this project ever to measure an instrument's output LEVEL, and three devices are badly out. At their defaults, at 48 kHz:

device raw peak dBFS
Skin 52.85 +34
Bronze 12.04 +21
Crucible, Quiver, Muster ~0.31 −10
Sideband 0.05 −26

⚠ The Level trims are applied and working — Skin defaults to −6 dB and Bronze to −9. The modelled voices underneath them are simply not calibrated, against full scale or against each other. On a track Skin clips hard and Sideband is inaudible beside it.

⛔ Deliberately not fixed here. Retuning three shipped devices changes every project that already uses them, so it is the owner's call rather than a silent change made while adding a browser feature. Previews are level-matched — which is right for a comparison anyway, and is what every sample library does — and Rendered::raw_peak keeps the true number so the defect stays measurable. the_percussion_voices_are_not_level_calibrated_and_this_says_so asserts the current state and fails the day somebody calibrates them, which is the reminder to update this table.

Changed — the generators are Freally MIDI Master's, not Oscillate's (Phase 8)

⛔ Oscillate carries no generation code, and the engine crate is gone. It had been an empty placeholder in the workspace since the first commit, waiting to be filled in by an import that is no longer going to happen.

▶ Freally MIDI Master ships as a stock hosted plugin instead, so its generators arrive through the plugin boundary Phase 6 already built — VST 3 and CLAP, out of process.

⚠ Why one source of truth beats a self-contained build here. Vendoring the engine would have meant a byte-for-byte copy held in place by a fork gate, a re-sync whenever the upstream moved, and another product's source living in two repositories. The two products have to agree — the same seed and the same style must give the same arrangement in both — and a copy is the thing that eventually stops agreeing.

Added — Oscillate's own devices (Phase 7)

⛔⛔ SIXTY-NINE OF THE SEVENTY-FIVE ARE BUILT, including all twenty-eight core effects, every one of the sixteen note devices, and nine instruments. ⚠ Six remain declared, and each is blocked on a seam rather than on DSP — they are listed by name under What is not built below, with what each one is waiting for.

⛔ The devices built since the roster last read fifty-four, and the seams each one forced:

⛔⛔ AND FOUR AUDIO-THREAD VIOLATIONS THE ALLOCATION GATE COULD NOT SEE. It walked every built device with no sample loaded and no note sounding, so every sampler's read path and every instrument's voice loop had never once been under the counting allocator. It now loads a clip into every slot and strikes an eight-note chord twice, and it found four:

▶ Previously: all twenty-eight core effects, eleven of the sixteen note devices, four instruments and eleven of the seventeen signature devices — fifty-four of the seventy-five. ▶ The core 28 is the whole of what the specification calls "everything a producer needs to finish a record without reaching for somebody else's plugin". The roster is 75 and the honest figure is what oscillate_dsp::registry::implemented_count() answers; both numbers are asserted in the code so neither can drift from a document. The other twenty-one appear in the browser named and greyed, with no controls at all — a control list without the DSP that gives its numbers meaning would be invented ranges that look authoritative.

The equalisers: Whetstone, Hilt, Temper. Dynamics: Vise, Weld, Strata, Parry, Ceiling, Peen, Quench. Drive and amplifiers: Shear, Triode, Rasp, Fold, Oxide, Furnace, Baffle. Delays: Relay, Spool, Sparks. Reverbs: Chamber, Foil, Coil. Modulation and filter: Shoal, Veer, Comb, Edge. Utility: Stance. Instrument: Crucible. Note devices: Runner, Stack, Hone, Weight, Hold, Scatter, Trail, Hand, Approach, Rake, Turn. Percussion: Skin, Bronze, Sideband. Signature: Waypoint, Guard, Lantern, Feint, Wear, Gild, Skywave, Toll, Vault, Cleave, Strand.

Fixed — three things only the shipped application could show

⛔⛔ oscillate-hostproc.exe WAS IN NO INSTALLER, SO NO PLUGIN COULD EVER LOAD. tauri.conf.json declared neither an externalBin nor a bundle.resources entry, and target/release/ never contained the binary either — so every VST 3 and every CLAP failed in the release application with

…\target\release\oscillate-hostproc.exe: The system cannot find the file specified. (os error 2)

▶ This is the answer to "no plugin was ever loaded by it": none could be. Phase 6's entire deliverable was dead in anything built with tauri build, and would have been dead in an installer a tester ran. It passed everywhere else because cargo test, cargo run and scripts/host-conformance.mjs all run out of target/debug/, where cargo had already put the binary beside them. ⚠ Fixed with scripts/build-hostproc.mjs, an externalBin declaration, and src-tauri/tests/the_bundle_carries_the_sandbox.rs — which fails if the declaration, the path or the build step that produces the file ever come apart. Both installers were rebuilt and both grew by the compressed size of the binary.

⛔⛔ THE SIXTY-NINE STOCK DEVICES WERE STILL UNREACHABLE FROM THE BROWSER, and this is the sixth instance of the same shape. PluginScan::index() correctly prepends the roster; PluginScan::state() cloned the raw scan one layer below that fix — and plugins_state is the only command the browser panel calls. So device_rack, device_add and locate_for_load all saw the roster, five gates called index() and passed, and a producer typing vise into the filter got nothing. ⚠ A pre-existing test asserted state().index.plugins.len() == 1, which had locked the defect in. The new gate goes through state() deliberately: that is the function with a user on the other end.

⛔ A control label and its status-bar hint were transposed in all eighteen catalogs. arrangement.metre held the sentence and hints.metre held the word, so the arrangement toolbar drew a wrapped paragraph where a one-word label belongs. Both strings existed and both were translated, so the parity gate — which compares catalogs to each other — was green over eighteen identical mistakes. There is now a gate on the shape: every toolbar label must be shorter than the hint that explains it.

Fixed — Rein offered Off at index three

⚠ Params::on answers index >= 1, so a control that offers Off anywhere but first reads as on to every device that asks. Rein's Target A and Target B listed it last. ⛔ Caught by every_switch_in_the_crate_reads_off_then_on, which is a crate-wide invariant and not a naming preference — and which is exactly the kind of rule that only pays for itself when somebody adds the seventieth device at four in the morning.

Changed — three gates rebuilt a device for every setting, and one of them cost nineteen minutes

⚠ Sixty-nine devices cost more than fifty-four, and two of the new ones cost a great deal more. Sift and Ply each run a 4 096-point transform per channel per hop; Muster hosts up to twelve synth engines and Quiver eight, and SynthEngine::new takes about a second. The gate suite went from three minutes to over twenty-five, where it was killed rather than finishing — and the first two attempts to make it affordable were aimed at the wrong thing.

⛔⛔ THE ACTUAL CAUSE WAS ONE SHAPE, IN THREE PLACES: A GATE THAT BUILT A FRESH DEVICE FOR EVERY SETTING IT TESTED. None of the three properties has anything to do with construction — set_param reaches every setting, and set_param, prepare and load_sample all run off the audio thread. The constructions were pure overhead, and with engine-bearing instruments in the roster they became the whole runtime:

Gate Constructions Now
no_device_makes_a_sound_out_of_silence 2 298 69
every_device_survives_every_block_size_at_every_extreme 12 400 276
the audio-thread allocation gate ~500 one per device

⛔ silence_in_silence_out was nineteen of those minutes on its own, and it was the last test still running when the suite was killed — which is why the two earlier fixes below looked like they had achieved nothing.

⚠ Nothing asserted was weakened. Every setting is still visited, in the same order, at every block size; each is reset before it renders; and each control is put back to its default before the next one moves, so every pass is still exactly one control away from the defaults — which is what building a fresh device used to give. ⚠ If anything the silence gate now checks harder: engines built lazily by an earlier setting stay built, so more of the device is running on nothing than before.

⚠ Two changes to the devices themselves, which were worth making anyway and which bought almost nothing on their own:

⚠ A gate nobody can afford to run is a gate that gets skipped, which is the same as not having it — and this project has seven defects on record that only a gate somebody actually ran would have caught.

Fixed — the sidecar declaration broke every cargo build, and ci:local was green over it

⛔⛔ AN externalBin DECLARATION IS A BUILD INPUT, NOT ONLY A BUNDLE INPUT. Tauri's build script refuses to build src-tauri at all unless the sidecar exists for the host triple — a plain cargo clippy or cargo test, not only tauri build. So the fix for "no installer carried the sandbox" immediately broke all three quality jobs with

resource path `binaries/oscillate-hostproc-aarch64-apple-darwin` doesn't exist

⛔ And npm run ci:local -- --slow was 18/18 while that was true. The machine that added the declaration had a sidecar staged by an earlier manual run of the build script, so every local run since had been quietly depending on a file some other command left behind. ⚠ A runner that does that is not running CI's environment, which is the only thing it is for — and this is the second time in this project that a green local suite sat over a broken tree.

⚠ The workflow now stages the sidecar before the Rust gates rather than after them; ci-local.mjs stages one itself as its first gate; and ci_stages_the_sandbox_before_anything_that_builds_this_crate asserts the ordering, because a staging step that ran after the gates is exactly what was already there and is exactly what failed. ⚠ --debug, because nothing is bundled in that job and the build script only checks that the path exists.

⚠ The release jobs passed throughout, because npm run tauri:build runs the staging script first. Only the jobs that build the crate without bundling it were affected — which is why nothing local caught it.

Added — nothing in the suite had ever handed a device a short final block

⛔⛔ EVERY FRAME COUNT IN THE GATE SUITE WAS AN EXACT MULTIPLE OF ITS BLOCK SIZE. 1 024 frames at a block of 64, 4 096 at 2 048, 8 192 at 64 in the silence gate — so across seventy-five devices and four block sizes, not one device was ever asked to process a partial block. A device that read block where it should have read frames.min(block) would have been green everywhere here.

⚠ The real engine produces short blocks constantly: the end of a span, a loop boundary, a device's own latency compensation. This was found while cutting the suite's runtime, not by a failure — which is the uncomfortable part, because nothing was going to find it otherwise.

⚠ frames_for now subtracts half a block, so every block size ends on a partial one, and every_block_size_ends_on_a_short_block asserts that property of the helper rather than trusting a comment — a block size added to BLOCKS that happened to divide evenly would otherwise put the suite back where it was, silently. ⚠ It is also about a fifth cheaper, which is the rare direction for a coverage fix.

⚠ The allocation gate is the third row of that table, and it is the one that made the shape visible: teaching it to load a sample and strike a chord sent it from twenty seconds to over twenty minutes in a single commit. It is built once per device now, with every setting still visited in the same order and every warm-up block still in place — 64 seconds, and still red when a Vec is put back into process.

Added — the seed can be re-rolled, and six devices woke up

⛔⛔ ops::reroll_seed HAD NO CALLER, AND THAT IS THE SEVENTH TIME. The project's seed has been in the model since Phase 3; it rides with the arrangement index and reaches Context::seed; six devices draw every random decision from it. Nothing in the interface could change it, so each of those six had exactly one behaviour for the life of a project — and every gate that hand-made a Context was green over it, because a hand-made context can carry any seed it likes.

⚠ There is now a ⚄ button on the arrangement toolbar, beside the tempo, because a seed is a property of the song rather than of the machine. The new number is generated by the caller and passed into the operation, so a re-opened project sounds like the one that was saved: an operation that rolled its own randomness would not replay.

Changed — a modal blurs the session behind it

⚠ Owner instruction: "when any modal shows over the DAW, the background parts of the DAW should be blurred and only that modal should be visible." A scrim alone leaves every fader and clip legible behind it. backdrop-filter on the scrim rather than a filter on the app root — a filter applies to a whole subtree and would have blurred the dialog too. ⚠ Measured on the shipped binary: the File Explorer's high-frequency energy falls from 7.18 to 3.94 with the dialog open.

Added — every control, in every language (Phase 7)

⛔⛔ The panels speak all eighteen languages. 482 distinct control labels across the 69 built devices, translated into every catalog Oscillate ships — the last thing in Phase 7 that was still English everywhere.

⚠ A label and its status-bar hint were transposed in all eighteen catalogs. arrangement.metre held the sentence "The project's time signature — how many beats a bar has, and what a beat is worth." and hints.metre held the word "Metre", so the arrangement toolbar drew a wrapped paragraph where a one-word label belongs. Nothing could have caught it: both strings existed, both were translated, and the parity gate compares catalogs to each other — every one of them was wrong in the same way. There is now a gate on the shape: a label is a word, a hint is a sentence.

▶ Industry convention, which is not the same as translating everything. Gain, Attack, Release, Threshold, Ratio, Low Cut stay as they are in German, French, Spanish, Italian, Dutch and the rest, because that is what a producer reads on every other DAW they have opened — Cubase and Studio One ship German panels saying exactly those words. Rendering them as Verstärkung, Ansprechzeit and Schwellwert is correct German and more foreign, not less. ⚠ The plainly descriptive vocabulary — Frequency, Level, Width, Length, Size, Noise, Material — is translated everywhere.

⚠ Japanese, Korean, Chinese, Russian, Ukrainian, Arabic and Hindi keep no Latin text at all, because their own DAWs do not: katakana アタック, hangul 어택, 起音, Атака. It is the same convention giving a different answer, and not one of those seven appears in the kept-English list — all 319 are checked in all seven.

⛔ The key is computed in Rust, from the label. Gain is one string for the whole roster rather than the eleven it would be keyed per device, and a renamed control produces a key the locale gate calls missing instead of a panel that quietly falls back to English with every test still green.

⚠ The grid is composed from a per-language pattern, not written out sixty times. Band 1 Frequency … Band 4 Shape and the four-band dynamics grid differ only in a digit or a band, so each language states its own word order once. ⛔ The first attempt used a single form per band and produced Hoch Ratio Up, Высокие Соотношение вверх and Tief Decay — all grammatically wrong. A band word that stands alone on a tone control is not the word that qualifies another noun: German needs Höhen- and a hyphen, Russian and Ukrainian need the genitive and go after the term. Both forms are now given separately.

⚠ Look Ahead and Lookahead were the same control spelled two ways — three devices said one, two said the other. Unified on Lookahead, which is what the majority used and what modern limiters print. That is one fewer duplicate translation in eighteen languages, and a gate now refuses two labels that differ only in spacing or case.

Added — four mechanisms that were correct with nothing calling them (Phase 7)

⛔⛔ Oscillate could not sound a note. Not from a virtual keyboard, not from a hardware one. oscillate-audio's midi module — ports(), Input::open(), EventQueue, Filter, decode() — was built, tested, and referenced by no crate outside its own; NoteBank was filled only by the playback worker, from clips. So every instrument on the roster could be added to a track and none could be played. live.rs is the missing path, and it is one queue because a virtual keyboard and a MIDI port are the same event arriving by different doors — two paths would be two answers to what velocity means, two all-notes-off rules and two decisions about what happens while the transport rolls, and the one used less would be the one that was wrong.

⛔⛔ Waypoint, Guard and Lantern read next_section_ticks: None every block, for ever. The look-ahead is this phase's headline claim — a plugin is told where the playhead is and never what is ahead of it — and graph.rs built its Context with ..Context::default(). Three signature devices were an ordinary compressor, gate and delay with a dead Lookahead control. ⚠ Every gate stayed green because every gate built its own Lookahead and passed it in. They all asked whether a device responds to one; not one asked whether it ever receives one. Same shape as the roster being unreachable from the browser, and the same question finds it: what would a producer actually press? The arrangement's structure is now computed off the audio thread and handed down as relative ticks.

⛔ The sample seam, and one voice behind every device that plays a file. Seven instruments were Declared for want of this and nothing else. A device is built from a type_id and a list of f64, and a path is neither — ParamSpec carries numbers because the audio thread may not see a String — so a sample could not arrive as a control. It arrives as an Arc<SampleData> swap instead, decoded on a worker. ⚠ The drop is the trap: releasing the last Arc frees on whichever thread let it go, so the audio thread hands the old one back rather than dropping it. ⚠ The read head has been in delay::sinc_read since AM-A1, documented as "the sampler's" — written, tested, and reachable by no sampler, because no sampler could be written. One voice serves all of them: pooled at prepare, never allocating after it, stolen quietest-then-oldest and faded rather than cut, because a hard cut at an arbitrary sample is a click at full level and it lands exactly when a producer is playing.

▶ The roster is 63 of 75, up from 54. Nine devices came off the blocked list: Sift; the note devices Weave, Tread and Revoice; and the samplers Flint, Armory, Bench, Hammer and Filings. ⚠ All 28 core effects were already built — every one of these was blocked on a seam rather than on anybody's time, which is why closing three seams moved nine devices.

Added — sharing what you made (Phase 7, TASK-084 and TASK-084A)

⛔⛔ A device's settings go in one line you can paste into a chat. No file, no upload, no account — which is how a community actually trades sounds.

oscillate:1:oxide:MC41LDAsMTUsMSwzLDAuNSwwLjMsMC40LC00OCwyMCwzMCww

▶ Every line that arrives is treated as hostile, because it came from a stranger through a chat window. The length is checked before anything is allocated; an unknown device is refused by name rather than guessed at; every value is checked against its own control's range and a line outside it is refused rather than clamped — a clamp turns a corrupted line into a device that looks right and sounds wrong, which is the failure a producer cannot diagnose. A NaN parses as a number and is refused too: one that reached a filter's state would silence the track for the rest of the session with nothing on the panel to say why.

⛔ Nothing is applied until you have read what would change. The panel lists the controls that move, from and to, in the DSP's own formatter — so the preview cannot say a number the knob then contradicts — and only then offers to apply it, as one undo step. ⚠ The paste is a text field rather than a clipboard read: readText() asks the operating system for whatever you last copied, and a field asks for nothing.

⚠ The line carries no deflate, which § F-18's sketch suggested. The target was under 200 characters and the whole roster measures at well under half that uncompressed, so compression would have bought characters nobody is counting for the price of two dependencies on a tree whose licence gate fails the build. The version byte is there so a future device that needs it can have it.

⛔⛔ AND A WHOLE RACK SAVES AS A FILE. "Can we save groups of VST3s and their settings as presets — or groups of stock plugins?" — yes, both, in order, with every device's controls, its bypass state and its plugin's own opaque state. It is Ableton's Audio Effect Rack and FL's mixer state, and nothing covered it: a recipe string is one device, and a vocal chain of four had no way to be saved at all.

Added — the gate suite, over the whole roster (Phase 7, TASK-193)

⛔⛔ No stock device allocates while it is processing, measured rather than assumed. Every built device, at every parameter extreme and with all its controls at maximum together, run through a counting global allocator — which lives in oscillate-audio's audio-thread suite because a process gets exactly one and that one is already there. ⚠ Watched failing against a device sabotaged to allocate eight floats. ▶ Section 3 of the build philosophy says no allocation on the audio thread; until this, that was a rule with a review behind it rather than a measurement.

⛔⛔ One suite, and it walks the registry rather than a list somebody maintains — so a device added tomorrow is in every gate without anybody remembering to add it. Fourteen gates: no allocation at four block sizes and every parameter extreme, declared latency against measured, silence in silence out, determinism per seed, note devices that report an overflow instead of dropping a chord, no stuck notes after an all-off, the drawn curve against the audio, the structural advantage, and no captured impulse response anywhere in the tree.

▶ Four of them assert an ABSENCE, and each is paired with a presence assertion — because a gate that asserts an absence passes trivially against a device that does nothing. The suite must first be shown to have run over a real roster, to have changed a real signal, to have filled a real note buffer.

Three real defects, found by writing it:

⚠ A frequency response is a small-signal quantity, and two devices needed that said out loud. Furnace answers −7.97 dB at 80 Hz to a −20 dBFS tone and −0.01 dB to a quiet one, because the first is being compressed. Oxide's wow and flutter move the read position, which is frequency modulation — a steady 4 kHz tone loses 3 dB into sidebands that no magnitude curve would ever draw. The gate holds modulation still and measures quietly, and says why in both places.

Added — the devices no plugin could be (Phase 7, AM-C)

⛔⛔ Three of them read the arrangement, and that is not a longer buffer.

▶ Reading two bars of audio ahead is latency, and every producer in the session pays for it. Reading the arrangement's structure two bars ahead costs nothing — the chorus is at bar 33 whether or not anybody has played it yet. No plugin format exposes a host's arrangement, which is why these can only exist inside a DAW that also wrote them. All three declare zero latency, and their gate is that changing the structure without changing a single sample changes what they do.

⛔ Three more re-roll identically from the project's seed, so a stutter that sounded right on the third pass is still there on the fortieth — and a re-roll is one undoable operation rather than a gamble. Feint, Wear and Skywave.

⚠ The "no plugin could be this" claim, re-checked against what shipped rather than against what was specified — Phase Gate 7 asks for exactly that, because an overclaim in a specification becomes an overclaim in marketing. ▶ Six of the eleven earn it: three read the arrangement's structure (Waypoint, Guard, Lantern) and three are deterministic from the project's seed (Feint, Wear, Skywave), neither of which any plugin format exposes. The other five — Gild, Toll, Vault, Cleave, Strand — are distinctive DSP and are described above on their own merits, with no claim that they could not have been written as plugins. The README said eleven and now says six.

Added — three drum voices that are modelled, not sampled (Phase 7, SP-02)

Added — what happens to a part before it is played (Phase 7, AM-D1)

⛔⛔ A note device now has somewhere to run, and until this it did not.

▶ Sixteen devices were on the roster with no seam to run in. A stock device could only write audio, so an arpeggiator had no way to emit a note; and the code that built a channel strip dropped every note device out of the chain, with a comment saying it did. The mechanism was missing rather than the devices being unwritten — which is the fifth time this project has found that shape.

Added — Oscillate is stereo, end to end (Phase 7)

⛔ Every audio path in Oscillate now carries two channels. It carried one, and that was not a limitation in a comment:

⚠ What is not built, said plainly.

Added — plugin hosting (Phase 6)

Added — the editors (Phase 5)

Fixed — ⛔⛔ THE WHOLE ROSTER WAS UNREACHABLE (Phase 7)

Fifty-four devices, their panels, their curves, their recipe strings — all built, all tested, and not one of them in the browser. A producer could not add a single one of Oscillate's own devices to a track.

▶ The plugin browser lists what the scanner found on disk, and a stock device is compiled into the binary rather than scanned — scan::describe_one refuses one by name. Nothing ever put the roster into that index, so device_add answered "no scanned plugin has the id hilt" for every one of the seventy-five, and the list showed none of them. ⚠ Every other gate was green, because every other gate asked whether a device worked. None asked whether anybody could reach one.

⛔ It is the fifth time this project has shipped a whole mechanism with nothing calling it, and the fifth time the question that found it was the same one: what would a producer actually press? The new gates in plugins::browser ask it — every device in the roster is in the browser, a stock device can actually be added to a track, the roster comes first and the scan is not lost — and all three were watched failing against exactly this.

⚠ A second half, in the interface. The format badge was format === 'vst3' ? 'VST3' : 'CLAP' — a two-branch expression over a three-variant type, so every one of Oscillate's own would have been labelled CLAP the moment they reached the list. It is a lookup now, so a fourth format is a compile error rather than whichever branch loses.

⚠ And a second reachability defect underneath it: the browser could not SEARCH them. § 1 is explicit — "the search index over that column is not optional; it is the only thing making an evocative name usable, and shipping the names without it makes the whole set worse than generic ones" — and the filter read the name and the vendor only. Vise is a compressor and nothing about the word says so. A device now carries what it is across the seam and the search looks at it, so typing "compressor" finds it.

⚠ The cache never carries the roster. It is a property of the build, not of the machine: a cache written by one version and read by another would resurrect devices that no longer exist, and the browser would offer one the registry answers None for.

Fixed — three gates that were watching the wrong thing (Phase 7)

Fixed — before any of the above shipped

⚠ Every one of these was found by the Phase Gate 5 review or by driving the built application, and every one was green in the test suite first.

Added — the arrangement (Phase 4)

Added — the mixer becomes part of the project

Added — the transport

Fixed

Added — recording, as a producer actually works (2026-08-26 and 27)

Added — the transport controls a producer reaches for (2026-08-27)

Fixed — the seams three reviews found (2026-08-27)

Removed

Fixed — what the phase gate found by driving the built application

⚠ Every one of these passed the full test suite and failed in a producer's hands. They were found by driving the release binary rather than a preview build, which is what the gate's hands-on pass is for.

Fixed — the model, and what a hostile file could do

Added — the project (Phase 3)

Changed

[0.1.0] — 2026-08-25

The shell, and a sound. Phases 1 and 2 together — the first rung where Oscillate looks like itself and opens a device, keeps time, mixes, meters and records. ⚠ Built and verified, not published (charter rule 5).

Added

Changed

Fixed

Changed — the splash and the icon, by owner decision (2026-08-25)

⛔ The splash is its own transparent window now. It was an overlay inside the application, which meant two things it could never escape: it could not be bigger than the application's window — 960×600 logical on a 2560×1600 display at 150 % scaling — and there was always something painted behind the cut-out figure. It is now splash.html, a separate page in a separate full-screen window with no background of any colour, built and sized in Rust from the monitor it opens on. What surrounds the Oscillate is your desktop.

⛔ The app icon is drawn again, not photographed. For one day the shipped icon was the splash figure cut out of its own matte; at 16 px a full-body photograph is mud. It is now a katana through a severed ring, generated from the design tokens by arithmetic — the ring for four producers, one session, the sword for the Oscillate, on the product's own −22° axis.

Changed — the splash's timing and behaviour

⛔ It is mandatory and unskippable, the way FL Studio's is. No skip button, no any-key listener. It plays the whole leap, strike and cut on every launch until you turn it off in Settings → Appearance, and that switch is only honoured once the splash has run through at least once on this machine — so a preferences file copied from another machine cannot skip a title card this one has never shown.

⚠ This reverses two of § F-21's rules, deliberately: "skippable at any point" and "never extends cold start". It now costs about two seconds. What survives is a ceiling — mandatory is not the same as unbounded, and a test still asserts it ends.

⚠ The old behaviour was worse than it looked. The sequence cut to the strike the moment the application was ready, and the application is ready almost immediately — so on any real machine the leap played about a fifth of itself and the sword never came down.

Found by the phase gate's own reviews, and fixed here:

Gates that could not fail, now made real:


Added — the audio engine (Phase 2)

Fixed — what Phase Gate 2's review found

⛔ Three of these were fully built and fully unit-tested, and did not work at all in the shipped application. They are listed in that spirit: the tests covered the pieces and never the seam between them.

Fixed — what a review of the fixes found

⛔ A second review, scoped to the fix commit rather than the bugs. It found six more defects, two severe, and one of them created by a fix. Written down because that is the argument for the pass existing at all.

⚠ Two tests could not fail, which is worse than not having them — both written at this gate, both now fixed: a metronome test whose input never entered the loop it was testing, and a per-strip meter test with one track, where the master leaked into the slot through the pan law and passed against the very behaviour it was written to reject.

Fixed — what the security review walked into

⚠ The /security-review found no HIGH or MEDIUM vulnerability. It cleared the meter scheme's CORS header specifically: register_uri_scheme_protocol is a webview interception rather than a socket, so the address is unreachable from off the machine. What it found on the way past was worse than what it looked for.

Changed — Windows audio, by owner decision (2026-08-25)

[0.0.1] — 2026-08-24

Foundation, the repository, and the CI spine. An empty window that builds, runs, and is defended by every gate this project will ever rely on — written before there is anything worth defending, because a gate added later is a gate that has to be retrofitted through everything built without it.

⚠ A tag, not a release. There are no downloads and will not be until v1.0.0. The installers for this rung were built, installed, launched and hashed, and then retained rather than published.

Added


The ladder

Version What it will land
v0.0.1 Foundation, CI spine, licence and EULA, and an empty window that already installs on three operating systems
v0.1.0 The shell and the audio engine — it looks like Oscillate and it makes a sound
v0.2.0 The project and the arrangement view — it is a DAW
v0.2.5 The editors — piano roll, drum grid, automation, comping, the Cut, Kata, Session Rewind
v0.3.0 Plugin hosting and the stock devices
v0.4.0 The generators, the File Explorer and Stem-to-Generators
v0.4.5 The web go/no-go, the drop onto the timeline, per-device faces and exclusive audio
v0.5.0 The room, core — serverless connection, presence, sync, chat
v0.5.5 The room, live — voice, video, screen share, Takeover, Kumite, the Split Sheet
v0.6.0 Identity — the Lyric Lane, Session Timelapse, session cards, the credits page
v0.6.5 The family look, the same in every Freally app — Blender-rendered knobs, faders and buttons, five skins in light and dark, interface sounds and a dark title bar — and one instrument library shared with Freally MIDI Master
v0.6.5 The family look and the right instruments — Blender-rendered knobs, faders and buttons, five skins in light and dark, interface sounds, a dark title bar, and one instrument library shared with Freally MIDI Master
v1.0.0b The public beta, the auto-updater, the docs site and the illustrated manual
v1.0.0RC Hardening, optimisation and the full review sweep, aimed by the beta
v1.0.0 Stable — and the first downloads that have ever existed