Skip to content

Add local MIDI upload and converted audio/MIDI export#2

Open
RobertAgee wants to merge 4 commits into
b1rdmania:masterfrom
RobertAgee:master
Open

Add local MIDI upload and converted audio/MIDI export#2
RobertAgee wants to merge 4 commits into
b1rdmania:masterfrom
RobertAgee:master

Conversation

@RobertAgee
Copy link
Copy Markdown

@RobertAgee RobertAgee commented May 26, 2026

Summary
This PR adds two major improvements to the Motif playback flow:

  1. Optional local MIDI upload (client-side)
  2. Save converted outputs from playback (.wav + .mid)

What Changed

  1. Optional Local MIDI Upload
  • Added upload entry points in two places:
    • Main search area: Upload MIDI
    • Selected source card: Replace with uploaded MIDI
  • Supports .mid / .midi file selection.
  • Uploads are processed fully client-side:
    • Validate file type/size
    • Read bytes with File.arrayBuffer()
    • Parse with existing MIDI parser
    • Load into the same preview/generate flow as searched MIDI
  • Added clear status/error messages for:
    • Invalid file type
    • Empty/unreadable file
    • Parse/non-playable MIDI failures
  • Introduced source-state discrimination (remote vs upload) for consistent behavior.
  1. Save Converted File from Playback
  • Added two buttons in generated playback controls:
    • Save Audio → exports converted motif as .wav
    • Save MIDI → exports converted motif as .mid
  • Implemented deterministic export helpers:
    • AudioBuffer -> WAV encoding
    • Motif note events -> MIDI bytes (Format 0 track)
  • Extended MotifEngine with export support and generated-content tracking:
    • hasGeneratedContent()
    • exportMotifWAVBytes()
    • exportMotifMIDIBytes()
  • Exports work for both:
    • Searched/retrieved MIDI sources
    • Local uploaded MIDI sources

Sharing Behavior

  • Existing remote-source share behavior is preserved.
  • For local uploads, sharing is disabled/hidden

Backend Impact

  • No backend API changes
  • No new upload endpoint
  • No SSRF/search/fetch/parse contract changes
  • /play shared-link behavior remains unchanged

Validation

  • ✅ npm run typecheck
  • ✅ npm run build

Summary by CodeRabbit

  • New Features

    • Upload local MIDI (.mid/.midi, non-empty, ≤10MB) and replace the current player selection
    • Save Audio (WAV) and Save MIDI exports for generated content (enabled when generation completes)
    • Copy link and Share to X actions for remotely sourced results only
  • Behavior Changes

    • Local uploads can be played and used for generation but cannot be shared; a notice and disabled share controls indicate this
    • Selecting remote results updates share-enabled state; resets clear upload selection

Review Change Stack

@vercel
Copy link
Copy Markdown

vercel Bot commented May 26, 2026

@RobertAgee is attempting to deploy a commit to the Boom Test Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

Warning

Review limit reached

@RobertAgee, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 17 minutes and 39 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6accc69a-4eb2-45fb-9815-537bb80752f5

📥 Commits

Reviewing files that changed from the base of the PR and between bbb2456 and 526290d.

📒 Files selected for processing (1)
  • src/main.ts
📝 Walkthrough

Walkthrough

The PR adds local MIDI file upload and replacement UI, engine export APIs for MIDI/WAV bytes, utils to serialize MIDI and WAV, UI wiring for save/copy/share, and source-aware sharing rules that prevent sharing local uploads.

Changes

Local MIDI upload and export functionality

Layer / File(s) Summary
Binary export utilities
src/utils/export.ts
New utility functions noteEventsToMidiBytes() (format-0 MIDI generation with delta times and tempo meta-event) and audioBufferToWavBytes() (16-bit PCM RIFF/WAVE encoding, up to 2 channels).
MotifEngine export API
src/core/MotifEngine.ts
Engine now retains last input events, transform mode, and generated events during generateFromMIDI() and exposes hasGeneratedContent(), exportMotifMIDIBytes(), and exportMotifWAVBytes(sampleRate?) for synchronous MIDI and async WAV exports; exports throw if no generated state.
UI initialization for upload and export controls
index.html, src/main.ts
HTML adds Upload/Replace inputs, Save Audio/MIDI, Copy link, Share to X buttons, and a local-upload-not-shareable notice. TS adds CurrentSource union, fields for upload/replace/save UI elements, and initializeUI() wiring.
Local MIDI file upload handler
src/main.ts
Upload/replace button listeners open file pickers; handleUploadInputChange/handleUploadedMIDI validate extension/MIME and size, parse MIDI, compute duration if missing, stop playback, reset generated state, set currentMIDI and currentSource to upload, update UI/title/metadata, and show player.
Search, result parsing, and selection state
src/main.ts
Search requests gated by requestId to avoid stale results; searches clear currentSource/results and parse metadata for top results with repeated stale-checks; resetToNewSearch() clears currentSource.
Player control enablement and UI state
src/main.ts
Player controls and copy/share buttons are enabled only for remote-origin results; setState() shows a share-notice for generated local uploads and conditions copy/share visibility on currentSource.type === 'remote'.
Export and save handlers
src/main.ts
Save handlers call engine export methods, sanitize filenames, and trigger downloads. handleCopyLink()/handleShareToX() early-reject local uploads with a status message. Save buttons enabled only when generation is complete and export data exists.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A whisker's worth of MIDI dreams,
Upload and save your sonic streams—
Local tunes can play and gen,
But share remains for remotes, then!
Bytes to WAV and events to MIDI.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and concisely summarizes the main changes: adding local MIDI upload capability and export functionality for converted audio/MIDI outputs.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main.ts (1)

248-257: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear stale search results when starting over.

setState() now keeps the results table visible whenever searchResults.length > 0, but neither reset path empties that array. After New search or a fresh search that finds nothing, uploading a local MIDI can bring back the previous remote results under the upload card.

Also applies to: 349-351, 988-990

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main.ts` around lines 248 - 257, resetToNewSearch currently resets
playback state but leaves the stale searchResults intact; update
resetToNewSearch to also clear the results array (e.g., set this.searchResults =
[] or call the centralized clear method) so the results table won't reappear
after uploading a local MIDI; apply the same change to the other reset paths
that perform similar state clears (the other methods that set hasGenerated =
false / isMotifPlaying = false) so any "new search" or empty-search path
consistently empties searchResults.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/MotifEngine.ts`:
- Around line 44-45: The current assignments to lastInputEvents and
lastTransformMode occur before generation and can leave export APIs returning
stale data if generation fails; update the code so you either (a) clear all
cached export state (lastInputEvents, lastTransformMode, lastGeneratedEvents)
before starting a new generation attempt, or (b) move the assignments to the
success path after procedural/passthrough setup and after lastGeneratedEvents is
refreshed, ensuring hasGeneratedContent() only reflects successful runs;
identify and change the logic around the generation entry point and the
procedural/passthrough setup functions to apply one of these fixes.

In `@src/main.ts`:
- Around line 402-405: The current guard only checks for empty files (file.size
<= 0) but doesn't prevent oversized files being read into memory; update the
upload handler (the block using file.size and calling this.updateStatus) to
enforce a maximum allowed upload size by introducing a MAX_UPLOAD_BYTES constant
and performing an early check like if (file.size > MAX_UPLOAD_BYTES) {
this.updateStatus('Upload error: File exceeds max size'); return; } before any
file reads/parsing so oversized files are rejected prior to loading into memory.
- Around line 407-443: The code currently clears UI/state (handleMotifStop,
stopPreview, hasGenerated=false, disablePlayerControls, updateStatus('Loading
uploaded MIDI…'), etc.) before awaiting file.arrayBuffer() and parsing, which
leaves the UI disabled if parsing fails; move all state-clearing and UI changes
that disable controls (handleMotifStop(), stopPreview(), setting hasGenerated,
disablePlayerControls(), updateStatus('Loading uploaded MIDI…')) to occur only
after the MIDI is successfully read/validated (after MIDIParser.parseMIDI and
getMIDIInfo succeed) and right before setting
this.currentMIDI/this.currentSource/selectedTitle/selectedMeta and enabling
controls, so failures in arrayBuffer/parse won't leave the previous currentMIDI
state inaccessible.
- Around line 394-399: The code treats an empty MIME type as valid; change the
validType check to require a non-empty, recognized MIME by replacing the current
validType definition (now: file.type === '' || file.type === 'audio/midi' ||
file.type === 'audio/x-midi') with one that only accepts 'audio/midi' or
'audio/x-midi' (i.e., file.type === 'audio/midi' || file.type ===
'audio/x-midi'), leaving the rest of the logic (the if that calls
this.updateStatus(...)) unchanged so files with blank/unknown MIME types are
rejected unless the filename extension is explicitly .mid/.midi.

In `@src/utils/export.ts`:
- Around line 40-42: The code treating velocity as (e.velocity || 0.7)
incorrectly treats an explicit 0 as missing; update the velocity fallback to
preserve zero by using a nullish/coalescing check (e.velocity ?? 0.7) or an
explicit undefined check, then continue to multiply by 127/round/clamp as before
(the change affects the expression used to compute vel where Math.round and
clamp are applied before pushing into midiEvents via midiEvents.push).

---

Outside diff comments:
In `@src/main.ts`:
- Around line 248-257: resetToNewSearch currently resets playback state but
leaves the stale searchResults intact; update resetToNewSearch to also clear the
results array (e.g., set this.searchResults = [] or call the centralized clear
method) so the results table won't reappear after uploading a local MIDI; apply
the same change to the other reset paths that perform similar state clears (the
other methods that set hasGenerated = false / isMotifPlaying = false) so any
"new search" or empty-search path consistently empties searchResults.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d40a9980-9617-4895-bb61-9c451365b62e

📥 Commits

Reviewing files that changed from the base of the PR and between 60a4b6a and 7682666.

📒 Files selected for processing (4)
  • index.html
  • src/core/MotifEngine.ts
  • src/main.ts
  • src/utils/export.ts

Comment thread src/core/MotifEngine.ts Outdated
Comment thread src/main.ts
Comment thread src/main.ts
Comment thread src/main.ts Outdated
Comment thread src/utils/export.ts
@RobertAgee
Copy link
Copy Markdown
Author

Thanks, validated all 5 points. I'll patch stale result clearing, upload size limits, upload state ordering, export cache consistency, and velocity nullish fallback; as well as require known MIDI MIME or .mid/.midi extension, to avoid rejecting valid browser uploads with inconsistent MIME handling.

@RobertAgee
Copy link
Copy Markdown
Author

Thanks for the review — addressed the actionable items in fdeb4b8:

  • Cleared stale searchResults in reset/search-start/no-results paths.
  • Added upload max-size guard (10MB) before file read.
  • Reordered upload flow so previous playable state is preserved when upload parse fails.
  • Tightened MIME validation to recognized MIDI types while still allowing .mid/.midi by extension.
  • Fixed MotifEngine export cache lifecycle to avoid stale exports on failed generation.
  • Switched velocity fallback from || to ?? in MIDI export.

Revalidated with npm run typecheck and npm run build.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main.ts (1)

354-380: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Ignore stale search completions after the active source changes.

A pending search still calls displayResults()/setState('results') even if the user uploads a local MIDI before the request resolves, so the late response can knock the app out of the uploaded-source flow and hide the selected upload again. Guard the post-await path with a request token or equivalent stale-response check before mutating UI state.

Suggested direction
 class MotifApp {
   private static readonly MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
+  private searchRequestId = 0;
   private motifEngine: MotifEngine;
   private midiService: MIDIService;
   ...
   private async handleSearch(): Promise<void> {
+    const requestId = ++this.searchRequestId;
     console.log('[MotifApp] handleSearch called');
     const songName = this.songInput.value.trim();
     ...
     try {
       const results = await this.midiService.search(songName);
+      if (requestId !== this.searchRequestId || this.currentSource?.type === 'upload') {
+        return;
+      }
       
       if (results.length === 0) {
         this.searchResults = [];
         this.updateStatus('No MIDI files found. Try a different search.');
         return;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main.ts` around lines 354 - 380, The pending search can mutate UI after
the active source changes—prevent this by adding a search-request token/ID on
the instance (e.g., this.currentSearchId) that you increment before calling
this.midiService.search(songName), capture into a local const (e.g., const reqId
= this.currentSearchId), and then after each await (both the search and the
parseMIDI loop) bail out if reqId !== this.currentSearchId; only then assign
this.searchResults, this.selectedResultIndex, call this.displayResults(),
this.setState('results'), and updateStatus. Ensure the same check is applied
before setting parsed metadata on results[i] so late parsing doesn't leak into a
stale UI.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/main.ts`:
- Around line 354-380: The pending search can mutate UI after the active source
changes—prevent this by adding a search-request token/ID on the instance (e.g.,
this.currentSearchId) that you increment before calling
this.midiService.search(songName), capture into a local const (e.g., const reqId
= this.currentSearchId), and then after each await (both the search and the
parseMIDI loop) bail out if reqId !== this.currentSearchId; only then assign
this.searchResults, this.selectedResultIndex, call this.displayResults(),
this.setState('results'), and updateStatus. Ensure the same check is applied
before setting parsed metadata on results[i] so late parsing doesn't leak into a
stale UI.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3fee877c-c916-4996-9007-bb4a648aadb1

📥 Commits

Reviewing files that changed from the base of the PR and between 7682666 and fdeb4b8.

📒 Files selected for processing (3)
  • src/core/MotifEngine.ts
  • src/main.ts
  • src/utils/export.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/main.ts (3)

404-458: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make upload completion source-aware, not just search-aware.

handleUploadedMIDI() bumps searchRequestId, but it never captures or checks it after await file.arrayBuffer(). A slower first upload can finish after a newer upload/search and then overwrite currentMIDI/currentSource with the wrong file, or surface a stale parse error.

Suggested fix
   private async handleUploadedMIDI(file: File): Promise<void> {
-    this.searchRequestId++;
+    const requestId = ++this.searchRequestId;
     const lowerName = file.name.toLowerCase();
     const validExt = lowerName.endsWith('.mid') || lowerName.endsWith('.midi');
     const validType = file.type === 'audio/midi' || file.type === 'audio/x-midi';
@@
     try {
       const midiBuffer = await file.arrayBuffer();
+      if (requestId !== this.searchRequestId) return;
+
       if (midiBuffer.byteLength < 14) {
         throw new Error('File is too small to be valid MIDI.');
       }
@@
       this.playerSection.classList.add('visible');
       this.resultsSection.classList.add('collapsed');
     } catch (error) {
-      this.updateStatus(`Upload error: ${error instanceof Error ? error.message : 'Failed to load MIDI.'}`);
+      if (requestId === this.searchRequestId) {
+        this.updateStatus(`Upload error: ${error instanceof Error ? error.message : 'Failed to load MIDI.'}`);
+      }
     }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main.ts` around lines 404 - 458, handleUploadedMIDI increments
this.searchRequestId but never checks it after awaiting async work, so a slower
upload can overwrite state from a newer upload; capture the current request id
into a local variable right after incrementing (e.g., const requestId =
this.searchRequestId) and after each await/long synchronous step (after
file.arrayBuffer(), after MIDIParser.parseMIDI, after MIDIParser.getMIDIInfo)
verify that this.searchRequestId === requestId and bail out early if it differs
to avoid applying stale results to currentMIDI/currentSource/state; keep all
existing validation and error handling but add these guard checks using the
unique symbols handleUploadedMIDI and searchRequestId.

445-453: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clear stale embed UI when switching to a local upload.

This success path flips the active source to upload, but it does not hide/reset the embed snippet. If I select a remote result first and then replace it with a local file, the page can keep showing embed code for the previous remote song even though uploads are intentionally non-shareable.

Suggested fix
       this.currentMIDI = { events, metadata: { ...metadata, duration: actualDuration } };
       this.currentSource = { type: 'upload', fileName: file.name };
+      if (this.embedSection) this.embedSection.style.display = 'none';
+      if (this.embedCodeEl) this.embedCodeEl.textContent = '';
       this.selectedTitle.textContent = this.cleanSongTitle(file.name);
       this.selectedMeta.textContent = 'Source: Local Upload';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main.ts` around lines 445 - 453, When switching the active source to an
upload (you already set this.currentSource = { type: 'upload', ... }), also
clear or hide any stale embed UI from a previously selected remote result: reset
the embed snippet/display and any embed-related state so the UI no longer shows
share/embed code for uploads. Locate the embed UI element or state used
elsewhere in the file (the snippet/embed container or variable that holds embed
code) and clear its textContent/state or call the existing UI-reset helpers (in
the vicinity of
clearSelectedResultHighlight/updateIOSAudioBanner/enablePlayerControls) so after
setting currentSource to 'upload' the embed snippet is removed and cannot be
shown for local uploads. Ensure updateStatus/setState remain unchanged.

389-393: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard stale search failures too.

requestId is only checked on the success path. If an older search rejects after a newer search or upload has started, Line 390 can overwrite the current status with a stale error, and Line 392 can re-enable the search button while the latest request is still in flight.

Suggested fix
     } catch (error) {
-      this.updateStatus(`Search error: ${error instanceof Error ? error.message : 'Unknown error'}`);
+      if (requestId === this.searchRequestId) {
+        this.updateStatus(`Search error: ${error instanceof Error ? error.message : 'Unknown error'}`);
+      }
     } finally {
-      this.searchBtn.disabled = false;
+      if (requestId === this.searchRequestId) {
+        this.searchBtn.disabled = false;
+      }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main.ts` around lines 389 - 393, The catch/finally blocks currently
unconditionally call this.updateStatus and re-enable this.searchBtn which can
apply stale results; capture the requestId used for the current search (e.g.,
store a localRequestId when the search starts) and in both the catch and finally
blocks verify that localRequestId === requestId (or that the component's
latestRequestId matches) before calling this.updateStatus(...) or setting
this.searchBtn.disabled = false so only the most recent request can change UI
state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/main.ts`:
- Around line 404-458: handleUploadedMIDI increments this.searchRequestId but
never checks it after awaiting async work, so a slower upload can overwrite
state from a newer upload; capture the current request id into a local variable
right after incrementing (e.g., const requestId = this.searchRequestId) and
after each await/long synchronous step (after file.arrayBuffer(), after
MIDIParser.parseMIDI, after MIDIParser.getMIDIInfo) verify that
this.searchRequestId === requestId and bail out early if it differs to avoid
applying stale results to currentMIDI/currentSource/state; keep all existing
validation and error handling but add these guard checks using the unique
symbols handleUploadedMIDI and searchRequestId.
- Around line 445-453: When switching the active source to an upload (you
already set this.currentSource = { type: 'upload', ... }), also clear or hide
any stale embed UI from a previously selected remote result: reset the embed
snippet/display and any embed-related state so the UI no longer shows
share/embed code for uploads. Locate the embed UI element or state used
elsewhere in the file (the snippet/embed container or variable that holds embed
code) and clear its textContent/state or call the existing UI-reset helpers (in
the vicinity of
clearSelectedResultHighlight/updateIOSAudioBanner/enablePlayerControls) so after
setting currentSource to 'upload' the embed snippet is removed and cannot be
shown for local uploads. Ensure updateStatus/setState remain unchanged.
- Around line 389-393: The catch/finally blocks currently unconditionally call
this.updateStatus and re-enable this.searchBtn which can apply stale results;
capture the requestId used for the current search (e.g., store a localRequestId
when the search starts) and in both the catch and finally blocks verify that
localRequestId === requestId (or that the component's latestRequestId matches)
before calling this.updateStatus(...) or setting this.searchBtn.disabled = false
so only the most recent request can change UI state.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8e8bd2ab-a3c9-4268-b00a-32a5116a57b9

📥 Commits

Reviewing files that changed from the base of the PR and between fdeb4b8 and bbb2456.

📒 Files selected for processing (1)
  • src/main.ts

@RobertAgee
Copy link
Copy Markdown
Author

Implemented follow-up updates from review and validated end-to-end.

Changes included

  • Added optional local MIDI upload flow (main upload + replace upload) using client-side parsing.
  • Added converted file exports from playback:
    • Save Audio (.wav)
    • Save MIDI (.mid)
  • Added source-aware behavior:
    • Local uploads are playable/generatable/exportable
    • Share actions remain remote-only
  • Hardened async UI state handling:
    • Guarded stale search completions
    • Guarded stale upload completions/errors
    • Guarded stale search error/finally UI updates
  • Improved reset/state consistency:
    • Clear stale search results on reset/new-search paths
  • Upload safety improvements:
    • Added max upload size guard
    • Preserved previous playable state on upload parse failure
  • Export correctness improvements:
    • Prevent stale export cache reuse on failed generation
    • Fixed velocity fallback to preserve explicit 0 velocity
  • Cleared stale embed UI when switching to local upload.

@b1rdmania
Copy link
Copy Markdown
Owner

Hey Robert — thanks a lot for this, it looks really nice.

Funny timing — I actually dug into the architecture for local upload back in January and then paused it. I've been heads-down on a legal AI project and the Fireship feature caught me a bit off guard, so I need to get my head back up to speed with where things are.

I'll give this a proper look over the next couple of days. I've also got a v2 of the sound engine I haven't shipped yet (still a bit glitchy), so I want to think about how everything fits together before merging. Either way I'll integrate this or accept the PR — the contribution is solid and it's the right feature to add.

Appreciate you taking the time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants