diff --git a/electron/native/ScreenCaptureKitRecorder.swift b/electron/native/ScreenCaptureKitRecorder.swift index 99980f867..6e4b0efdc 100644 --- a/electron/native/ScreenCaptureKitRecorder.swift +++ b/electron/native/ScreenCaptureKitRecorder.swift @@ -394,13 +394,8 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate { func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of outputType: SCStreamOutputType) { guard sessionStarted, sampleBuffer.isValid, isRecording else { return } - guard let presentationTime = adjustedPresentationTime(for: sampleBuffer, outputType: outputType) else { return } if outputType == .screen { - if frameCount > 0 && CMTimeCompare(presentationTime, lastVideoPresentationTime) <= 0 { - return - } - guard let attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: false) as? [[SCStreamFrameInfo: Any]], let attachment = attachments.first, let statusRawValue = attachment[SCStreamFrameInfo.status] as? Int, @@ -413,8 +408,10 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate { assetWriter?.status == .writing, videoInput.isReadyForMoreMediaData else { return } - if firstSampleTime == .zero { - firstSampleTime = sampleBuffer.presentationTimeStamp + // Only a complete frame that the writer can accept may establish time zero. + guard let presentationTime = adjustedPresentationTime(for: sampleBuffer, outputType: outputType) else { return } + if frameCount > 0 && CMTimeCompare(presentationTime, lastVideoPresentationTime) <= 0 { + return } lastSampleBuffer = sampleBuffer @@ -439,10 +436,16 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate { print("Recording started") fflush(stdout) } + } else if frameCount == 0 { + // A failed crop/append must not leave an empty interval before frame one. + firstSampleTime = .zero } return } + guard frameCount > 0, + let presentationTime = adjustedPresentationTime(for: sampleBuffer, outputType: outputType) else { return } + if outputType == .audio { guard let systemAudioInput else { return } appendAudioSampleBuffer(sampleBuffer, to: systemAudioInput, of: systemAudioWriter, firstSampleTime: &firstSystemAudioSampleTime, lastPresentationTime: &lastSystemAudioPresentationTime, presentationTime: presentationTime) diff --git a/electron/native/ScreenCaptureKitRecorder.test.ts b/electron/native/ScreenCaptureKitRecorder.test.ts index a885d75cf..54ae01b51 100644 --- a/electron/native/ScreenCaptureKitRecorder.test.ts +++ b/electron/native/ScreenCaptureKitRecorder.test.ts @@ -84,3 +84,19 @@ describe("ScreenCaptureKitRecorder window capture", () => { expect(recorderSource).toContain("self.windowCropRect = cropRect"); }); }); + + +describe("ScreenCaptureKitRecorder first frame timing", () => { + const callback = recorderSource.slice(recorderSource.indexOf("func stream(_ stream:"), recorderSource.indexOf("func stream(_ stream:") + 5000); + it("validates a complete frame and writer readiness before setting time zero", () => { + const clock = callback.indexOf("adjustedPresentationTime(for:"); + expect(clock).toBeGreaterThan(callback.indexOf("status == .complete")); + expect(clock).toBeGreaterThan(callback.indexOf("videoInput.isReadyForMoreMediaData")); + }); + it("resets the origin after a rejected first frame and gates audio on accepted video", () => { + expect(callback).toMatch(/else if frameCount == 0\s*\{[^}]*firstSampleTime = \.zero/); + const audioGuard = callback.indexOf("guard frameCount > 0,"); + expect(audioGuard).toBeGreaterThan(0); + expect(audioGuard).toBeLessThan(callback.indexOf("if outputType == .audio")); + }); +}); diff --git a/electron/native/bin/darwin-arm64/recordly-screencapturekit-helper b/electron/native/bin/darwin-arm64/recordly-screencapturekit-helper index 52c23c0e6..5a89a2a4c 100755 Binary files a/electron/native/bin/darwin-arm64/recordly-screencapturekit-helper and b/electron/native/bin/darwin-arm64/recordly-screencapturekit-helper differ diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index 8b289e3f5..5e1372d2e 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -91,7 +91,7 @@ import { } from "./types"; import { fromCursorSwaySliderValue, toCursorSwaySliderValue } from "./videoPlayback/cursorSway"; import { isZeroPadding } from "./videoPlayback/layoutUtils"; -import { supportsPreviewPlaybackRate } from "./videoPlayback/playbackRate"; +import { getPreviewPlaybackRateRange } from "./videoPlayback/playbackRate"; import { cursorSetAssets, getCursorStyleSizeMultiplier, @@ -523,15 +523,10 @@ interface SettingsPanelProps { selectedClipId?: string | null; selectedClipSpeed?: number | null; selectedClipMuted?: boolean | null; - selectedClipShowSourceAudio?: boolean | null; - hasClipSourceAudio?: boolean; + hasClipAudioOverrides?: boolean; + onResetClipAudio?: () => void; onClipSpeedChange?: (speed: number) => void; onClipMutedChange?: (muted: boolean) => void; - onClipShowSourceAudioChange?: (show: boolean) => void; - sourceAudioTrackMeta?: Array<{ id: string; label: string }>; - sourceAudioTrackSettings?: Record; - onSourceAudioTrackVolumeChange?: (id: string, volume: number) => void; - onSourceAudioTrackNormalizeChange?: (id: string, normalize: boolean) => void; onClipDelete?: (id: string) => void; selectedAudioId?: string | null; selectedAudioVolume?: number | null; @@ -983,15 +978,10 @@ export function SettingsPanel({ selectedClipId, selectedClipSpeed, selectedClipMuted, - selectedClipShowSourceAudio = false, - hasClipSourceAudio = false, + hasClipAudioOverrides = false, + onResetClipAudio, onClipSpeedChange, onClipMutedChange, - onClipShowSourceAudioChange, - sourceAudioTrackMeta = [], - sourceAudioTrackSettings = {}, - onSourceAudioTrackVolumeChange, - onSourceAudioTrackNormalizeChange, onClipDelete, selectedAudioId, selectedAudioVolume, @@ -1103,6 +1093,7 @@ export function SettingsPanel({ const { preference: themePreference, setPreference: setThemePreference } = useTheme(); const isBackgroundPanel = panelMode === "background"; const initialEditorPreferences = useMemo(() => loadEditorPreferences(), []); + const clipSpeedRange = useMemo(getPreviewPlaybackRateRange, []); const [builtInWallpapers, setBuiltInWallpapers] = useState(BUILT_IN_WALLPAPERS); const [wallpaperPreviewPaths, setWallpaperPreviewPaths] = useState([]); @@ -3020,164 +3011,40 @@ export function SettingsPanel({ ); const clipSectionContent = ( -
-
- {tSettings("clip.title", "Clip")} - {selectedClipSpeed != null && selectedClipSpeed !== 1 && ( - - {selectedClipSpeed}× - - )} -
- -
- {tSettings("speed.label", "Speed")} -
-
- {[ - { speed: 0.25, label: "0.25×" }, - { speed: 0.5, label: "0.5×" }, - { speed: 0.75, label: "0.75×" }, - { speed: 1, label: "1×" }, - { speed: 1.25, label: "1.25×" }, - { speed: 1.5, label: "1.5×" }, - { speed: 2, label: "2×" }, - { speed: 2.5, label: "2.5×" }, - { speed: 3, label: "3×" }, - { speed: 4, label: "4×" }, - { speed: 5, label: "5×" }, - { speed: 8, label: "8×" }, - { speed: 10, label: "10×" }, - { speed: 15, label: "15×" }, - { speed: 20, label: "20×" }, - { speed: 30, label: "30×" }, - ].map((option) => { - const isActive = selectedClipSpeed === option.speed; - return ( - - ); - })} -
- -
- {tSettings("audio.title", "Audio")} - -
-
- - {tSettings("clip.mute", "Mute")} - -

- {selectedClipMuted - ? tSettings("clip.mutedState", "Audio is muted") - : tSettings("clip.unmutedState", "Audio is playing")} -

-
- onClipMutedChange?.(v)} - className="data-[state=checked]:bg-[#06b6d4] scale-75" - /> -
- {hasClipSourceAudio && ( -
- - {tSettings( - "clip.separateClipFromAudio", - "Separate clip from audio", - )} - - onClipShowSourceAudioChange?.(v)} - className="data-[state=checked]:bg-[#06b6d4] scale-75" - /> -
+
+ {tSettings("clip.title", "Clip")} + onClipSpeedChange?.(value)} + formatValue={(value) => `${value}×`} + parseInput={(text) => Number.parseFloat(text)} + /> + {selectedClipSpeed != null && + (selectedClipSpeed < clipSpeedRange.min || selectedClipSpeed > clipSpeedRange.max) && ( +

+ {selectedClipSpeed}× — {tSettings("speed.unsupported", "Not supported for preview on this device")} +

)} -
- - {selectedClipId && hasClipSourceAudio && sourceAudioTrackMeta.length > 0 && ( -
- {sourceAudioTrackMeta.map((track) => { - const settings = sourceAudioTrackSettings[track.id] ?? { - volume: 1, - normalize: false, - }; - return ( -
-
- - {track.label} - - -
-
- - {tSettings("audio.normalize", "Normalize")} - - - onSourceAudioTrackNormalizeChange?.(track.id, v) - } - className="data-[state=checked]:bg-[#06b6d4] scale-75" - /> -
- - onSourceAudioTrackVolumeChange?.(track.id, v) - } - formatValue={(v) => `${Math.round(v * 100)}%`} - parseInput={(text) => - parseFloat(text.replace(/%$/, "")) / 100 - } - /> -
- ); - })} -
+ + {hasClipAudioOverrides && onResetClipAudio && ( + )}
); diff --git a/src/components/video-editor/SliderControl.tsx b/src/components/video-editor/SliderControl.tsx index dd0aaf0dc..e16f8747b 100644 --- a/src/components/video-editor/SliderControl.tsx +++ b/src/components/video-editor/SliderControl.tsx @@ -1,5 +1,5 @@ import type { PointerEvent as ReactPointerEvent } from "react"; -import { useCallback, useRef, memo, useEffect } from "react"; +import { useCallback, useRef, memo } from "react"; import { cn } from "@/lib/utils"; interface SliderControlProps { @@ -39,8 +39,6 @@ export const SliderControl = memo(function SliderControl({ parseInput: _parseInput, accentColor = "blue", }: SliderControlProps) { - const rootRef = useRef(null); - const valueTextRef = useRef(null); const boundsRef = useRef(null); const requestRef = useRef(null); @@ -51,13 +49,6 @@ export const SliderControl = memo(function SliderControl({ ? "bg-foreground/95 shadow-[0_0_10px_rgba(139,92,246,0.28)]" : "bg-foreground/95 shadow-[0_0_10px_rgba(37,99,235,0.28)]"; - // Sync initial and prop-driven changes to CSS variable - useEffect(() => { - if (rootRef.current) { - rootRef.current.style.setProperty("--slider-pct", String(pct / 100)); - } - }, [pct]); - const updateValue = useCallback( (clientX: number) => { const bounds = boundsRef.current; @@ -69,22 +60,10 @@ export const SliderControl = memo(function SliderControl({ const rawValue = min + normalized * (max - min); const nextValue = clamp(quantizeToStep(rawValue, min, step), min, max); const finalValue = Number(nextValue.toFixed(6)); - const finalPct = (((finalValue - min) / (max - min || 1)) * 100).toFixed(4); - - // Direct DOM update for instant feedback - if (rootRef.current) { - rootRef.current.style.setProperty("--slider-pct", String(Number(finalPct) / 100)); - rootRef.current.setAttribute("aria-valuenow", String(finalValue)); - rootRef.current.setAttribute("aria-valuetext", formatValue(finalValue)); - } - if (valueTextRef.current) { - valueTextRef.current.textContent = formatValue(finalValue); - } - // Notify parent onChange(finalValue); }, - [max, min, onChange, step, formatValue], + [max, min, onChange, step], ); const handlePointerDown = useCallback( @@ -143,7 +122,6 @@ export const SliderControl = memo(function SliderControl({ return (
{formatValue(value)} diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 6d22666a1..636da92c2 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -342,7 +342,6 @@ export default function VideoEditor() { activeEffectSection, appearance, timeline, - audio, zoomCommands, clipCommands, audioCommands, diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index 842343dc3..7f4f5d77d 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -1044,9 +1044,9 @@ const VideoPlayback = forwardRef( }); cropBoundsRef.current = result.cropBounds; - // Reset camera container to identity - cameraContainer.scale.set(1); - cameraContainer.position.set(0, 0); + // Layout updates the media geometry, not the composed camera pose. + // In particular, a ResizeObserver notification while paused must not + // replace the exported spring position with an unzoomed frame. const selectedId = selectedZoomIdRef.current; const activeRegion = selectedId @@ -1893,6 +1893,7 @@ const VideoPlayback = forwardRef( video, getClips: () => clipRegionsRef.current, onTime: (time, source) => { + timelineTimeRef.current = time; if (source !== null) currentTimeRef.current = source * 1000; onTimeUpdate(time); }, @@ -1909,6 +1910,7 @@ const VideoPlayback = forwardRef( transport.seek(timelineTimeRef.current); const handleSeeked = () => { isSeekingRef.current = false; + shouldSnapPausedFrameRef.current = true; }; const handleSeeking = () => { isSeekingRef.current = true; @@ -2016,6 +2018,7 @@ const VideoPlayback = forwardRef( if ( !shouldComposePreviewFrame({ motionMode, + isSeeking: isSeekingRef.current || Boolean(videoRef.current?.seeking), contentTimeChanged, shouldSnapPausedFrame: shouldSnapPausedFrameRef.current, }) diff --git a/src/components/video-editor/audio/useClipAudioReset.test.ts b/src/components/video-editor/audio/useClipAudioReset.test.ts new file mode 100644 index 000000000..e60131ef1 --- /dev/null +++ b/src/components/video-editor/audio/useClipAudioReset.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; +import type { SourceAudioTrackSettings } from "./audioTypes"; +import { useClipAudioReset } from "./useClipAudioReset"; + +function harness(selectedClipId: string | null, defaults: SourceAudioTrackSettings = {}) { + let saved = { + selected: { mic: { volume: 0, normalize: true } }, + other: { mic: { volume: 0.4, normalize: true } }, + } as Record; + const initial = saved; + const render = () => + useClipAudioReset({ + selectedClipId, + defaultSourceAudioTrackSettings: defaults, + sourceAudioTrackSettingsByClip: saved, + setSourceAudioTrackSettingsByClip: (update) => { + saved = typeof update === "function" ? update(saved) : update; + }, + }); + return { render, initial, read: () => saved }; +} + +describe("saved clip audio recovery", () => { + it("restores silent saved tracks and clears normalization only for the selected clip", () => { + const state = harness("selected"); + expect(state.render().hasClipAudioOverrides).toBe(true); + state.render().onResetClipAudio(); + expect(state.read().selected.mic).toEqual({ volume: 1, normalize: false }); + expect(state.read().other).toBe(state.initial.other); + expect(state.initial.selected.mic.volume).toBe(0); + expect(state.render().hasClipAudioOverrides).toBe(false); + // These settings are serialized directly in the project, so recovery survives reload. + expect(JSON.parse(JSON.stringify(state.read())).selected.mic.volume).toBe(1); + }); + + it("neutralizes inherited settings without changing defaults or other clips", () => { + const defaults = { system: { volume: 0, normalize: true } }; + const state = harness("new-clip", defaults); + expect(state.render().hasClipAudioOverrides).toBe(true); + state.render().onResetClipAudio(); + expect({ ...defaults, ...state.read()["new-clip"] }).toEqual({ + system: { volume: 1, normalize: false }, + }); + expect(defaults.system.volume).toBe(0); + expect(state.read().selected).toBe(state.initial.selected); + expect(state.render().hasClipAudioOverrides).toBe(false); + }); + + it("does nothing without a selection and hides recovery for untouched clips", () => { + const state = harness(null); + expect(state.render().hasClipAudioOverrides).toBe(false); + state.render().onResetClipAudio(); + expect(state.read()).toBe(state.initial); + expect(harness("new-clip").render().hasClipAudioOverrides).toBe(false); + }); +}); diff --git a/src/components/video-editor/audio/useClipAudioReset.ts b/src/components/video-editor/audio/useClipAudioReset.ts new file mode 100644 index 000000000..38d1cffc7 --- /dev/null +++ b/src/components/video-editor/audio/useClipAudioReset.ts @@ -0,0 +1,42 @@ +import type { useTimelineState } from "../state/useTimelineState"; + +type Input = Pick< + ReturnType, + | "selectedClipId" + | "sourceAudioTrackSettingsByClip" + | "defaultSourceAudioTrackSettings" + | "setSourceAudioTrackSettingsByClip" +>; + +export function useClipAudioReset({ + selectedClipId, + sourceAudioTrackSettingsByClip, + defaultSourceAudioTrackSettings, + setSourceAudioTrackSettingsByClip, +}: Input) { + const settings = { + ...defaultSourceAudioTrackSettings, + ...(selectedClipId ? sourceAudioTrackSettingsByClip[selectedClipId] : {}), + }; + return { + hasClipAudioOverrides: + selectedClipId !== null && + Object.values(settings).some((setting) => setting.volume !== 1 || setting.normalize), + onResetClipAudio: () => { + if (!selectedClipId) return; + setSourceAudioTrackSettingsByClip((current) => { + const effective = { + ...defaultSourceAudioTrackSettings, + ...current[selectedClipId], + }; + // Explicit neutral values also override inherited project settings. + return { + ...current, + [selectedClipId]: Object.fromEntries( + Object.keys(effective).map((id) => [id, { volume: 1, normalize: false }]), + ), + }; + }); + }, + }; +} diff --git a/src/components/video-editor/layout/useEditorSettingsPanelProps.ts b/src/components/video-editor/layout/useEditorSettingsPanelProps.ts index 5ea46b498..4c4c8c696 100644 --- a/src/components/video-editor/layout/useEditorSettingsPanelProps.ts +++ b/src/components/video-editor/layout/useEditorSettingsPanelProps.ts @@ -1,6 +1,6 @@ import type { ComponentProps, Dispatch, SetStateAction } from "react"; import type { AspectRatio } from "@/utils/aspectRatioUtils"; -import type { useVideoEditorAudio } from "../audio/useVideoEditorAudio"; +import { useClipAudioReset } from "../audio/useClipAudioReset"; import type { useAutoCaptionController } from "../captions/useAutoCaptionController"; import type { useAnnotationRegionCommands } from "../hooks/useAnnotationRegionCommands"; import type { useAudioRegionCommands } from "../hooks/useAudioRegionCommands"; @@ -16,7 +16,6 @@ type Input = { activeEffectSection: EditorEffectSection; appearance: ReturnType; timeline: ReturnType; - audio: ReturnType; zoomCommands: ReturnType; clipCommands: ReturnType; audioCommands: ReturnType; @@ -45,7 +44,6 @@ export function useEditorSettingsPanelProps(input: Input): ComponentProps region.id === timeline.selectedAudioId, ); + const clipAudioReset = useClipAudioReset(timeline); + return { + ...clipAudioReset, panelMode: "editor", activeEffectSection, selected: appearance.wallpaper, @@ -94,17 +95,9 @@ export function useEditorSettingsPanelProps(input: Input): ComponentProps { for (const speed of [0, -1, NaN, Infinity]) expect(supportsPreviewPlaybackRate(speed)).toBe(false); }); + +it.each([ + { lower: 0.25, upper: 16 }, + { lower: 0.5, upper: 4 }, + { lower: 1, upper: 30 }, +])("exposes only supported steps between $lower and $upper", async ({ lower, upper }) => { + vi.stubGlobal("document", { createElement: () => ({ + set playbackRate(rate: number) { + if (rate < lower || rate > upper) throw new DOMException("Unsupported", "NotSupportedError"); + }, + }) }); + const { getPreviewPlaybackRateRange } = await import("./playbackRate"); + expect(getPreviewPlaybackRateRange()).toEqual({ min: lower, max: upper }); +}); diff --git a/src/components/video-editor/videoPlayback/playbackRate.ts b/src/components/video-editor/videoPlayback/playbackRate.ts index 98dcefa38..ef3c03978 100644 --- a/src/components/video-editor/videoPlayback/playbackRate.ts +++ b/src/components/video-editor/videoPlayback/playbackRate.ts @@ -16,3 +16,12 @@ export function supportsPreviewPlaybackRate(rate: number): boolean { return false; } } + +/** Contiguous quarter-step range supported by the runtime, anchored at normal speed. */ +export function getPreviewPlaybackRateRange(): { min: number; max: number } { + let min = 1; + let max = 1; + while (min > 0.25 && supportsPreviewPlaybackRate(min - 0.25)) min -= 0.25; + while (max < 30 && supportsPreviewPlaybackRate(max + 0.25)) max += 0.25; + return { min, max }; +} diff --git a/src/components/video-editor/videoPlayback/sceneMotion.test.ts b/src/components/video-editor/videoPlayback/sceneMotion.test.ts index 3c42588b3..e6043729a 100644 --- a/src/components/video-editor/videoPlayback/sceneMotion.test.ts +++ b/src/components/video-editor/videoPlayback/sceneMotion.test.ts @@ -43,13 +43,13 @@ describe("resolveSceneZoomTarget", () => { }); describe("resolvePreviewMotionMode", () => { - it("preserves the composed frame on a plain pause", () => { + it.each([false, true])("preserves a plain pause with classic mode %s", (zoomClassicMode) => { expect( resolvePreviewMotionMode({ isPlaying: false, isSeeking: false, shouldSnapPausedFrame: false, - zoomClassicMode: false, + zoomClassicMode, }), ).toBe("preserve"); }); @@ -97,3 +97,16 @@ describe("shouldComposePreviewFrame", () => { ).toBe(true); }); }); + + +describe("preview seek completion", () => { + it("holds the composed frame until seeking finishes, even with a pending refresh", () => { + const pending = { + motionMode: "snap" as const, + contentTimeChanged: true, + shouldSnapPausedFrame: true, + }; + expect(shouldComposePreviewFrame({ ...pending, isSeeking: true })).toBe(false); + expect(shouldComposePreviewFrame({ ...pending, isSeeking: false })).toBe(true); + }); +}); diff --git a/src/components/video-editor/videoPlayback/sceneMotion.ts b/src/components/video-editor/videoPlayback/sceneMotion.ts index 7d4930aca..20e7c374a 100644 --- a/src/components/video-editor/videoPlayback/sceneMotion.ts +++ b/src/components/video-editor/videoPlayback/sceneMotion.ts @@ -32,7 +32,7 @@ export function resolvePreviewMotionMode({ shouldSnapPausedFrame: boolean; zoomClassicMode: boolean; }): PreviewMotionMode { - if (isSeeking || shouldSnapPausedFrame || zoomClassicMode) { + if (isSeeking || shouldSnapPausedFrame || (isPlaying && zoomClassicMode)) { return "snap"; } @@ -42,14 +42,17 @@ export function resolvePreviewMotionMode({ /** Match export's one-composition-per-media-frame behavior. */ export function shouldComposePreviewFrame({ motionMode, + isSeeking = false, contentTimeChanged, shouldSnapPausedFrame, }: { motionMode: PreviewMotionMode; + isSeeking?: boolean; contentTimeChanged: boolean; shouldSnapPausedFrame: boolean; }): boolean { - if (motionMode === "preserve") { + // Do not consume the pending composition against the old decoded image. + if (isSeeking || motionMode === "preserve") { return false; } diff --git a/src/i18n/locales/de/settings.json b/src/i18n/locales/de/settings.json index d0837fae2..3dae35b70 100644 --- a/src/i18n/locales/de/settings.json +++ b/src/i18n/locales/de/settings.json @@ -19,7 +19,8 @@ }, "clip": { "title": "Clip", - "mute": "Stummschalten", + "mute": "Clip stummschalten", + "resetAudioSettings": "Audioeinstellungen zurücksetzen", "mutedState": "Audio ist stummgeschaltet", "unmutedState": "Audio wird wiedergegeben", "separateClipFromAudio": "Clip vom Audio trennen", diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json index 596109cb3..9db0e74eb 100644 --- a/src/i18n/locales/en/settings.json +++ b/src/i18n/locales/en/settings.json @@ -25,7 +25,8 @@ }, "clip": { "title": "Clip", - "mute": "Mute", + "mute": "Mute clip", + "resetAudioSettings": "Reset audio settings", "mutedState": "Audio is muted", "unmutedState": "Audio is playing", "separateClipFromAudio": "Separate clip from audio", diff --git a/src/i18n/locales/es/settings.json b/src/i18n/locales/es/settings.json index 3510d6bbb..5eafdfdde 100644 --- a/src/i18n/locales/es/settings.json +++ b/src/i18n/locales/es/settings.json @@ -19,7 +19,8 @@ }, "clip": { "title": "Clip", - "mute": "Silenciar", + "mute": "Silenciar clip", + "resetAudioSettings": "Restablecer ajustes de audio", "mutedState": "El audio está silenciado", "unmutedState": "El audio se está reproduciendo", "separateClipFromAudio": "Separar clip del audio", diff --git a/src/i18n/locales/fr/settings.json b/src/i18n/locales/fr/settings.json index d7e4e6753..26283e789 100644 --- a/src/i18n/locales/fr/settings.json +++ b/src/i18n/locales/fr/settings.json @@ -19,7 +19,8 @@ }, "clip": { "title": "Clip", - "mute": "Sourdine", + "mute": "Couper le son du clip", + "resetAudioSettings": "Réinitialiser les réglages audio", "mutedState": "Le son est coupé", "unmutedState": "Le son est activé", "separateClipFromAudio": "Séparer le clip de l'audio", diff --git a/src/i18n/locales/it/settings.json b/src/i18n/locales/it/settings.json index 8027ac02c..b43fdba9a 100644 --- a/src/i18n/locales/it/settings.json +++ b/src/i18n/locales/it/settings.json @@ -19,7 +19,8 @@ }, "clip": { "title": "Clip", - "mute": "Disattiva audio", + "mute": "Disattiva l'audio del clip", + "resetAudioSettings": "Ripristina impostazioni audio", "mutedState": "Audio disattivato", "unmutedState": "Audio in riproduzione", "separateClipFromAudio": "Separa clip dall'audio", diff --git a/src/i18n/locales/ko/settings.json b/src/i18n/locales/ko/settings.json index b2063381b..33488fc0b 100644 --- a/src/i18n/locales/ko/settings.json +++ b/src/i18n/locales/ko/settings.json @@ -19,7 +19,8 @@ }, "clip": { "title": "클립", - "mute": "음소거", + "mute": "클립 음소거", + "resetAudioSettings": "오디오 설정 초기화", "mutedState": "오디오가 음소거됨", "unmutedState": "오디오가 재생 중", "separateClipFromAudio": "클립에서 오디오 분리", diff --git a/src/i18n/locales/nl/settings.json b/src/i18n/locales/nl/settings.json index 4fda91805..9d863cb53 100644 --- a/src/i18n/locales/nl/settings.json +++ b/src/i18n/locales/nl/settings.json @@ -19,7 +19,8 @@ }, "clip": { "title": "Clip", - "mute": "Dempen", + "mute": "Clip dempen", + "resetAudioSettings": "Audio-instellingen resetten", "mutedState": "Audio is gedempt", "unmutedState": "Audio wordt afgespeeld", "separateClipFromAudio": "Clip van audio scheiden", diff --git a/src/i18n/locales/pt-BR/settings.json b/src/i18n/locales/pt-BR/settings.json index 1cb1b72ee..7541913a7 100644 --- a/src/i18n/locales/pt-BR/settings.json +++ b/src/i18n/locales/pt-BR/settings.json @@ -19,7 +19,8 @@ }, "clip": { "title": "Clipe", - "mute": "Mudo", + "mute": "Silenciar clipe", + "resetAudioSettings": "Redefinir configurações de áudio", "mutedState": "O áudio está mudo", "unmutedState": "O áudio está tocando", "separateClipFromAudio": "Separar áudio do clipe", diff --git a/src/i18n/locales/ru/settings.json b/src/i18n/locales/ru/settings.json index 7ec21ff1c..aadb70e02 100644 --- a/src/i18n/locales/ru/settings.json +++ b/src/i18n/locales/ru/settings.json @@ -19,7 +19,8 @@ }, "clip": { "title": "Клип", - "mute": "Без звука", + "mute": "Отключить звук клипа", + "resetAudioSettings": "Сбросить настройки звука", "mutedState": "Звук выключен", "unmutedState": "Звук включен", "separateClipFromAudio": "Отделить аудио от клипа", diff --git a/src/i18n/locales/zh-CN/settings.json b/src/i18n/locales/zh-CN/settings.json index 053d10951..ba4a68c6a 100644 --- a/src/i18n/locales/zh-CN/settings.json +++ b/src/i18n/locales/zh-CN/settings.json @@ -19,7 +19,8 @@ }, "clip": { "title": "片段", - "mute": "静音", + "mute": "将片段静音", + "resetAudioSettings": "重置音频设置", "mutedState": "音频已静音", "unmutedState": "音频正在播放", "separateClipFromAudio": "将剪辑与音频分离", diff --git a/src/i18n/locales/zh-TW/settings.json b/src/i18n/locales/zh-TW/settings.json index 7de885204..b7b944030 100644 --- a/src/i18n/locales/zh-TW/settings.json +++ b/src/i18n/locales/zh-TW/settings.json @@ -19,7 +19,8 @@ }, "clip": { "title": "片段", - "mute": "靜音", + "mute": "將片段靜音", + "resetAudioSettings": "重設音訊設定", "mutedState": "音訊已靜音", "unmutedState": "音訊正在播放", "separateClipFromAudio": "將片段與音訊分離",