Skip to content

fix: studio save, memory and storage issues in multitrack recording - #28

Open
Nowkoo wants to merge 18 commits into
mainfrom
fix/multitrack-recording-bugs
Open

Nowkoo wants to merge 18 commits into
mainfrom
fix/multitrack-recording-bugs

Conversation

@Nowkoo

@Nowkoo Nowkoo commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Fixes bugs and performance problems found while reviewing the multitrack studio released in v3.3.0, focused on server-thread stalls, memory use and disk growth. One commit per topic.

Correctness

  • Stopping a take no longer blocks the server thread. The final take uses the shared ordered writer, following any in-flight checkpoint and replacing obsolete queued snapshots; it stays in memory while it is written.
  • Keeping a take uses the track's current volume and mute, not the mix captured when recording started.
  • Reviewing a take recorded over a muted or zero-volume track plays the take. Listening to a mix or a track with nothing audible is refused instead of running silently.
  • Stop cancels private samples (previews, backing tracks, metronome) already sent to the client. They now use the Players category; live instruments and jukeboxes keep Records and are not cut.
  • One unreadable project file no longer breaks the library. It is skipped, logged once and left on disk; the player can still list, create, open and delete their other projects. A damaged active project leaves the player without an active one.
  • A hopper taking a recording disc from a jukebox stops its song at once instead of at the next once-a-second check. Notes already sent end on their own: each note is short, and stopping a sound by name would also cut a live instrument playing the same sample.
  • Studio files share a 32 MB read/write limit, checked before replacing the previous file.
  • Delete confirmations capture the full immutable project and compare it again before deleting. Changes to notes, track mix, tempo, metronome or a pending take invalidate an older confirmation even when its title, track count and duration stay the same. Active projects use the latest cached content, including failed writes; archived projects read only the chosen song.

Server-thread time and memory

  • Edits (volume, mute, tempo, metronome, title, keep, discard) apply to the cached project at once and are written in the background, in order, after the final take and older edits. They no longer wait for a take to finish saving. Each owner has at most one queued snapshot and shared completion future, besides the single snapshot currently being written. Further edits replace the queued snapshot; changes arriving during an in-flight save queue at the tail so other musicians can save first. Creating, opening and deleting projects still wait for queued writes, because they read or remove project files.
  • Opening another project only rewrites the small selection file. Creating one no longer rewrites the previous project unless its last write failed.
  • A failed write keeps the change in memory, tells the player, and is retried by the next change, every minute, on disconnect and on shutdown. Shutdown waits up to 30 seconds for queued writes. A transient failure first discovered during that drain is retried once on the writer if the shutdown deadline has not expired; persistent failures do not retry indefinitely.
  • The library, /music projects and the 36-project limit read a per-owner library.index of project summaries instead of decoding every project (36 projects of 7,500 notes: about 1.9 s and 40 MB down to under 5 ms). Delete confirmations load only the requested project to check its full content.
  • Decoded notes share one copy of each instrument and sample name (a 7,500-note project: about 1.11 MB to 0.22 MB of heap).
  • Disconnect and shutdown write only unsaved projects, and a disconnect evicts the cached project.
  • Jukebox playback reads the record once a second instead of copying the block state every tick. The jukebox click handler checks the block type before getState().
  • ItemsAdder station lookups are cached, and are skipped entirely while ItemsAdder is not enabled. Before, an ia.* station without ItemsAdder made every right-click on any block fail a reflective class lookup.

Disk

  • New compact file format 2: each track lists its instrument and sample names once and stores each note as one line. With a 6,000-note project on a development machine: 459 KB to 132 KB, save 31 ms to 7 ms, load 45 ms to 4 ms, edition load for a jukebox 40 ms to 3 ms, republishing unchanged music 72 ms to 5 ms.
  • The selected project is no longer kept as a second full copy; projects/<owner>/selected holds its ID.
  • Edition IDs are derived from their content, so republishing unchanged music reuses the earlier edition. Editions leave out muted, silent and empty tracks and silent notes, which playback never emits.
  • New publish-cooldown-seconds (default 30) and max-editions-per-day (default 20, per player in any 24 hours, tracked in editions/<owner>.log) in studio.yml; 0 disables either. Republishing existing music does not count, and a refused publication keeps the disc.
  • Startup removes studio-*.tmp files left by an interrupted write.

Behaviour changes

  • No data migration: the studio only ran on TF Dev. Files written by 3.3.0 (format 1, and projects/<owner>.yml) are not read; format 1 files are logged and skipped. Older plugin versions cannot read format 2.
  • Servers with an existing studio.yml use the default max-editions-per-day until the key is added.

Validation

  • Java 21 mvn clean verify: 326 tests passed; 100% instruction and branch coverage, with no production exclusions.
  • The damaged-file warning test checks for the expected file independently of filesystem enumeration order, so the same assertion works on Windows and Linux.
  • Six save regressions cover 1,000 edits sharing one queued write, ordering and fairness across musicians, immutable in-flight snapshots, transient shutdown failures, persistent failures and an exhausted shutdown deadline.
  • Thirteen deletion regression cases cover a real replacement recording of the same duration, individual note and mix changes, tempo, metronome, pending takes, archived projects and newer cached content after a failed write. The replacement-recording test reproduced the destructive failure before the fix and passes afterward.
  • Earlier TF Dev protocol-bot checks were performed on a58a610. The latest save and deletion fixes (through 4b09d1f) were verified locally and have not been installed or activated on any server; bots have not been rerun on these commits.

🤖 Generated with Claude Code

@github-actions github-actions Bot added the size:L 100-499 effective changed lines (tests excluded in mixed PRs). label Oct 11, 2026
@coderabbitai

coderabbitai Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: bf745c2e-3194-436e-92dd-894b9b1c1867

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Nowkoo and others added 5 commits October 11, 2026 20:46
Decoded notes now share one copy of each instrument and sample name,
cutting a 7,500-note project from about 1.1 MB to 0.2 MB of heap. Saves
check the legacy single-project layout once per owner instead of parsing
the selected file on every save and checkpoint. The sample-name pattern
is compiled once, and publishing checks audibility without building a
full timeline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cached projects already match their files except for a final take that
is still saving or whose write failed. Disconnects and shutdown now
retry only those, instead of rewriting every cached project and risking
the shutdown timeout. A disconnect evicts a current project from memory.
"Take saved" is announced only for finished takes, not for these saves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The library, /music projects, the 36-project limit and deletion now use
project summaries (title, tracks, length, pending slot) instead of
decoding every project on the server thread. Summaries are cached in a
per-owner library.index file keyed by each project file's size and
modification time. save() updates it, and listing rebuilds missing,
stale or damaged entries, so files changed outside the studio stay
correct.

With 36 projects of 7,500 notes, listing drops from about 1.9 s and
40 MB to under 5 ms. The first listing after upgrading builds the index
once.

The delete confirmation keeps a summary rather than the whole project,
and rejects the deletion if the summary changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Published editions are kept forever because discs reference them, and
republishing over a recorded disc added a new file every two clicks.
Edition IDs are now derived from their content, so republishing
unchanged music reuses the earlier edition. A configurable
publish-cooldown-seconds (default 30, 0 disables) limits how often a
player can publish.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Jukebox playback reads the cheap block type every tick and the record,
  which copies the block state and item, once a second. Ejecting or
  breaking the jukebox still stops playback immediately.
- The jukebox click handler checks the block type before getState(), so
  right-clicking chests no longer copies their inventories.
- ItemsAdder station lookups resolve their reflective methods once
  instead of on every right-click and menu refresh.
- Saving serializes the project once for both copies.
- Startup removes studio-*.tmp files left by an interrupted write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Nowkoo Nowkoo changed the title fix: save studio takes asynchronously and preserve playback controls fix: studio save, memory and storage issues in multitrack recording Oct 11, 2026
Nowkoo and others added 9 commits October 11, 2026 21:59
A pending take inherits its track's volume and mute, so reviewing a take
recorded over a muted or zero-volume track played nothing for the whole
song while holding a session. The take review now plays the take
audibly with the saved backing. Listening to a mix or a single track
with nothing audible is refused instead of running silently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every save and checkpoint wrote the whole project twice: once to its
per-project file and once to projects/<owner>.yml as the selection.
The selection is now a small "selected" file holding the project ID in
the owner's folder, which halves project storage and write volume.

Existing data migrates once per owner on first access: the newer of
projects/<owner>.yml and its per-project copy becomes the project file,
the selection pointer is written and the old file is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Format 1 wrote every note as a five-item YAML list, about 77 bytes per
note. Format 2 lists each track's instrument and sample names once and
stores each note as one text line of "<ticks since the previous note>
<instrument> <sample> <volume> <pitch>". Format 1 files remain readable
and are rewritten in format 2 the next time they are saved.

With a 6,000-note project (TF Main limits) on a development machine:
- File size: 459 KB to 132 KB (about 22 bytes per note).
- Save: 31 ms to 7 ms. Load: 45 ms to 4 ms.
- Loading an edition for a jukebox: 40 ms to 3 ms.
- Republishing unchanged music: 72 ms to 5 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One unreadable project file (invalid YAML, an unknown format, the wrong
owner or ID, or a name that is not a project ID) made listing fail, so
the player could not open the library, create a project or even delete
the broken one. A damaged active project also blocked creating or
opening another.

Unreadable files are now skipped and logged once, and left on disk for
an administrator. The library index remembers them until the file
changes, so they are not reparsed on every listing. A damaged active
project or selection leaves the player without an active project. An
unreadable legacy projects/<owner>.yml is moved to <owner>.yml.damaged.
Disk access errors are still reported to the player as before.

A checkpoint test now waits for the save to finish instead of reading
the file while it is replaced, which can fail on Windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every volume, mute, tempo, metronome, title, keep or discard change
wrote the whole project on the server thread. Edits now apply to the
cached project at once and are queued on the existing ordered writer,
after the final take and any older edit, so the latest write always
holds the latest project. Edits no longer wait for a take to finish
saving.

Creating, opening and deleting projects still wait for queued writes,
because they read or remove project files. Opening a project now only
rewrites the small selection file instead of both projects, and creating
one no longer rewrites the previous project unless its last write
failed.

A failed write keeps the change in memory, tells the player and is
retried by the next change, every minute, on disconnect and on
shutdown, instead of only on disconnect or shutdown. Shutdown waits up
to 30 seconds for queued writes instead of 5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The publish cooldown limited how often a player could publish, not how
many editions piled up: small mix changes could still add one file
every 30 seconds, kept forever. A new max-editions-per-day setting in
studio.yml (default 20, 0 disables) limits new editions per player in
any 24 hours, tracked in editions/<owner>.log. Republishing music that
is already published does not count, and a refused publication keeps
the disc.

Editions now leave out muted, zero-volume and empty tracks and silent
notes, which playback never emits. Muting a silent track therefore
reuses the same edition. Republishing compares the existing file's
bytes instead of parsing it again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With an ia.* station configured and ItemsAdder missing or disabled,
every right-click on any block tried to load the ItemsAdder classes by
reflection, failed with ClassNotFoundException, and also searched the
block for entities. Stations now match nothing until ItemsAdder is
enabled. Methods missing from the installed ItemsAdder version are also
remembered instead of looked up again on each click.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A hopper taking a recording disc from a jukebox left the song playing
until the next once-a-second record check. The listener now stops it
as soon as the disc moves, like breaking or ejecting does. The cheap
item and inventory type checks run first, since hoppers move items
constantly.

Notes already sent when a jukebox stops are left to end on their own:
each note is short, and stopping a sound by name would also cut a live
instrument playing the same sample nearby.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The studio only ran on TF Dev, with one test song that will be removed,
so there is no data to carry over. Remove the one-time move from
projects/<owner>.yml to the per-project layout and the reader for
format 1 notes. Format 1 files written by 3.3.0 are now reported as an
unsupported format and skipped like any other unreadable file. The
library index is also written in format 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added size:XL 500-999 effective changed lines (tests excluded in mixed PRs). and removed size:L 100-499 effective changed lines (tests excluded in mixed PRs). labels Oct 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 effective changed lines (tests excluded in mixed PRs).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant