Skip to content

Editor: camera cuts, audio silencing, 50x speed, native preview audio & reliable export - #3

Merged
pranavms13 merged 5 commits into
Glyph-Software:mainfrom
AleksDoesCode:feat/editor-audio-camera-export
Oct 8, 2026
Merged

pranavms13 merged 5 commits into
Glyph-Software:mainfrom
AleksDoesCode:feat/editor-audio-camera-export

Conversation

@AleksDoesCode

@AleksDoesCode AleksDoesCode commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Builds on #2 (clip editing). This branch contains #2's two commits plus three new ones. Merge #2 first, or merge this PR on its own and close #2.

Summary

This PR follows up on clip editing. I used the editor on real recordings (an external 24-bit wireless mic, 5–7 GB camera files, 50× timelapses) and fixed what broke. It also adds the track-level editing tools that were missing.

New editing features

  • CapCut-style splitting. Press S with nothing selected and every track (clips, camera, audio) is cut at the playhead. Select a block first and only that block is cut.
  • Independent camera cuts. The camera row can be split separately from the screen. Each camera cut has a time shift, set by dragging or with ⌥←/→, so a camera that drifts out of sync can be slid back into place. A gap between camera cuts hides the camera in both preview and export.
  • Audio splitting and silencing. Mic and system audio can be split into pieces. ⌫ toggles silence on the selected piece, and export only includes the audible ranges.
  • Up to 50× clip speed. Presets go up to 50×, and the custom speed input works now (before, it silently clamped at 16×). Above WebKit's 16× playbackRate cap, the preview seek-steps instead of playing.
  • Auto-subtitles. Whisper-1 transcription with an OpenAI key the user provides. The key is stored in a 0600 file and never returned to the webview. The same Canvas2D renderer draws captions in the preview and in the export.

Fixes

Problem Cause Fix
24-bit USB/wireless mic recorded NaN noise PCM was always read as f32 audio_tap.rs decodes by the real format: float/int, 16/24/32-bit, BE/LE
No sound at all in the editor preview (bundled app) Web Audio output is silent in WKWebView New preview_audio.rs: native cpal/CoreAudio playback of the sidecar WAVs (mmap), synced via preview_audio_set(playing, t, rate)
Export crashed on multi-GB camera files WebContent fetched the whole file into a blob Probe the size first (Range: bytes=0-0), stream large files
Export hung at 1% / frames froze after ~10 s WebKit purges paused <video> decoders under memory pressure New video_frames.rs: a forward-streaming ffmpeg decoder per session (VideoToolbox, BT.709 to match the preview) sends RGBA frames over IPC via export_video_frame
Preview audio went silent after a timelapse clip Gain window compared source time against output duration Compare against source duration

Why

Recordings from real hardware (pro mics, long sessions, big camera files) used to fail in ways the user couldn't work around: silence, crashes, frozen exports. With these fixes the edit → export loop is reliable. Per-track cuts cover the everyday needs that were missing: fixing a desynced camera, removing a cough, speeding up a boring part.

Screenshots

Camera cut + shift
camera cuts

Audio split + silenced piece (dark / light)
audio pieces
audio pieces light

50× speed
speed 50x

Testing

  • bunx tsc --noEmit and cargo check are clean.
  • Tested manually in a release build (bun run tauri build) on Apple Silicon:
    • 24-bit external mic recording.
    • Preview audio before and after a 50× timelapse.
    • Camera cut/shift/hide.
    • Audio silencing.
    • Export of a recording with a ~7 GB camera file, with no frozen frames and no crash.
  • AGENTS.md is updated with the new modules, commands and pitfalls: native audio, native export frames, source vs output time.

🤖 Generated with Claude Code

Review follow-up

  • Global splits preserve source-time zoom/effect segments, avoiding extra easing and crossfade dips. Explicitly selected segments can still be split.
  • Camera, audio, zoom and effect block selections clear competing block selections.
  • Transcription merges into the latest caption state and ignores results after a recording/project change, preserving style and position edits.
  • Zoom/effect chip drags preserve the complete source segment, clamp the grabbed portion to its clip, and expose resize handles only at actual segment edges. Drag offsets no longer change segment length.
  • Cancel/restart run discard cleanup on a blocking worker, keeping recording finalization and deletion in order without blocking the UI thread.

Validation for this follow-up: bunx tsc --noEmit, bun run build, cargo check, and bun test tests/segmentChip.test.ts (5 passing tests). bun run tauri dev built and launched successfully. Automated UI click-through was blocked by macOS denying Accessibility access to osascript; the complete recording/editor interaction flow was not re-tested for this follow-up.

AleksDoesCode and others added 3 commits September 26, 2026 16:32
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ion commands

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…native export frames, captions

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@pranavms13 pranavms13 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Played around with this, really nice set of features. Left a few comments, mostly around the split/selection stuff and one in capture.rs.

Comment thread src/components/Editor/index.tsx Outdated
if (r) selectClip(r.rightId);
return;
}
setZoomSegments(splitSegmentsAt(zoomSegments, tMs));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think the global split should touch zoom/effect segments. Both of them ease back to neutral at their edges (zoom does the release + lead-in, effects have the 220ms crossfade), so cutting one in half at the playhead gives you a little zoom out/in bump and a fade dip right at the cut. Pretty easy to see if there's an auto-zoom over the split point, and it ends up in the export too.

Since segments live in source time anyway, the clip split doesn't need them split at all. I'd just drop these two lines and only split a segment when it's the thing that's selected.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 3ea6bad: global splits now leave zoom/effect segments intact. Explicit selected-segment splits remain available.

const t = await native.transcribeAudio(src.paths, language || null);
const words = wordsFromTranscript(t);
if (words.length === 0) setError("No speech was detected in this audio.");
setCaptions({ words, language: t.language, enabled: true });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This setCaptions is the one from the render where you hit Generate, and it spreads state.captions from back then. Transcription can take a few minutes, so if you switch the preset or drag the subtitles somewhere else while it's running, all of that gets reverted when it finishes. Same if you open a different recording mid-way, the transcript lands on that one.

Probably want a functional update here (setState(s => ...)) plus a check that we're still on the same artifact.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 3ea6bad: caption patches use a functional state update, preserving the current style/box. A recording/project generation guard rejects stale transcription results, including after reopening the same artifact.

if (!cameraRow || e.button !== 0) return;
e.preventDefault();
e.stopPropagation();
cameraRow.onSelectCut(id);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Selecting a camera cut doesn't clear audioPieceSel or selectedEffectId (selectCamera only clears zoom / audio key / clip). So if you click an audio piece and then a camera cut, both stay selected:

  • ⌫ deletes the cut and silences the audio piece
  • S splits the audio piece instead of the cut that's highlighted

Same thing with an effect selected first, ⌫ deletes both. Should clear the other selections here like selectClip does.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 3ea6bad: camera-cut selection clears audioPieceSel and selectedEffectId. I also made zoom/effect selection clear competing block selections.

Comment thread src/components/Editor/index.tsx Outdated
// range lands in the edited sequence and map chip edits back to source.
const srcMsFromOut = (c: PlacedClip, outMs: number) =>
Math.max(0, Math.min(srcDuration * 1000, (c.srcStart + (outMs / 1000 - c.outStart) * c.speed) * 1000));
const mapChipPatch = <P extends { startMs?: number; endMs?: number }>(c: PlacedClip, patch: P): P => ({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

When a zoom/effect segment spans a clip boundary it's drawn as one chip per clip, but this maps the whole patch through just that chip's clip.

Repro: zoom over 2-8s, cut at 5s, drag the right chip 1s to the right. The segment becomes 6-9s and the 2-5s part is just gone. Dragging that chip's left edge (which is really just the clip boundary, not the segment start) also moves the actual segment start.

It doesn't clamp to the clip either, so you can drag into source time that's been cut out and the segment vanishes from the timeline.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 3ea6bad: dragging uses a source-time delta against the entire original segment and preserves its full duration. The grabbed portion is clamped to its clip; handles at artificial clip boundaries are hidden. The drag mapping is frozen at mouse-down, and the grab offset is stored separately from segment start. Added five regression tests for boundary-spanning movement, hidden edges, clipping and speed/offset mapping.

Comment thread src-tauri/src/capture.rs Outdated
std::thread::sleep(Duration::from_millis(400));
// SCRecordingOutput writes the file asynchronously after stop; wait for its finish callback.
let deadline = std::time::Instant::now() + Duration::from_secs(15);
while !seg.finished.load(Ordering::SeqCst) && std::time::Instant::now() < deadline {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This used to be a 400ms sleep and now it can wait up to 15s. Fine for stop/pause since those are async commands, but cancel_capture and restart_capture are still plain sync commands, so they run on the main thread and hit this through discard_recording. If stop_capture errors or the finish callback never shows up, the whole app hangs for 15s when you hit Cancel.

For discard we delete the file right after anyway, so there's nothing to wait for. Maybe skip the wait on that path, or make those two commands async as well?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 3ea6bad: cancel/restart are async commands and discard cleanup runs through spawn_blocking. This keeps finalization before deletion, while the UI thread stays responsive during the callback wait. Restart also releases the recording-state mutex before waiting.

@AleksDoesCode

Copy link
Copy Markdown
Contributor Author

@pranavms13 Thanks for trying this out and for the detailed review. I've addressed all five points in 3ea6bad:

  • Global splits leave zoom/effect segments intact, so they don't introduce extra zoom easing or effect fade dips.
  • Transcription now merges into the current caption state and discards stale results when another recording/project is opened.
  • Camera-cut selection clears audio-piece and effect selections; zoom/effect selection clears competing block selections too.
  • Dragging a chip moves the complete source segment, preserves its duration, and clamps the visible part to its clip. Resize handles are only shown at real segment boundaries. I also fixed the grab-offset calculation that could change segment length.
  • Cancel/restart perform discard cleanup on a blocking worker, so waiting for recording finalization no longer stalls the UI thread. I kept finalization before file deletion rather than skipping that wait.

Validation: TypeScript, frontend build, cargo check, and five segment-mapping regression tests pass. The Tauri dev app builds and launches. Full UI click-through remains unverified for this follow-up because macOS denied Accessibility access to the automation tool.

This will be my last addition to this PR for now. If you see any further optimizations or improvements, please feel free to add them directly to this PR. Thanks!

@pranavms13

Copy link
Copy Markdown
Contributor

There seem to be 2 conflicts due to changes in #2. I will resolve and merge this.
Thank you for the contribution @AleksDoesCode.

Resolve conflicts with the Glyph-Software#2 follow-up fixes on main:
- capture.rs: take main's cancel/restart handling (#[tauri::command(async)]
  plus skipping the finish wait when stop_capture fails), which covers this
  branch's spawn_blocking version.
- Editor: keep this branch's segment chip mapping (segmentChip.ts, grabOffset,
  boundary-locked resize handles) over main's mapSegmentPiecePatch/lengthMs.
@pranavms13
pranavms13 merged commit c42663a into Glyph-Software:main Oct 8, 2026
1 check failed
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