Skip to content

feat: add audio hooks/music timeline support and sync with upstream timeline updates#427

Open
imAaryash wants to merge 13 commits intosiddharthvaddem:mainfrom
imAaryash:feat/background-music-upload
Open

feat: add audio hooks/music timeline support and sync with upstream timeline updates#427
imAaryash wants to merge 13 commits intosiddharthvaddem:mainfrom
imAaryash:feat/background-music-upload

Conversation

@imAaryash
Copy link
Copy Markdown
Collaborator

@imAaryash imAaryash commented Apr 12, 2026

Description

This PR improves the video editor audio workflow and timeline usability by adding:

  • Click-to-preview for music and hook tracks in the audio library.
  • Use-to-timeline for hook sound effects as short overlay clips.
  • Support for multiple overlapping hook sound effects.
  • Hook clips visible on timeline with drag, resize, and reposition support.
  • Timeline row visibility settings to hide/show tracks like Speed, Hooks, Music, etc. so users can focus on active lanes.
  • UI polish for music and hooks cards, including a playing indicator near actions and improved card readability.
  • Upstream sync fixes needed to keep TimelineEditor, SettingsPanel, and VideoEditor integrations compatible.

Motivation

As the editor gained more timeline lanes and audio options, the workflow became harder to navigate:

  • Users could not quickly audition tracks from the library.
  • Hook SFX assignment was less timeline-centric.
  • Timeline became long, making lower lanes like Music harder to access.
  • Visual feedback during preview and card readability needed improvement for production quality.

This PR solves those pain points by making audio selection faster, timeline editing more precise, and lane management more user-controlled.

Type of Change

  • New Feature
  • Bug Fix
  • Refactor / Code Cleanup
  • Documentation Update
  • Other (please specify)

Related Issue(s)

#397

Screenshots / Video

Screenshot 2026-04-12 102023 Screenshot 2026-04-12 102031 Screenshot 2026-04-12 102043 Screenshot 2026-04-12 102050 Screenshot 2026-04-12 111255

Testing

Reviewers can verify with these steps:

  1. Open editor and go to Audio section.
  2. In Music tab:
  • Click a track card to preview.
  • Confirm playing indicator appears near the action area.
  • Use a track and verify behavior remains correct.
  1. In Hooks tab:
  • Click a hook card to preview.
  • Click Use to add hook clip to timeline.
  • Add multiple hook clips at overlapping times.
  1. In Timeline:
  • Confirm hook clips are visible and can be moved/resized.
  • Open timeline settings and hide lanes (for example Speed) to reduce vertical clutter.
  • Confirm hidden lanes disappear from view and can be restored.
  1. Build validation:
  • Run npm run build-vite and confirm success.

Checklist

  • I have performed a self-review of my code.
  • I have added any necessary screenshots or videos.
  • I have linked related issue(s) and updated the changelog if applicable.

Summary by CodeRabbit

  • New Features

    • Background music import via audio file picker, with volume and timeline regions.
    • Audio hooks (assignable sound effects) that preview during playback and are included in exports.
    • Built-in audio library for music and hook sounds.
  • UI Improvements

    • Audio settings panel (upload, library, hooks) and timeline music/hook tracks with add/remove and visibility toggles.
    • Removed old zoom speed options UI.
  • Localization

    • Audio UI strings added for EN, ES, FR, KO, TR, ZH-CN.
  • Export

    • Exports now include background audio and hook playback.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds end-to-end background-music and audio-hook support: new Electron audio picker IPC, audio library/types, editor state/persistence, Settings UI for music/hooks with preview, timeline/music/hook items and visibility, playback preview/triggering, and export-time mixing/triggering of hook/background audio.

Changes

Cohort / File(s) Summary
Electron audio picker
electron/electron-env.d.ts, electron/ipc/handlers.ts, electron/preload.ts
Expose openAudioFilePicker via preload → IPC; validate allowed audio extensions and readable file paths in main handler; new IPC handler open-audio-file-picker.
Types & defaults
src/components/video-editor/types.ts, src/lib/audioLibrary.ts
Add AudioHookType, HookRegion, AudioHooksConfig, DEFAULT_AUDIO_HOOKS, and typed audio library exports.
Editor state & persistence
src/hooks/useEditorHistory.ts, src/components/video-editor/projectPersistence.ts, src/components/video-editor/projectPersistence.test.ts
Extend EditorState, INITIAL_EDITOR_STATE and normalization to include background music, volumes, audio hooks, and hookRegions; update tests/fixtures.
UI — Settings Panel
src/components/video-editor/SettingsPanel.tsx
Add audio accordion (upload/library/hooks), preview playback and hook assignment UI; remove zoom-speed UI/props and related controls.
Timeline & items
src/components/video-editor/timeline/TimelineEditor.tsx, src/components/video-editor/timeline/Item.tsx, src/components/video-editor/timeline/ItemGlass.module.css
Add music/hook item variants and glassBlue style; track visibility toggles (music row non-toggleable), music creation flow, region normalization/overlap handling; remove per-item zoom-duration controls.
Playback & editor wiring
src/components/video-editor/VideoPlayback.tsx, src/components/video-editor/VideoEditor.tsx
Background music audio element + region gating/sync; editor wiring for music/hook regions, selection, preview playback, and project serialization.
Exporter & audio processing
src/lib/exporter/audioEncoder.ts, src/lib/exporter/videoExporter.ts
AudioProcessor and VideoExporter extended for background audio, audio hooks, hookSoundLayers, trigger lead handling; renderPitchPreservedTimelineAudio mixes background and triggers hook playback during export.
Audio library data
src/lib/audioLibrary.json
Add JSON audio library of music and hook assets (data-only).
Tests — browser fixtures
src/lib/exporter/gifExporter.browser.test.ts, src/lib/exporter/videoExporter.browser.test.ts
Conditionalize “real browser” tests on Worker availability; test wrappers updated.
Streaming decoder
src/lib/exporter/streamingDecoder.ts
Resolve filesystem-style paths to absolute URLs before fetch; derive filename from resolved URL.
i18n
src/i18n/locales/en/settings.json, src/i18n/locales/en/timeline.json, src/i18n/locales/{es,fr,ko-KR,tr,zh-CN}/settings.json
Add audio-related localization keys for settings and timeline across multiple locales.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • siddharthvaddem

Poem

the editor learned to hum at 2am,
tiny hooks and music regions clicking like a drum,
a native picker says "choose" and hands you a file,
previews snicker, exports mix — kinda cursed, kinda wild 🎧✨

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the PR's main changes: adding audio hooks/music timeline support and syncing upstream updates.
Description check ✅ Passed The description follows the template structure with all key sections filled: Description, Motivation, Type of Change, Related Issue, Screenshots, Testing, and Checklist.

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


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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a966428c93

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/lib/exporter/audioEncoder.ts
Comment thread src/lib/exporter/audioEncoder.ts Outdated
Comment thread src/components/video-editor/VideoEditor.tsx Outdated
Copy link
Copy Markdown
Contributor

@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: 10

Caution

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

⚠️ Outside diff range comments (2)
src/components/video-editor/VideoEditor.tsx (1)

346-403: ⚠️ Potential issue | 🟠 Major

Add webcamSizePreset back to currentProjectSnapshot.

This memo depends on webcamSizePreset, but the field never gets serialized into the snapshot payload. That makes unsaved-change detection lie here: webcam size edits can be missed, and projects with a non-default webcam size can show as dirty immediately after load.

🧩 Minimal fix
 		return createProjectSnapshot(currentProjectMedia, {
 			wallpaper,
 			backgroundMusicPath,
 			backgroundMusicRegions,
 			backgroundMusicVolume,
 			audioHooks,
 			audioHooksVolume,
 			shadowIntensity,
 			showBlur,
 			motionBlurAmount,
 			borderRadius,
 			padding,
 			cropRegion,
 			zoomRegions,
 			trimRegions,
 			speedRegions,
 			annotationRegions,
 			hookRegions,
 			aspectRatio,
 			webcamLayoutPreset,
 			webcamMaskShape,
+			webcamSizePreset,
 			webcamPosition,
 			exportQuality,
 			exportFormat,
 			gifFrameRate,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/VideoEditor.tsx` around lines 346 - 403, The
snapshot payload returned by createProjectSnapshot is missing the
webcamSizePreset field, so add webcamSizePreset to the object passed into
createProjectSnapshot (the same place where wallpaper, backgroundMusicPath, etc.
are listed) so the memo for currentProjectSnapshot correctly serializes webcam
size changes; update the object in the return of the createProjectSnapshot call
in VideoEditor.tsx (where createProjectSnapshot is invoked) to include
webcamSizePreset.
src/components/video-editor/SettingsPanel.tsx (1)

1-18: ⚠️ Potential issue | 🟡 Minor

imports need sorting per biome

pipeline's complaining about import organization. quick fix:

npx biome check --write src/components/video-editor/SettingsPanel.tsx

or let your IDE's organize imports do the thing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/SettingsPanel.tsx` around lines 1 - 18, The
import statements in SettingsPanel.tsx are not sorted per Biome rules; run the
Biome formatter or organize imports so external packages and named imports are
alphabetized and grouped (e.g., ensure "Block" import and the "lucide-react"
named icons like Bug, Crop, Download, Film, Image, Lock, Music, Palette, Search,
Sparkles, Star, Trash2, Unlock, Upload, X are ordered per your project's Biome
configuration). Fix by running the suggested command (npx biome check --write
src/components/video-editor/SettingsPanel.tsx) or using your IDE's "Organize
Imports" to reorder and format the imports.
🧹 Nitpick comments (5)
src/components/video-editor/projectPersistence.test.ts (1)

34-55: Coverage gap: new audio persistence fields are only validated in empty/default state.

At Line 35 and Line 55 (mirrored at Line 116 and Line 136), fixtures only use empty regions and disabled hooks. that won’t catch regressions in actual hook/music timeline serialization (especially overlapping hooks, which this PR explicitly enables). nit: cleaner if we add one non-empty fixture case with overlapping hookRegions + non-default volumes.

Also applies to: 115-137

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/projectPersistence.test.ts` around lines 34 - 55,
Add a second non-default fixture to projectPersistence.test.ts that exercises
audio persistence with real data: populate backgroundMusicRegions and
hookRegions with overlapping regions, set audioHooks flags (e.g.,
zoom/trim/speed true as appropriate), and use non-default volumes
(backgroundMusicVolume and audioHooksVolume not zero/0.35) so
serialization/deserialization tests validate non-empty timelines; update the
existing assertions that currently only test empty arrays to also validate the
new fixture round-trip (look for the fixture object used in
serialize/deserialize tests and the fields backgroundMusicRegions, hookRegions,
audioHooks, backgroundMusicVolume, audioHooksVolume and add mirrored cases where
the other tests currently use only empty/default state).
electron/ipc/handlers.ts (1)

768-776: Use i18n keys here instead of hardcoded dialog strings.

Line 768 and Lines 772-776 are hardcoded English, while nearby file dialogs are localized via mainT(...). This will feel inconsistent in non-English locales.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/ipc/handlers.ts` around lines 768 - 776, Replace hardcoded dialog
strings in the file dialog options (the title property and the filter name
values under filters) with i18n lookups using mainT(...); specifically update
the title ("Select audio") and filter names ("Audio Files", "All Files") to call
mainT with appropriate keys (e.g. mainT('dialogs.selectAudio'),
mainT('dialogs.audioFiles'), mainT('dialogs.allFiles')) so the dialog is
localized while leaving defaultPath: app.getPath("music") unchanged; ensure the
chosen i18n keys exist or add them to the locale files and use the same keys
where other file dialogs in the codebase use mainT.
src/components/video-editor/SettingsPanel.tsx (3)

1374-1374: nit: magic number 80

the slice limit works fine but extracting const MAX_VISIBLE_TRACKS = 80 would make the intent clearer. same applies to line 1548. totally optional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/SettingsPanel.tsx` at line 1374, Extract the
magic number 80 into a clearly named constant (e.g., const MAX_VISIBLE_TRACKS =
80) and use it instead of the literal in the filteredMusicLibrary.slice call(s)
(replace filteredMusicLibrary.slice(0, 80) and the similar occurrence around
line with the same symbol with filteredMusicLibrary.slice(0,
MAX_VISIBLE_TRACKS)); declare the constant near the top of the SettingsPanel
component or file so intent is clear and reuse is straightforward.

709-746: minor: potential stale setState on unmount

there's a small race window where if the component unmounts while await getAssetPath is in flight, setPreviewingMusicTrackId could fire on an unmounted component. same pattern in the hook preview handler.

not a huge deal in practice (react will just warn), but you could add a mounted ref check if you want to be squeaky clean:

🛡️ optional guard pattern
const mountedRef = useRef(true);
useEffect(() => () => { mountedRef.current = false; }, []);

// then in handler:
if (!mountedRef.current) return;
setPreviewingMusicTrackId(trackId);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/SettingsPanel.tsx` around lines 709 - 746, The
handler handleMusicLibraryPreview can call setPreviewingMusicTrackId after an
awaited getAssetPath while the component may have unmounted; add a mountedRef
(useRef<boolean>) set to true and flipped to false in a useEffect cleanup, then
check mountedRef.current before calling setPreviewingMusicTrackId (and before
any other setState or toast calls) inside handleMusicLibraryPreview and the
similar preview handler to avoid updating state on an unmounted component.

726-729: hardcoded error strings

these toast messages are the only strings not going through t(). kinda inconsistent with the rest of the i18n setup.

♻️ use translation keys
-				toast.error("Unable to preview this track");
+				toast.error(t("audio.previewError"));

and same pattern for the hook preview error messages.

Also applies to: 741-744, 775-778, 790-793

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/SettingsPanel.tsx` around lines 726 - 729,
Replace hardcoded toast messages in SettingsPanel.tsx with i18n translation keys
using the existing t() function: where audio.play().catch(() => {
stopMusicPreview(); toast.error("Unable to preview this track"); }) (and the
similar hook preview error handlers) appear, call
toast.error(t('settings.preview_unable_to_play')) or similarly named keys
instead of raw strings; ensure t is imported/available in the component
(useTranslation hook) and add corresponding keys to your translation files for
the new entries (e.g., settings.preview_unable_to_play,
settings.preview_hook_error) so all preview error toasts use t() consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@electron/preload.ts`:
- Around line 70-72: The open-audio-file-picker IPC call is exposed without
validating the caller; update the main process IPC handler for
"open-audio-file-picker" to verify event.sender before proceeding (similar to
how "save-project-file" uses isTrustedProjectPath) — check the event.sender or
its webContents/id against your trusted window list or call
isTrustedProjectPath-equivalent and return/reject if not trusted, so only
known/trusted renderer windows can trigger the handler (refer to the
"open-audio-file-picker" channel, the ipcRenderer.invoke call in preload.ts, and
the existing "save-project-file"/isTrustedProjectPath logic to mirror the
validation approach).

In `@src/components/video-editor/SettingsPanel.tsx`:
- Around line 1396-1583: The pipeline flagged formatting drift in the
SettingsPanel component JSX (SettingsPanel.tsx around the music/hooks JSX blocks
and handlers like handleHookLibraryPreview, onMusicTrackSelect,
onHookTimelineAdd), so run the project's formatter on that file and commit the
changes: execute npx biome check --write
src/components/video-editor/SettingsPanel.tsx (or the equivalent biome format
command), review the resulting edits to the JSX/indentation, stage and commit
them, and push so CI no longer fails.
- Around line 211-213: The props selectedZoomInDuration, selectedZoomOutDuration
and onZoomDurationChange are dead — remove them from the SettingsPanel props
interface and any type declarations in the SettingsPanel component (search for
SettingsPanel and its props/interface) and also remove them from where
SettingsPanel is instantiated (VideoEditor) so you no longer pass these unused
props; ensure Typescript types and any prop spreading remain consistent after
deletion.

In `@src/components/video-editor/timeline/Item.tsx`:
- Around line 166-179: The JSX branches for isMusic and isHook always render the
literal strings "Music" and "Hook", hiding the actual clip label passed from
TimelineEditor; update the isMusic and isHook branches in Item.tsx to render the
incoming label variable (e.g., label or clip.label used elsewhere in this
component) instead of the hardcoded text while keeping the Music2 and Sparkles
icons and the existing classes so stacked SFX/hook clips show their real labels.

In `@src/components/video-editor/timeline/TimelineEditor.tsx`:
- Around line 791-805: The hint string for the hook row is hardcoded; replace
hint="Use hook sounds from Audio settings" with a localized call (e.g.
hint={t('timeline.hookRow.hint')} or your project’s i18n key) so the Row with
id={HOOK_ROW_ID} uses translation like the neighboring rows; update the
translation keys/file to include the new string and keep usage consistent with
other rows that call t(...), and ensure hookItems, selectedHookId, and
onSelectHook behavior is unchanged.

In `@src/components/video-editor/VideoEditor.tsx`:
- Around line 190-196: hookSoundLayers is only component state and must be
persisted to the project's audio config so projects keep their layer assignments
across load/save; update initialization and setters to read/write
project.audioConfig.hookSoundLayers. Specifically: when mounting/init (where
hookSoundLayers is created from DEFAULT_HOOK_SOUND_ASSETS) load from
project.audioConfig.hookSoundLayers if present (use hookSoundLayers and
setHookSoundLayers), and when users change layers call setHookSoundLayers and
also update project.audioConfig.hookSoundLayers so the value is included in
saveProject; also ensure loadProject populates the component state from
project.audioConfig.hookSoundLayers and that preview/export routines read from
project.audioConfig.hookSoundLayers (or from the authoritative hookSoundLayers
state) so reopen/preview/export produce the same audio.

In `@src/components/video-editor/VideoPlayback.tsx`:
- Around line 1186-1194: The useEffect for resetting music lists
backgroundMusicPath in its deps but never references it, triggering
exhaustive-deps; update the effect body (the useEffect that accesses
backgroundMusicRef and calls backgroundMusic.pause()/backgroundMusic.currentTime
= 0) to also reference backgroundMusicPath in the guard (e.g., check if
backgroundMusicPath is falsy alongside backgroundMusic) so the dependency is
intentional and lint-clean while preserving the reset behavior.
- Around line 1212-1222: The code is seeking the looped audio directly to the
video's currentTime which can exceed the audio duration and cause clamp/loop
jitter; update both places that set backgroundMusic.currentTime to compute a
wrappedTime = (backgroundMusic.duration > 0 &&
isFinite(backgroundMusic.duration)) ? (currentTime % backgroundMusic.duration) :
currentTime, guard against NaN/zero duration, and assign
backgroundMusic.currentTime = wrappedTime instead of the raw currentTime (apply
the same modulo logic in the pause branch and the playing-sync branch for
backgroundMusic).

In `@src/lib/audioLibrary.json`:
- Around line 1-365: The audioLibrary.json manifest is failing biome formatting
(biome check) starting at the top of the file; fix by running the Biome
formatter (or the repo's prep script that regenerates the manifest) against
audioLibrary.json to apply the expected formatting and JSON normalization
(ensure the top-level array and objects remain valid JSON—no trailing commas or
extra characters), then commit the formatted file so biome check passes.

In `@src/lib/exporter/audioEncoder.ts`:
- Around line 312-364: playHookSound currently clamps audioHooksVolume above
zero (uses Math.max(0.01, ...) and sets initial gain to 0.0001), causing a
slider value of 0 to still produce sound; change both the file-backed branch and
synth branch to respect true zero: for file-backed nodes set gain.gain.value =
Math.min(1, Math.max(0, audioHooksVolume)); for the oscillator path compute peak
= Math.min(0.22, Math.max(0, audioHooksVolume * 0.22)) and if peak === 0 simply
skip creating/starting the oscillator (or create it but set gain to 0 and do not
schedule ramps) to avoid exponential/linear ramps from a non-zero floor; update
any other occurrences in playHookSound (and the similar code block at the other
range noted) that use Math.max(..., 0.01) or initial 0.0001 to use 0 so mute =
truly silent (refer to playHookSound, audioHooksVolume, audioContext,
destinationNode, HOOK_DURATIONS, HOOK_FREQUENCIES, disconnectHookNode,
activeHookNodes).

---

Outside diff comments:
In `@src/components/video-editor/SettingsPanel.tsx`:
- Around line 1-18: The import statements in SettingsPanel.tsx are not sorted
per Biome rules; run the Biome formatter or organize imports so external
packages and named imports are alphabetized and grouped (e.g., ensure "Block"
import and the "lucide-react" named icons like Bug, Crop, Download, Film, Image,
Lock, Music, Palette, Search, Sparkles, Star, Trash2, Unlock, Upload, X are
ordered per your project's Biome configuration). Fix by running the suggested
command (npx biome check --write src/components/video-editor/SettingsPanel.tsx)
or using your IDE's "Organize Imports" to reorder and format the imports.

In `@src/components/video-editor/VideoEditor.tsx`:
- Around line 346-403: The snapshot payload returned by createProjectSnapshot is
missing the webcamSizePreset field, so add webcamSizePreset to the object passed
into createProjectSnapshot (the same place where wallpaper, backgroundMusicPath,
etc. are listed) so the memo for currentProjectSnapshot correctly serializes
webcam size changes; update the object in the return of the
createProjectSnapshot call in VideoEditor.tsx (where createProjectSnapshot is
invoked) to include webcamSizePreset.

---

Nitpick comments:
In `@electron/ipc/handlers.ts`:
- Around line 768-776: Replace hardcoded dialog strings in the file dialog
options (the title property and the filter name values under filters) with i18n
lookups using mainT(...); specifically update the title ("Select audio") and
filter names ("Audio Files", "All Files") to call mainT with appropriate keys
(e.g. mainT('dialogs.selectAudio'), mainT('dialogs.audioFiles'),
mainT('dialogs.allFiles')) so the dialog is localized while leaving defaultPath:
app.getPath("music") unchanged; ensure the chosen i18n keys exist or add them to
the locale files and use the same keys where other file dialogs in the codebase
use mainT.

In `@src/components/video-editor/projectPersistence.test.ts`:
- Around line 34-55: Add a second non-default fixture to
projectPersistence.test.ts that exercises audio persistence with real data:
populate backgroundMusicRegions and hookRegions with overlapping regions, set
audioHooks flags (e.g., zoom/trim/speed true as appropriate), and use
non-default volumes (backgroundMusicVolume and audioHooksVolume not zero/0.35)
so serialization/deserialization tests validate non-empty timelines; update the
existing assertions that currently only test empty arrays to also validate the
new fixture round-trip (look for the fixture object used in
serialize/deserialize tests and the fields backgroundMusicRegions, hookRegions,
audioHooks, backgroundMusicVolume, audioHooksVolume and add mirrored cases where
the other tests currently use only empty/default state).

In `@src/components/video-editor/SettingsPanel.tsx`:
- Line 1374: Extract the magic number 80 into a clearly named constant (e.g.,
const MAX_VISIBLE_TRACKS = 80) and use it instead of the literal in the
filteredMusicLibrary.slice call(s) (replace filteredMusicLibrary.slice(0, 80)
and the similar occurrence around line with the same symbol with
filteredMusicLibrary.slice(0, MAX_VISIBLE_TRACKS)); declare the constant near
the top of the SettingsPanel component or file so intent is clear and reuse is
straightforward.
- Around line 709-746: The handler handleMusicLibraryPreview can call
setPreviewingMusicTrackId after an awaited getAssetPath while the component may
have unmounted; add a mountedRef (useRef<boolean>) set to true and flipped to
false in a useEffect cleanup, then check mountedRef.current before calling
setPreviewingMusicTrackId (and before any other setState or toast calls) inside
handleMusicLibraryPreview and the similar preview handler to avoid updating
state on an unmounted component.
- Around line 726-729: Replace hardcoded toast messages in SettingsPanel.tsx
with i18n translation keys using the existing t() function: where
audio.play().catch(() => { stopMusicPreview(); toast.error("Unable to preview
this track"); }) (and the similar hook preview error handlers) appear, call
toast.error(t('settings.preview_unable_to_play')) or similarly named keys
instead of raw strings; ensure t is imported/available in the component
(useTranslation hook) and add corresponding keys to your translation files for
the new entries (e.g., settings.preview_unable_to_play,
settings.preview_hook_error) so all preview error toasts use t() consistently.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 11160942-dcee-4322-a6a5-34e7d517d266

📥 Commits

Reviewing files that changed from the base of the PR and between a6ae0e6 and a966428.

⛔ Files ignored due to path filters (38)
  • public/audio/hooks/annotation.mp3 is excluded by !**/*.mp3
  • public/audio/hooks/blur.wav is excluded by !**/*.wav
  • public/audio/hooks/speed.mp3 is excluded by !**/*.mp3
  • public/audio/hooks/trim.wav is excluded by !**/*.wav
  • public/audio/hooks/zoom.wav is excluded by !**/*.wav
  • public/audio/library/hooks/openscreen-sfx-alarm-001.mp3 is excluded by !**/*.mp3
  • public/audio/library/hooks/openscreen-sfx-alert.mp3 is excluded by !**/*.mp3
  • public/audio/library/hooks/openscreen-sfx-beep-001.wav is excluded by !**/*.wav
  • public/audio/library/hooks/openscreen-sfx-camera-001.wav is excluded by !**/*.wav
  • public/audio/library/hooks/openscreen-sfx-camera-004.mp3 is excluded by !**/*.mp3
  • public/audio/library/hooks/openscreen-sfx-click-001.wav is excluded by !**/*.wav
  • public/audio/library/hooks/openscreen-sfx-click-002.mp3 is excluded by !**/*.mp3
  • public/audio/library/hooks/openscreen-sfx-clock-001.mp3 is excluded by !**/*.mp3
  • public/audio/library/hooks/openscreen-sfx-fade-out-005.wav is excluded by !**/*.wav
  • public/audio/library/hooks/openscreen-sfx-glitch-001.mp3 is excluded by !**/*.mp3
  • public/audio/library/hooks/openscreen-sfx-glitch-002.mp3 is excluded by !**/*.mp3
  • public/audio/library/hooks/openscreen-sfx-misc-002.mp3 is excluded by !**/*.mp3
  • public/audio/library/hooks/openscreen-sfx-misc-014.wav is excluded by !**/*.wav
  • public/audio/library/hooks/openscreen-sfx-money-002.mp3 is excluded by !**/*.mp3
  • public/audio/library/hooks/openscreen-sfx-money-003.mp3 is excluded by !**/*.mp3
  • public/audio/library/hooks/openscreen-sfx-notification-001.mp3 is excluded by !**/*.mp3
  • public/audio/library/hooks/openscreen-sfx-notification-003.mp3 is excluded by !**/*.mp3
  • public/audio/library/hooks/openscreen-sfx-notification-004.mp3 is excluded by !**/*.mp3
  • public/audio/library/hooks/openscreen-sfx-reveal-013.mp3 is excluded by !**/*.mp3
  • public/audio/library/hooks/openscreen-sfx-typing-001.mp3 is excluded by !**/*.mp3
  • public/audio/library/hooks/openscreen-sfx-typing-007.mp3 is excluded by !**/*.mp3
  • public/audio/library/hooks/openscreen-sfx-whoosh-001.wav is excluded by !**/*.wav
  • public/audio/library/hooks/openscreen-sfx-whoosh-004.mp3 is excluded by !**/*.mp3
  • public/audio/library/hooks/openscreen-sfx-whoosh-005.mp3 is excluded by !**/*.mp3
  • public/audio/library/hooks/openscreen-sfx-whoosh-006.mp3 is excluded by !**/*.mp3
  • public/audio/library/music/openscreen-ambient-001.mp3 is excluded by !**/*.mp3
  • public/audio/library/music/openscreen-ambient-002.mp3 is excluded by !**/*.mp3
  • public/audio/library/music/openscreen-corporate-001.mp3 is excluded by !**/*.mp3
  • public/audio/library/music/openscreen-corporate-002.mp3 is excluded by !**/*.mp3
  • public/audio/library/music/openscreen-lofi-001.mp3 is excluded by !**/*.mp3
  • public/audio/library/music/openscreen-lofi-002.mp3 is excluded by !**/*.mp3
  • public/audio/library/music/openscreen-track-001.mp3 is excluded by !**/*.mp3
  • public/audio/library/music/openscreen-upbeat-001.mp3 is excluded by !**/*.mp3
📒 Files selected for processing (25)
  • electron/electron-env.d.ts
  • electron/ipc/handlers.ts
  • electron/preload.ts
  • scripts/prepare_audio_library.py
  • src/components/video-editor/SettingsPanel.tsx
  • src/components/video-editor/VideoEditor.tsx
  • src/components/video-editor/VideoPlayback.tsx
  • src/components/video-editor/projectPersistence.test.ts
  • src/components/video-editor/projectPersistence.ts
  • src/components/video-editor/timeline/Item.tsx
  • src/components/video-editor/timeline/ItemGlass.module.css
  • src/components/video-editor/timeline/TimelineEditor.tsx
  • src/components/video-editor/types.ts
  • src/hooks/useEditorHistory.ts
  • src/i18n/locales/en/settings.json
  • src/i18n/locales/en/timeline.json
  • src/i18n/locales/es/settings.json
  • src/i18n/locales/fr/settings.json
  • src/i18n/locales/ko-KR/settings.json
  • src/i18n/locales/tr/settings.json
  • src/i18n/locales/zh-CN/settings.json
  • src/lib/audioLibrary.json
  • src/lib/audioLibrary.ts
  • src/lib/exporter/audioEncoder.ts
  • src/lib/exporter/videoExporter.ts

Comment thread electron/preload.ts
Comment thread src/components/video-editor/SettingsPanel.tsx
Comment thread src/components/video-editor/SettingsPanel.tsx Outdated
Comment thread src/components/video-editor/timeline/Item.tsx
Comment thread src/components/video-editor/timeline/TimelineEditor.tsx Outdated
Comment thread src/components/video-editor/VideoEditor.tsx Outdated
Comment thread src/components/video-editor/VideoPlayback.tsx
Comment thread src/components/video-editor/VideoPlayback.tsx
Comment thread src/lib/audioLibrary.json Outdated
Comment thread src/lib/exporter/audioEncoder.ts
Copy link
Copy Markdown
Contributor

@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: 1

♻️ Duplicate comments (1)
src/components/video-editor/timeline/TimelineEditor.tsx (1)

822-827: ⚠️ Potential issue | 🟡 Minor

Hook-row hint is still hardcoded.

Line 826 is still bypassing t(...), so this row stays English-only while neighboring rows are localized.

nit: cleaner i18n wiring
-				<Row
-					id={HOOK_ROW_ID}
-					isEmpty={hookItems.length === 0}
-					hint="Use hook sounds from Audio settings"
-				>
+				<Row
+					id={HOOK_ROW_ID}
+					isEmpty={hookItems.length === 0}
+					hint={t("hints.pressHook")}
+				>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/timeline/TimelineEditor.tsx` around lines 822 -
827, The Hook row currently passes a hardcoded English hint string to the Row
component (see Row with id=HOOK_ROW_ID and hint="Use hook sounds from Audio
settings"), so localizations are skipped; replace the literal with a translated
string using the project's i18n function (e.g., call t('timeline.hookHint') or
the existing translation hook used in this file) and supply that result to the
hint prop (keep the check using hookItems.length and rowVisibility.hook
unchanged).
🧹 Nitpick comments (1)
src/components/video-editor/timeline/TimelineEditor.tsx (1)

950-955: Harden localStorage hydration for row visibility.

Current spread trusts JSON shape directly. If persisted data is malformed (or manually edited), checkbox state can get kinda cursed. Worth sanitizing each key to booleans before setRowVisibility.

safe parse/sanitize pattern
-			const parsed = JSON.parse(raw) as Partial<TimelineRowVisibility>;
-			setRowVisibility({
-				...DEFAULT_ROW_VISIBILITY,
-				...parsed,
-				music: true,
-			});
+			const parsed = JSON.parse(raw) as Partial<Record<TimelineRowKey, unknown>>;
+			setRowVisibility({
+				zoom: parsed.zoom === true ? true : DEFAULT_ROW_VISIBILITY.zoom,
+				trim: parsed.trim === true ? true : DEFAULT_ROW_VISIBILITY.trim,
+				annotation: parsed.annotation === true ? true : DEFAULT_ROW_VISIBILITY.annotation,
+				blur: parsed.blur === true ? true : DEFAULT_ROW_VISIBILITY.blur,
+				speed: parsed.speed === true ? true : DEFAULT_ROW_VISIBILITY.speed,
+				hook: parsed.hook === true ? true : DEFAULT_ROW_VISIBILITY.hook,
+				music: true,
+			});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/timeline/TimelineEditor.tsx` around lines 950 -
955, The localStorage hydration in TimelineEditor trusts the parsed object shape
(parsed) and can set invalid checkbox states; update the hydration before
calling setRowVisibility to (1) guard JSON.parse with try/catch and ignore/clear
malformed raw, (2) build a sanitized object by iterating the keys of
DEFAULT_ROW_VISIBILITY (or TimelineRowVisibility keys) and coercing each value
to a boolean (use parsed[key] === true), falling back to
DEFAULT_ROW_VISIBILITY[key] when missing/invalid, and (3) still enforce music:
true; then call setRowVisibility with the merged sanitized object instead of
directly spreading parsed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/video-editor/timeline/TimelineEditor.tsx`:
- Around line 969-977: Replace the hardcoded English labels in the
TimelineEditor with i18n keys: use the component's translation helper (e.g., t
or useTranslation) to change the array rowVisibilityOptions to return { key:
"zoom", label: t("timeline.row.zoom") } etc. Do the same for the other hardcoded
UI strings referenced in this component (the fallback hook label and the
menu/track labels flagged elsewhere in the TimelineEditor) — add new translation
keys for Zoom/Trim/Annotation/Blur/Speed/Hook/Music in your locale files and
call t(...) where those raw strings are currently used so all locales can
translate them.

---

Duplicate comments:
In `@src/components/video-editor/timeline/TimelineEditor.tsx`:
- Around line 822-827: The Hook row currently passes a hardcoded English hint
string to the Row component (see Row with id=HOOK_ROW_ID and hint="Use hook
sounds from Audio settings"), so localizations are skipped; replace the literal
with a translated string using the project's i18n function (e.g., call
t('timeline.hookHint') or the existing translation hook used in this file) and
supply that result to the hint prop (keep the check using hookItems.length and
rowVisibility.hook unchanged).

---

Nitpick comments:
In `@src/components/video-editor/timeline/TimelineEditor.tsx`:
- Around line 950-955: The localStorage hydration in TimelineEditor trusts the
parsed object shape (parsed) and can set invalid checkbox states; update the
hydration before calling setRowVisibility to (1) guard JSON.parse with try/catch
and ignore/clear malformed raw, (2) build a sanitized object by iterating the
keys of DEFAULT_ROW_VISIBILITY (or TimelineRowVisibility keys) and coercing each
value to a boolean (use parsed[key] === true), falling back to
DEFAULT_ROW_VISIBILITY[key] when missing/invalid, and (3) still enforce music:
true; then call setRowVisibility with the merged sanitized object instead of
directly spreading parsed.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 413ccc24-f1f9-429f-80fc-55054d1260fc

📥 Commits

Reviewing files that changed from the base of the PR and between a966428 and 4a83f8d.

📒 Files selected for processing (1)
  • src/components/video-editor/timeline/TimelineEditor.tsx

Comment thread src/components/video-editor/timeline/TimelineEditor.tsx Outdated
Reason:\n- background music drifted during trim jumps because only source media sought\n- hook audio remained audible at 0 volume due to a non-zero gain clamp\n- browser exporter tests failed with ERR_INVALID_URL for fixture paths in this runtime\n\nWhat changed:\n- keep background media in timeline sync when source skips trims\n- allow true zero gain for hook export mix\n- preserve bundled /audio URLs in editor track resolution\n- normalize relative/root URLs before fetch in streaming decoder\n- make browser-only exporter tests environment-safe under Node runtimes
Copy link
Copy Markdown
Contributor

@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: 3

♻️ Duplicate comments (1)
src/lib/exporter/audioEncoder.ts (1)

481-490: ⚠️ Potential issue | 🟠 Major

Dropped hook triggers when multiple events are crossed in one frame

lowkey risky: on Line 487, events.some(...) fires at most once per tick per hook type. If two events land in the same frame window (easy with dense edits or higher playbackRate), one gets dropped.

💡 Minimal fix
- const events = hookEventTimes[hook] ?? [];
- if (events.some((timeMs) => crossed(timeMs))) {
-   playHookSound(hook);
- }
+ const events = hookEventTimes[hook] ?? [];
+ const crossedCount = events.reduce(
+   (count, timeMs) => count + (crossed(timeMs) ? 1 : 0),
+   0,
+ );
+ for (let i = 0; i < crossedCount; i += 1) {
+   playHookSound(hook);
+ }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/exporter/audioEncoder.ts` around lines 481 - 490, The current logic
in the hasAnyHook/hookEventTimes block uses events.some((timeMs) =>
crossed(timeMs)) which only triggers playHookSound(hook) once per hook type per
frame, dropping additional events that also crossed; change the logic to iterate
all events for each hook (using forEach or a filter+forEach) and call
playHookSound(hook) for every timeMs where crossed(timeMs) is true so multiple
events in the same frame are all fired; look for HOOK_FREQUENCIES,
AudioHookType, hookEventTimes, crossed, and playHookSound to locate and update
the loop accordingly.
🧹 Nitpick comments (2)
src/lib/exporter/audioEncoder.ts (1)

35-38: Doc comment is stale relative to runtime mode selection

nit: cleaner if the process() docblock is updated — Line 35-38 still says timeline render is speed-only, but Line 66 also routes background/hook cases through that path now.

Also applies to: 65-67

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/exporter/audioEncoder.ts` around lines 35 - 38, Update the stale
docblock above the process() function to reflect current runtime routing:
instead of saying the timeline render pipeline is used only for speed regions,
state that audio export uses the fast WebCodecs trim-only pipeline when no
special processing is needed, and uses the pitch-preserving rendered timeline
pipeline when speed regions are present or when background/hook cases require
rendered timeline processing; mention both conditions explicitly and reference
the process() function name so readers know which function's behavior is
described.
src/lib/exporter/videoExporter.browser.test.ts (1)

9-36: Refactor suggestion: extract shared electronAPI fixture mock helper.

These setup/teardown blocks are basically copy-pasted between GIF and Video browser tests. kinda cursed to maintain long-term; one helper would keep behavior aligned.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/exporter/videoExporter.browser.test.ts` around lines 9 - 36, Extract
the repeated electronAPI mock into a shared test helper: create a helper
function (e.g., createElectronAPIMock or withMockedElectronAPI) that accepts
parameters like readBinaryFile implementation or fixture path (sampleVideoPath)
and returns a teardown restore function or the originalElectronAPI; replace the
current beforeAll/afterAll pair that mutates windowWithElectron.electronAPI and
originalElectronAPI in videoExporter.browser.test.ts (and the GIF test) with a
call to that helper inside beforeAll to install the mock and call the returned
restore in afterAll (keep the same readBinaryFile behavior that returns {
success, data, path, message } and still uses readFile). Ensure the helper
references windowWithElectron and the readBinaryFile signature so both tests can
reuse it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/exporter/audioEncoder.ts`:
- Around line 464-466: The current crossed(timeMs) predicate (used with
previousTimeMs and sourceMedia.currentTime) only checks numeric crossing and can
fire for times that are inside trimmed/ skipped timeline ranges because
trim/skipping logic runs later; modify crossed (or wrap its usage) to first
consult the trim/skip test so it returns false for any timeMs that falls within
a trimmed-out region (e.g., incorporate the existing trim-check predicate used
around lines ~500 or call isTrimmed(timeMs) inside crossed), and apply the same
change to the other similar checks referenced (the blocks around 493-499 and
500-518) so no hook/clip start is emitted for times inside trimmed ranges.
Ensure you update references to crossed wherever it’s used so the early
trim-skip guard prevents firing before the jump.

In `@src/lib/exporter/gifExporter.browser.test.ts`:
- Around line 1-2: The tests import Node-only modules (node:fs/promises,
node:path) which break Vitest browser mode; replace their use in
src/lib/exporter/gifExporter.browser.test.ts and
src/lib/exporter/videoExporter.browser.test.ts by loading fixture assets via the
browser-friendly URL/fetch pattern: construct the fixture URL with new
URL('./fixtures/yourfile.bin', import.meta.url) or use a hardcoded dev-fixture
URL, then fetch() that URL and call response.arrayBuffer()/blob() instead of
readFile, and remove any path.join/path.resolve usage; update any helper
utilities in these tests that call readFile or path to accept ArrayBuffer/Blob
or convert fetched data as needed. Ensure tests no longer import node:fs or
node:path.

---

Duplicate comments:
In `@src/lib/exporter/audioEncoder.ts`:
- Around line 481-490: The current logic in the hasAnyHook/hookEventTimes block
uses events.some((timeMs) => crossed(timeMs)) which only triggers
playHookSound(hook) once per hook type per frame, dropping additional events
that also crossed; change the logic to iterate all events for each hook (using
forEach or a filter+forEach) and call playHookSound(hook) for every timeMs where
crossed(timeMs) is true so multiple events in the same frame are all fired; look
for HOOK_FREQUENCIES, AudioHookType, hookEventTimes, crossed, and playHookSound
to locate and update the loop accordingly.

---

Nitpick comments:
In `@src/lib/exporter/audioEncoder.ts`:
- Around line 35-38: Update the stale docblock above the process() function to
reflect current runtime routing: instead of saying the timeline render pipeline
is used only for speed regions, state that audio export uses the fast WebCodecs
trim-only pipeline when no special processing is needed, and uses the
pitch-preserving rendered timeline pipeline when speed regions are present or
when background/hook cases require rendered timeline processing; mention both
conditions explicitly and reference the process() function name so readers know
which function's behavior is described.

In `@src/lib/exporter/videoExporter.browser.test.ts`:
- Around line 9-36: Extract the repeated electronAPI mock into a shared test
helper: create a helper function (e.g., createElectronAPIMock or
withMockedElectronAPI) that accepts parameters like readBinaryFile
implementation or fixture path (sampleVideoPath) and returns a teardown restore
function or the originalElectronAPI; replace the current beforeAll/afterAll pair
that mutates windowWithElectron.electronAPI and originalElectronAPI in
videoExporter.browser.test.ts (and the GIF test) with a call to that helper
inside beforeAll to install the mock and call the returned restore in afterAll
(keep the same readBinaryFile behavior that returns { success, data, path,
message } and still uses readFile). Ensure the helper references
windowWithElectron and the readBinaryFile signature so both tests can reuse it.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b079d26b-86a0-4faa-b20c-e46358819edb

📥 Commits

Reviewing files that changed from the base of the PR and between 5e0733d and 2703191.

📒 Files selected for processing (5)
  • src/components/video-editor/VideoEditor.tsx
  • src/lib/exporter/audioEncoder.ts
  • src/lib/exporter/gifExporter.browser.test.ts
  • src/lib/exporter/streamingDecoder.ts
  • src/lib/exporter/videoExporter.browser.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/video-editor/VideoEditor.tsx

Comment thread src/lib/exporter/audioEncoder.ts
Comment thread src/lib/exporter/gifExporter.browser.test.ts Outdated
Comment thread src/lib/exporter/videoExporter.browser.test.ts Outdated
@imAaryash
Copy link
Copy Markdown
Collaborator Author

@siddharthvaddem can you review this PR? from my side all looks good just make sure merge my PR first :) don't wanna deal with more conflicts

@siddharthvaddem
Copy link
Copy Markdown
Owner

siddharthvaddem commented Apr 15, 2026

@siddharthvaddem can you review this PR? from my side all looks good just make sure merge my PR first :) don't wanna deal with more conflicts

Sure thing. I'll make sure to prioritise getting this in since it's a larger change. But please give me some time to get back on this.

Copy link
Copy Markdown

@theabhipareek theabhipareek left a comment

Choose a reason for hiding this comment

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

Review Summary: Audio Hooks & Music Timeline Support

Thanks for this comprehensive audio feature! The UX improvements are solid, but there are 2 critical bugs + linting issues that need fixing before merge.

🔴 Critical Blockers

1. Missing webcamSizePreset in Project Snapshot

  • File: src/components/video-editor/VideoEditor.tsx (lines 346-403)
  • Issue: The currentProjectSnapshot memo excludes webcamSizePreset, so unsaved-change detection breaks for webcam size edits—projects with non-default sizes show as dirty after reload.
  • Fix: Add webcamSizePreset to the createProjectSnapshot() object alongside other fields like wallpaper, backgroundMusicPath, etc.

2. Multiple Hook Events Dropped When Firing Simultaneously

  • File: src/lib/exporter/audioEncoder.ts (lines 481-490)
  • Issue: Using events.some() only fires playHookSound() once per frame. If 2+ hook events land in the same frame (common with dense edits), the extras are silently dropped.
  • Current: if (events.some((timeMs) => crossed(timeMs))) { playHookSound(hook); }
  • Fix: Iterate all events: events.forEach((timeMs) => { if (crossed(timeMs)) playHookSound(hook); })

🟡 Linting + i18n Issues

3. Biome Import Formatting Failure

  • File: src/components/video-editor/SettingsPanel.tsx (lines 1-18)
  • Fix: npx biome check --write src/components/video-editor/SettingsPanel.tsx

4. Hardcoded English Strings (Not Localized)
Missing t(...) / mainT(...) calls:

  • electron/ipc/handlers.ts (line 768): Dialog title "Select audio" + filter names
  • SettingsPanel.tsx (lines 726-729, 741-744): Toast error messages ("Unable to preview this track")
  • TimelineEditor.tsx (line 826): Hook row hint "Use hook sounds from Audio settings"

All should use i18n keys for consistency with the rest of the UI.

🟢 Minor Improvements (Optional)

  • Line 709-746 (SettingsPanel): Add mountedRef guard to prevent setState on unmounted component during async preview loading
  • Line 950-955 (TimelineEditor): Sanitize localStorage hydration to coerce each boolean key instead of trusting JSON shape
  • Line 1374 (SettingsPanel): Extract magic number 80const MAX_VISIBLE_TRACKS = 80
  • Line 35-38 (audioEncoder.ts): Update stale docblock—speed regions aren't the only case anymore (background/hook also use timeline render)
  • projectPersistence.test.ts (lines 34-55): Add test fixture with overlapping hookRegions + non-default volumes

✅ Strengths

  • Clear feature scope, good UX motivation
  • Solid audio library (30+ assets)
  • Proper Electron IPC security validation
  • i18n structure in place (just needs completeness)
  1. Fix webcamSizePreset serialization
  2. Fix hook event loop (iterate all crossings)
  3. Run Biome on SettingsPanel
  4. Replace hardcoded strings with i18n
  5. Consider: unmount guard, storage sanitization, test coverage

Copy link
Copy Markdown
Owner

@siddharthvaddem siddharthvaddem left a comment

Choose a reason for hiding this comment

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

can you take another look - the audio stuff seems to work fine, but I want to make sure nothing else is affected.

Comment thread src/components/video-editor/VideoEditor.tsx Outdated
Comment thread src/components/video-editor/timeline/Item.tsx
Comment thread src/components/video-editor/timeline/TimelineEditor.tsx
Comment thread src/components/video-editor/timeline/TimelineEditor.tsx
@n0taaryash
Copy link
Copy Markdown

ahh shit, I was tweaking audio hook sync with zoom timing and kinda ended up committing everything in the same branch, my bad
I was trying to fix how the hook timing feels when zoom durations are heavily customized since it felt a bit off with the visuals. while doing that, I simplified the timeline zoom and accidentally removed the zoom transition handle too

@LucaFontanot
Copy link
Copy Markdown

LucaFontanot commented Apr 16, 2026

@n0taaryash I have tested your feature. I want to report you that the sample audio files are not included in the build artifact. This results in a "unable to preview this track". Consider adding them to the electron builder file.

P.s. I'm still testing, I have noticed that the trim functionality also cuts the music, resulting in the music not being continuous.

P.s. It would be nice to have some options on the audio tracks. Like fade in and fade out

P.s. When exporting the file to video, the audio is not included

@imAaryash
Copy link
Copy Markdown
Collaborator Author

hey @LucaFontanot thanks for the reviews, ill fix these right away

…ettings UX

- add top-right export action with dedicated ExportSettingsPopup

- move support actions (GitHub, bug report, Discord) into header controls

- add searchable audio libraries with Fuse.js and smooth list scrolling

- extract audio preview/hook logic into useAudioPreview hook

- add background music fade-in/fade-out support and persist/export fields

- include packaged public audio assets in electron-builder
@imAaryash imAaryash requested a review from theabhipareek April 17, 2026 03:43
@siddharthvaddem
Copy link
Copy Markdown
Owner

@imAaryash fix the final merge conflicts and should be good to merge. I'll cut release here, want to do extensive testing before making a release

@imAaryash imAaryash added the enhancement New feature or request label Apr 19, 2026
@siddharthvaddem
Copy link
Copy Markdown
Owner

@imAaryash gentle bump

@imAaryash
Copy link
Copy Markdown
Collaborator Author

@imAaryash gentle bump

yoo, i thought we decided to push this feature in the next release, so I kept it idle. I'll fix the conflicts

@LucaFontanot
Copy link
Copy Markdown

Is this pull request still active? Is help needed?

@siddharthvaddem
Copy link
Copy Markdown
Owner

@LucaFontanot it is still active - the author is currently occupied and will get to it when he can.

@imAaryash
Copy link
Copy Markdown
Collaborator Author

hey @LucaFontanot im just stuck with my exams, this pr is still active :)

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants