@@ -84,6 +84,7 @@ final class EditorState {
8484 var exportProgress : Double = 0
8585 var exportETA : Double ?
8686 var exportTask : Task < Void , Never > ?
87+ var exportStatusMessage : String ?
8788 var isPreviewMode = false
8889
8990 var backgroundStyle : BackgroundStyle = . solidColor( CodableColor ( r: 0 , g: 0 , b: 0 ) )
@@ -463,7 +464,9 @@ final class EditorState {
463464 micProcessingTask? . cancel ( )
464465 guard let micURL = result. microphoneAudioURL, micNoiseReductionEnabled else {
465466 if let old = processedMicAudioURL {
466- try ? FileManager . default. removeItem ( at: old)
467+ if !isURLInsideProjectBundle( old) {
468+ try ? FileManager . default. removeItem ( at: old)
469+ }
467470 processedMicAudioURL = nil
468471 }
469472 isMicProcessing = false
@@ -473,9 +476,25 @@ final class EditorState {
473476 return
474477 }
475478
479+ let intensity = micNoiseReductionIntensity
480+
481+ if let proj = project,
482+ let cachedURL = proj. denoisedMicAudioURL,
483+ let cachedIntensity = proj. metadata. editorState? . audioSettings? . cachedNoiseReductionIntensity,
484+ abs ( cachedIntensity - intensity) < 0.001
485+ {
486+ let oldURL = processedMicAudioURL
487+ processedMicAudioURL = cachedURL
488+ isMicProcessing = false
489+ playerController. swapMicAudioFile ( url: cachedURL)
490+ if let oldURL, oldURL != cachedURL, !isURLInsideProjectBundle( oldURL) {
491+ try ? FileManager . default. removeItem ( at: oldURL)
492+ }
493+ return
494+ }
495+
476496 isMicProcessing = true
477497 micProcessingProgress = 0
478- let intensity = micNoiseReductionIntensity
479498 let state = self
480499 micProcessingTask = Task {
481500 try ? await Task . sleep ( for: . milliseconds( 500 ) )
@@ -497,22 +516,44 @@ final class EditorState {
497516 try ? FileManager . default. removeItem ( at: tempURL)
498517 return
499518 }
500- let oldURL = processedMicAudioURL
501- processedMicAudioURL = tempURL
502- isMicProcessing = false
503- playerController. swapMicAudioFile ( url: tempURL)
504- if let oldURL { try ? FileManager . default. removeItem ( at: oldURL) }
519+
520+ var finalURL = tempURL
521+ if let proj = state. project {
522+ let destURL = proj. denoisedMicAudioDestinationURL
523+ try ? FileManager . default. removeItem ( at: destURL)
524+ do {
525+ try FileManager . default. copyItem ( at: tempURL, to: destURL)
526+ try ? FileManager . default. removeItem ( at: tempURL)
527+ finalURL = destURL
528+ } catch {
529+ state. logger. warning ( " Failed to cache denoised audio in bundle: \( error) " )
530+ }
531+ }
532+
533+ let oldURL = state. processedMicAudioURL
534+ state. processedMicAudioURL = finalURL
535+ state. isMicProcessing = false
536+ state. playerController. swapMicAudioFile ( url: finalURL)
537+ if let oldURL, oldURL != finalURL, !state. isURLInsideProjectBundle ( oldURL) {
538+ try ? FileManager . default. removeItem ( at: oldURL)
539+ }
540+ state. scheduleSave ( )
505541 } catch {
506542 guard !Task. isCancelled else {
507543 try ? FileManager . default. removeItem ( at: tempURL)
508544 return
509545 }
510- isMicProcessing = false
511- logger. error ( " Mic noise reduction failed: \( error) " )
546+ state . isMicProcessing = false
547+ state . logger. error ( " Mic noise reduction failed: \( error) " )
512548 }
513549 }
514550 }
515551
552+ private func isURLInsideProjectBundle( _ url: URL ) -> Bool {
553+ guard let bundleURL = project? . bundleURL else { return false }
554+ return url. path. hasPrefix ( bundleURL. path)
555+ }
556+
516557 func isCameraFullscreen( at time: Double ) -> Bool {
517558 cameraFullscreenRegions. contains { time >= $0. startSeconds && time <= $0. endSeconds }
518559 }
@@ -646,9 +687,22 @@ final class EditorState {
646687 isExporting = true
647688 exportProgress = 0
648689 exportETA = nil
690+ exportStatusMessage = nil
649691 defer {
650692 isExporting = false
651693 exportTask = nil
694+ exportStatusMessage = nil
695+ }
696+
697+ if isMicProcessing {
698+ exportStatusMessage =
699+ " Waiting for noise reduction… \( Int ( micProcessingProgress * 100 ) ) % "
700+ while isMicProcessing {
701+ try await Task . sleep ( for: . milliseconds( 100 ) )
702+ exportStatusMessage =
703+ " Waiting for noise reduction… \( Int ( micProcessingProgress * 100 ) ) % "
704+ }
705+ exportStatusMessage = nil
652706 }
653707
654708 let cursorSnapshot = showCursor ? activeCursorProvider? . makeSnapshot ( ) : nil
@@ -722,6 +776,7 @@ final class EditorState {
722776 micAudioVolume: effectiveMicAudioVolume,
723777 micNoiseReductionEnabled: micNoiseReductionEnabled,
724778 micNoiseReductionIntensity: micNoiseReductionIntensity,
779+ processedMicAudioURL: processedMicAudioURL,
725780 progressHandler: { progress, eta in
726781 state. exportProgress = progress
727782 state. exportETA = eta
@@ -953,13 +1008,21 @@ final class EditorState {
9531008 }
9541009 var audioSettings : AudioSettingsData ?
9551010 if hasSystemAudio || hasMicAudio {
1011+ var cachedIntensity : Float ?
1012+ if micNoiseReductionEnabled,
1013+ let proj = project,
1014+ proj. denoisedMicAudioURL != nil
1015+ {
1016+ cachedIntensity = micNoiseReductionIntensity
1017+ }
9561018 audioSettings = AudioSettingsData (
9571019 systemAudioVolume: systemAudioVolume,
9581020 micAudioVolume: micAudioVolume,
9591021 systemAudioMuted: systemAudioMuted,
9601022 micAudioMuted: micAudioMuted,
9611023 micNoiseReductionEnabled: micNoiseReductionEnabled,
962- micNoiseReductionIntensity: micNoiseReductionIntensity
1024+ micNoiseReductionIntensity: micNoiseReductionIntensity,
1025+ cachedNoiseReductionIntensity: cachedIntensity
9631026 )
9641027 }
9651028 return EditorStateData (
@@ -1212,7 +1275,9 @@ final class EditorState {
12121275 }
12131276 playerController. teardown ( )
12141277 if let url = processedMicAudioURL {
1215- try ? FileManager . default. removeItem ( at: url)
1278+ if !isURLInsideProjectBundle( url) {
1279+ try ? FileManager . default. removeItem ( at: url)
1280+ }
12161281 processedMicAudioURL = nil
12171282 }
12181283 }
0 commit comments