From e85285e03dc9a27d97251c917863473eb13988d4 Mon Sep 17 00:00:00 2001 From: baileileicn <50839413+baileileicn@users.noreply.github.com> Date: Mon, 28 Sep 2026 14:50:11 +0000 Subject: [PATCH] Route guidance audio through a selectable navigation stream type CarPlay marks guidance audio apart from media, but the sink always built its AudioTrack from usage/content-type attributes. On head units that expose navigation on a dedicated mixer output, guidance shares the media channels, so the next turn is announced over the music in every seat. BYD puts navigation on stream type 14, the driver-side speaker. Add an "Audio routing" section with a 0-20 stream type grid and a test tone, persist the selection, and build the AudioTrack on that stream type. When the ROM rejects the stream type the track is rebuilt from usage attributes, so devices without such an output keep today's behavior (the diagnostic log reports route=usage or route=streamType=N(fallback=usage)). The advanced audio channel mapping switch is now visible on the mobile flavor too, still off by default, so usage/content-type routing can be picked at runtime instead of by editing resources, and the automotive map routes default and compatibility to the navigation bus rather than sharing media. --- .../shilapi/xcertplay/AirPlayPersistence.kt | 11 ++ .../shilapi/xcertplay/CarPlayHostActivity.kt | 8 +- .../com/shilapi/xcertplay/DiPlayActivity.kt | 126 ++++++++++++++++++ mobile/src/main/res/values/config.xml | 2 +- shared/src/main/assets/navigation_test.pcm | Bin 0 -> 384452 bytes .../xcertplay/media/AndroidMediaSink.kt | 73 ++++++++-- .../xcertplay/media/AudioChannelMapping.kt | 36 +++-- .../media/AudioChannelMappingTest.kt | 38 +++++- 8 files changed, 262 insertions(+), 32 deletions(-) create mode 100644 shared/src/main/assets/navigation_test.pcm diff --git a/common/src/main/java/com/shilapi/xcertplay/AirPlayPersistence.kt b/common/src/main/java/com/shilapi/xcertplay/AirPlayPersistence.kt index 3cdf082..0371d16 100644 --- a/common/src/main/java/com/shilapi/xcertplay/AirPlayPersistence.kt +++ b/common/src/main/java/com/shilapi/xcertplay/AirPlayPersistence.kt @@ -39,6 +39,7 @@ object AirPlayPersistence { private const val KEY_HEVC_ENABLED = "hevc_enabled" private const val KEY_HEVC_SOFTWARE_DECODER = "hevc_software_decoder" private const val KEY_ADVANCED_AUDIO_CHANNEL_MAPPING = "advanced_audio_channel_mapping" + private const val KEY_NAVIGATION_STREAM_TYPE = "navigation_stream_type" private const val KEY_WIRELESS_ENABLED = "wireless_enabled" private const val KEY_WIRELESS_HOTSPOT_MODE = "wireless_hotspot_mode" private const val KEY_MANUAL_HOTSPOT_SSID = "manual_hotspot_ssid" @@ -131,6 +132,16 @@ object AirPlayPersistence { .apply() } + fun loadNavigationStreamType(context: Context): Int = + context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + .getInt(KEY_NAVIGATION_STREAM_TYPE, 14) + + fun saveNavigationStreamType(context: Context, streamType: Int) { + context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit() + .putInt(KEY_NAVIGATION_STREAM_TYPE, streamType) + .apply() + } + fun loadWirelessEnabled(context: Context): Boolean = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) .getBoolean(KEY_WIRELESS_ENABLED, true) diff --git a/common/src/main/java/com/shilapi/xcertplay/CarPlayHostActivity.kt b/common/src/main/java/com/shilapi/xcertplay/CarPlayHostActivity.kt index 8b38f43..1ee20a3 100644 --- a/common/src/main/java/com/shilapi/xcertplay/CarPlayHostActivity.kt +++ b/common/src/main/java/com/shilapi/xcertplay/CarPlayHostActivity.kt @@ -275,6 +275,7 @@ class CarPlayHostActivity : ComponentActivity() { private var hevcSoftwareDecoderEnabled = false private var advancedAudioChannelMappingSupported = false private var advancedAudioChannelMapping = false + private var navigationStreamType = 14 private var debugLogsEnabled = false private var autoStartOnBoot = false private var manufacturer = AirPlayPersistence.DEFAULT_MANUFACTURER @@ -437,6 +438,7 @@ class CarPlayHostActivity : ComponentActivity() { advancedAudioChannelMapping = advancedAudioChannelMappingSupported && AirPlayPersistence.loadAdvancedAudioChannelMapping(this) + navigationStreamType = AirPlayPersistence.loadNavigationStreamType(this) debugLogsEnabled = AirPlayPersistence.loadDebugLogsEnabled(this) autoStartOnBoot = AirPlayPersistence.loadAutoStartOnBoot(this) manufacturer = AirPlayPersistence.loadManufacturer(this) @@ -561,6 +563,9 @@ class CarPlayHostActivity : ComponentActivity() { requestLocationPermission() } wirelessPermissionsReady = !wirelessEnabled || hasRequiredWirelessPermissions() + advancedAudioChannelMapping = + advancedAudioChannelMappingSupported && + AirPlayPersistence.loadAdvancedAudioChannelMapping(this) if (DiLink51ClusterLayout.automatic(this) && clusterMonitor == null) { clusterMonitor = DiLink51ClusterMonitor(this, ::onClusterActivityState).also { it.start() } } else if (!DiLink51ClusterLayout.automatic(this)) { @@ -960,7 +965,7 @@ class CarPlayHostActivity : ComponentActivity() { settingsSwitchRow( label = "Advanced audio channel mapping", checked = advancedAudioChannelMapping, - description = "Route AAOS audio buses by CarPlay audio type", + description = "Use usage / content-type routing instead of stream type", ) { checked -> advancedAudioChannelMapping = checked appendLog( @@ -2913,6 +2918,7 @@ class CarPlayHostActivity : ComponentActivity() { videoHeight = videoHeight, preferSoftwareHevcDecoder = hevcSoftwareDecoderEnabled, advancedAudioChannelMapping = advancedAudioChannelMapping, + navigationStreamType = navigationStreamType, onScreenStreamActiveChanged = { type, active -> onScreenStreamStateChanged(controllerGeneration, type, active) }, diff --git a/common/src/main/java/com/shilapi/xcertplay/DiPlayActivity.kt b/common/src/main/java/com/shilapi/xcertplay/DiPlayActivity.kt index 45923c3..95d53dc 100644 --- a/common/src/main/java/com/shilapi/xcertplay/DiPlayActivity.kt +++ b/common/src/main/java/com/shilapi/xcertplay/DiPlayActivity.kt @@ -12,12 +12,15 @@ import android.content.res.Configuration import android.graphics.Color import android.graphics.Typeface import android.graphics.drawable.GradientDrawable +import android.media.AudioFormat +import android.media.AudioTrack import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Handler import android.os.Looper import android.provider.Settings +import android.util.Log import android.view.Gravity import android.view.View import android.view.ViewGroup @@ -50,6 +53,9 @@ class DiPlayActivity : ComponentActivity() { private var initialLaunch = true private var notificationTransport = true private var exportInProgress = false + private var navigationStreamType = 14 + private var testToneTrack: AudioTrack? = null + private var toneStop: Runnable? = null private var exportButton: Button? = null private val notificationPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) { connect(notificationTransport) @@ -243,6 +249,42 @@ class DiPlayActivity : ComponentActivity() { AirPlayPersistence.saveHideTopBar(this, it); AirPlayPersistence.saveHideBottomBar(this, it) } } + section(content, "Audio routing") { card -> + val channelTitle = label("Navigation stream type", 18, TEXT, true) + val channelHint = label("Tap a number to test. 14 = driver speaker on BYD.", 14, MUTED) + val grid = channelSelector() + val saveButton = button("Save", true) { + AirPlayPersistence.saveNavigationStreamType(this, navigationStreamType) + toast("Saved: $navigationStreamType") + } + fun applyChannelEnabled(enabled: Boolean) { + val alpha = if (enabled) 1f else 0.4f + listOf(channelTitle, channelHint, saveButton).forEach { + it.isEnabled = enabled + it.alpha = alpha + } + for (i in 0 until grid.childCount) { + grid.getChildAt(i).let { child -> + child.isEnabled = enabled + child.alpha = alpha + } + } + } + val aaosSupported = resources.getBoolean(R.bool.config_advanced_audio_channel_mapping) + if (aaosSupported) { + toggle(card, "Advanced audio channel mapping", + "Use usage / content-type routing instead of stream type", + AirPlayPersistence.loadAdvancedAudioChannelMapping(this)) { + AirPlayPersistence.saveAdvancedAudioChannelMapping(this, it) + applyChannelEnabled(!it) + } + } + card.addView(channelTitle) + card.addView(channelHint) + card.addView(grid) + card.addView(saveButton, matchButton(12, 56)) + if (aaosSupported) applyChannelEnabled(!AirPlayPersistence.loadAdvancedAudioChannelMapping(this)) + } if (com.shilapi.xcertplay.hud.BydOutputSettings.available(this)) section(content, "BYD navigation", R.drawable.ic_dp_navigation) { card -> toggle(card, "Navigation on HUD and instrument cluster", "Show phone navigation arrows, distance and street names on supported BYD displays. Vehicle compatibility varies.", @@ -799,6 +841,90 @@ class DiPlayActivity : ComponentActivity() { } private fun openSystem(intent: Intent) { runCatching { startActivity(intent) }.onFailure { toast("Open this setting from your car’s Settings app.") } } private fun toast(message: String) { Toast.makeText(this, message, Toast.LENGTH_LONG).show() } + + private fun playTestTone(streamType: Int) { + toneStop?.let { handler.removeCallbacks(it) } + toneStop = null + testToneTrack?.let { runCatching { it.stop(); it.release() } } + testToneTrack = null + val pcm = assets.open("navigation_test.pcm").use { it.readBytes() } + val track = AudioTrack( + streamType, + 44100, + AudioFormat.CHANNEL_OUT_MONO, + AudioFormat.ENCODING_PCM_16BIT, + pcm.size, + AudioTrack.MODE_STREAM + ) + if (track.state != AudioTrack.STATE_INITIALIZED) { + Log.w("DiPlay", "playTestTone streamType=$streamType state=${track.state}") + track.release() + toast("Stream type $streamType is unavailable here (state=${track.state})") + return + } + track.write(pcm, 0, pcm.size) + track.play() + Log.i("DiPlay", "playTestTone streamType=$streamType state=${track.state} playState=${track.playState}") + testToneTrack = track + val stop = Runnable { + track.stop() + track.release() + if (testToneTrack === track) testToneTrack = null + toneStop = null + } + toneStop = stop + handler.postDelayed(stop, 4500) + } + + private val channelButtons = mutableListOf