diff --git a/Sources/Fluid/Persistence/BackupService.swift b/Sources/Fluid/Persistence/BackupService.swift index 00cf2610a..0f3dc4f34 100644 --- a/Sources/Fluid/Persistence/BackupService.swift +++ b/Sources/Fluid/Persistence/BackupService.swift @@ -50,7 +50,9 @@ struct SettingsBackupPayload: Codable, Equatable { let accentColorOption: SettingsStore.AccentColorOption let transcriptionStartSound: SettingsStore.TranscriptionStartSound let transcriptionSoundVolume: Float - let transcriptionSoundIndependentVolume: Bool + // Independent Volume was removed, but the key is still written (always false) so backups + // from this build decode on app versions that require it. Ignored on restore. + let transcriptionSoundIndependentVolume: Bool? let autoUpdateCheckEnabled: Bool let betaReleasesEnabled: Bool let enableDebugLogs: Bool diff --git a/Sources/Fluid/Persistence/SettingsStore.swift b/Sources/Fluid/Persistence/SettingsStore.swift index edf17b9b2..aef55fed4 100644 --- a/Sources/Fluid/Persistence/SettingsStore.swift +++ b/Sources/Fluid/Persistence/SettingsStore.swift @@ -2374,17 +2374,6 @@ final class SettingsStore: ObservableObject { } } - var transcriptionSoundIndependentVolume: Bool { - get { - let value = self.defaults.object(forKey: Keys.transcriptionSoundIndependentVolume) - return value as? Bool ?? false - } - set { - objectWillChange.send() - self.defaults.set(newValue, forKey: Keys.transcriptionSoundIndependentVolume) - } - } - var transcriptionStartSound: TranscriptionStartSound { get { self.migrateTranscriptionStartSoundIfNeeded() @@ -3274,7 +3263,7 @@ final class SettingsStore: ObservableObject { accentColorOption: self.accentColorOption, transcriptionStartSound: self.transcriptionStartSound, transcriptionSoundVolume: self.transcriptionSoundVolume, - transcriptionSoundIndependentVolume: self.transcriptionSoundIndependentVolume, + transcriptionSoundIndependentVolume: false, autoUpdateCheckEnabled: self.autoUpdateCheckEnabled, betaReleasesEnabled: self.betaReleasesEnabled, enableDebugLogs: self.enableDebugLogs, @@ -3406,7 +3395,6 @@ final class SettingsStore: ObservableObject { self.accentColorOption = payload.accentColorOption self.transcriptionStartSound = payload.transcriptionStartSound self.transcriptionSoundVolume = payload.transcriptionSoundVolume - self.transcriptionSoundIndependentVolume = payload.transcriptionSoundIndependentVolume self.autoUpdateCheckEnabled = payload.autoUpdateCheckEnabled self.betaReleasesEnabled = payload.betaReleasesEnabled self.enableDebugLogs = payload.enableDebugLogs @@ -5353,7 +5341,6 @@ private extension SettingsStore { static let enableTranscriptionSounds = "EnableTranscriptionSounds" static let transcriptionStartSound = "TranscriptionStartSound" static let transcriptionSoundVolume = "TranscriptionSoundVolume" - static let transcriptionSoundIndependentVolume = "TranscriptionSoundIndependentVolume" static let pressAndHoldMode = "PressAndHoldMode" static let hotkeyMode = "HotkeyMode" static let enableStreamingPreview = "EnableStreamingPreview" diff --git a/Sources/Fluid/Services/TranscriptionSoundPlayer.swift b/Sources/Fluid/Services/TranscriptionSoundPlayer.swift index 977b3f659..4adf42835 100644 --- a/Sources/Fluid/Services/TranscriptionSoundPlayer.swift +++ b/Sources/Fluid/Services/TranscriptionSoundPlayer.swift @@ -1,5 +1,4 @@ import AVFoundation -import CoreAudio import Foundation final class TranscriptionSoundPlayer { @@ -7,7 +6,6 @@ final class TranscriptionSoundPlayer { private let playbackQueue = DispatchQueue(label: "app.fluidvoice.transcription-sounds", qos: .userInteractive) private var players: [String: AVAudioPlayer] = [:] - private var savedSystemVolume: Float? private init() {} @@ -16,11 +14,7 @@ final class TranscriptionSoundPlayer { guard settings.enableTranscriptionSounds else { return } let selected = settings.transcriptionStartSound guard let soundName = selected.startSoundFileName else { return } - self.play( - soundName: soundName, - desiredVolume: settings.transcriptionSoundVolume, - independentVolume: settings.transcriptionSoundIndependentVolume - ) + self.play(soundName: soundName, desiredVolume: settings.transcriptionSoundVolume) } func playStopSound() { @@ -28,40 +22,24 @@ final class TranscriptionSoundPlayer { guard settings.enableTranscriptionSounds else { return } let selected = settings.transcriptionStartSound guard let soundName = selected.stopSoundFileName else { return } - self.play( - soundName: soundName, - desiredVolume: settings.transcriptionSoundVolume, - independentVolume: settings.transcriptionSoundIndependentVolume - ) + self.play(soundName: soundName, desiredVolume: settings.transcriptionSoundVolume) } /// Preview a specific sound at the current volume setting (used in Settings UI). func playPreview(sound: SettingsStore.TranscriptionStartSound) { guard let soundName = sound.startSoundFileName else { return } let settings = SettingsStore.shared - self.play( - soundName: soundName, - desiredVolume: settings.transcriptionSoundVolume, - independentVolume: settings.transcriptionSoundIndependentVolume - ) + self.play(soundName: soundName, desiredVolume: settings.transcriptionSoundVolume) } /// Preview current sound at a specific volume (used when slider is released). func playPreviewAtVolume(_ volume: Float) { let selected = SettingsStore.shared.transcriptionStartSound guard let soundName = selected.startSoundFileName else { return } - self.play( - soundName: soundName, - desiredVolume: volume, - independentVolume: SettingsStore.shared.transcriptionSoundIndependentVolume - ) + self.play(soundName: soundName, desiredVolume: volume) } - private func play( - soundName: String, - desiredVolume: Float, - independentVolume: Bool - ) { + private func play(soundName: String, desiredVolume: Float) { let startedAt = ProcessInfo.processInfo.systemUptime DebugLogger.shared.benchmark( "APP_BENCH", @@ -79,7 +57,6 @@ final class TranscriptionSoundPlayer { soundName: soundName, url: url, desiredVolume: desiredVolume, - independentVolume: independentVolume, startedAt: startedAt ) } @@ -89,17 +66,8 @@ final class TranscriptionSoundPlayer { soundName: String, url: URL, desiredVolume: Float, - independentVolume: Bool, startedAt: TimeInterval ) { - if independentVolume { - let currentSystemVol = Self.getSystemVolume() - guard currentSystemVol > 0.001 else { return } - // Save current system volume and temporarily set it to desired level - self.savedSystemVolume = currentSystemVol - Self.setSystemVolume(desiredVolume) - } - do { let player: AVAudioPlayer if let existing = self.players[soundName] { @@ -111,87 +79,18 @@ final class TranscriptionSoundPlayer { } player.currentTime = 0 - if independentVolume { - player.volume = 1.0 - } else { - player.volume = desiredVolume - } + player.volume = desiredVolume player.play() DebugLogger.shared.benchmark( "APP_BENCH", message: "sound_play_dispatched sound=\(soundName) elapsedMs=\(Int(((ProcessInfo.processInfo.systemUptime - startedAt) * 1000).rounded()))", source: "AppBenchmark" ) - - // Restore system volume after the sound finishes - if independentVolume, let saved = self.savedSystemVolume { - let duration = player.duration - self.playbackQueue.asyncAfter(deadline: .now() + duration + 0.05) { [weak self] in - Self.setSystemVolume(saved) - self?.savedSystemVolume = nil - } - } } catch { - // Restore system volume on error - if let saved = self.savedSystemVolume { - Self.setSystemVolume(saved) - self.savedSystemVolume = nil - } DebugLogger.shared.error( "Failed to play sound \(soundName).m4a: \(error.localizedDescription)", source: "TranscriptionSoundPlayer" ) } } - - // MARK: - System Volume via CoreAudio - - private static func getDefaultOutputDeviceID() -> AudioObjectID? { - var address = AudioObjectPropertyAddress( - mSelector: kAudioHardwarePropertyDefaultOutputDevice, - mScope: kAudioObjectPropertyScopeGlobal, - mElement: kAudioObjectPropertyElementMain - ) - var deviceID = AudioObjectID(0) - var size = UInt32(MemoryLayout.size) - let status = AudioObjectGetPropertyData( - AudioObjectID(kAudioObjectSystemObject), - &address, - 0, - nil, - &size, - &deviceID - ) - guard status == noErr, deviceID != kAudioObjectUnknown else { return nil } - return deviceID - } - - static func getSystemVolume() -> Float { - guard let deviceID = getDefaultOutputDeviceID() else { return 1.0 } - var address = AudioObjectPropertyAddress( - mSelector: kAudioHardwareServiceDeviceProperty_VirtualMainVolume, - mScope: kAudioDevicePropertyScopeOutput, - mElement: kAudioObjectPropertyElementMain - ) - var volume: Float32 = 1.0 - var size = UInt32(MemoryLayout.size) - let status = AudioObjectGetPropertyData(deviceID, &address, 0, nil, &size, &volume) - guard status == noErr else { return 1.0 } - return volume - } - - private static func setSystemVolume(_ volume: Float) { - guard let deviceID = getDefaultOutputDeviceID() else { return } - var address = AudioObjectPropertyAddress( - mSelector: kAudioHardwareServiceDeviceProperty_VirtualMainVolume, - mScope: kAudioDevicePropertyScopeOutput, - mElement: kAudioObjectPropertyElementMain - ) - var vol = Float32(max(0, min(1, volume))) - let size = UInt32(MemoryLayout.size) - let status = AudioObjectSetPropertyData(deviceID, &address, 0, nil, size, &vol) - if status != noErr { - DebugLogger.shared.error("Failed to set system volume: OSStatus \(status)", source: "TranscriptionSoundPlayer") - } - } } diff --git a/Sources/Fluid/UI/SettingsView.swift b/Sources/Fluid/UI/SettingsView.swift index bf6eead2c..ef2b90692 100644 --- a/Sources/Fluid/UI/SettingsView.swift +++ b/Sources/Fluid/UI/SettingsView.swift @@ -374,16 +374,6 @@ struct SettingsView: View { } .frame(width: 150) } - - self.settingsToggleRow( - title: "Independent Volume", - description: "Sound volume stays constant regardless of system volume. Mute is still respected.", - footnote: "Temporarily changes system volume during playback, which may briefly affect other audio.", - isOn: Binding( - get: { SettingsStore.shared.transcriptionSoundIndependentVolume }, - set: { SettingsStore.shared.transcriptionSoundIndependentVolume = $0 } - ) - ) } Divider().opacity(0.2) diff --git a/Tests/FluidDictationIntegrationTests/DictationE2ETests.swift b/Tests/FluidDictationIntegrationTests/DictationE2ETests.swift index 99206d980..a8851645c 100644 --- a/Tests/FluidDictationIntegrationTests/DictationE2ETests.swift +++ b/Tests/FluidDictationIntegrationTests/DictationE2ETests.swift @@ -2424,6 +2424,19 @@ extension DictationE2ETests { settings.restore(from: legacyBackup.settings) XCTAssertEqual(settings.spokenFormattingActionRules, normalizedRulesBeforeLegacyRestore) } + + func testBackupKeepsDeprecatedIndependentVolumeKeyAndDecodesWithoutIt() async throws { + let document = await BackupService.shared.makeBackupDocument() + let encoded = try BackupService.shared.encode(document) + var root = try XCTUnwrap(JSONSerialization.jsonObject(with: encoded) as? [String: Any]) + var encodedSettings = try XCTUnwrap(root["settings"] as? [String: Any]) + XCTAssertEqual(encodedSettings["transcriptionSoundIndependentVolume"] as? Bool, false) + + encodedSettings.removeValue(forKey: "transcriptionSoundIndependentVolume") + root["settings"] = encodedSettings + let strippedBackup = try BackupService.shared.decode(JSONSerialization.data(withJSONObject: root)) + XCTAssertNil(strippedBackup.settings.transcriptionSoundIndependentVolume) + } } @MainActor