diff --git a/apps/ios/GuideDogs/Assets/Localization/en-US.lproj/Localizable.strings b/apps/ios/GuideDogs/Assets/Localization/en-US.lproj/Localizable.strings index a2384bcb3..e6705c261 100644 --- a/apps/ios/GuideDogs/Assets/Localization/en-US.lproj/Localizable.strings +++ b/apps/ios/GuideDogs/Assets/Localization/en-US.lproj/Localizable.strings @@ -1650,6 +1650,18 @@ /* Shake callouts info */ "callouts.shake_callouts.info" = "Shake the device to repeat the last callout"; +/* Title for a toggle under Manage Callouts. Controls whether callouts include a leading sound effect. */ +"callouts.sound_effects" = "Sound Effects"; + +/* Description for Manage Callouts sound effects toggle. */ +"callouts.sound_effects.info" = "Sounds that play before the spoken messages"; + +/* Title for a toggle under Manage Callouts. Controls whether callouts pause between announcements. */ +"callouts.delay_between" = "Delays"; + +/* Description for Manage Callouts delay toggle. */ +"callouts.delay_between.info" = "Short pauses between callouts"; + /* Callouts Title, Automatic Callouts */ "callouts.automatic_callouts" = "Automatic Callouts"; diff --git a/apps/ios/GuideDogs/Code/App/Settings/AutoCalloutSettingsProvider.swift b/apps/ios/GuideDogs/Code/App/Settings/AutoCalloutSettingsProvider.swift index e331d4bdf..b874eff8f 100644 --- a/apps/ios/GuideDogs/Code/App/Settings/AutoCalloutSettingsProvider.swift +++ b/apps/ios/GuideDogs/Code/App/Settings/AutoCalloutSettingsProvider.swift @@ -3,11 +3,14 @@ // Soundscape // // Copyright (c) Microsoft Corporation. +// Copyright (c) Soundscape Community Contributors. // Licensed under the MIT License. // protocol AutoCalloutSettingsProvider: AnyObject { var automaticCalloutsEnabled: Bool { get set } + var calloutSoundEffectsEnabled: Bool { get set } + var calloutPausesEnabled: Bool { get set } var placeSenseEnabled: Bool { get set } var landmarkSenseEnabled: Bool { get set } var mobilitySenseEnabled: Bool { get set } diff --git a/apps/ios/GuideDogs/Code/App/Settings/SettingsContext.swift b/apps/ios/GuideDogs/Code/App/Settings/SettingsContext.swift index bca0eddd0..072b39219 100644 --- a/apps/ios/GuideDogs/Code/App/Settings/SettingsContext.swift +++ b/apps/ios/GuideDogs/Code/App/Settings/SettingsContext.swift @@ -3,6 +3,7 @@ // Soundscape // // Copyright (c) Microsoft Corporation. +// Copyright (c) Soundscape Community Contributors. // Licensed under the MIT License. // @@ -44,6 +45,8 @@ class SettingsContext { fileprivate static let useOldBeacon = "GDASettingsUseOldBeacon" fileprivate static let playBeaconStartEndMelody = "GDAPlayBeaconStartEndMelody" fileprivate static let automaticCalloutsEnabled = "GDASettingsAutomaticCalloutsEnabled" + fileprivate static let calloutSoundEffectsEnabled = "GDASettingsCalloutSoundEffectsEnabled" + fileprivate static let calloutPausesEnabled = "GDASettingsCalloutPausesEnabled" fileprivate static let shakeCalloutsEnabled = "GDASettingsShakeCalloutsEnabled" fileprivate static let sensePlace = "GDASettingsPlaceSenseEnabled" fileprivate static let senseLandmark = "GDASettingsLandmarkSenseEnabled" @@ -99,6 +102,8 @@ class SettingsContext { Keys.useOldBeacon: false, Keys.playBeaconStartEndMelody: false, Keys.automaticCalloutsEnabled: true, + Keys.calloutSoundEffectsEnabled: true, + Keys.calloutPausesEnabled: true, Keys.shakeCalloutsEnabled: false, Keys.sensePlace: true, Keys.senseLandmark: true, @@ -425,6 +430,24 @@ extension SettingsContext: AutoCalloutSettingsProvider { NotificationCenter.default.post(name: .automaticCalloutsEnabledChanged, object: self, userInfo: [Keys.enabled: newValue]) } } + + var calloutSoundEffectsEnabled: Bool { + get { + return userDefaults.bool(forKey: Keys.calloutSoundEffectsEnabled) + } + set(newValue) { + userDefaults.set(newValue, forKey: Keys.calloutSoundEffectsEnabled) + } + } + + var calloutPausesEnabled: Bool { + get { + return userDefaults.bool(forKey: Keys.calloutPausesEnabled) + } + set(newValue) { + userDefaults.set(newValue, forKey: Keys.calloutPausesEnabled) + } + } var shakeCalloutsEnabled: Bool { get { diff --git a/apps/ios/GuideDogs/Code/Behaviors/Default/AutoCalloutGenerator.swift b/apps/ios/GuideDogs/Code/Behaviors/Default/AutoCalloutGenerator.swift index f007d716e..116f1a54d 100644 --- a/apps/ios/GuideDogs/Code/Behaviors/Default/AutoCalloutGenerator.swift +++ b/apps/ios/GuideDogs/Code/Behaviors/Default/AutoCalloutGenerator.swift @@ -3,6 +3,7 @@ // Soundscape // // Copyright (c) Microsoft Corporation. +// Copyright (c) Soundscape Community Contributors. // Licensed under the MIT License. // @@ -118,7 +119,6 @@ class AutoCalloutGenerator: AutomaticGenerator, ManualGenerator { // MARK: - Private Constants private let inVehicleBeaconUpdateDistance: CLLocationDistance = 1000.0 // meters - private let calloutDelay = 0.75 // MARK: - Private Properties @@ -141,6 +141,10 @@ class AutoCalloutGenerator: AutomaticGenerator, ManualGenerator { } // MARK: - Helper Properties + + private var calloutDelay: TimeInterval { + return settings.calloutPausesEnabled ? 0.75 : 0.0 + } private var calloutRangeContext: CalloutRangeContext { return spatialData.motionActivityContext.isInVehicle ? .automotive : .standard @@ -435,7 +439,7 @@ class AutoCalloutGenerator: AutomaticGenerator, ManualGenerator { guard let dataView = spatialData.getDataView(for: location, searchDistance: context.searchDistance) else { let callouts = filterAnnounceablePOIs(prioritizedPOIs, near: location, context: context) { - return POICallout(.auto, poi: $0, location: location) + return POICallout(.auto, poi: $0, location: location, includePrefixSound: settings.calloutSoundEffectsEnabled) } if callouts.isEmpty { @@ -452,11 +456,11 @@ class AutoCalloutGenerator: AutomaticGenerator, ManualGenerator { // Get the POIs sorted by distance, and filtered for duplicate names let prioritizedCallouts = filterAnnounceablePOIs(prioritizedPOIs, near: location, context: context) { - return POICallout(.auto, poi: $0, location: location) + return POICallout(.auto, poi: $0, location: location, includePrefixSound: settings.calloutSoundEffectsEnabled) } let defaultCallouts = !settings.automaticCalloutsEnabled ? [] : filterAnnounceablePOIs(dataView.pois, near: location, context: context) { - return POICallout(.auto, key: $0.key, location: location) + return POICallout(.auto, key: $0.key, location: location, includePrefixSound: settings.calloutSoundEffectsEnabled) } if defaultCallouts.count + prioritizedCallouts.count == 0 { diff --git a/apps/ios/GuideDogs/Code/Behaviors/Default/BeaconCalloutGenerator.swift b/apps/ios/GuideDogs/Code/Behaviors/Default/BeaconCalloutGenerator.swift index f381ddd27..3af479525 100644 --- a/apps/ios/GuideDogs/Code/Behaviors/Default/BeaconCalloutGenerator.swift +++ b/apps/ios/GuideDogs/Code/Behaviors/Default/BeaconCalloutGenerator.swift @@ -3,6 +3,7 @@ // Soundscape // // Copyright (c) Microsoft Corporation. +// Copyright (c) Soundscape Community Contributors. // Licensed under the MIT License. // @@ -62,7 +63,6 @@ class BeaconCalloutGenerator: AutomaticGenerator, ManualGenerator { // MARK: - Private Constants private let inVehicleBeaconUpdateDistance: CLLocationDistance = 1000.0 // meters - private let calloutDelay: TimeInterval = 0.75 // MARK: - Private Properties @@ -73,6 +73,10 @@ class BeaconCalloutGenerator: AutomaticGenerator, ManualGenerator { private var userLocation: CLLocation? private var destinationKey: String? private var beaconUpdateFilter: MotionActivityUpdateFilter + + private var calloutDelay: TimeInterval { + return settings.calloutPausesEnabled ? 0.75 : 0.0 + } // MARK: - Initialization @@ -100,7 +104,8 @@ class BeaconCalloutGenerator: AutomaticGenerator, ManualGenerator { func handle(event: UserInitiatedEvent, verbosity: Verbosity) -> HandledEventAction? { switch event { case let event as BeaconCalloutEvent: - return .playCallouts(CalloutGroup([DestinationCallout(.auto, event.beaconId)], action: .interruptAndClear, logContext: event.logContext)) + let callout = DestinationCallout(.auto, event.beaconId, false, settings.calloutSoundEffectsEnabled) + return .playCallouts(CalloutGroup([callout], action: .interruptAndClear, logContext: event.logContext)) case let event as BeaconChangedEvent: guard settings.automaticCalloutsEnabled else { @@ -302,7 +307,7 @@ class BeaconCalloutGenerator: AutomaticGenerator, ManualGenerator { configureDestinationUpdates() // Build the destination callout - return [DestinationCallout(origin, key, causedAudioDisable)] + return [DestinationCallout(origin, key, causedAudioDisable, settings.calloutSoundEffectsEnabled)] } // MARK: - Helper Methods diff --git a/apps/ios/GuideDogs/Code/Behaviors/Default/Callouts/DestinationCallout.swift b/apps/ios/GuideDogs/Code/Behaviors/Default/Callouts/DestinationCallout.swift index aaf398f2b..5b2d0cd75 100644 --- a/apps/ios/GuideDogs/Code/Behaviors/Default/Callouts/DestinationCallout.swift +++ b/apps/ios/GuideDogs/Code/Behaviors/Default/Callouts/DestinationCallout.swift @@ -3,6 +3,7 @@ // Soundscape // // Copyright (c) Microsoft Corporation. +// Copyright (c) Soundscape Community Contributors. // Licensed under the MIT License. // @@ -23,7 +24,7 @@ struct DestinationCallout: POICalloutProtocol { return "destination" } - let includePrefixSound = true + let includePrefixSound: Bool var prefixSound: Sound? { return GlyphSound(.startJourney) @@ -52,10 +53,11 @@ struct DestinationCallout: POICalloutProtocol { return marker?.getPOI() } - init(_ calloutOrigin: CalloutOrigin, _ entityKey: String, _ causedAudioDisabled: Bool = false) { + init(_ calloutOrigin: CalloutOrigin, _ entityKey: String, _ causedAudioDisabled: Bool = false, _ includePrefixSound: Bool = true) { self.origin = calloutOrigin self.entityKey = entityKey self.causedAudioDisabled = causedAudioDisabled + self.includePrefixSound = includePrefixSound } func hasSameEntity(_ rhs: POICallout) -> Bool { diff --git a/apps/ios/GuideDogs/Code/Behaviors/Default/Callouts/IntersectionCallout.swift b/apps/ios/GuideDogs/Code/Behaviors/Default/Callouts/IntersectionCallout.swift index c2827f361..f1d112f5e 100644 --- a/apps/ios/GuideDogs/Code/Behaviors/Default/Callouts/IntersectionCallout.swift +++ b/apps/ios/GuideDogs/Code/Behaviors/Default/Callouts/IntersectionCallout.swift @@ -3,6 +3,7 @@ // Soundscape // // Copyright (c) Microsoft Corporation. +// Copyright (c) Soundscape Community Contributors. // Licensed under the MIT License. // @@ -25,7 +26,7 @@ struct IntersectionCallout: CalloutProtocol { return "intersection" } - let includePrefixSound = true + let includePrefixSound: Bool var prefixSound: Sound? { return GlyphSound(SuperCategory.intersections.glyph) @@ -38,11 +39,12 @@ struct IntersectionCallout: CalloutProtocol { return SpatialDataCache.intersectionByKey(key) } - init(_ calloutOrigin: CalloutOrigin, _ intersectionKey: String, _ isRoundabout: Bool = false, _ userHeading: CLLocationDirection) { + init(_ calloutOrigin: CalloutOrigin, _ intersectionKey: String, _ isRoundabout: Bool = false, _ userHeading: CLLocationDirection, _ includePrefixSound: Bool = true) { self.origin = calloutOrigin self.key = intersectionKey self.isRoundabout = isRoundabout self.heading = userHeading + self.includePrefixSound = includePrefixSound } func sounds(for location: CLLocation?, isRepeat: Bool, automotive: Bool = false) -> Sounds { diff --git a/apps/ios/GuideDogs/Code/Behaviors/Default/IntersectionGenerator.swift b/apps/ios/GuideDogs/Code/Behaviors/Default/IntersectionGenerator.swift index bf36af9a1..336098fd8 100644 --- a/apps/ios/GuideDogs/Code/Behaviors/Default/IntersectionGenerator.swift +++ b/apps/ios/GuideDogs/Code/Behaviors/Default/IntersectionGenerator.swift @@ -3,6 +3,7 @@ // Soundscape // // Copyright (c) Microsoft Corporation. +// Copyright (c) Soundscape Community Contributors. // Licensed under the MIT License. // @@ -136,7 +137,11 @@ class IntersectionGenerator: AutomaticGenerator { return .noAction case let event as IntersectionArrivalEvent: - let callout = IntersectionCallout(.intersection, event.key, event.isRoundabout, event.heading) + let callout = IntersectionCallout(.intersection, + event.key, + event.isRoundabout, + event.heading, + SettingsContext.shared.calloutSoundEffectsEnabled) GDATelemetry.track("callout", with: ["context": "intersection.arrival", "type": callout.logCategory, @@ -146,7 +151,9 @@ class IntersectionGenerator: AutomaticGenerator { return .playCallouts(CalloutGroup([callout], action: .interruptAndClear, logContext: "intersections")) case let event as IntersectionDepartureEvent: - let callout = event.geocodedLocation.buildCallout(origin: .intersection, sound: true, useClosestRoadIfAvailable: true) + let callout = event.geocodedLocation.buildCallout(origin: .intersection, + sound: SettingsContext.shared.calloutSoundEffectsEnabled, + useClosestRoadIfAvailable: true) return .playCallouts(CalloutGroup([callout], action: .enqueue, logContext: "intersections")) default: diff --git a/apps/ios/GuideDogs/Code/Visual UI/Controls/Settings/CalloutSettingsCellView.swift b/apps/ios/GuideDogs/Code/Visual UI/Controls/Settings/CalloutSettingsCellView.swift index 8b6d09251..3f865d6c4 100644 --- a/apps/ios/GuideDogs/Code/Visual UI/Controls/Settings/CalloutSettingsCellView.swift +++ b/apps/ios/GuideDogs/Code/Visual UI/Controls/Settings/CalloutSettingsCellView.swift @@ -3,6 +3,7 @@ // Soundscape // // Copyright (c) Microsoft Corporation. +// Copyright (c) Soundscape Community Contributors. // Licensed under the MIT License. // @@ -13,7 +14,7 @@ protocol CalloutSettingsCellViewDelegate: AnyObject { } internal enum CalloutSettingCellType { - case all, poi, mobility, beacon, shake + case all, poi, mobility, beacon, shake, soundEffects, delays } class CalloutSettingsCellView: UITableViewCell { @@ -44,6 +45,12 @@ class CalloutSettingsCellView: UITableViewCell { return case .shake: settingSwitch.isOn = SettingsContext.shared.shakeCalloutsEnabled + return + case .soundEffects: + settingSwitch.isOn = SettingsContext.shared.calloutSoundEffectsEnabled + return + case .delays: + settingSwitch.isOn = SettingsContext.shared.calloutPausesEnabled } } } @@ -97,6 +104,14 @@ class CalloutSettingsCellView: UITableViewCell { case .shake: SettingsContext.shared.shakeCalloutsEnabled = isOn GDATelemetry.track("settings.shake_callouts", value: isOn.description) + return + case .soundEffects: + SettingsContext.shared.calloutSoundEffectsEnabled = isOn + GDATelemetry.track("settings.callout_sound_effects", value: isOn.description) + return + case .delays: + SettingsContext.shared.calloutPausesEnabled = isOn + GDATelemetry.track("settings.callout_delay", value: isOn.description) } } } diff --git a/apps/ios/GuideDogs/Code/Visual UI/View Controllers/Settings/SettingsViewController.swift b/apps/ios/GuideDogs/Code/Visual UI/View Controllers/Settings/SettingsViewController.swift index 2839b8c08..4cb69d71b 100644 --- a/apps/ios/GuideDogs/Code/Visual UI/View Controllers/Settings/SettingsViewController.swift +++ b/apps/ios/GuideDogs/Code/Visual UI/View Controllers/Settings/SettingsViewController.swift @@ -30,10 +30,12 @@ class SettingsViewController: BaseTableViewController { private enum CalloutsRow: Int, CaseIterable { case all = 0 - case poi = 1 - case mobility = 2 - case beacon = 3 - case shake = 4 + case soundEffects = 1 + case delays = 2 + case poi = 3 + case mobility = 4 + case beacon = 5 + case shake = 6 } private static let cellIdentifiers: [IndexPath: String] = [ @@ -51,6 +53,8 @@ class SettingsViewController: BaseTableViewController { IndexPath(row: CalloutsRow.mobility.rawValue, section: Section.callouts.rawValue): "mobilityCallouts", IndexPath(row: CalloutsRow.beacon.rawValue, section: Section.callouts.rawValue): "beaconCallouts", IndexPath(row: CalloutsRow.shake.rawValue, section: Section.callouts.rawValue): "shakeCallouts", + IndexPath(row: CalloutsRow.soundEffects.rawValue, section: Section.callouts.rawValue): "soundCallouts", + IndexPath(row: CalloutsRow.delays.rawValue, section: Section.callouts.rawValue): "delayCallouts", IndexPath(row: 0, section: Section.streetPreview.rawValue): "streetPreview", IndexPath(row: 0, section: Section.troubleshooting.rawValue): "troubleshooting", @@ -62,7 +66,9 @@ class SettingsViewController: BaseTableViewController { IndexPath(row: CalloutsRow.poi.rawValue, section: Section.callouts.rawValue), IndexPath(row: CalloutsRow.mobility.rawValue, section: Section.callouts.rawValue), IndexPath(row: CalloutsRow.beacon.rawValue, section: Section.callouts.rawValue), - IndexPath(row: CalloutsRow.shake.rawValue, section: Section.callouts.rawValue) + IndexPath(row: CalloutsRow.shake.rawValue, section: Section.callouts.rawValue), + IndexPath(row: CalloutsRow.soundEffects.rawValue, section: Section.callouts.rawValue), + IndexPath(row: CalloutsRow.delays.rawValue, section: Section.callouts.rawValue) ] // MARK: Properties @@ -91,7 +97,7 @@ class SettingsViewController: BaseTableViewController { switch sectionType { case .general: return 6 case .audio: return 1 - case .callouts: return SettingsContext.shared.automaticCalloutsEnabled ? 5 : 1 + case .callouts: return SettingsContext.shared.automaticCalloutsEnabled ? 7 : 1 case .streetPreview: return 1 case .troubleshooting: return 1 case .about: return 1 @@ -118,6 +124,8 @@ class SettingsViewController: BaseTableViewController { case .mobility: cell.type = .mobility case .beacon: cell.type = .beacon case .shake: cell.type = .shake + case .soundEffects: cell.type = .soundEffects + case .delays: cell.type = .delays } } diff --git a/apps/ios/GuideDogs/Code/Visual UI/Views/Settings.storyboard b/apps/ios/GuideDogs/Code/Visual UI/Views/Settings.storyboard index 0807f1425..1fb6a2069 100644 --- a/apps/ios/GuideDogs/Code/Visual UI/Views/Settings.storyboard +++ b/apps/ios/GuideDogs/Code/Visual UI/Views/Settings.storyboard @@ -405,6 +405,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +