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.
- Settings ▸ MIDI lists your inputs. Pick one and it plays the armed track exactly as the on-screen keyboard does, and records into takes the same way. A dot lights when notes arrive, and the section says so when no track is armed.
- What it carries: notes with their velocity, the sustain pedal, and the panic messages (All Notes Off and All Sound Off), so a keyboard's panic button stops everything.
- ⚠ The pedal is held by Oscillate, not by the instrument. No instrument here reads a pedal, so Oscillate holds the note-offs until the pedal lifts. It works the same on every instrument, and a recorded take keeps each note for as long as it was heard, not only as long as the key was down.
- ⛔ Not carried yet, and not faked: pitch bend, the mod wheel and other controllers, channel pressure, polyphonic aftertouch and program change. The instruments can take bend and pressure; the live path from a keyboard to them cannot yet. Pitch bend, the mod wheel and pressure are to follow after 1.0.
- Unplug it mid-note and the note stops. The MIDI library reports no plug or unplug events, so Oscillate checks once a second while a port is chosen: a keyboard that has gone releases every note it was holding and the section says not plugged in. Plug it back in and it reconnects by name.
- The choice is remembered in
midi.json, besideaudio.json, and the port is reopened at startup before the window appears. Started with the keyboard unplugged, Oscillate opens normally and says so. - A port another application is holding says so, could not be opened, with the driver's own reason, and connects once the port is free.
- ⚠ Known limit: an unplug and replug inside the same one-second check can leave the port showing connected while nothing sounds. Choosing the port again fixes it.
Two on-screen keyboard defects, fixed on the way because both kinds of keyboard now share one path:
- Arming another track while holding a key sent the release to the new track, so the note on the old one sounded until Oscillate was restarted. A note is now released on the track it was played on.
- ***All notes off emptied the whole note queue*, throwing away the other keyboard's pending releases with it.
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
- ⛔⛔ In the application you install, arrangement clips, MIDI clips and automation never reached the engine, and had not since 27 August.
main.rscalled Tauri'sBuilder::setuptwice, and a second call replaces the first rather than adding to it. The first one started the playback worker, the Masking Radar and the loudness meter, so none of the three ran. Every test passed, because no test ran the builder. There is one setup now, and a gate (the_builder_has_one_setup.rs) refuses a second. - ⛔⛔ Every export played only the notes at the very start of the song. The offline render never moved its own transport. A companion-folder export carried the notes at the first tick with no note-offs, its volume automation frozen and its tempo-synced devices running at 120. Exports now play the whole song with its tempo map and its volume automation, and audio clips no longer run dry part-way through.
- ⛔⛔ Reopening a project left every VST 3 and CLAP plugin silent, and a plugin's own settings were never saved at all. Hosted plugins were loaded only when you added one or opened its window. They now load after every open, undo, redo, chain-preset load and peer update. Each plugin's state is captured before every save and restored when the project opens.
- ⛔ Effects restarted fifty times a second while the transport rolled. The playback worker rebuilt the whole render plan every 20 ms whether or not anything had changed, and every rebuild reset every stock device. Reverb tails were cut, compressors started over, held voices dropped, and each device glided back from its default settings. Now a plan is rebuilt only when something in it changed, and a rebuild carries the running devices across instead of replacing them.
- ⛔ The loop region never looped. The ruler drew it and the project saved it, but nothing told the transport. Playback now wraps at the loop end, and clip audio no longer runs past the loop end and then plays late.
- ⛔ Undo, redo, opening a project and a peer's edit did not redraw the other windows. The piano roll, the audio editor, an open device panel and every detached window went on showing the state from before, and a second trim worked out from a stale view could overwrite the first. Every change now moves one counter that every window listens to.
- ⛔ After File ▸ Open, the crash-recovery offer never appeared, and the next Save deleted the journal it would have offered. It is now offered whenever a project opens, and Save keeps the journal until you answer.
- ⛔ The security holes, next.
Security
- A peer's live edit could name a file on your machine, which Oscillate would then open. Snapshots from a peer were cleaned on the way in; single edits were not. Every inbound edit now has its file paths and plugin state stripped, exactly as outgoing ones always did.
- A network path in a project file or in a peer's edit could leak your Windows login hash when you hosted a room or read a take, because Windows signs in to a network share on its own. Every reader now checks that a path is a file on a local drive before any filesystem call, and refuses network shares and device paths outright.
- A room member could take over another member's identity by relaying a connection offer in their name, and with it their credits, the tracks handed to them and their Kumite bars. It is refused in the page and again in the room process.
- An opened project, or a peer's snapshot, reached the audio thread unchecked. An automation value of 1000 was +60 dB, and a gain of 1000 dB became infinity. Both are now validated when they arrive. Damage that cannot be repaired is refused, out-of-range numbers are clamped, and you are told what was changed.
- Each peer now introduces itself directly when a link opens, which is what lets a battle seat everybody. That introduction binds the link once, never renames it, and cannot claim somebody another link already speaks for.
- A pull request from a fork no longer runs on the self-hosted Windows build machine, which holds the updater's signing key.
Playback and the engine
- Stuck notes on Stop, on a jump of the playhead and at the loop wrap. All notes are released at each of those now, and a drum lane muted while a note was sounding no longer leaves it held.
- A note device's own delay was not compensated, so a part through Tread played late by exactly that delay.
- The playhead drifted from the audio through a tempo ramp, which was computed one segment late. A short locate moved the playhead and left the audio where it was.
- Repitched clips clicked where the file was read in chunks. Reversed clips stuttered past the end of their file. A fade-out was measured wrongly across a tempo change. A trim before bar 1, or on a repitched clip, read the wrong part of the file, and the tail of a split read from the head of the take.
- A reused mixer slot played the previous track's leftovers.
- Meters and loudness read the left channel only. A track panned hard right showed nothing, and the loudness figures ignored the right side. Both sides are measured now.
- Denormal numbers are now flushed on the threads that render, live and offline, so a long decay fading towards silence can no longer spike the processor. ⚠ It needed one new
unsafesite,denormal.rs, reviewed and approved by the owner;SECURITY.mdlists it. - A second preview could start part-way through, or not at all.
Export
- Muted tracks leaked into the master mix.
- After a track was deleted or reordered, exported parts landed on the wrong channels.
- Exporting disturbed live playback. It used the session's own audio streams, so stems could carry live audio, and Play started from the wrong place afterwards. An export now renders from streams of its own.
- The sample rate you chose was ignored. Everything was rendered and written at 48 kHz; it now renders at the chosen rate.
- AIFF files claimed twice their sample rate, so other software played them an octave up. AIFF-C float files now carry a complete header.
- 24-bit FLAC was scaled and dithered wrongly.
- Exported
.midfiles cut off repeated notes in other DAWs. A note's start was written before the previous note's end on the same tick. - ⚠ Exports are still rendered without VST 3 and CLAP plugins. See Still open.
Recording
- A 44.1 kHz input under a 48 kHz output recorded with gaps, about 8% of the take silent. The input now opens at the output's rate, or is converted.
- A take's latency varied with how long the input had been open.
- Count-in and punch takes landed up to a buffer late, and the block at the punch-out was dropped. A MIDI punch kept notes played before the punch-in.
- **A dropped file whose copy failed left a partial file in Imports.**
- Onset detection's minimum gap between notes was one analysis step too long.
Plugins
- Knob moves in a VST 3 plugin's own window never reached the sound, and parameter changes past 512 in one block were dropped.
- Undoing a device removal brings back its sound as it was.
- Removing a track closes its plugin processes, and a new or opened project releases the previous one's.
- A crashed or hung plugin could freeze the application. A faulted plugin is now stopped before Oscillate waits on it, and no lock is held while one starts. A fault in a plugin's control connection is now reported.
- A plugin you located by hand was never added to the index, and a rescan undid unblocks and located plugins added while it ran.
- A plugin that changes its own latency now gets a new, compensated plan.
- File dialogs ran on the main thread (open, save, save as, chain presets, Locate…, adding a plugin folder, choosing a preview folder, importing lyrics and audio) and could deadlock the window. They all run off it now.
The editors
- Dragging a note's end edge now changes its length, and dragging its start moves the start and keeps the end, as one undo step.
- A diagonal note drag, and a diagonal copy-drag, are each one undo step, and a
Ctrl/Altdrag copies instead of moving. - The automation lane under a clip that is not at bar 1 drew and wrote its points at bar 1. It now works in the clip's real place. Under a drum kit it no longer follows the hidden piano roll's scroll, and Save as Kata and Stamp cover the clip's own span.
- Silencing a span inside a quiet gain envelope swelled the audio around it. Splitting, cutting or left-trimming a clip now keeps its gain envelope where the audio is.
- Transient markers on a split or trimmed clip showed hits the clip does not play. A trim from the audio editor keeps an earlier trim's offset, in a detached editor window too. The audio editor no longer draws a stale window after a zoom, or a reversed clip forwards at sample zoom.
- In the arrangement, a selected clip scrolled out of view was dropped in the wrong place by a move or copy, and a left trim could run past the start of the audio and pull it early. A trim or fade previewed the whole selection while only the grabbed clip changed; the preview now shows exactly what the release does. A gesture the operating system cancelled was committed rather than abandoned.
- The drum grid's click-and-sweep worked only in the test browser, and a right-click no longer toggles a cell.
- Typing a value into a knob: 6 dB now means 6 dB, not full travel, and 50 in a pan box no longer means R100.
- The on-screen keyboard and a shortcut on the same letter both fired. Arrow keys in the browser list and on the ruler fell through to nudging the arrangement.
- Transport ▸ Home, End, Pointer and Blade did nothing.
- Tabbing out of a lyric line wiped its syllable timing. A lyric line dropped exactly on another line's start is no longer refused.
- The Pattern Rack refreshes after undo, redo and a peer's edit, and the pattern chooser opens on the current pattern.
- Arabic: the scrollbar, the splitter, the lyric lane and the session card were mirrored wrongly.
- Every sentence with a number in it has plural forms in all eighteen languages.
- Smaller ones: the
.midpreview's playhead ran at double speed; a dropped.midwhose first note is at zero landed one tick late; a CC-lane sweep zig-zagged; a zero-length fade handle could not be grabbed; a track-height drag stayed misaligned after a refusal; 32-bit integer WAVs, and 24-bit audio in a 32-bit container, drew and played as full-scale noise.
Devices and instruments
- Turning a knob on a factory instrument (Cello, Sub Bass and the rest) turned it into a default Wavetable synth, and undo did not bring it back. Factory instruments now open a panel, show their sound-knob row, and copy a recipe that names them.
- Chamber and Vault were unstable at their default settings. Their Low Decay control could make the reverb grow without limit. Freeze now neither fades nor grows.
- The Quench de-esser was always working, not only on the esses.
- Relay's repeats drifted later with every echo, Sparks clicked at its defaults, Gild clicked between blocks, and Ceiling set one gain per block. The shared oversampler glitched at block boundaries at the default buffer size, which touched every drive, amp and saturator.
- Feedback could run away in Shoal and Relay and latch the track into silence.
- Controls that did nothing now do something: Guard's Grid Bias, Vault's Diffusion, Coil's Tension across its whole range, Foil's Driver Position, Wear's Medium and Ceiling's Oversample.
- Also corrected: Lantern's Halve and Flush, Rein at a zero or moving echo time, Comb through zero, Strand's threshold, and the dry signal in Cleave and Strand, which was not delayed to match the processed one.
- No more stuck notes from Scatter, Turn, Hone, Stack, Approach, Hold, Tread, Runner or Revoice, and note devices now play the same whatever the audio buffer size.
- Hammer sounds at Hold 0 and no longer doubles in level on a retrigger; a stolen sampler voice fades instead of clicking; Skin's clap follows velocity; Sideband's Feedback works on all eight algorithms; Armory no longer crashes above note 127. Every instrument's glides are now smooth whatever the buffer size.
- The curves on the device cards were measured while the device was still gliding, so they were wrong.
Rooms and sessions
- A Kumite battle existed only on the machine that pressed Start. Peers' clips in their own bars were dropped without a word, and the turn order could differ between machines. A battle is part of the project now, started and stopped by the host, and the same on every machine.
- Takes: a take you already hold no longer counts against the host; takes arriving together are no longer lost; a transfer no longer causes dropouts; a half-written take file is removed.
- Every message on a link this machine had offered was handled twice.
- The microphone could stay live while its button said it was off, after another capture was stopped. Push-to-talk also fired while you typed in a text field.
- Session Rewind froze the application while it replayed. Undo to here undid one step and now undoes to the point; Return to now restores the live arrangement; a branch that fails says so.
- The Split Sheet names your own row, refreshes when another peer joins, and no longer credits a template's scaffolding to anybody.
- Edits that could break a session: a whole track-order write could drop tracks; deleting a track or a clip left pattern rows pointing at nothing, so every later rack gesture on that pattern was refused; a take still used on another track could be deleted; two machines could disagree for good when one edited a whole track while the other edited something inside it; a clip moved onto a track somebody else holds was checked against the wrong track.
- An edit made just after saving did not mark the project unsaved, and a quick edit could merge into an undo or redo.
- Parts of a file written by a newer Oscillate now survive a save.
- Rejoining a room no longer appends to the last session's journal.
- A timelapse of a session started from a template no longer opens with a bogus Room opened chapter; the connection check no longer reports a symmetric NAT that is not there; a fifth peer can no longer join.
Windows and settings
- Detached windows overwrote each other's settings. Settings are now written one key at a time, under one lock, and every window rereads them.
- Quitting while minimised lost the window's position and size.
- Two crash reports written at the same moment overwrote each other.
- The recovery journal is flushed when Oscillate quits, when you stop, and when a take lands.
- Yes, check at startup on the first-run question was silently refused, and a failed update install vanished without a word.
- Binding
Spacein Settings ▸ Shortcuts stole Play; an error screen replaced the title bar so the window could not be closed; a window could stay unscaled after a drag interrupted mid-way.
The updater and release tooling
- Windows would have had no update manifest at all. The release script looked for installer names Tauri 2 does not produce.
1.0.0would have matched the1.0.0-rcinstallers, so the final release's manifest could have pointed at the release candidate for ever.- A release-candidate tag could not build its installer, because MSI refuses a pre-release version. A pre-release now builds NSIS only.
- Release notes were cut out by a prefix match, so
1.0.0would have taken the release candidate's notes, and missing notes only warned. It is now exactly this version's section, and a miss fails the release. - Gates that could not fail, or ran nowhere: CI's parity check lacked its WebAssembly target; the download check failed every Windows-only release; end-to-end tests in two files could not fail; running the end-to-end tests rewrote the docs screenshots; the published changelog page carried duplicate section ids. A new gate refuses control characters in the changelog and the docs.
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.
- ⛔⛔ After a track was removed or a project was opened, the on-screen keyboard, a MIDI keyboard and the piano roll's note preview played the wrong track or nothing at all, and recording captured nothing. Live notes were addressed by a channel's id while the engine reads them by its position, and the two agree only until the first track is removed. Notes now find their track by position, re-read after every change, and a key held while a track is removed is released on its own track.
- ⛔ With the loop on, the MIDI notes at the loop start were dropped on every repeat after the first. Audio clips were fine. Note scheduling now knows the loop.
- ⛔ Room edits that touched several things at once could land differently on different machines, for good and silently (moving two clips while someone recoloured one, or deleting a track while someone edited its pattern). Each edit inside an operation is now resolved on its own, and a new gate replays three machines through every arrival order.
- ⛔ A peer could take over your
Ctrl+Zby reusing the id of your last edit. An id already held by somebody else is now refused, and it costs them. - ⛔ Saving could crash a plugin whose settings were larger than about 19 MB, and every save stored another full copy of the track in the project file, so files grew without limit. Plugin settings now travel as bytes and are kept once in the saved file: never in the undo history, never in the room.
- Plugins:
- A plugin that refuses its saved settings no longer has its defaults saved over yours.
- Latency a plugin reports no longer marks a project changed, adds an undo step, or wipes a collaborator's plugin settings.
- A plugin that is slow once is no longer treated as crashed, and one that answers instantly is no longer mistaken for a stalled one and reloaded.
- Undo and redo bring plugins back with the settings they had, including when a collaborator deleted the track and undid it, and when the plugin was missing at the time.
- VST 3 preset changes that move more than 128 parameters all reach the sound, and a plugin is no longer restarted every block. A VST 3 restart no longer throws away the other changes the plugin announced during it.
- Plugins follow the audio device's sample rate.
- File dialogs belong to the main window and open one at a time. Save As refuses if the project was edited or replaced while the dialog was up, and Open is no longer refused because a collaborator edited meanwhile.
- Playback and recording:
- Unplugging and resuming the audio device, or changing tempo while playing, no longer leaves the audio out of step with the playhead.
- Tracks compensated for a plugin's latency no longer drop out whenever the mix is rebuilt or the loop wraps.
- Keys you are holding are no longer cut when the loop wraps or the playhead moves.
- Notes played during the count-in no longer pile onto the take's first beat. A note played just ahead of the downbeat (within a block or 30 ms) is kept and lands on it.
- A slightly late input callback no longer leaves gaps in a recording, and input monitoring no longer drops out for a few milliseconds whenever the input backlog is trimmed.
- Notes with a Chance setting no longer repeat the same pattern after you move the playhead as the next loop pass would have.
- Multichannel interfaces no longer swap inputs after an overflow.
- High sample-rate inputs no longer alias into a lower-rate session.
- Export:
- MP3 above 48 kHz failed after the whole song had rendered. MP3 is now written at 48 kHz, and Settings ▸ Export says so.
- An export that is refused now tells you why under the File menu; it used to fail in silence.
- A look-ahead device no longer makes an export start late and lose its end.
- MIDI in an export no longer drifts early after a tempo change or ramp.
- The Dither switch was ignored: every 16- and 24-bit export was dithered even with it off. It is honoured now, and stems carry independent dither.
- Files dragged in from a temporary folder no longer leave copies in Imports.
- The Loudness panel no longer calls every figure "incomplete" at 88.2 kHz and above.
- Rooms and sessions:
- Anybody can end a Kumite battle, including one whose host left, and a battle is never saved into the project file; two Starts at once resolve to one battle.
- Joining a room on a project you also have on disk no longer deletes that project's crash recovery.
- "Undo to here" in a room sends every undo to the other producers, and takes back changes you redid after that moment.
- A take cut off by a crash lands the next time it is sent.
- Crash recovery keeps a collaborator's fader drag your own edit interrupted.
- Opening a project inside a room no longer lets older peer edits overwrite it; rejoining a room no longer costs you a strike.
- Two separate turns of one knob are two undo steps, not one.
- Detached windows no longer undo each other's panel sizes, shortcuts or export settings.
- File ▸ New no longer offers to recover the session you just left.
- A knob you turn on a device a collaborator is dragging keeps its value on every machine, and a fader or knob dragged away and back to where it started reaches the room instead of costing you a strike.
- A track or take deleted while a collaborator recolours or fades one of its clips is gone on every machine, and undoing a delete while a collaborator changes the same clip brings it back on every machine.
- Bouncing several MIDI clips to audio at once reaches the other producers, and a pattern that played two of them no longer goes on naming one.
- Ending a battle somebody else started no longer stops
Ctrl+Zworking, and a guest leaving the room no longer ends the host's battle for everyone. - A producer who rejoins keeps the edits they made while reconnecting, their name and battle seat come back as soon as their old link goes, and a second person claiming their identity can no longer take it over.
- A peer's refused edit no longer lets this machine reuse its stamp, and accepting crash recovery inside a room no longer lets older peer edits overwrite the recovered work.
- Editors:
- The first knob drag on an older project's instrument (a factory Cello, say) is one undo step, and in a room it no longer overwrites a collaborator's edits on that track.
- Under the drum grid, the automation lane lines up with the steps.
- A lyric line dropped exactly on another line's start is placed after it, and nothing else moves.
- WAV files whose format header names something other than plain PCM or float are refused with a message instead of played as noise.
- A pattern row whose clip was removed shows no clip, rather than a nameless one that cannot be opened.
- MIDI input: keys played while a track with nowhere to play is armed are refused rather than saved up and played later all at once; two identical keyboards can both be chosen; Settings ▸ MIDI no longer stalls behind a slow driver.
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.
- Recording ignores the loop. While you record, the loop is off, for audio and MIDI alike, and the take runs straight through past the loop end. The loop still applies to playback. Before, each pass of a looped recording was written back to back, and the MIDI take disagreed with the audio one.
- An export can be up to ten minutes long at any sample rate. It is refused up front, with a reason, if the machine does not have the memory to hold it. Before this build the limit was ten minutes at 48 kHz, and exports ignored the rate you chose.
- Ceiling's latency is 100 samples at its defaults (it was 72), and the Oversample selector now changes it. What Ceiling declares is its look-ahead plus its detector's own delay, so plugin-delay compensation lines it up.
- Comb: through-zero is on by default, and the sweep centre is one Delay step lower.
- Coil's Tension now maps 0 to 1.92 (was 1.2) and 1 to 0.75 (was 1.0); 0.5 is unchanged.
- Wear, Spool and Oxide: the 6 Hz component now answers Flutter rather than Wow. Wear's Vinyl and Converter media are new sounds; Tape is unchanged.
- Sparks: grains are now clean Hann windows when the cloud is sparse and at the defaults, so the grain shape changes even if you never touched it.
- Guard opens only within its Grid Bias of a note.
- Vise, Weld, Parry, Peen and Guard: the sidechain is filtered before it is measured, so the effective thresholds move by several decibels.
- Quench de-esses by different amounts.
- Edge: Attack and Release are now four times slower, as labelled.
- Triode, Rasp, Shear and Fold: bass and sag change at high oversampling. Furnace: the sag timing changes.
- Chamber, Foil, Coil, Vault and Baffle: the reverb's output taps change, so level and stereo image move. Vault's late field now starts at the pre-delay, and its diffusers are shorter.
- Shoal has less feedback when Voices is above 1. Rein's drive now fades in with its amount.
- Relay's repeats land on time, and only the echoes going round again pass through its new feedback limiter, so a hot input is not saturated. Spool's extra heads repeat at the times they are set to.
- Sideband now oversamples four times for real and declares 24 samples of latency. Hammer no longer doubles in level on a retrigger, and Skin's clap follows velocity.
- Integrated LUFS readings change: the gating now uses 400 ms blocks with 75% overlap, as the standard says.
- Exports are no longer dithered unless Dither is on. The switch was ignored before, so a 16- or 24-bit export made with an earlier build carried dither you did not ask for.
- Recording and input monitoring are about 5 ms later at a 10 ms audio period. The input now keeps a small margin so a slightly late callback cannot leave a gap in a take; the latency is constant.
Still open
- ⚠ Exports are rendered without VST 3 and CLAP plugins (TASK-098). The README in every export folder says so. Fixing it needs an offline render to get a plugin instance of its own, which is a design decision rather than a defect.
- ⚠ A MIDI keyboard's pitch bend, mod wheel and pressure are not carried yet. Notes, velocity and the sustain pedal are. A hardware note sounds at the start of the audio block that receives it, and recorded takes are not yet compensated for input and output latency.
- ⚠ Plugin settings are stored in the project file as a list of numbers, about three and a half times their size, and crash recovery restores them as they were at the last save.
- ⚠ Four rare races in a room can still leave machines different: a clip placed on a track at the same instant a collaborator deletes that track; an edit made in the moment a Kumite battle opens, before its sender saw the battle; a knob on an older instrument turned by one producer that reaches a third machine ahead of the first knob move that prepared it; and a clip whose delete is undone while a collaborator recolours it, which comes back everywhere but can keep the recolour on some machines only.
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.
PlacePatterntakes an optionalsource. One variant, not two: a siblingPlaceExistingPatternwould be the two-doors-onto-one-field defect thatArrangementEdit::SetClipLoopwas deleted for on 2026-09-17.Noneis the old behaviour exactly, and the double-click on bare timeline now says so.- The rack places a row's pattern at the playhead, taking the clip's own length rather than a guessed bar, and the clip is named after the pattern so two placements of Verse read as Verse twice rather than as two strangers.
- ⚠ A button, not a drag. Tauri's WebView2 hands every HTML5 drag to the OS file-drop handler before the page sees it, so
draggable+onDragStartis green under Playwright — Chromium has no Tauri layer beneath it — and dead in the shipped app. A button is also what a keyboard and a screen reader get for free. - ⛔ Placing reveals the arrangement. The rack and the timeline are the same centre slot, so from inside the rack a successful placement changed nothing on screen — the same "it looks like it does nothing" this release also fixes in New pattern, and a placement a producer cannot see is one they press again.
- ⛔⛔ The gate is the live reference itself: place one pattern twice, edit the notes once, and assert both placements changed — then Make unique on one and assert the other did not. ⚠ Anything less passes on a copy-on-place implementation, which looks identical on screen the day it ships and cannot be unpicked afterwards. Proven by writing that implementation and watching this test, and only this test, go red.
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.
- The new pattern is now the open one, found by set difference rather than by position:
patterns.at(-1)would be an unstated second claim about the order the model returns patterns in, true today and silent when it stops being. - A new pattern no longer takes a name that is already taken. The number came from the count, so over Pattern 1 / 2 / 3 with Pattern 2 deleted the next one was a second Pattern 3 — while the comment beside it claimed that shape was what prevented exactly this. The chooser lists patterns by name, so two that share one are indistinguishable in the only place a producer picks between them.
- ⚠ **The test that existed asserted the edit was sent.** It was. Every gate was green over a button the owner reported as dead — trap 8 almost word for word: the tests covered the pieces and never the seam. The new one asks what the producer is looking at afterwards.
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.
- Re-imported at
ff99972c, a pushed revision rather than a working tree, so what was copied could not move underneath the copy. The wholesrcwas taken rather than the three files that differed, which is also what proves nothing else had drifted —gitreports exactly those three, plus the header. - What came with it is one feature: a three-band tone control on each part's output, after the drive stage. Flat by default, and
is_flat()skips the whole stage rather than running three biquads at unity. - ⚠ No factory sound moved, and that is measured rather than argued. All 1,648 baked one-shots were fingerprinted before the import and re-baked after it: every one is byte-identical. The bypass is exact.
- ⛔ The crate's own header had been claiming for four commits that it was "identical apart from rustfmt line-wrapping" to the original import. That stopped being true in
b6149993and stayed on the page until0e36a6ab. It now names the four commits that moved it. This product's gate anchored an assertion to the retired sentence, so the gate moved to the one that replaced it — a header that no longer says the crate is imported is still a failure, and on a machine with no sibling checkout it is the only thing checking.
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:
- A default patch renders bit-identically to one with absurd band frequencies and every gain at zero — so a flat EQ cannot leak its centre frequencies into the output.
- A band that is moved is audible. ⚠ Without this, the bypass test above would pass just as happily on an EQ wired to nothing at all, which is this project's most-recorded defect.
- Each band does what its name says, measured as a magnitude response in dB. ⛔ A test asserting only that the output changed passes on an EQ wired up backwards. A low shelf lifts 50 Hz by 12 dB and moves 12 kHz by under half of one; the mid peak moves its own centre and leaves both ends; Q really decides the width. And every band at 0 dB is unity —
resonator.rswarns in its own words that a shelf and a peak from two derivations would not sum flat, and the engine's bypass is what would hide it. - A patch carrying
NaNand infinite fields still renders finite audio. ⛔ The gains are non-zero on purpose, so the flat-bypass cannot rescue it and the clamp has to do the work. Deleting the clamp really does put non-finite samples into the output bus — a patch arrives from a project file, a preset or a session blob, so those numbers come off disk. - A patch saved before the field existed still loads, and loads flat, which is every project file a producer already has. ⚠ It asserts the
eqkey was found before deleting it, so a rename cannot leave the test quietly loading a modern document and reporting success. resetclears the tone control's memory, which is the path all 1,648 baked one-shots take, back to back, through one engine.- The coefficients follow a sample rate change — see below.
⚠ 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
the_profile's saving-cost gate flaked, and its own numbers disproved its message. It read 1.007 µs/operation at 1 000 entries, 4.399 at 10 000 and 2.030 at 40 000, then failed saying "the cost per operation is climbing with the log". ⛔ A cost that climbs does not come back down — the largest sample was less than half the one that failed. The middle reading was a stall, taken whilecargo-test-parallelhad this binary sharing the machine with a 265 s and a 198 s neighbour.- ⚠ Three measurements now, keeping the smallest, and the bound is untouched. Scheduler noise, a page fault and an allocator growing its arena can only ever add time, so the minimum is the closest reading of the real cost. ▶ The proof that this is a better measurement rather than a looser one: it returns 0.484 µs/operation, which is the "about half a microsecond" the test's own comment has documented since it was written. The 2.0 bound keeps the 4× headroom it was given.
- ⚠ It matters most where it is not free — the same binary runs on the macOS job at a 10× billing multiplier, where a flake costs money and a re-run is a human deciding to spend it.
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
- Splitting an audio clip made both halves play the same bars.
tail_ofadvancedcontent_offset_ticksand the engine readsource_start_frame, so cutting an eight-bar take at bar five replayed bars one to four over five to eight and made the take's real ending unreachable. ⚠ The same path is cut, recording over a take, and dragging a clip's left edge.clipwindow.rsis the one place that adds the two together now. - Repitch plus Reverse walked off the end of the file into silence, and Repitch plus Repeat was a silent no-op. The editor has always disabled the mode for both; the engine never checked.
- A click about 85 ms into every repitched clip that started at the beginning of its file — the resampler assumed a kernel lead-in it had not been given, then jumped backwards by it at the next chunk boundary.
- A drum lane's M button and its choke group were saved and never played. A muted hat still sounded; two hats in group 1 still rang together. Both are the scheduler's now, so a closed hat cuts an open one off.
Controls that reached nothing
- All ten of Settings ▸ Export. Format, depth, dither, rate, the three Writes boxes, the stem pattern, the folder, normalise and the tail were written to
preferences.jsonand read by nothing: the one export in the product hard-coded 24-bit WAV and asked for a folder every time. A producer who chose FLAC got WAV, and nothing said so. ⚠ Normalise and the reverb tail are newly implemented rather than merely connected, and Writes → master can now produce the mix it always defaulted to asking for. - The clip menu was one item over a backend that could do seven things. Mute this clip — the engine has honoured
Clip::mutedsince Phase 5 and no control could set it, so the only mute was the whole track's. Make unique, Clear the comp. It opens on a blank clip now too. - Clip gain and the slip edit, both in the audio editor. The slip is the commonest audio edit there is and the product had no way to do it.
- A device group could never be renamed, so every group was called New group for ever. Double-click its name.
- The snap grid never persisted. It reverted to Adaptive on every restart and differed between the main window and each detached one. ⚠ It is a project setting now, stored as a mode rather than a tick count — adaptive is a rule about zoom and bar and triplet follow the time signature, so a number could never have held the answer.
- The dropout count can be reset — press it. A count accumulating since launch cannot tell a producer whether the buffer size they just changed helped.
- A hosted plugin's window can be closed, not only opened.
Ctrl+2did nothing. It was listed in Settings, printed in the Patterns button's tooltip, and flipped a preference the frame had stopped reading.- Settings ▸ Shortcuts drew a raw translation key as the Pattern Rack row's action name, in all eighteen languages.
Things that would have failed on somebody else's machine
- Every macOS build was broken.
include_bytes!pointed at a splash PNG deleted with the animation it belonged to — a hard compile error on the one platform a Windows machine never compiles. - The release workflow could never finish a draft. It stopped building on three runners and the updater manifest still demanded all four platforms, so the step exited 1 on every run. A release now declares which platforms it covers, and says in capitals what it leaves un-updatable.
- Two CI gates ran nowhere — the relay-kind check and the installer licence check — while the workflow's own header claimed every gate ran.
Honest about what is not built
- A CC lane says it is not played. The curve is drawn, committed, saved and read back, and no instrument reads it:
NoteMessagehas no variant for a controller and no device has a control to map one onto. The lane keeps working as an editor and no longer implies it is heard.
Changed
- The first-run update question blocks the keyboard as well as the mouse. Its scrim stopped clicks and nothing else, so
Spacestarted the transport andBchose the blade behind a dialog that was meant to be blocking the window. - Fader and knob drags send one write at a time. Each was its own task at 60 to 120 a second against a commit that is not atomic, so a drag that changed direction could apply its values backwards and leave the fader showing a value the hand had passed through.
Writes' default is all three. It was master-only — written for a bounce dialog that was never built — while the only export this product has is the companion folder, whose whole specification is stems and MIDI.
Removed
ArrangementEdit::SetClipLoop, a second seam door onto the field the audio editor's Repeat already owned. Two doors onto one field can drift.- Five registered commands nothing could reach, including a whole second recording path duplicating ordering that
transport_toggle_recordowns. InputRouting::midi_channel— neither written nor read anywhere, and a field in the file format implies a feature exists.- A dead second copy of the file browser's navigation, and a 1.4 MB duplicate of the status-bar logo that nothing imported.
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.
- Every
#[tauri::command]that is registered is called by something. The existing gate checked declared↔registered both ways and could not see the case the review actually found seven times. - Every
include_bytes!path in the Rust tree resolves — read as text, so it sees the lines a Windows build never compiles. - Every gate
ci:localdefines runs on CI, or says why not. - Every path
tauri.conf.jsonnames exists, and the installer artwork is really a bitmap. - The export defaults are the same number in Rust and TypeScript.
preferences.tsclaimed a gate kept them in step. No such test existed, and the two had already drifted. - The shortcut translations are now derived from the binding table rather than listed by hand. The hand-written list was eighteen ids out of date.
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.
- Ghost notes. The other patterns on the timeline, drawn behind the one you are editing, so a bassline can be written against the chords it sits under. ⚠ Aligned by the TIMELINE, not tick-zero to tick-zero: a ghost sits where it actually sounds, so dragging its clip moves what you see. They are drawn without note ids and so cannot be clicked, dragged or deleted — you cannot edit the wrong pattern by accident.
- Per-note chance. A note with
chance: 60plays six times in ten. ⚠ The roll is deterministic — the same note at the same place answers the same way every render, so a bounce sounds like the audition — and the answer changes each time round a loop brace, which is how you hear it. - Per-note velocity deviation. A half-width around the note's own velocity, so a hat line breathes without being re-drawn. Both fields have their own lane in the roll, beside the velocity lane.
- Arpeggiate, strum, flam and chop — four tools that turn one selection into many notes. ⚠ Strum moves each attack and leaves each end, so the chord still lands together; arpeggiate moves the whole note.
- Chord stamp. Draw one note, get the chord. The root keeps its identity, so undo and re-edit behave; an interval that would land off the keyboard is dropped rather than wrapped into the wrong octave.
- Glue. Joins notes of the same pitch that actually meet. Per pitch, never across the selection's outer bounds.
- Audition on click. Press a note and hear it — held while you hold, and silenced the moment you start dragging it, so moving a note is not a stream of pitches. ⚠ It sounds on the clip's own track, not the armed one.
- Multi-clip editing. Select several clips in the arrangement and a roll tool reaches all of them. ⛔ Per pattern, never over their union — reversing two clips as one span would swap their contents rather than reverse each.
- Scale lock (owner's ask). With it on, nothing lands out of key: drawing, dragging, every transform, and notes played in from a MIDI keyboard. ⚠ A drum track is exempt, because a kit's pitches are slots and not notes. Imported
.midfiles and a peer's edits are deliberately left alone — the lock guards what you play, and does not rewrite what you were given.
Added — three more the owner asked for while these were being built
[and]on a clip's edges. The cursor says which edge you are about to trim before you press, and reverts to the move cursor in the middle.- Bounce the selection to audio. Several MIDI clips become audio files in one press, and one undo puts every one of them back as MIDI. ⚠ The render honours chance and velocity deviation, so the audio is what you heard.
- The shortcuts page and the app agree. A test now compares the app's bindings against the documentation table in both directions, so a binding added without its row — or a row left behind after a binding moved — fails the build rather than reaching a reader.
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:
- ⛔ VST 3's sample position was a free-running counter, not the playhead. It agreed until the first relocate and then disagreed permanently, by exactly the distance jumped — so a plugin that trusts the sample-accurate clock drifted off the grid the moment you looped.
- ⛔ The seconds timeline was
0, while CLAP was told it was valid. - ⛔ The loop was zero-length, while VST 3 declared the cycle fields trustworthy.
- ⛔ The meter was always four-four, so anything that syncs to bars was wrong in 3/4 or 7/8; the bar always claimed to start at the top of the song; and the recording flag was always false.
▸ 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/.
- ✅ The DSP compiles to
wasm32-unknown-unknownfirst try, with no source changes —oscillate-synthand the 248-instrument factory included — and one modelled grand piano at full polyphony costs about 11% of a 128-frame block. - ✅ A VST 3 can reach a browser tab through the native bridge: a loopback round trip is p99 0.049 ms, about 2% of a block.
- ⛔ But it does not sound the same. WASM and native disagree in the first block by 2.4e-5 and settle around −60 dBFS, because 551 transcendental calls per block are computed by Rust's own
libmport on wasm and by the MSVC CRT natively. ▶ The remedy is bounded — thelibmcrate on both sides — and it moves the desktop's output too, so it is a decision rather than a patch.
⚠ 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
- ⛔ Every stock device card was labelled with its ID. A rack read
whetstone,vise,factory.cello— an internal identifier in the one place a producer looks to see what is on their track. - ⛔ The 248 instruments would have had no knobs on their cards at all. They are not
registry::ALLentries and resolve through a second lookup that the card path did not make. - ⛔ A knob on a card moved the wrong parameter. Card knobs skip choices, so a knob's position on the card is not its index in the device's list — and on any device whose panel opens with a mode selector, every card knob moved the control above the one it was labelled with. Silently.
- ⛔ The "project → renderable mixer" recipe was split. The loop wiring each channel to its stream slot lived inside one Tauri command and nowhere else, so anything building a mixer without going through it rendered silence.
Changed
- The arrangement view detaches into its own window. ⚠ Detaching and hiding are different things: hiding it would leave a DAW with nowhere to put a clip, so it still has no titlebar button — but a producer with two screens wants the timeline on the big one.
cargo test --workspacewent from 417 s to 166 s, and the one-shot bake from 168 s to 15 s, with nothing skipped and no sharding.
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.
- A waveform, for instruments and samples alike — min and max per column rather than every Nth sample, so the transient in a drum hit survives being drawn. The ground behind the played part changes colour as it runs, with a playhead and an elapsed / total seconds readout.
- Play and stop, a loop that can be turned off again, and ← / → for backwards and forwards. Reverse is the direction the source index walks, not a second buffer and not a second code path.
- Per-instrument note, velocity and length, measured rather than guessed:
CrucibleandMusterhold a C3 for over two seconds because a wavetable patch and a twelve-player section are still moving at one;Skinis held for 40 ms because a kick is all tail;Sidebandis struck, released and left to ring.
⚠ 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.
- 197 clips, 22.6 MB, baked in 110 s — mono 16-bit, level-matched to −1 dBFS so comparing two instruments compares their sound and not their gain, and trimmed where the sound actually stops rather than at a guessed budget.
- Previewing is now a file read: mean 0.19 ms, worst 0.5 ms. In the shipped binary over CDP: the list of 197 appears in 134 ms, and
Violin,TubaandGrand Pianopreview in 75 / 45 / 51 ms. - ⚠ Two gates hold it: every instrument has a clip and none is silent, and reading one stays under 50 ms.
⛔ 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.
- ← and → above the list are NAVIGATION, not playback. Back returns to the categories a step at a time; forward retraces the trail, or — with nothing ahead — opens the folder you were last in. ⚠ A history, not a mode flag: a boolean
showingCategoriescannot answer "forward into the last folder you were in", which is the whole point of the arrow. - The filter searches whatever you are looking at, and searches what a device is as well as what it is called —
compressorfindsVise, whose name says nothing about it. Pluginsmeans somebody else's plugins, as it does in Ableton. Oscillate's own seventy-five are in the scan index — that is what made the roster reachable at all — and are filtered back out here, or every one of them would appear twice and Plugins would be the only place anybody needed.- The preview player stays pinned below the browser, as asked.
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:
no_instrument_is_cut_off_while_it_is_still_soundingrenders each audition and measures the last tenth against its own peak — a ratio, because three of these six are decades apart in absolute level and an absolute threshold would only re-measure that defect. Within 30 dB of the peak at the end means the buffer ends mid-note.an_instrument_that_holds_a_note_demonstrates_for_at_least_two_secondsrequires two to four seconds of any device that holds its note for a second — the violin-and-brass case the owner named — while leaving a struck hi-hat short. Both watched failing:cruciblecut to 1.5 s reports "still sounding at −27.9 dB below its peak".
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:
Filings— a grain cloud from one clip, sixty-four grains from a preallocated pool. Its own gates found two defects before it shipped: the summed grain envelopes wereDensity × Grain Sizeloud, so thickening a cloud from forty grains a second to two hundred gained 14 dB; and a grain sprayed past the end of its source fell silent at full envelope, measured 23 dB down.Armory— eight zones of eight round-robins over a flat 128×128 key-by-velocity table built off the audio thread, so a note costs one index and no arithmetic. Equal power at the seams, because two zones of a multisample are different recordings and a linear crossfade between uncorrelated sources dips 3 dB. The voice pool gained a real filter envelope for it.Sift— spectral noise removal at 4096/1024, which meantStfthad to take its frame length at construction rather than from a crate constant. Power-domain subtraction rather than magnitude: a tone 53 dB above the bed loses 0.12 dB one way and less than a thousandth of a decibel the other.Yield— keeps a part clear of another track's register. Needed a cross-track pitch-occupancy seam:Contextnow carries a&[PitchSet], which is read-only and lock-free by type rather than by promise, plus three lookahead horizons so itsLookaheadcontrol means something at 0, 1 and 2 beats.Press— the MPE shaper, which neededNoteMessageto carryPressure,TimbreandBend. Keyed by note rather than by channel, because a member channel is a MIDI 1.0 transport detail. The sixteenth simultaneous note is refused and counted, never stolen.Ply— a harmoniser that follows the chord the arrangement actually has. Three defects came out of its gates: the input pitch detector read the loudest bin, which for a sung vowel is whichever partial the formants favour — a voice on middle C was detected an octave and a fifth high; bin-rounding left gaps when shifting up; and the dry path was not delayed to match the wet, so at the default mix it was a comb filter whose declared latency the graph would have compensated for by pulling the dry early.Rein— an effect performed by a MIDI clip rather than an LFO. Needed per-track velocity and note density in the arrangement index.Muster— one part played by a section of up to twelve, every player's timing, tuning, level and attack drawn once from the project seed and held.Quiver— eight chains under one keyboard, half-open zones with an equal-power fade.
⛔⛔ 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:
Skin,BronzeandSidebandeach readblock.notes.to_owned()— aVecallocated in the render callback on every block that carried a note.Crucibleloaded its engine fromprocess, and the imported synth builds a voice's modelled state the first time that voice is claimed — so the ninth simultaneous note allocated in the callback.
▶ 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:
Musterbuilds only the players that will sound. A section of four was paying for twelve, in the product as well as in the suite; the rest are built whenPlayersrises, inset_param, which never runs on the audio thread.Quiverbuilds only the chains that can sound. Seven of its eight open silent with an empty zone, and a chain that cannot sound does not need an engine.
⚠ 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.
- A file, not a line, and the distinction is load-bearing. A stock device's state is a handful of numbers. A hosted plugin's is an opaque vendor blob and is routinely megabytes; a serialiser that tried to make one pasteable would either truncate it or emit something nobody can send.
- ⛔ A preset naming a plugin you do not have STILL LOADS — keeping its settings, its state and its position in the chain, drawn as the placeholder that offers Locate…. Refusing the whole preset because one device is missing is how a shared preset becomes useless.
- ⚠ It records what it cannot promise. A preset carries the plugin version it was saved with, and recalling it against a different version restores the state and says so — a vendor is free to change what a state means between releases, and silently loading a mangled one is worse than a warning.
- ⛔ A preset never carries a latency figure. Latency is what a device reports when it loads; a file that could set it would let a stranger's preset misalign every track in your project, and you would hear it as your own timing going wrong.
- It is the same container as
.oscillate, deliberately — the magic, the version refusal, the chunk count checked before the directory is reserved and the checked arithmetic on every offset are all already right there, and the second copy of all that is the one that would be got wrong.
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:
- ⛔
Furnacedrew its tone stack and left out two stages of its own audio path — the bright cap across the gain control and the output transformer's roll-off — and was 2.9 dB out at 4 kHz against what it actually did. A curve missing a stage is a decoration of the device rather than a measurement of it. Fixed, and the curve now tracks the audio to a tenth of a decibel. - ⛔
Lanternlooked like it ignored the arrangement and did not: the gate's render was 8 192 samples long and its delay was 12 000, so nothing had come back yet. A gate that measures a tail has to run past it. - ⛔ "The declared latency is where the impulse peaks" is wrong, and two devices prove it in opposite directions.
Tolldeclares zero and peaks at 1 332 — it is a resonator, and its ring builds after an impulse that went straight through.Strand's window rings 517 samples before its peak. Both declarations are honest; the measurement was not. The gate now measures where the response begins and where it peaks, and asserts what each can prove: nothing may arrive later than declared, and nothing may peak earlier.
⚠ 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.
Waypointis already holding the chorus when it arrives, rather than reacting to it a few milliseconds late.Guardopens for a hit that has not happened yet. A threshold cannot: by the time the level is there, the transient is past.Lanternflushes a delay INTO the bar line, so the tail runs out rather than being cut. A cut is a click, and this device exists to avoid the mess a manual flush makes.
⛔ 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.
Gildlets you choose the harmonics. A waveshaper gives whatever mix falls out of its curve; a Chebyshev basis answers exactly the harmonic asked for, so the second and the fifth are separate controls that do separate things.Tollmakes any sound ring like a struck object, driven by the input's own transients — so a snare through a tube rings like a tube being hit by a snare, and a held note barely rings it at all.Vaultcomputes a reverb from a room in metres. The dimensions decide the reflections' arrival times and the tank's line lengths: a forty-metre room's first reflection really does arrive four times later than a three-metre one.Cleaveseparates the hits from the tone, by asking of every point in the spectrum whether it is loud now and quiet a moment either side — a hit — or loud here and quiet a bin either side — a note. It needs no threshold and nothing to learn: it is a statement about the shape of the sound.Strandrebuilds a sound out of the partials it can find in it, so the breath and the bow noise can be turned down separately from the notes.
⚠ 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)
Skinis a struck membrane, not a sample. Its partials are the Bessel zeros — the second is 1.59 times the first, not twice it, which is the whole difference between a drum and a pitched note. Where the beater lands decides which modes are excited, how hard it is decides how long it stays in contact, and a snare's wires only rattle once the head moves enough to throw them off it. A kick holds its pitch to a cent over a four-second decay.Bronzeis a dispersive plate. Sixty-four partials that pull apart as they climb, which is why a cymbal is a wash and a string is a chord. A hi-hat's pedal really clamps it: closed, the tail is more than twenty decibels shorter, and the move takes the time the Closure control says.Sidebandis four-operator FM across eight algorithms, oversampled, with its feedback operator held below unity in the DSP as well as on the panel — a self-modulating operator at one is a noise generator that never comes back.- ⛔ The same parameters render the same hit, every time, on every machine. Every generator is reseeded when the voice is reset rather than carrying on from wherever it happened to be, so a bounce sounds like what was heard.
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.
- Arpeggiate, harmonise, humanise, echo, correct and mutate a part, in a chain, before the instrument hears it. Eleven of the sixteen are built.
- ⛔ Nothing here can leave a note stuck. Every device is held to it: a corrector moves a note-off the same way it moved its note-on, a chord builder lets up every voice it sounded, and an arpeggiator's own notes stop when the transport does.
- ⛔ Every randomising device re-rolls identically from the project's seed, so a bounce sounds like what was heard.
Scatterdraws its pitch, velocity and timing from three separate streams, so re-rolling one leaves the others byte-identical — a producer can fix the timing of a part without losing the notes they liked. - A note chain that runs out of room says so, and the count is kept. An arpeggiator that quietly lost a chord would be a defect found a week later.
Temperboosts and cuts the same frequency and does not cancel. That is the whole point of a passive program equaliser and it is the part every imitation built from a parametric gets wrong: the boost and the cut share the inductor, so 6 dB of each at 100 Hz leaves +2.0 dB underneath and a −1.4 dB dip at 180 Hz rather than a flat line.Stratasplits four bands that sum back to what went in. Within 0.01 dB across every crossover region, which takes all-pass corrections on the bands that skipped a crossover — without them a multiband processor combs in exactly the places it exists to work on. Each band compresses downward and lifts upward from one detector, so the two halves cannot disagree.Stanceis bit-identical when you have not asked it for anything. Not "close enough" — the same numbers out as in. Every stage is skipped rather than run at unity, because a phase rotator and a crossover are all-pass networks, and an all-pass left running smears every transient on the track for a control nobody touched.- Three reverbs, one tank, and "two seconds" means two seconds in all of them.
Chamber,FoilandCoilshare a sixteen-line feedback delay network with an orthogonal mixing matrix, so the decay time is set by the decay control and by nothing else: measured RT60 lands within 5 % of what the panel says from 0.2 seconds to 12. Coildrips, and the drip is the mechanism rather than an impression of it. A spring carries low frequencies more slowly than high ones, so an impulse comes back as a downward chirp; each spring is 120 dispersive all-pass stages, which puts 38 ms of delay on 500 Hz against 7.6 ms on 4 kHz. The tank has one set of springs and two pickups, which is where its stereo comes from — not from running the whole thing twice.Foil's bass does not ring longer than its top, which is the one thing that separates a plate from a small hall and the thing every plate emulation gets wrong.Spool's repeats get duller and dirtier each time round, because the saturation and the head's gap loss are inside the feedback loop rather than on the way out. Tape or bucket brigade: the bucket path's bandwidth follows its clock down, so a long delay is a dark delay — which is what a bucket-brigade part actually is, rather than a delay with a filter after it.Combis a true through-zero flanger. The dry path is delayed too, so the modulated one sweeps through it and out the other side; at the crossing the two carry the same samples to better than 60 dB, which is a null a flanger that can only approach zero cannot reach. ⚠ That dry delay is real latency and is declared, so throwing the switch does not put the track early.Sparkstransposes without drifting, to better than a thousandth of a cent across ±24 semitones, and its grain windows sum to unity at every density rather than only at the one overlap where Hann windows happen to.Shoalkeeps its width through a mono fold. Widening moves the mono sum by nothing at all, at every width and every frequency, because the sum and the width are built separately rather than the width being a polarity trick that cancels when it is folded.Oxide's tape is duller at 7½ ips than at 15, and duller again as the head wears — because a playback head cannot resolve a wavelength shorter than its own gap, which is|sin(x)/x|and one line rather than a curve somebody drew. Its head bump moves with the tape speed for the same reason, and it distorts a rising edge differently from a falling one, which is what a magnetic loop does and a waveshaper cannot.Furnacegives way rather than just getting squarer. Its supply sags under what the output valves draw, so a chord pulls the rails down and the next note is quieter until they come back: 4 dB of compression at 6 dB past clip, with the Sag control having real authority over that number.Bafflehas no impulse responses in it, and that is literal. A resonant driver, a cone break-up peak, a ported alignment, a baffle-edge step, a microphone with a distance and an axis, and a short room — every one of them a coefficient rather than a file. A twelve-inch driver resonates at 75 Hz, a fifteen lower, an eight higher.
- They make a sound in the application, not only in a test. A device on a track is built when the render plan is, runs between the strip's sum and its fader — where an insert goes — and the delay it declares is what every other track is levelled against. ⛔ There is exactly one place a stock device can be constructed, and it hands back the latency in the same call, so the two cannot come apart.
- A panel you can actually turn. One shell for every device so they read as a family, and one panel drawn from the device's own description. ⛔ The panel never formats a number: it is handed the string the DSP's own formatter made, so the readout cannot disagree with what you will hear.
- Controls that behave like controls. 270° of travel, a drag ratio matched to a hand,
Shiftfor fine — which shows you the decimal it bought — a grip zone that is fine near the centre and coarse at the rim, detents that resist rather than snap so a bipolar control can still be set to +0.3 dB,Alt-click and double-click to reset, the wheel, full keyboard, and a drag that keeps tracking when it leaves the panel. - Every device says what it costs you. A device with a latency shows it, in samples and milliseconds, on its own panel.
- A MIDI clip makes a sound. An instrument on a track receives the notes the arrangement holds, sample-accurately within the block, and its output is what the strip's chain and fader work on. ⚠ Before this the notes reached the graph and stopped there.
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:
- A stereo file plays in stereo. A stereo import was averaged to one channel by the playback worker before it ever reached the render graph. A producer could import a stereo bounce, look at a stereo waveform, and be listening to a centre sum — with nothing anywhere saying so.
- Every channel strip has a pan that works. There was no per-channel pan in the product at all: the pan law was applied once, at the master.
- ⛔ A pan and a balance are two different laws, and Oscillate uses both. A mono source is placed at constant power, so panning it across does not change its loudness. A signal that is already stereo is tilted at unity, so a bus that passes it on does not attenuate it. Applying the placing law at every summing point is how a mix quietly loses 3 dB per bus — which it did, once, and two tests now say so.
- Delay compensation is per channel. A delay line is memory; one line shared by two channels would put each channel's history into the other's output, which is a mono sum with extra steps.
- A file with more channels than Oscillate has is folded honestly. Channels 1 and 2 are the pair; anything past them joins both sides at equal power. A six-channel stem loses something in a two-channel product, and losing it evenly is the answer that does not invent a mix nobody made.
⚠ What is not built, said plainly.
- Six devices: four audio effects, one instrument and one that plays hardware. ⚠ Every core effect and every note device is built. It was twenty-one before the sample seam, the note stream, the look-ahead, the cross-track occupancy seam and the MPE seam were closed; what is left is blocked on something a device genuinely cannot see, and each one says what.
Frequency Carvecompares this track's bands against every other track's audio. The occupancy seam carries what other tracks are playing — their notes — and not what they sound like: a per-track band-energy publication is a different thing, and it has to be filled after each track renders rather than before any of them do.Reverse Reverbbuilds its reverse tail from audio that has not played yet, which means a worker thread rendering the next four bars of this track's arrangement into a ring. Nothing in the graph renders ahead of the playhead; the look-ahead carries structure, not sound.Chain Morphmorphs between two full effect chains at coefficient level. A device can now contain another device's engine —Quiverholds eight andMustertwelve — but a chain of arbitrary devices has nowhere to keep its controls: a device's parameters are a flatVec<f64>, and a nested chain's would have to live inside it.Ember"plays back what this track just played", so it needs the track's own post-fader output captured into a ring. The sample seam hands a device a file; this needs a tap on the graph, which is the recording path's shape and not the sample path's.Salvage"builds a drum kit from your own folders", which needs the producer's library: it is blocked on the File Explorer, and Phase 8 is where it comes off this list.Outpostplays external hardware over a MIDI port with a measured audio return. ⛔ Oscillate has no MIDI port I/O at all —oscillate_audio::midiexists and nothing calls it — so this is blocked on a layer rather than on a device.Reinhas noPressure Depthcontrol, where § 2 lists one.oscillate_project's stored note carries a pitch, a velocity, a release velocity and a channel; poly pressure is not recorded, so the control would be a knob with nothing behind it. ⚠NoteMessage::Pressureexists as ofPress, so the DSP seam is there — what is missing is the stored note.Strandtransposes by moving bins rather than with a phase vocoder, where its specification asks for phase-continuous oscillators — so a transposed partial lands on the nearest bin centre and is smeared rather than clean. ⚠ It runs at 1024 points where the specification asks for 4096.Stftnow takes its frame length at construction —SiftandPlyboth run at 4096 — so this is a re-measurement rather than a limitation, and it has not been done.Sift's bottom octave comes down 8.4 dB of a set 12, where every band from 63 Hz to 16 kHz lands within half a decibel. At a 4096-point frame the octave from 22 to 45 Hz is three bins, and § 2's own 64-frame average is 1.4 seconds — too short for the bottom of a real noise bed to repeat. The gate asserts what it actually does and says why.HoneandStacktake their key from their own panel, where the specification also offers the project key and the chord track as sources. Both need the project, so neither is offered rather than being offered and inert.Approachbuilds its fill from what is held, where the specification asks for the incoming section's chord. The section boundary itself is real — it comes from the look-ahead the engine already computes — so the fill arrives in the right place with the right shape.Spool's hiss andShoal's clock whine only sound while something is going through. A real machine hisses whether or not anything is playing; a plugin that hisses on a silent track is one a producer removes the first time they solo a quiet passage.FoilandCoilbehave like a plate and a spring; they are not solutions of the physics. The specification asks for several thousand resonators from the dispersive thin-plate equation and for three tanks of modelled stages. What is built is the shared reverb tank shaped to behave like each, withCoil's dispersion being the real mechanism andFoil's mode spacing making no claim to follow the plate law.Stratahas no linear-phase crossover mode, where its specification asks for one at 4096 taps and 2048 samples of latency. So there is no Filter Mode selector on the panel either — a selector with one working position is worse than an absent one. The magnitude criterion is met on the LR4 path; the ±0.5° phase criterion belongs to the mode that is not here, and an LR4 tree cannot meet it.Oxide's hysteresis is not a Jiles-Atherton solve, andFurnace's tone stack is not the nodal circuit solution its specification asks for — so the stack's ±0.3 dB criterion is not claimed. What each of them is is written in its own module header, beside what is real:Oxide's gap loss and speed behaviour are the physics, andCoil's dispersion is the mechanism.Tempermodels the passive network's behaviour rather than solving it. The interaction a producer reaches for is measured against the specification's own numbers; the fourth-order nodal state-space underneath it is not the circuit, and its drive stage is an oversampled nonlinearity rather than a valve stage with a supply of its own.Whetstonehas four bands and no linear-phase path, where its specification asks for twelve and both phase modes.- The analyser and the gain-reduction trace are not LIVE. The curve a device applies is drawn, calibrated, on a real frequency-and-decibel grid, and the gate suite holds it to a tenth of a decibel of the audio. What is beside it — the gain reduction and the correlation — are the device's resting values, read from one built to answer the question and thrown away. A moving trace needs a tap on the engine's own instance pushing into a lock-free ring, and that is not built. ⚠ Said in
panel.rs's module header rather than left for somebody to wonder why the needle never moves. - The curve is the SHAPE, not the gain staging. Where a device dips and lifts is drawn and gated; how loud it is overall is not, because a valve amplifier's small-signal level is three triode slopes and a feedback loop, and putting that in the drawn curve would be the non-linear model written a second time somewhere nothing holds it to the first.
- The stereo devices that were blocked are not any more.
Stance,Chamber,Foil,Coil,ShoalandVaultwere waiting on a render graph with two channels rather than on anybody's time. That graph is here andStanceis built; the other five are now simply unwritten, like the rest. - A chain preset cannot be dropped on a track from the file manager, and the rack library it is the format for (200 first-party racks) is not written. What is built is the format, the save, the recall and the warnings — the menu asks for a file through a native dialog, which is the only path this application will ever trust.
- Recipe strings are one device. Pasting one onto a track to create the device is § F-18's second half and is not wired: the preview says which device the line is for and offers nothing, rather than adding something the producer did not ask for.
Added — plugin hosting (Phase 6)
- Oscillate loads the VST 3 and CLAP plugins you already own, each one in a process of its own. ⛔ A plugin that crashes greys out one device with its name still on it and a Reload button beside it; the transport keeps rolling and the project is untouched.
- ⛔ The application binary cannot load a plugin at all. The loaders live behind a Cargo feature that only the sandbox binary turns on — so the process you run does not link
dlopenfor a.vst3or a.clapand could not open one if it were asked to. That is a property of the build, not a rule somebody follows. - ⚠ A hosted plugin runs one block behind, which is how every out-of-process host works — and that block is added to the latency the device reports, so the compensation is for the delay that really exists rather than the one the plugin claimed.
- A plugin scan that never makes you wait. Each plugin is opened in a throwaway process with a timeout, the panel says which file it is looking at by name, and you keep working through it. Anything that killed the scanner twice is set aside — visibly, with what happened and a Try again — rather than silently disappearing from your collection.
- Where Oscillate looks is yours to decide. The formats' own standard folders are listed as the folders they are and can be switched off but not edited, and you add up to five of your own through the operating system's own picker. ⛔ The limit is said out loud with the reason attached — a scan starts a process for every plugin it finds. A folder on a drive you have unplugged is shown as missing and kept.
- The device rack. One card per device, in chain order, showing what it is (
STOCK,CLAPorVST3), its preset, and how much delay it costs you — in samples and in milliseconds. No host shows that well, and a producer tracking vocals needs to know which device is costing them 40 ms. - Bypass keeps the device loaded and keeps its delay compensated, so switching one off does not move every other track relative to it. That is the difference between bypass and remove, and it is the one people notice.
- Remove is one press of undo, and it comes back with everything you had set. Losing an hour of tweaking to a mis-click is not acceptable.
- Reorder by dragging, and a chain a device may not go into says why — "a note device has to come before the instrument" — rather than refusing in silence.
- Plugin-delay compensation, graph-wide. Every summing point levels what arrives at it, so a track with a linear-phase EQ on it and a track with nothing land together. ⛔ Proved by a null test: the same signal through a latency-reporting device and around it, cancelling to exactly silence — and the same test with the compensation removed, proving it is measuring something.
- A plugin that is not on this machine becomes a named placeholder that keeps its state and its automation and offers Locate… — and Locate… verifies the file you point at before it binds, rather than silently attaching your settings to a different plugin.
- Freeze and bounce in place, which are deliberately two different gestures: freezing releases the track's plugin processes and is exactly reversible; bouncing replaces the clips and removes the devices. ⛔ A freeze that no longer matches what is on the track is thawed, never played — hearing yesterday's audio is worse than hearing none.
- A conformance harness. The shipped sandbox binary is driven over a real pipe on every CI run — it has to answer, refuse a hostile block without dying, and name a plugin that is not there — and free plugins pinned by SHA256 are loaded and described where they are present.
Legal — the VST 3 licence, read rather than assumed (Phase 6)
- ⛔ Steinberg's VST 3 SDK is MIT, from 3.8 onwards, and
docs/legal/vst3-sdk.mdrecords the licence text from inside the pinned revision — not from a news article. Free of charge, usable in a proprietary product, no registration, and the only condition is the notice. - ⛔ The trademark is recorded separately and is declined. The VST logo is optional under MIT; Oscillate does not ship it, and "VST" appears in no product name.
- ⛔ So there is no C++ in this repository. The bridge the roadmap allowed for is not built: the Rust bindings ship pre-generated, so no SDK is vendored, no C++ toolchain is added to the build, and none is added to any of the three CI runners.
Added — the editors (Phase 5)
- The arrangement plays. The render plan reads the project's clips: an audio clip streams from its file at its place on the timeline, honouring its content offset, both fades and its gain, summed into its track. ⛔ The samples come off a disk through a ring a worker fills — never a file read on the audio thread.
- The piano roll. Notes drawn, moved, resized, transposed and deleted with the same gestures the arrangement uses; a velocity lane; controller lanes you draw a curve across, where one sweep is one press of undo; and a project scale that folds the rows out of key.
- The audio clip editor. Double-click an audio clip and it opens, at last — its own waveform at its own zoom, down to the sample: below the resolution of the arrangement's peak cache the samples are read from the file for the window on screen.
- A clip gain envelope drawn over the waveform. ⛔ A third control, kept apart from the clip's fixed gain and from the track's automation — three things most DAWs conflate at least two of — so you can duck one word of a vocal without touching anything else.
- Fade handles and both edges, which are the arrangement's own operations rather than a second set that could disagree with them.
- Reverse, normalise, silence a selection, and DC offset removal. ⛔ Every one is non-destructive: they are fields on the clip, in the undo log, never a rewrite of the file. Your sample library is not ours to edit.
- The playhead over the waveform while it plays, and a range you drag out to silence.
- Seven note transforms, each one press and one press of undo: invert, reverse, stretch, legato, quantize, humanize and move-to-the-scale.
- ⛔ Quantize has a strength, not a switch. At half strength a note moves halfway to the grid — how you tighten a take without flattening the performance that made it worth keeping. Swing pushes the off-beats only.
- ⛔ Humanize is reproducible. The randomness is seeded and the seed is part of the operation, so the same session opened on another machine — or replayed from its own log — gives back the same music.
- ⚠ Five have a keyboard shortcut. Invert and stretch do not: they need a number, and a key that guessed it would do the wrong thing quickly.
- One rule decides which editor opens, in one place: a MIDI clip opens the piano roll, an audio clip opens the waveform editor, and a MIDI clip on a drum track opens the drum grid.
- The drum grid. A clip on a track you have marked as a kit opens as a grid of lanes rather than a piano roll — one lane per sound, named, with velocity in the shading, per-lane mute and choke groups so a closed hat stops an open one. ⛔ Being a kit is a property of the TRACK, which is what you mean when you say "my drum track".
- Automation lanes for every parameter. Draw, pencil, line and curve tools; read, write, latch and touch modes; a curve handle on every point. ⛔ It is sample-accurate: the engine is told where each span of a block sits on the timeline, so a curve is swept correctly even across a loop point that falls in the middle of a buffer.
- Kata — a curve you can name and reuse. Stamp a shape anywhere, with its own amount, offset and variance; edit the shape and every stamp follows, because a stamp is a reference rather than a copy. ⚠ The variance is seeded, so a session sounds the same tomorrow.
- Comping with take lanes. Record over a part as many times as you like: every take keeps its own lane and nothing is overwritten. Swipe across the lanes to choose which take plays where, and the seams are real crossfades — both takes are read and mixed, at equal power, so the level does not dip in the middle of every join.
- Shadow Takes. A take you did not choose is never deleted. It collapses into a ghost lane and stays in the file until you explicitly delete that take.
- The Cut. Draw a blade stroke across the arrangement and every clip it crosses is split where it crosses — and the angle of the stroke is the crossfade: steep is a hard cut, shallow is a long blend, with the length in milliseconds shown while you draw. ⛔ The whole stroke is one press of undo.
- Session Rewind. A second timeline — not of the song, of the session. Drag it and watch the arrangement rebuild itself backwards. ⛔ Scrubbing is a view: nothing is changed until you choose one of the three things offered when you let go — return to now, undo to here, or Branch, which forks the project from that moment and keeps both.
- Panels you can pull out onto a second display. The File Explorer, Generators, Editor, Mixer and Room each open in a window of their own, with their own titlebar and their own status bar. ⛔ Every window shows the same session: an edit made in one appears in all of them, the track you select in the detached mixer is the track selected in the arrangement, and double-clicking a clip opens it in whichever window the editor is in. ⚠ Closing a detached panel puts it back where it was rather than destroying it, and panels always come back inside the application when you restart — a window restored onto a display you have unplugged is one you cannot reach.
- A clip can repeat to fill its own length. The field had been in the file since Phase 3 and the arrangement had been drawing the repeat marks since Phase 4; now there is a button, in both editors, and the material actually repeats.
- Find the hits in a take. One press marks every transient on the waveform — where a producer cuts, lines up to the grid, or trims to the first note.
- MIDI clips are scheduled. A pattern's notes reach the audio thread, on their beat, transposed by the placement they sit in. ⚠ There is nothing at the end of the stream to voice them yet — the instruments are Phase 7 — but the scheduler is there and proven, which is what stops that phase discovering it needs one.
- A file recorded at a different sample rate than the session now plays at the right speed. A 44.1 kHz sample in a 48 kHz session used to come out about a semitone and a half sharp and 9 % short, with nothing to say why. ⛔ The conversion is a windowed-sinc interpolator that filters to the lower of the two Nyquists before it decimates, so nothing folds back into the band you can hear. ⚠ The editor still tells you both numbers: a resampled file is not a native one, and if you are chasing the last half-decibel of a master you are entitled to know which of your sources are being converted.
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)
- ⛔⛔ Two Rust types were writing one TypeScript file, and the bindings gate could not see it.
oscillate_dsp::device::Domainandoscillate_project::track::Domainboth generatedsrc/bindings/Domain.ts, so whichever crate ran its tests last won — the working tree flickered, and the check reported drift on a tree nobody had touched. ⚠ The first fix removed one crate'sexportand appeared to work, because the ordering happened to favour the project; it came back the next time aoscillate-dsptest ran last. ts-rs writes a type whenever anything exported can reach it, and an exportedDeviceSpeccarries adomain. ▶ The DSP's is nowDeviceDomainin TypeScript, andcheck-bindingsrefuses any two exported types that would land on one path — watched failing against exactly this defect. What that gate would eventually have reported is no drift on a tree that had really changed. - ⛔ A chain preset was written with
fs::write.SECURITY.mdstates that Oscillate writes beside a target withO_EXCLand renames over it — so a symbolic link planted where a file would go is refused rather than written through — and that had exactly one implementation, inside the project save. The second writer had none of it. ▶io::write_atomicis now that implementation, and both callers go through it. - ⚠ The locale gate called a correctly-declared plural a missing key. i18next resolves
t('a.b', { count })througha.b_oneanda.b_otherand never througha.b. The gate now understands that and requires both forms — a key with only_otherrenders as the key itself for a count of one, which is the exact failure it exists to catch, only harder to see.
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.
- The arrangement went silent the moment you pressed Play. A pass that updates automation while the transport rolls was replacing the render plan fifty times a second with one that had no clips attached to it.
- Panels detached to a second display could not see the session. Every window is its own little browser with its own copy of everything, and nothing was telling the others when something changed.
- Detaching a panel could hang the application, and a fresh project opened to a window full of empty panels.
- A file with more than two channels stuttered, and could stop all playback for the rest of the session if the clip was set to repeat.
- Automation drawn late in a session played at the wrong level and stepped instead of sweeping — right at the top of the song, wrong everywhere else.
- A stamped Kata neither sounded nor drew, though it was saved and counted.
- Notes after a long one did not play. A held note at the top of a bar blocked everything behind it until it ended.
- Most comp seams were hard cuts rather than the crossfade you set.
- The transient markers on a trimmed clip would have been in the wrong place.
Added — the arrangement (Phase 4)
- The arrangement view. A timeline you can work in: a ruler with bars and beats, a grid that follows the zoom, clips drawn from their own content, and track headers down the side. Zoom is logarithmic and anchored under the pointer, so the bar you are looking at stays where you are looking.
- Clips drawn from what they actually are. An audio clip draws its real waveform from a cached peak file; a MIDI clip draws its notes, at the pitches that are in it. ⛔ A clip whose content has not arrived yet draws its body and nothing else — a placeholder waveform is a lie about audio.
- Move, copy, trim, split, fade, mute, loop, rename and recolour, and every one is a single press of undo however many clips it touched.
Alt-drag copies. Trim from either edge; the material moves with the edge rather than sliding under it. The blade cuts every selected clip in one stroke. - The playhead. A cyan line — the one colour reserved for now — drawn from the position the audio thread actually reached, sixty times a second. ⛔ Never interpolated ahead of the engine: a playhead that predicts is wrong every time a buffer is late, and it looks like the audio is stuttering when it is not. Click the ruler to locate, drag to scrub.
- A timeline that grows. The ruler extends as you place content near its end, and you can drag the end marker further out by hand. ⛔ The ceiling is two hours, said out loud in the toolbar and reported when you reach it — never a drop that silently does nothing.
- Track groups. Drag a track header onto a group and it folds in. ⛔ A group is a real bus with its own fader and chain, so muting it silences everything in it — not a drawing convenience.
- Markers, the loop brace, tempo and time signature, all on the timeline rather than in a dialog.
Alt-drag the ruler to draw a loop. - Snap, including adaptive — the grid follows the zoom, so it is always the finest division you can actually see.
- 200 tracks × 2 000 clips. Measured, not claimed: a screenful of a 400 000-clip project is answered in 2.4 ms against a 16 ms frame.
Added — the mixer becomes part of the project
- Every mixer control is now an undoable change. A fader move, a mute, a solo, an arm, an input choice and a rename all go into the operation log, into the file and into the crash journal — because a track is a project entity and the mixer is a projection of it. ⛔ Before this, nothing in the interface committed an operation at all: Undo and Redo were permanently greyed out and the recovery offer could never appear.
- Select a strip and press
Deleteto remove that track and everything on it, in one undo press. Selection moves to the next track. ⛔ The master cannot be deleted. - Drag strips to reorder them, in one undo press. The order is the project's, so the mixer, the arrangement and the saved file always agree.
- The master fader is saved. It was the one control in the mixer that was neither undoable nor written to the file — a session mixed to −3 dB opened at unity the next morning.
- Double-click a strip's name to rename the track.
Added — the transport
- Record starts on the press and stops on the press, and pressing Record again turns it off. ⛔ There is no count-in unless you switch one on in Settings → Recording, where it offers 3–5 seconds or 1–4 bars.
- Play, Stop, Record, Arm, Input, Mute and Solo all answer immediately. Each press is one round trip instead of two, and the button acknowledges the press while the engine's answer is in flight. ⛔ What it draws is still what the engine published — the acknowledgement is of the press, never a claim about the audio.
- The transport moved out of the mixer to sit over the arrangement, so hiding the mixer panel no longer hides Play.
- Double-clicking a
.oscillatefile opens it, whether or not Oscillate is already running.
Fixed
- **The status bar said what a panel was for only over its titlebar.** One pixel below the title it fell back to the idle sentence. It now reads the same from anywhere inside a region.
- The mixer's fader looked like a square. Its cap was the same colour as the strip behind it, so the control had no edge across its lower half. It now has a groove, a cap with a grip and a scale — and the contrast is measured against every background in both themes rather than eyeballed.
- A resting mouse silenced the keyboard. With hints on whole panels, a mouse left anywhere in the window out-ranked the keyboard for the rest of the session — blank for exactly the people who most need the status bar. The gesture that moved last now answers.
Added — recording, as a producer actually works (2026-08-26 and 27)
- A finished take becomes a clip on the armed track. It used to write a
.wav, report where it was, and leave the timeline empty. - Each take is its own clip, and two takes back to back both land — so the material for comping is there rather than one take quietly replacing another on the disk.
- A new take replaces what it covers. Punch in over an existing take and the old audio stops dead where the new one starts. ⛔ Nothing is destroyed: the displaced clips are trimmed and split, every sample stays in its source file, and one press of Undo puts it all back.
- The waveform draws while you sing, growing under the playhead — from the engine's own metered peak, so what you watch and the level on your strip cannot disagree. A silent take still draws its clip, at its real length.
- The metronome makes a sound. It had a project setting, an operation and an engine flag, and no path joining any of them — the clicks were computed every block and read by nothing. There is now a voice, a switch in the transport strip, and a count-in you can hear.
- A count-in you can see. The seconds left before recording starts are shown where you can catch them out of the corner of an eye, counted by the audio thread rather than by a timer on the page.
- The arrangement says where the playhead is — bar and beat, the time, where the track ends, and the two-hour ceiling. It used to say only how long the project was.
- One track selection across the mixer and the arrangement, so Record cannot capture into a track you were not looking at.
- Stop twice returns the playhead to the beginning.
- Fades cannot cross. Pulling a fade-in past the fade-out clamps against the room the other leaves.
- Track names are capped and legible. The arrangement's headers give the name its own line — six 24 px buttons in a 196 px header had left it about ten.
Added — the transport controls a producer reaches for (2026-08-27)
- A metronome you can watch. An
Mbetween two dots, the fill swapping from one to the other on every beat, so the pair reads like a conductor's hand rather than a light blinking. ⚠ The dots are the size of the Record button's and they snap rather than fade — a beat is an instant, and a metronome that eases into one reads as late even when the click is exactly on time. ⚠ Drawn from the beat the audio thread actually reached — a timer at the tempo drifts against the engine within seconds, and a metronome that disagrees with what you hear is worse than none. - A loop brace you can work. One is already there when a project opens — bar 1 to bar 5, switched off — so you never have to discover how to make one. Drag the connector and the whole loop slides, keeping its length; drag either handle and only that end moves. ⛔ They can never cross: each clamps at one bar of the metre in force where it is.
Ctrl/Cmd-click turns the loop on and off, and every one of those is a single press of undo. - Click anywhere in the arrangement to put the playhead there, not only on the 24-pixel ruler. ⚠ A drag is still a marquee — they are told apart by whether the pointer moved.
- The metre is editable, beside the tempo where it belongs. It has been in the model since Phase 3,
bars.tshas read it and the ruler has drawn it, and nothing in the interface could change it.
Fixed — the seams three reviews found (2026-08-27)
- A gesture's later edits were validated against the project as it was before its earlier ones. And once that was fixed, trimming the left edge of a clip that ended on the two-hour ceiling was refused: the intermediate state ran past the end even though the final state did not.
- The snap grid ignored every time-signature change after the first. A session that moves to 6/8 at bar 33 snapped the rest of the arrangement to 4/4 bars — and even the right grid was counted from tick zero rather than from where the metre changed, so clips landed between the bar lines you could see.
- Scrolling fast could leave the timeline showing bars you had already scrolled away from, and claiming to have loaded ones it never did.
- The row bands inverted as you scrolled, which is the row-following they exist for.
- A cancelled track-height drag left the header the wrong size for the rest of the session.
- The recovery journal could silently discard everything after an oversized record and report the file as an ordinary crash truncation.
- A
.wavcarrying an emptyLIST,factorPADchunk beforefmtwas rejected as truncated — no waveform, and the clip played silence. - A reply of the wrong shape took the window down rather than being refused.
Removed
- The arrangement's file-drop target, which could never have worked. The webview never sees a file drag — Tauri owns the drop — and even if it did, a path supplied by the page is exactly the thing this product refuses to accept. ⚠ Importing audio is unaffected: it is reachable through the file picker. A real drop has to be handled in Rust, and that is a design decision rather than a bug fix.
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.
- ⛔ A mixer edit was invisible to Undo, and to the unsaved-changes marker. Adding a track put a strip in the rack and reached nothing else —
File ▸ Undostayed greyed out, the dirty dot never lit, and the arrangement's track headers still showed the tracks from before. A producer who spent a session in the mixer and quit was never told they had unsaved work. Every command that changes the project now answers with the whole seam. - ⛔ Undo threw, and did nothing.
project_undo,project_open,project_newandproject_saveanswered with a bare summary while the front end expected the whole seam, so undo failed silently inside the code that landed it. Opening a project also never re-read the mixer from the new project's tracks. - ⛔
File ▸ UndoreadUndo {{gesture}}. With nothing to undo, the menu drew its own template. There is now a separate label in all eighteen languages — an empty interpolation is not enough, and leaves a dangling colon in Russian and a stranded particle in Japanese. - ⛔ Every fader in the mixer drew its scale with no unity mark. Unity was matched against a fixed scale position with floating-point equality, and the mixer computes its own from the travel, so nothing ever matched. The mark is now drawn at unity rather than at the nearest tick, which also fixes faders whose unity sits nowhere near one.
- The count-in only reached the engine if you opened Settings. It was pushed by the panel that draws the picker, so on every launch the engine counted in by nothing until that screen was visited.
- The arrangement re-rendered sixty times a second while stopped, rebuilding every track row, for a playhead position that only two buttons read.
Fixed — the model, and what a hostile file could do
- ⛔ A
.wavpath inside a project file was opened without being checked. A.oscillatearrives by download or from a collaborator, and a hand-edited one could name a path on another machine — which Windows would authenticate to automatically, leaking the producer's credentials, on nothing more than opening the file. Oscillate now refuses any path that is not a plain local file before it opens it. - ⛔ An import command accepted a file path from the interface. It is now taken only from a native picker, like every other path in the product — and a test reads the source to keep it that way.
- ⛔ Removing a track could lose its clips for good. A removal built outside the normal gesture recorded no way back for the clips it deleted, so undo restored the track empty. It is refused now.
- Deleting a group left the tracks inside it pointing at it. The mixer and the arrangement both drew a group that no longer existed. Its children are lifted into the group above instead.
- A recording cut short by a crash imported with no content identity and a length taken from its header rather than from what was actually there. It is refused, with a sentence saying what happened.
- A project could switch its own two-hour limit off with one edited field.
- The peak reader sized a buffer from a number in the file.
Added — the project (Phase 3)
- One instance of Oscillate, not several. Launching Oscillate while it is already running brings the existing window to the front instead of starting a second process. ⛔ Two Oscillates would fight over the audio device, over
preferences.jsonand over the takes folder — and the first of those shows up as a banner that reads like broken hardware. - The operation log. Every change a producer makes is one
Operation, and undo, redo, collaboration, Session Rewind and the authorship ledger are four readings of that one log. A compound gesture is one operation — dragging forty notes takes one press of undo, not forty. - Unlimited undo and redo, per author.
Ctrl/⌘+Zundoes your last change and never somebody else's, from the first commit, before there is anybody to share a session with. 10,000 operations measure 5.8 MB and the slowest single undo 7 µs — against budgets of 200 MB and 16 ms. - Patterns. A clip references its content and never owns it — for MIDI as well as for audio. Place a four-bar drum part thirty-two times and there are thirty-two clips and one pattern: edit it once and every placement changes. Make unique is an explicit action that forks it.
- The
.oscillatefile. A versioned container: a JSON manifest plus binary chunks. ⛔ Every length is bounds-checked before it reaches an allocator, so a corrupt or hostile file errors with a reason and never aborts the process. Every prefix of a valid file and every single-byte mutation of one are tested. - Save, Save As, Open, and a session history you can act on. Recent projects can be opened, revealed in your file manager, forgotten one at a time or cleared — the list is a record of what you have been working on, so it is yours to clear. A project whose file has moved greys out and keeps its row, so plugging the drive back in restores it.
- A default session. Save the arrangement you always start from — tracks, names, colours, routing, device chains, tempo and metre — and
New Projectopens it. ⛔ Never your clips and never your audio. Reset to the factory default puts Oscillate's own back. - Crash recovery to the moment of the crash. Every operation is journalled to a sidecar as it happens, so there is nothing to do at crash time — the work is already on disk. ⛔ Recovery is offered, never applied: your saved project is not written until you say so. The window a power cut could cost is stated in the File menu rather than implied.
- A file from a newer Oscillate refuses to open, names both versions, and offers read-only. ⛔ And a field this build does not understand is carried through a save rather than silently dropped.
Ctrl/⌘+N,O,S,Shift+S,ZandShift+Z, all remappable, and all ignored while you are typing in a field.
Changed
- A save never truncates the file it is replacing. The new project is written beside the old one, synced to the platter, and only then renamed over it — so there is no instant at which the path holds half a project.
[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
- A borderless window with its own titlebar and − □/❐ ✕. The drag region excludes the controls, double-click maximises, and aero-snap keeps working because the drag is handed to the operating system's own move loop. Window position and size are remembered per machine — and never restored onto a display that is no longer connected.
- The panel system: File Explorer, Generators, Editor, Mixer and Room, each shown and hidden by an image button that is the region's state, on
Ctrl/⌘+1…5. - Twelve panel glyphs, drawn in-house on a 24 × 24 grid in
currentColor, and a gate that renders every one of them at 16 px and fails if any two share a silhouette. - The layout frame — the grid from the design, keyboard-operable splitters, and regions that collapse in a documented order as the window narrows, with the button for a region that cannot fit disabled and saying why.
- Proportional resizing: one scale factor, computed in one place, with type scaling more slowly than geometry, swept at eight widths from 1280 to 3840 for dead gutters and undersized hit targets.
- Three themes states — system, light and dark — remembered per machine, and the swap rides the cut.
- The cut: one diagonal wipe, on one axis, used by every modal, every panel and the theme swap. Under
prefers-reduced-motionit is a different path rather than a shorter one, and the wipe layer is never mounted. - The Settings modal with all ten sections,
Esc, a focus trap, live apply and per-machine persistence. Sections with no subsystem behind them yet say so rather than drawing controls that change nothing. - A remappable keyboard shortcut table — one table, read by the handlers and rendered into Settings, with conflicts refused by name.
- Six core controls — Knob, Fader, Meter, Toggle, Selector and NumberField — sharing one gesture, with meter ballistics that are pure and tested frame by frame.
- The splash: the Oscillate leaps, the blade strikes, and the cut wipes it away to reveal the arrangement. Any key or click skips it, it never extends cold start, and the 2 MB artwork is downscaled at build time.
- The first-run agreement, rendered from
EULA.mditself and recorded against a hash of that text, so a materially changed agreement asks again. Nothing works before acceptance — including every keyboard shortcut. - Bug and crash reporting. A Rust panic writes a report before the process goes; a render failure writes one and leaves playback running. Reports are local, shown in full, capped at the newest twenty, and redacted of every absolute path and address. ⛔ Nothing is ever transmitted automatically, and open a pre-filled issue hands a URL to your browser rather than making the request.
Changed
- The
bindingsgate is a real drift gate rather than a test that overwrites what it checks: it snapshots, regenerates and compares, and fails when the export produces nothing at all. - The Playwright gates select by project rather than by naming spec files — a new spec used to run locally and never in CI.
Fixed
- The cut's mask animation ran the wrong way, so every modal, every region and the splash faded to completely invisible about 260 ms after appearing, while remaining clickable. Found by eye; now pinned by a keyframe assertion and by tests that compare real pixels.
- A preference changed within 250 ms of quitting was lost, because the write is debounced and nothing flushed it on the way out.
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 splash bundle is 0.7 kB of JavaScript and 1 kB of CSS, entirely separate from the application's 189 kB. It is the first thing decoded on a cold start.
- The jump is a jump. It enters from below the bottom edge, rises to an apex, and falls back down — closing on you continuously through both halves, 0.13 → 2.0 scale, finishing well past the edges of the screen. The keyframes are samples of two smooth curves rather than hand-placed poses.
- ⚠ The application's window stays hidden until the splash hands over, and Rust arms a watchdog so a page that never calls back cannot leave a running process with no window at all.
⛔ 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.
- The figure is far bigger and genuinely leaps at you — absolutely centred rather than a flex child fighting the wordmark for the column, growing 2.7× across the leap and ending nearly full height.
- The splash is dark in both themes. On a light machine it was a small figure in a white field; the product's own line is a dark studio with one blade of light in it. It is the one surface that does not follow the theme, and
splash.contrast.test.tschecks its own colours instead. - The artwork is generated at full resolution (1531 px tall at 2×), because it is now displayed large enough for the old 1280 px asset to soften.
- ⛔ The splash no longer wipes IN, only out. It is the first surface on screen with nothing behind it to be revealed from, and masking it in meant the application frame showed through it for the length of the cut — a flash of the very interface it exists to introduce, on every launch.
Found by the phase gate's own reviews, and fixed here:
- ⛔ A crash report left the machine unredacted. The Rust side redacted on the way to disk, but the dialog re-joined the raw error itself — so the copy a user could put on the clipboard, download, or paste into a public GitHub issue was the one nothing had redacted, under a banner promising it contained no file paths. There is now one report: composed and redacted once, in Rust, and that is the text shown and the only text that can be sent. With no backend to redact with, the send buttons are not offered at all.
- ⛔ Quitting maximised destroyed the window's real size. The stored rectangle was the maximised one, so un-maximising next launch restored to the size it already was, and the arranged size was gone for good.
- ⛔ The two processes that write
preferences.jsoncould each erase the other's half. On close the front end's flush and Rust's geometry save fire together; whichever landed second won the whole file, costing either the window position or the last quarter-second of theme, layout and acceptance. Both writes now merge, so the order stops mattering. - ⛔ The window's minimum size was declared in logical pixels while every other use of the same constants is physical. At 150 % scaling the first window opened screen-sized and hanging off the edge instead of centred, and the collapse breakpoints below 1280 could never fire.
- ⛔ A peer address at the end of a sentence, or with a port, was not redacted — the token carried the full stop, so it split into five parts instead of four and the check failed open. IPv6 was never redacted at all.
- ⛔ Two crash reports in the same second overwrote each other, on a feature whose rule is never to ask a user to reproduce before collecting the file.
- ⛔ The reflow lock was a flag rather than a count, so the first pointer released cleared a hold another drag still owned — the window reflowing under a hand still dragging, which is the exact failure § F-35 forbids.
- A shortcut rebound to
+could never fire: the chord's separator and its key are the same character, so the key parsed as empty. It stored, displayed and did nothing. - The declined agreement card had no focus trap, so a keyboard user could Tab out through the opaque scrim and press the titlebar's buttons.
initialLocale's stored-locale rule was unreachable code; the real decision skipped its validation, handing a language Oscillate does not ship straight to i18next. The footer's language picker also never persisted the choice.- Focus was returned only when a modal stayed mounted, and two of the three unmount instead.
- ⛔ A maximised window never reopened maximised. Windows gives a maximised window an invisible resize frame that hangs ~11 px off every edge, so its stored
yis-11— and the restore guard demanded the whole 64 px title strip be on screen. Every maximised window ever stored was therefore judged unusable and silently replaced with a centred default. Found by building the release binary and running it.
Gates that could not fail, now made real:
- The
bindingsdrift gate never emptied the directory before regenerating, and ts-rs only writes — so its "stale binding" and "nothing generated at all" branches were both unreachable. Verified by planting an orphan binding and watching the gate go red. - Four e2e assertions resolved against state that was already true before the keystroke they were testing, and
dismissSplashsampled for a splash that had not mounted yet — letting the overlay swallow the next click in every test that opened the application.
Added — the audio engine (Phase 2)
- A real-time audio engine that opens a device and renders blocks with no allocation, no lock and no syscall in the callback — enforced by a counting global allocator armed around the callback, not by inspection.
- A lock-free render graph handed to the audio thread by atomic pointer swap, with the retired plan freed on the UI thread. The audio thread never frees anything and never waits.
- The transport: play, stop, record, locate, loop, a tempo map, a time-signature map, a metronome and a count-in — all in fixed-point subticks (960 PPQ × 65 536) so an hour-long session does not drift.
- The mixer: channels, fader, constant-power pan (−3.01 dB centre), mute, solo, sends, groups and a master, ordered by a topological sort over both parent and send relations.
- Metering into a shared bank, read over a custom URI scheme rather than JSON IPC — a fixed binary layout, so sixty reads a second cost no parsing.
- Audio recording: arm, input select, monitoring (off by default), count-in and punch, written direct-to-disk through a lock-free ring by a writer thread. The WAV header is written before the first sample, and a take whose header never got patched is recoverable from the file size.
- MIDI input with a lossless pack/unpack over a lock-free queue.
- Device hot-plug and sample-rate change handling — unplug an interface and the transport stops, the strip names the device and the project is untouched; Resume reopens it.
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.
- ⛔ Recording never ran outside the tests. The production callback passed no record tap — every part of the path existed and nothing called it.
- ⛔ Every track meter was dead. A strip's meter slot was assigned only to the master, and the bank was sized for two channels regardless of track count.
- ⛔ No meter could move in the packaged build. The snapshot is served from a different origin than the page and carried no
Access-Control-Allow-Origin. The CSP already allowed the scheme — CSP is not CORS, and a dev server never shows it. - ⛔ Any loop that did not start at bar 1 jumped to the project start. The wrap subtracted a tick value from a subtick position. ⚠ Every existing loop test used bar 1, where the broken arithmetic is accidentally correct.
- A mid-block loop wrap replayed the head of the input, because each span advanced the output slice but not the input one — a stutter on every repeat while monitoring.
- A locate arriving with a record ate the count-in. One ordinary button ("record from the selection start") posts both inside a single block gap.
- ⛔ **Pressing Stop could freeze the window.** The take was closed on the UI thread, joining a writer that may be blocked on a stalled disk — and Oscillate draws its own titlebar, so there was no OS chrome to fall back on.
- A device that failed on its first block was reopened in a tight loop, with no pause and no cap; and an error raised while the stream was opening could be swallowed, leaving a silent application reporting a healthy device.
- Mixer edits made while a device was opening were dropped from the audio path, so a fader moved during a reconnect read one value and sounded another.
- The metronome could overflow and panic inside the audio callback on a locate near the end of the range.
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.
- ⛔ An infinite loop in the audio callback. Making the metronome's
span_endandfirstsaturating missed the loop's own step. In a release build it wrapped and spun through ~10¹¹ iterations — worse than the panic it replaced. - ⛔ **One Resume press cost two, and could park the device thread for ever.** Two loops consumed the same reopen flag, so a press during the half-second to two-second device open tore down the device that had just opened.
- A take could never finish if the transport was still rolling: the writer read its stop flag only on an empty drain, and the audio thread kept refilling the ring. The drain tail is now bounded.
- A strip past the meter bank was handed a slot that does not exist — the dead-meter defect returning at a higher channel number. It now gets no meter, which is honest, rather than one frozen at −∞.
- The meter snapshot carried all 1 024 slots sixty times a second — 16 KB a frame for a session with two strips. It now carries the channels that have actually carried a sample.
- The dropped-sample count was read before the drain, so it missed exactly the drops a slow disk causes.
⚠ 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.
- ⛔ A take could be silently truncated. Take files were named from a wall clock under a comment claiming a monotonic counter, and the writer used
File::create, which truncates. ⚠ Latent until closing a take moved off the UI thread — after which a new take could overwrite one still being written. Nowcreate_new, a real per-session sequence, and a guard that refuses to start while the previous take is still closing. Access-Control-Allow-Originis an allowlist rather than*— taken as hardening, not because it was exploitable.
Changed — Windows audio, by owner decision (2026-08-25)
- ⛔ ASIO is out of the product. Steinberg dual-licensed the SDK in October 2025 (GPLv3-or-later, or a free proprietary agreement). ⛔ The GPLv3 option was rejected: it would put Oscillate's own source under copyleft, and the GPL expressly permits others to sell what they receive. The proprietary option — free, one signature, closed-source-permitted — was declined rather than sign a contract for a capability WASAPI already delivers. ⚠ It remains available and reversible; nothing in the code depends on the choice.
- Windows low-latency is WASAPI exclusive +
IAudioClient3, targeted forv0.4.0. ⚠ There is no MIT/Apache route to ASIO — every wrapper is permissively licensed but compiles against Steinberg's headers, and a wrapper's licence cannot grant rights to Steinberg's. - The supported floor is Windows 10 1607+, which
IAudioClient3and WebView2 already require. ⛔ Windows 11 is not required and must not become required.
[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 repository, the All Rights Reserved licence, the EULA, and a
.gitignorethat keeps the private planning documents out from the first commit. - A Cargo workspace of thirteen members — every crate the architecture calls for, each a compiling stub — and the Tauri 2 shell.
- The React 19 / Vite / TypeScript / Tailwind 4 front end, and Oscillate's design tokens: both themes, with the light palette named exactly once.
- All 18 locale catalogues, bundled rather than fetched, English first in the picker and the other 17 alphabetically.
- Fifteen gates, and
npm run ci:localto run them the way CI does: formatting, linting, types, unit tests, clippy under-D warnings, the licence allowlist, the generated credits, the product-name check, the no-AI/no-telemetry/no-HTTP check, Playwright, and an 18 × 2 theme and language sweep. - GitHub Actions across Windows, macOS and Linux, with every action pinned to a commit SHA and every downloaded executable checked against a SHA256.
THIRD-PARTY-NOTICES.mdand the in-app credits data, both generated from the real dependency graph so neither can drift from what is actually linked.
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 |