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 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
4 changes: 4 additions & 0 deletions .github/workflows/ios-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ jobs:
fetch-depth: 0 # Ensure that we can operate on the full history
ref: main

- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '16.0'
Copy link
Contributor

Choose a reason for hiding this comment

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

Had to bump this to resolve some whack build error that I couldn't repro locally. Can bump to 16.1 whenever GitHub updates (final release just dropped).


- name: Build iOS XCFramework
run: ./build-ios.sh --release
working-directory: common
Expand Down
10 changes: 7 additions & 3 deletions .github/workflows/ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ jobs:
with:
file_pattern: 'Package.swift'

- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '16.0'

- name: Build iOS XCFramework
run: ./build-ios.sh --release
working-directory: common
Expand Down Expand Up @@ -82,7 +86,7 @@ jobs:
- name: Configure Package.swift for local development
run: sed -i '' 's/let useLocalFramework = false/let useLocalFramework = true/' Package.swift

- name: Download libferrostar-rs.xcframework.
- name: Download libferrostar-rs.xcframework
uses: actions/download-artifact@v4
with:
path: common
Expand All @@ -94,7 +98,7 @@ jobs:

- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '15.3'
xcode-version: '16.0'

- name: Install xcbeautify
run: brew install xcbeautify
Expand Down Expand Up @@ -149,7 +153,7 @@ jobs:

- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '15.3'
xcode-version: '16.0'

- name: Install xcbeautify
run: brew install xcbeautify
Expand Down
Empty file removed .gitmodules
Empty file.
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
9 changes: 9 additions & 0 deletions apple/Sources/FerrostarCore/NavigationState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,13 @@ public struct NavigationState: Hashable {

return annotationJson
}

public var isNavigating: Bool {
switch tripState {
case .navigating:
true
case .complete, .idle:
false
}
}
}
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
}
}
76 changes: 76 additions & 0 deletions apple/Sources/FerrostarCore/Speech/SpokenInstructionObserver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
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
private let queue = DispatchQueue(label: "ferrostar-spoken-instruction-observer", qos: .default)

/// 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)
}

queue.async {
self.synthesizer.speak(utterance)

Check warning on line 41 in apple/Sources/FerrostarCore/Speech/SpokenInstructionObserver.swift

View workflow job for this annotation

GitHub Actions / test (FerrostarCore-Package, platform=iOS Simulator,name=iPhone 15,OS=17.2)

capture of 'utterance' with non-sendable type 'AVSpeechUtterance' in a `@Sendable` closure; this is an error in the Swift 6 language mode
}
}

/// Toggle the mute.
public func toggleMute() {
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 {
queue.async {
self.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: navigationState?.isNavigating == 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: navigationState?.isNavigating == 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))
}
Loading
Loading