Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added VibroGuide/VibroGuide_User_Evaluation_Survey.pdf
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -1611,6 +1611,10 @@
// MARK: - Callouts
//------------------------------------------------------------------------------





/* Title for the button panel at the bottom of the home view */
"callouts.panel.title" = "Hear My Surroundings";

Expand All @@ -1626,6 +1630,18 @@
/* Button, Allow Callouts */
"callouts.allow_callouts" = "Allow Callouts";

/* Toggle label for disabling and enabling sound effects */
"callouts.sound_effects" = "Sound Effects";

/* Toggle label for enabling/disabling callout sound effects */
"callouts.sound_effects.info" = "Sounds that play before callouts";

/* Toggle label for enabling and disabling delays between callouts */
"callouts.delays" = "Delays";

/* Toggle description for enabling/disabling pauses between automatic callouts */
"callouts.delays.info" = "Short pauses between callouts";

/* Callouts Title, Places and Landmarks */
"callouts.places_and_landmarks" = "Places and Landmarks";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,35 @@
//

protocol AutoCalloutSettingsProvider: AnyObject {
///Whether or not callouts are enabled globally
///false - no automatic callouts generated
var automaticCalloutsEnabled: Bool { get set }
/// Whether callout sound effects (earcons) should play before spoken callouts.
/// When false, only the spoken text will be played with no preceding audio cue.
var calloutsEarconEnabled: Bool { get set }

/// Whether delays should be inserted between automatic callouts.
/// When false, callouts will play back-to-back with no pause between them.
var calloutsDelayEnabled: Bool { get set }

/// Whether place callouts are enabled
var placeSenseEnabled: Bool { get set }

/// Whether landmark callouts are enabled
var landmarkSenseEnabled: Bool { get set }

/// Whether mobility callouts are enabled
var mobilitySenseEnabled: Bool { get set }

/// Whether information callouts are enabled
var informationSenseEnabled: Bool { get set }

/// Whether safety callouts are enabled
var safetySenseEnabled: Bool { get set }

/// Whether intersection callouts are enabled
var intersectionSenseEnabled: Bool { get set }

/// Whether destination/beacon callouts are enabled
var destinationSenseEnabled: Bool { get set }
}
26 changes: 26 additions & 0 deletions apps/ios/GuideDogs/Code/App/Settings/SettingsContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import CoreLocation

extension Notification.Name {
static let automaticCalloutsEnabledChanged = Notification.Name("GDAAutomaticCalloutsChanged")
static let calloutsEarconEnabled = Notification.Name("GDASettingsCalloutsEarconEnabled")
static let calloutsDelayEnabled = Notification.Name("GDASettingsCalloutsDelayEnabled")
static let shakeCalloutsEnabledChanged = Notification.Name("GDAShakeCalloutsChanged")
static let autoCalloutCategorySenseChanged = Notification.Name("GDAAutomaticCalloutSenseChanged")
static let beaconVolumeChanged = Notification.Name("GDABeaconVolumeChanged")
Expand Down Expand Up @@ -44,6 +46,8 @@ class SettingsContext {
fileprivate static let useOldBeacon = "GDASettingsUseOldBeacon"
fileprivate static let playBeaconStartEndMelody = "GDAPlayBeaconStartEndMelody"
fileprivate static let automaticCalloutsEnabled = "GDASettingsAutomaticCalloutsEnabled"
fileprivate static let calloutsEarconEnabled = "GDASettingsCalloutsEarconEnabled"
fileprivate static let calloutsDelayEnabled = "GDASettingsCalloutsDelayEnabled"
fileprivate static let shakeCalloutsEnabled = "GDASettingsShakeCalloutsEnabled"
fileprivate static let sensePlace = "GDASettingsPlaceSenseEnabled"
fileprivate static let senseLandmark = "GDASettingsLandmarkSenseEnabled"
Expand Down Expand Up @@ -99,6 +103,8 @@ class SettingsContext {
Keys.useOldBeacon: false,
Keys.playBeaconStartEndMelody: false,
Keys.automaticCalloutsEnabled: true,
Keys.calloutsEarconEnabled: true,
Keys.calloutsDelayEnabled: true,
Keys.shakeCalloutsEnabled: false,
Keys.sensePlace: true,
Keys.senseLandmark: true,
Expand Down Expand Up @@ -426,6 +432,26 @@ extension SettingsContext: AutoCalloutSettingsProvider {
}
}

var calloutsEarconEnabled: Bool {
get {
return userDefaults.bool(forKey: Keys.calloutsEarconEnabled)
}
set(newValue) {
userDefaults.set(newValue, forKey: Keys.calloutsEarconEnabled)
NotificationCenter.default.post(name: .calloutsEarconEnabled, object: self)
}
}

var calloutsDelayEnabled: Bool {
get {
return userDefaults.bool(forKey: Keys.calloutsDelayEnabled)
}
set(newValue) {
userDefaults.set(newValue, forKey: Keys.calloutsDelayEnabled)
NotificationCenter.default.post(name: .calloutsDelayEnabled, object: self)
}
}

var shakeCalloutsEnabled: Bool {
get {
return userDefaults.bool(forKey: Keys.shakeCalloutsEnabled)
Expand Down
6 changes: 3 additions & 3 deletions apps/ios/GuideDogs/Code/Audio/AudioSessionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ class AudioSessionManager {
} catch let error as NSError {
let avError = AVAudioSession.ErrorCode(rawValue: error.code) ?? .unspecified

GDLogAudioSessionWarn("An error occured setting the audio session category: \(error) \(avError.description)")
GDLogAudioSessionWarn("An error occurred setting the audio session category: \(error) \(avError.description)")

var telemetryInfo = currentStateInfo
telemetryInfo["error.code"] = String(error.code)
Expand Down Expand Up @@ -180,7 +180,7 @@ class AudioSessionManager {
} catch let error as NSError {
let avError = AVAudioSession.ErrorCode(rawValue: error.code) ?? .unspecified

GDLogAudioSessionWarn("An error occured activating the audio session: \(error) \(avError.description)")
GDLogAudioSessionWarn("An error occurred activating the audio session: \(error) \(avError.description)")

var telemetryInfo = currentStateInfo
telemetryInfo["error.code"] = String(error.code)
Expand Down Expand Up @@ -221,7 +221,7 @@ class AudioSessionManager {
} catch let error as NSError {
let avError = AVAudioSession.ErrorCode(rawValue: error.code) ?? .unspecified

GDLogAudioSessionWarn("An error occured deactivating the audio session: \(error) \(avError.description)")
GDLogAudioSessionWarn("An error occurred deactivating the audio session: \(error) \(avError.description)")

var telemetryInfo = currentStateInfo
telemetryInfo["error.code"] = String(error.code)
Expand Down
1 change: 1 addition & 0 deletions apps/ios/GuideDogs/Code/Audio/DynamicAudioPlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -434,3 +434,4 @@ fileprivate extension AVAudioPCMBuffer {
return buffer
}
}

2 changes: 1 addition & 1 deletion apps/ios/GuideDogs/Code/Audio/FadeableAudioPlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//

// Allows audio to fade in whenver needed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix typo and incomplete description in comment.

The comment has two issues:

  • Typo: "whenver" should be "whenever"
  • The description mentions only fade in functionality, but the class also implements fade out (fadeOut method)
📝 Proposed fix
-//  Allows audio to fade in whenver needed.
+//  Allows audio to fade in and fade out as needed.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Allows audio to fade in whenver needed.
// Allows audio to fade in and fade out as needed.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ios/GuideDogs/Code/Audio/FadeableAudioPlayer.swift` at line 8, Update
the class comment for FadeableAudioPlayer to fix the typo ("whenver" →
"whenever") and broaden the description to mention both fadeIn and fadeOut
functionality; locate the comment above the FadeableAudioPlayer declaration (or
near methods fadeIn/fadeOut) and change the text to something like "Allows audio
to fade in and out whenever needed" or equivalent concise description that
references both fadeIn and fadeOut behaviors.

import AVFoundation

class FadeableAudioPlayer: AVAudioPlayer {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,9 @@ class AutoCalloutGenerator: AutomaticGenerator, ManualGenerator {
// MARK: - Private Constants

private let inVehicleBeaconUpdateDistance: CLLocationDistance = 1000.0 // meters
private let calloutDelay = 0.75
private var calloutDelay: Double {
return SettingsContext.shared.calloutsDelayEnabled ? 0.75 : 0.0
}
Comment on lines +121 to +123

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use the injected settings provider for calloutDelay, not the global singleton.

Line 121 currently reads from SettingsContext.shared, which bypasses settings and can desync behavior when a non-shared AutoCalloutSettingsProvider is supplied.

🔧 Proposed fix
-    private var calloutDelay: Double {
-        return SettingsContext.shared.calloutsDelayEnabled ? 0.75 : 0.0
-    }
+    private var calloutDelay: Double {
+        return settings.calloutsDelayEnabled ? 0.75 : 0.0
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ios/GuideDogs/Code/Behaviors/Default/AutoCalloutGenerator.swift` around
lines 121 - 123, The computed property calloutDelay currently reads
SettingsContext.shared.calloutsDelayEnabled which bypasses the injected settings
provider; change it to use the instance-level settings provider (e.g.,
settings.calloutsDelayEnabled or the appropriate injected
AutoCalloutSettingsProvider property) so calloutDelay returns 0.75 when the
injected provider indicates calloutsDelayEnabled and 0.0 otherwise; update the
calloutDelay getter to reference the injected provider instead of
SettingsContext.shared to avoid desync.


// MARK: - Private Properties

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ class BeaconCalloutGenerator: AutomaticGenerator, ManualGenerator {
// MARK: - Private Constants

private let inVehicleBeaconUpdateDistance: CLLocationDistance = 1000.0 // meters
private let calloutDelay: TimeInterval = 0.75
private var calloutDelay: Double {
return SettingsContext.shared.calloutsDelayEnabled ? 1.00 : 0.0
}
Comment on lines +65 to +67

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Keep the beacon delay aligned with the existing default.

This toggle also changes beacon pacing from 0.75 to 1.0, while apps/ios/GuideDogs/Code/Behaviors/Default/AutoCalloutGenerator.swift:121-123 still uses 0.75. If the goal here is only to make the delay configurable, this introduces an unadvertised timing change and inconsistent cadence across generators.

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

In `@apps/ios/GuideDogs/Code/Behaviors/Default/BeaconCalloutGenerator.swift`
around lines 65 - 67, The calloutDelay property in BeaconCalloutGenerator
(private var calloutDelay) changed the enabled delay to 1.00, causing a mismatch
with AutoCalloutGenerator which uses 0.75; change BeaconCalloutGenerator to use
the same default pacing (0.75) when SettingsContext.shared.calloutsDelayEnabled
is true, or better yet read a single shared constant/config value used by
AutoCalloutGenerator (e.g., a shared defaultCalloutDelay on SettingsContext) so
both generators use the same delay.


// MARK: - Private Properties

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ struct DestinationCallout: POICalloutProtocol {

var key: String {
return entityKey



}

var marker: ReferenceEntity? {
Expand Down Expand Up @@ -73,7 +76,7 @@ struct DestinationCallout: POICalloutProtocol {
case .auto, .beaconChanged, .preview:
var sounds = [Sound]()

if includePrefixSound {
if includePrefixSound && SettingsContext.shared.calloutsEarconEnabled{
let category = SuperCategory(rawValue: marker.getPOI().superCategory) ?? SuperCategory.undefined
sounds.append(GlyphSound(category.glyph, at: markerLocation))
}
Expand All @@ -90,19 +93,22 @@ struct DestinationCallout: POICalloutProtocol {

// Inform the user why the audio beacon has stopped
if causedAudioDisabled {
let earcon = GlyphSound(.beaconFound)
var text = GDLocalizedString("beacon.beacon_location_within_audio_beacon_muted", formattedDistance)
// Append suggestion to launch NaviLens if available at location
if (poi != nil) && LocationDetail(entity: poi!).source.hasNaviLens {
text += " " + GDLocalizedString("beacon.suggest_navilens")
}
let tts = TTSSound(text, at: markerLocation)

guard let layered = LayeredSound(earcon, tts) else {
return Sounds([earcon, tts])

if SettingsContext.shared.calloutsEarconEnabled {
let earcon = GlyphSound(.beaconFound)
guard let layered = LayeredSound(earcon, tts) else {
return Sounds([earcon, tts])
}
return Sounds(layered)
}
return Sounds(layered)

return Sounds(tts)
}

return Sounds(TTSSound(GDLocalizedString("beacon.beacon_location_within", formattedDistance), at: markerLocation))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ struct IntersectionCallout: CalloutProtocol {
var sounds: [Sound] = []

// Add the sound effect
if includePrefixSound {
if includePrefixSound && SettingsContext.shared.calloutsEarconEnabled{
sounds.append(GlyphSound(.poiSense, direction: .ahead))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ struct POICallout: POICalloutProtocol {

var sounds = [Sound]()

if includePrefixSound {
if includePrefixSound && SettingsContext.shared.calloutsEarconEnabled{
sounds.append(GlyphSound(category.glyph, at: soundLocation))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,9 @@ class ExplorationGenerator: ManualGenerator, AutomaticGenerator {
return nil
}
}

/// Handles manual exploration events by the home screen buttons
/// Around Me, Ahead of Me, My Location, Nearby Markers
/// Generates callouts for POIs in the direction and returns them as a CalloutGroup
func handle(event: UserInitiatedEvent, verbosity: Verbosity) -> HandledEventAction? {
guard let event = event as? ExplorationModeToggled else {
return nil
Expand Down Expand Up @@ -197,8 +199,9 @@ class ExplorationGenerator: ManualGenerator, AutomaticGenerator {
if callouts.isEmpty {
callouts.append(RelativeStringCallout(event.mode.origin, event.mode.noCalloutsMessage, position: 0.0))
}

let group = CalloutGroup(callouts, action: .interruptAndClear, playModeSounds: true, logContext: event.logContext)
/// This part is for the manual exploration buttons, (Around Me and Ahead of Me options)
/// calloutdelay will be set to 0 when the user disables the delay in settings, or there will be a 0.75 second delay
let group = CalloutGroup(callouts, action: .interruptAndClear, playModeSounds: true, calloutDelay: SettingsContext.shared.calloutsDelayEnabled ? 0.75 : 0.0, logContext: event.logContext)
group.delegate = self
group.onComplete = event.completionHandler

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class BeaconUpdateFilter {

// MARK: State Properties

/// State of the filter (location and time) when the most recent update occured
/// State of the filter (location and time) when the most recent update occurred
private(set) var lastUpdate: FilterUpdateSnapshot?

/// Indicates if the filter is in the reset state (`false`) or if `update(...)` has been called (`true`)
Expand Down
45 changes: 27 additions & 18 deletions apps/ios/GuideDogs/Code/Behaviors/Preview/PreviewGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,27 +106,33 @@ struct PreviewGenerator<DecisionPoint: RootedPreviewGraph>: ManualGenerator {
let formattedDistance = LanguageFormatter.formattedDistance(from: SettingsContext.shared.enterImmediateVicinityDistance)

callouts.append(GenericCallout(.preview, description: "arrived at beacon (in preview)") { (_, _, _) -> [Sound] in
let earcon = GlyphSound(.beaconFound)
let tts = TTSSound(GDLocalizedString("beacon.beacon_location_within_audio_beacon_muted", formattedDistance), at: event.location)

guard let layered = LayeredSound(earcon, tts) else {
return [earcon, tts]

if SettingsContext.shared.calloutsEarconEnabled {
let earcon = GlyphSound(.beaconFound)
guard let layered = LayeredSound(earcon, tts) else {
return [earcon, tts]
}
return [layered]
}
return [layered]

return [tts]
})
} else {
let formattedDistance = LanguageFormatter.string(from: event.distance, accuracy: 0.0, name: GDLocalizedString("beacon.generic_name"))

callouts.append(GenericCallout(.preview, description: "beacon update (in preview)") { (_, _, _) -> [Sound] in
let earcon = GlyphSound(SuperCategory.places.glyph, at: event.location)
let tts = TTSSound(formattedDistance, at: event.location)

guard let layered = LayeredSound(earcon, tts) else {
return [earcon, tts]

if SettingsContext.shared.calloutsEarconEnabled {
let earcon = GlyphSound(SuperCategory.places.glyph, at: event.location)
guard let layered = LayeredSound(earcon, tts) else {
return [earcon, tts]
}
return [layered]
}
return [layered]

return [tts]
})
}

Expand All @@ -146,14 +152,17 @@ struct PreviewGenerator<DecisionPoint: RootedPreviewGraph>: ManualGenerator {

case let event as BehaviorDeactivatedEvent:
let callouts = [GenericCallout(.preview, description: "preview ended") { (_, _, _) -> [Sound] in
let earcon = GlyphSound(.previewEnd)
let tts = TTSSound(GDLocalizedString("preview.callout.end"))

guard let layered = LayeredSound(earcon, tts) else {
return [earcon, tts]

if SettingsContext.shared.calloutsEarconEnabled {
let earcon = GlyphSound(.previewEnd)
guard let layered = LayeredSound(earcon, tts) else {
return [earcon, tts]
}
return [layered]
}
return [layered]

return [tts]
}]

// Play the sound that caps off the end of the preview mode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,20 @@ struct IntersectionDecisionPoint: RootedPreviewGraph {
let earcon = GlyphSound(.poiSense, compass: direction)
let tts = TTSSound(approach, compass: direction)

guard let ttsSound = ConcatenatedSound(earcon, tts), let layered = LayeredSound(ttsSound, GlyphSound(.travelEnd, compass: direction)) else {
GDLogPreviewError("Unable to concatenate and layer sounds...")
return []
let baseSound:Sound
if SettingsContext.shared.calloutsEarconEnabled {
guard let concatenated = ConcatenatedSound(earcon, tts) else {
GDLogPreviewError("Failed to concatenate sounds for intersection callout")
return []
}
baseSound = concatenated
} else {
baseSound = tts
}

guard let layered = LayeredSound(baseSound, GlyphSound(.travelEnd, compass: direction)) else {
GDLogPreviewError("Unable to concatenate and layer sounds ")
return []
}

guard let roads = roads, !roads.isEmpty else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ struct AlongRoadLocationCallout: LocationCalloutProtocol {
}

// If we aren't playing the mode enter/exit sounds, play the category sound instead
if includePrefixSound {
if includePrefixSound && SettingsContext.shared.calloutsEarconEnabled{
sounds.append(GlyphSound(.locationSense, direction: .ahead))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ struct InsideLocationCallout: LocationCalloutProtocol {
}

// If we aren't playing the mode enter/exit sounds, play the category sound instead
if includePrefixSound {
if includePrefixSound && SettingsContext.shared.calloutsEarconEnabled{
sounds.append(GlyphSound(.locationSense, direction: .ahead))
}

Expand Down
Loading