Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ios mute fix after discussion #296

Merged
merged 17 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from 8 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
4 changes: 3 additions & 1 deletion apple/DemoApp/Demo/DemoNavigationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ struct DemoNavigationView: View {
private let navigationDelegate = NavigationDelegate()
// NOTE: This is probably not ideal but works for demo purposes.
// This causes a thread performance checker warning log.
private let spokenInstructionObserver = AVSpeechSpokenInstructionObserver(isMuted: false)
private let spokenInstructionObserver = SpokenInstructionObserver.initAVSpeechSynthesizer(isMuted: false)

private var locationProvider: LocationProviding
@ObservedObject private var ferrostarCore: FerrostarCore
Expand Down Expand Up @@ -79,6 +79,8 @@ struct DemoNavigationView: View {
styleURL: style,
camera: $camera,
navigationState: ferrostarCore.state,
isMuted: spokenInstructionObserver.isMuted,
onTapMute: spokenInstructionObserver.toggleMute,
onTapExit: { stopNavigation() },
makeMapContent: {
let source = ShapeSource(identifier: "userLocation") {
Expand Down
54 changes: 0 additions & 54 deletions apple/Sources/FerrostarCore/Speech.swift

This file was deleted.

33 changes: 33 additions & 0 deletions apple/Sources/FerrostarCore/Speech/SpeechSynthesizer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import AVFoundation
import Foundation

/// An abstracted speech synthesizer that is used by the ``SpokenInstructionObserver``
///
/// Any functions that are needed for use in the ``SpokenInstructionObserver`` should be exposed through
/// this protocol.
public protocol SpeechSynthesizer {
// TODO: We could further abstract this to allow other speech synths.
// E.g. with a `struct SpeechUtterance` if and when another speech service comes along.

var isSpeaking: Bool { get }
func speak(_ utterance: AVSpeechUtterance)
@discardableResult
func stopSpeaking(at boundary: AVSpeechBoundary) -> Bool
}

extension AVSpeechSynthesizer: SpeechSynthesizer {
// No def required
}

class PreviewSpeechSynthesizer: SpeechSynthesizer {
public var isSpeaking: Bool = false

public func speak(_: AVSpeechUtterance) {
// No action for previews
}

public func stopSpeaking(at _: AVSpeechBoundary) -> Bool {
// No action for previews
true
}
}
74 changes: 74 additions & 0 deletions apple/Sources/FerrostarCore/Speech/SpokenInstructionObserver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import AVFoundation
import Combine
import FerrostarCoreFFI
import Foundation

/// An Spoken instruction provider that takes a speech synthesizer.
public class SpokenInstructionObserver: ObservableObject {
@Published public private(set) var isMuted: Bool

private let synthesizer: SpeechSynthesizer

/// Create a spoken instruction observer with any ``SpeechSynthesizer``
///
/// - Parameters:
/// - synthesizer: The speech synthesizer.
/// - isMuted: Whether the speech synthesizer is currently muted. Assume false if unknown.
public init(
synthesizer: SpeechSynthesizer,
isMuted: Bool
) {
self.synthesizer = synthesizer
self.isMuted = isMuted
}

public func spokenInstructionTriggered(_ instruction: FerrostarCoreFFI.SpokenInstruction) {
guard !isMuted else {
return
}

let utterance: AVSpeechUtterance = if #available(iOS 16.0, *),
let ssml = instruction.ssml,
let ssmlUtterance = AVSpeechUtterance(ssmlRepresentation: ssml)
{
ssmlUtterance
} else {
AVSpeechUtterance(string: instruction.text)
}

synthesizer.speak(utterance)
}

/// Toggle the mute.
public func toggleMute() {
// TODO: We flip the publisher before actually stopping the synthesizer for a responsive.
// But this still seems a little jumpy/slow. We may want to use some Tasks/concurrency
// to separate UI on main, and speech on another queue.
let isCurrentlyMuted = isMuted
isMuted = !isCurrentlyMuted

// This used to have `synthesizer.isSpeaking`, but I think we want to run it regardless.
ianthetechie marked this conversation as resolved.
Show resolved Hide resolved
if isMuted {
stopAndClearQueue()
}
}

public func stopAndClearQueue() {
synthesizer.stopSpeaking(at: .immediate)
}
}

public extension SpokenInstructionObserver {
/// Create a new spoken instruction observer with AFFoundation's AVSpeechSynthesizer.
///
/// - Parameters:
/// - synthesizer: An instance of AVSpeechSynthesizer. One is provided by default, but you can inject your own.
/// - isMuted: If the synthesizer is muted. This should be false unless you're providing a "hot" synth that is
/// speaking.
/// - Returns: The instance of SpokenInstructionObserver
static func initAVSpeechSynthesizer(synthesizer: AVSpeechSynthesizer = AVSpeechSynthesizer(),
isMuted: Bool = false) -> SpokenInstructionObserver
{
SpokenInstructionObserver(synthesizer: synthesizer, isMuted: isMuted)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public struct DynamicallyOrientingNavigationView: View, CustomizableNavigatingIn
public var midLeading: (() -> AnyView)?
public var bottomTrailing: (() -> AnyView)?

let isMuted: Bool
let onTapMute: () -> Void
var onTapExit: (() -> Void)?

public var minimumSafeAreaInsets: EdgeInsets
Expand All @@ -49,13 +51,17 @@ public struct DynamicallyOrientingNavigationView: View, CustomizableNavigatingIn
camera: Binding<MapViewCamera>,
navigationCamera: MapViewCamera = .automotiveNavigation(),
navigationState: NavigationState?,
isMuted: Bool,
minimumSafeAreaInsets: EdgeInsets = EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16),
onTapMute: @escaping () -> Void,
onTapExit: (() -> Void)? = nil,
@MapViewContentBuilder makeMapContent: () -> [StyleLayerDefinition] = { [] }
) {
self.styleURL = styleURL
self.navigationState = navigationState
self.isMuted = isMuted
self.minimumSafeAreaInsets = minimumSafeAreaInsets
self.onTapMute = onTapMute
self.onTapExit = onTapExit

userLayers = makeMapContent()
Expand Down Expand Up @@ -88,6 +94,9 @@ public struct DynamicallyOrientingNavigationView: View, CustomizableNavigatingIn
navigationState: navigationState,
speedLimit: speedLimit,
speedLimitStyle: speedLimitStyle,
isMuted: isMuted,
showMute: true,
onMute: onTapMute,
showZoom: true,
onZoomIn: { camera.incrementZoom(by: 1) },
onZoomOut: { camera.incrementZoom(by: -1) },
Expand All @@ -109,6 +118,9 @@ public struct DynamicallyOrientingNavigationView: View, CustomizableNavigatingIn
navigationState: navigationState,
speedLimit: speedLimit,
speedLimitStyle: speedLimitStyle,
isMuted: isMuted,
showMute: true,
onMute: onTapMute,
showZoom: true,
onZoomIn: { camera.incrementZoom(by: 1) },
onZoomOut: { camera.incrementZoom(by: -1) },
Expand Down Expand Up @@ -151,7 +163,9 @@ public struct DynamicallyOrientingNavigationView: View, CustomizableNavigatingIn
return DynamicallyOrientingNavigationView(
styleURL: URL(string: "https://demotiles.maplibre.org/style.json")!,
camera: .constant(.center(userLocation.clLocation.coordinate, zoom: 12)),
navigationState: state
navigationState: state,
isMuted: true,
onTapMute: {}
)
.navigationFormatterCollection(FoundationFormatterCollection(distanceFormatter: formatter))
}
Expand All @@ -170,7 +184,9 @@ public struct DynamicallyOrientingNavigationView: View, CustomizableNavigatingIn
return DynamicallyOrientingNavigationView(
styleURL: URL(string: "https://demotiles.maplibre.org/style.json")!,
camera: .constant(.center(userLocation.clLocation.coordinate, zoom: 12)),
navigationState: state
navigationState: state,
isMuted: true,
onTapMute: {}
)
.navigationFormatterCollection(FoundationFormatterCollection(distanceFormatter: formatter))
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public struct LandscapeNavigationView: View, CustomizableNavigatingInnerGridView
public var midLeading: (() -> AnyView)?
public var bottomTrailing: (() -> AnyView)?

let isMuted: Bool
let onTapMute: () -> Void
var onTapExit: (() -> Void)?

public var minimumSafeAreaInsets: EdgeInsets
Expand All @@ -49,13 +51,17 @@ public struct LandscapeNavigationView: View, CustomizableNavigatingInnerGridView
camera: Binding<MapViewCamera>,
navigationCamera: MapViewCamera = .automotiveNavigation(),
navigationState: NavigationState?,
isMuted: Bool,
minimumSafeAreaInsets: EdgeInsets = EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16),
onTapMute: @escaping () -> Void,
onTapExit: (() -> Void)? = nil,
@MapViewContentBuilder makeMapContent: () -> [StyleLayerDefinition] = { [] }
) {
self.styleURL = styleURL
self.navigationState = navigationState
self.isMuted = isMuted
self.minimumSafeAreaInsets = minimumSafeAreaInsets
self.onTapMute = onTapMute
self.onTapExit = onTapExit

userLayers = makeMapContent()
Expand All @@ -82,6 +88,9 @@ public struct LandscapeNavigationView: View, CustomizableNavigatingInnerGridView
navigationState: navigationState,
speedLimit: speedLimit,
speedLimitStyle: speedLimitStyle,
isMuted: isMuted,
showMute: true,
onMute: onTapMute,
showZoom: true,
onZoomIn: { camera.incrementZoom(by: 1) },
onZoomOut: { camera.incrementZoom(by: -1) },
Expand Down Expand Up @@ -119,7 +128,9 @@ public struct LandscapeNavigationView: View, CustomizableNavigatingInnerGridView
return LandscapeNavigationView(
styleURL: URL(string: "https://demotiles.maplibre.org/style.json")!,
camera: .constant(.center(userLocation.clLocation.coordinate, zoom: 12)),
navigationState: state
navigationState: state,
isMuted: true,
onTapMute: {}
)
.navigationFormatterCollection(FoundationFormatterCollection(distanceFormatter: formatter))
}
Expand All @@ -140,7 +151,9 @@ public struct LandscapeNavigationView: View, CustomizableNavigatingInnerGridView
return LandscapeNavigationView(
styleURL: URL(string: "https://demotiles.maplibre.org/style.json")!,
camera: .constant(.center(userLocation.clLocation.coordinate, zoom: 12)),
navigationState: state
navigationState: state,
isMuted: true,
onTapMute: {}
)
.navigationFormatterCollection(FoundationFormatterCollection(distanceFormatter: formatter))
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import SwiftUI
struct LandscapeNavigationOverlayView: View, CustomizableNavigatingInnerGridView {
@Environment(\.navigationFormatterCollection) var formatterCollection: any FormatterCollection

private var navigationState: NavigationState?
private let navigationState: NavigationState?

@State private var isInstructionViewExpanded: Bool = false

Expand All @@ -20,17 +20,27 @@ struct LandscapeNavigationOverlayView: View, CustomizableNavigatingInnerGridView

var speedLimit: Measurement<UnitSpeed>?
var speedLimitStyle: SpeedLimitView.SignageStyle?

var showZoom: Bool
var onZoomIn: () -> Void
var onZoomOut: () -> Void

var showCentering: Bool
var onCenter: () -> Void

var onTapExit: (() -> Void)?

let showMute: Bool
let isMuted: Bool
let onMute: () -> Void

init(
navigationState: NavigationState?,
speedLimit: Measurement<UnitSpeed>? = nil,
speedLimitStyle: SpeedLimitView.SignageStyle? = nil,
isMuted: Bool,
showMute: Bool = true,
onMute: @escaping () -> Void,
showZoom: Bool = false,
onZoomIn: @escaping () -> Void = {},
onZoomOut: @escaping () -> Void = {},
Expand All @@ -41,6 +51,9 @@ struct LandscapeNavigationOverlayView: View, CustomizableNavigatingInnerGridView
self.navigationState = navigationState
self.speedLimit = speedLimit
self.speedLimitStyle = speedLimitStyle
self.isMuted = isMuted
self.onMute = onMute
self.showMute = showMute
self.showZoom = showZoom
self.onZoomIn = onZoomIn
self.onZoomOut = onZoomOut
Expand Down Expand Up @@ -87,6 +100,9 @@ struct LandscapeNavigationOverlayView: View, CustomizableNavigatingInnerGridView
NavigatingInnerGridView(
speedLimit: speedLimit,
speedLimitStyle: speedLimitStyle,
isMuted: isMuted,
showMute: showMute,
onMute: onMute,
showZoom: showZoom,
onZoomIn: onZoomIn,
onZoomOut: onZoomOut,
Expand Down
Loading
Loading