From 3f4890c3cbec73f7b3ff41d50c4fe20dcaeceb2b Mon Sep 17 00:00:00 2001 From: Dave Craig Date: Fri, 20 Mar 2026 09:38:27 +0000 Subject: [PATCH 1/6] Ensure a response to "Nearby Markers" even if there are none This is a longstanding easy to fix bug. We now report "No nearby markers" rather than stay silent. --- .../soundscape/geoengine/GeoEngine.kt | 12 ++++++++++-- app/src/main/res/values/strings.xml | 2 ++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/GeoEngine.kt b/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/GeoEngine.kt index f8ba8755..a88e1088 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/GeoEngine.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/GeoEngine.kt @@ -848,8 +848,16 @@ class GeoEngine { val gridFinishTime = timeSource.markNow() Log.e(GridState.TAG, "Time to calculate NearbyMarkers: ${gridFinishTime - gridStartTime}") - if(results.isEmpty()) - return null + if(results.isEmpty()) { + results.add( + PositionedString( + text = localizedContext.getString(R.string.callouts_no_nearby_markers), + type = AudioType.STANDARD, + earcon = NativeAudioEngine.EARCON_MODE_EXIT + ) + ) + } + return TrackedCallout( userGeometry = userGeometry, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6e46a8f1..48dcc24a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -304,6 +304,8 @@ "There is nothing to call out right now" "Nearby Markers" + +"There are no nearby markers" "Sleep" From 2ece0def890daf0b0115678333484a4b7248488b Mon Sep 17 00:00:00 2001 From: Dave Craig Date: Fri, 20 Mar 2026 09:46:19 +0000 Subject: [PATCH 2/6] Improve myLocation responsiveness to user action My location does reverse geocoding to find the users current location. This can involve network and take a few seconds. This change immediately plays the Earcon so that the user has feedback that their action had an effect. --- .../soundscape/services/SoundscapeService.kt | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/services/SoundscapeService.kt b/app/src/main/java/org/scottishtecharmy/soundscape/services/SoundscapeService.kt index 76da4630..5c60f83a 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/services/SoundscapeService.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/services/SoundscapeService.kt @@ -540,10 +540,19 @@ class SoundscapeService : MediaSessionService() { fun myLocation() { coroutineScope.launch { - val results = geoEngine.myLocation() - if(results != null) { + if (requestAudioFocus()) { + // The call to myLocation can take a second or so as it might be doing network + // based reverse geocoding. Ensure that the user has feedback that the action is + // taking place by immediately playing the earcon. audioEngine.clearTextToSpeechQueue() - speakCallout(results, true) + audioEngine.createEarcon(EARCON_MODE_ENTER, AudioType.STANDARD) + val results = geoEngine.myLocation() + if (results != null) { + speakCallout(results, false) + } + audioEngine.createEarcon(EARCON_MODE_EXIT, AudioType.STANDARD) + } else { + Log.w(TAG, "myLocation: Could not get audio focus.") } } } From eb21aa44b31c755a8f4e70b90a6a3590e2bc9b2d Mon Sep 17 00:00:00 2001 From: Dave Craig Date: Fri, 20 Mar 2026 10:38:59 +0000 Subject: [PATCH 3/6] Improve audio queue and hear my surroundings cancellation Waiting for "Around Me" or "Ahead of Me" audio to finish can take a while. On iOS tapping the button again would stop it immediately, and this change takes a similar approach. As well as some adding job cancellation in the SoundscapeService functions a new awaitHandle call has been added down into the C++ code. This waits for the passed in handle to be no longer queued - either because it has played out, or it has been cleared. In this way each calloutJob only completes when the callout has been fully read out. --- app/src/main/cpp/AudioBeacon.cpp | 2 + app/src/main/cpp/AudioBeacon.h | 2 + app/src/main/cpp/AudioEngine.cpp | 40 ++++++++-- app/src/main/cpp/AudioEngine.h | 3 +- .../soundscape/audio/AudioEngine.kt | 1 + .../soundscape/audio/NativeAudioEngine.kt | 10 +++ .../soundscape/geoengine/GeoEngine.kt | 3 +- .../soundscape/services/SoundscapeService.kt | 75 ++++++++++++++----- 8 files changed, 108 insertions(+), 28 deletions(-) diff --git a/app/src/main/cpp/AudioBeacon.cpp b/app/src/main/cpp/AudioBeacon.cpp index 27571362..ae3ba8f4 100644 --- a/app/src/main/cpp/AudioBeacon.cpp +++ b/app/src/main/cpp/AudioBeacon.cpp @@ -17,6 +17,8 @@ PositionedAudio::PositionedAudio(AudioEngine *engine, m_Eof(false), m_Dimmable(dimmable) { + static std::atomic s_nextHandle{1}; + m_Handle = s_nextHandle.fetch_add(1); m_pEngine = engine; m_UtteranceId = std::move(utterance_id); } diff --git a/app/src/main/cpp/AudioBeacon.h b/app/src/main/cpp/AudioBeacon.h index 520b136e..8f79285f 100644 --- a/app/src/main/cpp/AudioBeacon.h +++ b/app/src/main/cpp/AudioBeacon.h @@ -1,4 +1,5 @@ #pragma once +#include #include #include "AudioBeaconBuffer.h" @@ -47,6 +48,7 @@ namespace soundscape { AudioEngine *m_pEngine; std::string m_UtteranceId; + uint64_t m_Handle; protected: void Init(double degrees_off_axis, diff --git a/app/src/main/cpp/AudioEngine.cpp b/app/src/main/cpp/AudioEngine.cpp index 079dda6c..1abf923a 100644 --- a/app/src/main/cpp/AudioEngine.cpp +++ b/app/src/main/cpp/AudioEngine.cpp @@ -386,6 +386,17 @@ const BeaconDescriptor AudioEngine::msc_BeaconDescriptors[] = return m_QueuedBeacons.size(); } + bool AudioEngine::IsHandleActive(uint64_t handle) { + std::lock_guard guard(m_BeaconsMutex); + for (const auto &queued : m_QueuedBeacons) { + if (queued->m_Handle == handle) return true; + } + for (const auto &beacon : m_Beacons) { + if (beacon->m_Handle == handle) return true; + } + return false; + } + void AudioEngine::UpdateAudioConfig(std::string &utterance_id, int sample_rate, int audio_format, @@ -399,7 +410,7 @@ const BeaconDescriptor AudioEngine::msc_BeaconDescriptors[] = } } - void AudioEngine::AddBeacon(PositionedAudio *beacon, bool queued) + uint64_t AudioEngine::AddBeacon(PositionedAudio *beacon, bool queued) { std::lock_guard guard(m_BeaconsMutex); if(queued) @@ -418,6 +429,7 @@ const BeaconDescriptor AudioEngine::msc_BeaconDescriptors[] = m_Beacons.insert(beacon); TRACE("AddBeacon -> %zu beacons", m_Beacons.size()); } + return beacon->m_Handle; } void AudioEngine::RemoveBeacon(PositionedAudio *beacon) @@ -586,6 +598,20 @@ Java_org_scottishtecharmy_soundscape_audio_NativeAudioEngine_getQueueDepth(JNIEn } return 0L; } + +extern "C" +JNIEXPORT jboolean JNICALL +Java_org_scottishtecharmy_soundscape_audio_NativeAudioEngine_isHandleActive(JNIEnv *env MAYBE_UNUSED, + jobject thiz MAYBE_UNUSED, + jlong engine_handle, + jlong handle) { + auto* ae = reinterpret_cast(engine_handle); + if(ae) { + return ae->IsHandleActive(static_cast(handle)) ? JNI_TRUE : JNI_FALSE; + } + return JNI_FALSE; +} + extern "C" JNIEXPORT void JNICALL Java_org_scottishtecharmy_soundscape_audio_NativeAudioEngine_destroyNativeBeacon(JNIEnv *env MAYBE_UNUSED, @@ -643,9 +669,11 @@ Java_org_scottishtecharmy_soundscape_audio_NativeAudioEngine_createNativeTextToS if (not tts) { TRACE("Failed to create text to speech"); tts.reset(nullptr); + return 0L; } - auto ret = reinterpret_cast(tts.release()); - return ret; + auto handle = tts->m_Handle; + tts.release(); + return static_cast(handle); } return 0L; } @@ -692,9 +720,11 @@ Java_org_scottishtecharmy_soundscape_audio_NativeAudioEngine_createNativeEarcon( if (not earcon) { TRACE("Failed to create Earcon"); earcon.reset(nullptr); + return 0L; } - auto ret = reinterpret_cast(earcon.release()); - return ret; + auto handle = earcon->m_Handle; + earcon.release(); + return static_cast(handle); } return 0L; } diff --git a/app/src/main/cpp/AudioEngine.h b/app/src/main/cpp/AudioEngine.h index c22216af..06640cc7 100644 --- a/app/src/main/cpp/AudioEngine.h +++ b/app/src/main/cpp/AudioEngine.h @@ -69,7 +69,7 @@ namespace soundscape { void SetBeaconType(int beaconType); const BeaconDescriptor *GetBeaconDescriptor() const; - void AddBeacon(PositionedAudio *beacon, bool queued = false); + uint64_t AddBeacon(PositionedAudio *beacon, bool queued = false); void RemoveBeacon(PositionedAudio *beacon); bool ToggleBeaconMute(); @@ -91,6 +91,7 @@ namespace soundscape { void ClearQueue(); unsigned int GetQueueDepth(); + bool IsHandleActive(uint64_t handle); void SetUseHrtf(bool use) { if (m_pMixer) m_pMixer->setUseHrtf(use); } void SetSuppressRestart(bool suppress) { if (m_pMixer) m_pMixer->setSuppressRestart(suppress); } diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/audio/AudioEngine.kt b/app/src/main/java/org/scottishtecharmy/soundscape/audio/AudioEngine.kt index c688f799..cfd702bd 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/audio/AudioEngine.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/audio/AudioEngine.kt @@ -14,6 +14,7 @@ interface AudioEngine { fun createEarcon(asset: String, type: AudioType, latitude: Double = Double.NaN, longitude: Double = Double.NaN, heading: Double = Double.NaN) : Long fun clearTextToSpeechQueue() fun getQueueDepth() : Long + fun isHandleActive(handle: Long) : Boolean fun updateGeometry(listenerLatitude: Double, listenerLongitude: Double, listenerHeading: Double?, focusGained: Boolean, duckingAllowed: Boolean, proximityNear: Double) fun setBeaconType(beaconType: String) fun getListOfBeaconTypes() : Array diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/audio/NativeAudioEngine.kt b/app/src/main/java/org/scottishtecharmy/soundscape/audio/NativeAudioEngine.kt index 8fd35b06..a3c066ee 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/audio/NativeAudioEngine.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/audio/NativeAudioEngine.kt @@ -68,6 +68,7 @@ class NativeAudioEngine @Inject constructor(val service: SoundscapeService? = nu private external fun createNativeEarcon(engineHandle: Long, asset:String, mode: Int, latitude: Double, longitude: Double, heading: Double) : Long private external fun clearNativeTextToSpeechQueue(engineHandle: Long) private external fun getQueueDepth(engineHandle: Long) : Long + private external fun isHandleActive(engineHandle: Long, handle: Long) : Boolean private external fun updateGeometry(engineHandle: Long, latitude: Double, longitude: Double, heading: Double, focusGained: Boolean, duckingAllowed: Boolean, proximityNear: Double) private external fun setBeaconType(engineHandle: Long, beaconType: String) private external fun getListOfBeacons() : Array @@ -338,6 +339,15 @@ class NativeAudioEngine @Inject constructor(val service: SoundscapeService? = nu return 0 } + override fun isHandleActive(handle: Long) : Boolean { + synchronized(engineMutex) { + if (engineHandle != 0L) { + return isHandleActive(engineHandle, handle) + } + } + return false + } + override fun getAvailableSpeechEngines() : List { return ttsEngine.getAvailableEngines() } diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/GeoEngine.kt b/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/GeoEngine.kt index a88e1088..c8944cce 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/GeoEngine.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/GeoEngine.kt @@ -852,8 +852,7 @@ class GeoEngine { results.add( PositionedString( text = localizedContext.getString(R.string.callouts_no_nearby_markers), - type = AudioType.STANDARD, - earcon = NativeAudioEngine.EARCON_MODE_EXIT + type = AudioType.STANDARD ) ) } diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/services/SoundscapeService.kt b/app/src/main/java/org/scottishtecharmy/soundscape/services/SoundscapeService.kt index 5c60f83a..083a437b 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/services/SoundscapeService.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/services/SoundscapeService.kt @@ -36,6 +36,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.delay +import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest @@ -103,6 +104,9 @@ class SoundscapeService : MediaSessionService() { // secondary service private var timerJob: Job? = null + // Guard to prevent duplicate user-triggered callouts + private var calloutJob: Job? = null + // Wake lock — keeps CPU running while screen is off so audio callbacks continue private var wakeLock: PowerManager.WakeLock? = null @@ -538,19 +542,37 @@ class SoundscapeService : MediaSessionService() { geoEngine.updateBeaconLocation(null) } + private suspend fun awaitHandle(handle: Long) { + while (handle != 0L && audioEngine.isHandleActive(handle)) { + delay(100) + } + } + + private fun cancelCallout(): Boolean { + val wasActive = calloutJob?.isActive == true + if (wasActive) { + calloutJob?.cancel() + audioEngine.clearTextToSpeechQueue() + } + return wasActive + } + fun myLocation() { - coroutineScope.launch { + if (cancelCallout()) return + calloutJob = coroutineScope.launch { if (requestAudioFocus()) { // The call to myLocation can take a second or so as it might be doing network // based reverse geocoding. Ensure that the user has feedback that the action is // taking place by immediately playing the earcon. - audioEngine.clearTextToSpeechQueue() audioEngine.createEarcon(EARCON_MODE_ENTER, AudioType.STANDARD) val results = geoEngine.myLocation() + ensureActive() + var lastHandle = 0L if (results != null) { - speakCallout(results, false) + lastHandle = speakCallout(results, false) } audioEngine.createEarcon(EARCON_MODE_EXIT, AudioType.STANDARD) + awaitHandle(lastHandle) } else { Log.w(TAG, "myLocation: Could not get audio focus.") } @@ -558,32 +580,41 @@ class SoundscapeService : MediaSessionService() { } fun whatsAroundMe() { - coroutineScope.launch { + if (cancelCallout()) return + calloutJob = coroutineScope.launch { val results = geoEngine.whatsAroundMe() + ensureActive() + var lastHandle = 0L if(results.positionedStrings.isNotEmpty()) { - audioEngine.clearTextToSpeechQueue() - speakCallout(results, true) + lastHandle = speakCallout(results, true) } + awaitHandle(lastHandle) } } fun aheadOfMe() { - coroutineScope.launch { + if (cancelCallout()) return + calloutJob = coroutineScope.launch { val results = geoEngine.aheadOfMe() + ensureActive() + var lastHandle = 0L if(results != null) { - audioEngine.clearTextToSpeechQueue() - speakCallout(results, true) + lastHandle = speakCallout(results, true) } + awaitHandle(lastHandle) } } fun nearbyMarkers() { - coroutineScope.launch { + if (cancelCallout()) return + calloutJob = coroutineScope.launch { val results = geoEngine.nearbyMarkers() + ensureActive() + var lastHandle = 0L if(results != null) { - audioEngine.clearTextToSpeechQueue() - speakCallout(results, true) + lastHandle = speakCallout(results, true) } + awaitHandle(lastHandle) } } @@ -729,24 +760,25 @@ class SoundscapeService : MediaSessionService() { audioEngine.createTextToSpeech(text, AudioType.STANDARD) } - fun speakCallout(callout: TrackedCallout?, addModeEarcon: Boolean) { + fun speakCallout(callout: TrackedCallout?, addModeEarcon: Boolean) : Long { - if(callout == null) return + if(callout == null) return 0L if (!requestAudioFocus()) { Log.w(TAG, "SpeakCallout: Could not get audio focus.") - return + return 0L } - if(addModeEarcon) audioEngine.createEarcon(EARCON_MODE_ENTER, AudioType.STANDARD) + var lastHandle = 0L + if(addModeEarcon) lastHandle = audioEngine.createEarcon(EARCON_MODE_ENTER, AudioType.STANDARD) for(result in callout.positionedStrings) { if(result.location == null) { var type = result.type if(type == AudioType.LOCALIZED) type = AudioType.STANDARD - if(result.earcon != null) { + if(result.earcon != null) audioEngine.createEarcon(result.earcon, type, 0.0, 0.0, result.heading?:0.0) - } - audioEngine.createTextToSpeech(result.text, type, 0.0, 0.0, result.heading?:0.0) + + lastHandle = audioEngine.createTextToSpeech(result.text, type, 0.0, 0.0, result.heading?:0.0) } else { if(result.earcon != null) { @@ -757,7 +789,7 @@ class SoundscapeService : MediaSessionService() { result.location.longitude, result.heading?:0.0) } - audioEngine.createTextToSpeech( + lastHandle = audioEngine.createTextToSpeech( result.text, result.type, result.location.latitude, @@ -766,10 +798,13 @@ class SoundscapeService : MediaSessionService() { ) } } + // Don't set lastHandle on the earcon, we don't really care if this has finished or not, + // we just want to wait on the TTS. if(addModeEarcon) audioEngine.createEarcon(EARCON_MODE_EXIT, AudioType.STANDARD) callout.calloutHistory?.add(callout) callout.locationFilter?.update(callout.userGeometry) + return lastHandle } fun toggleAutoCallouts() { From d373ebabe51d8f61189568ef16148a7f3aa214e0 Mon Sep 17 00:00:00 2001 From: Dave Craig Date: Fri, 20 Mar 2026 10:58:11 +0000 Subject: [PATCH 4/6] Skipping through waypoints on route is laborious The TTS isn't cleared and so is read out in full when each skip is done. This backs up the audio queue. This change means that only when the route player automatically moves to the next waypoint is it guaranteed to be read out. User initiated skipping clears the TTS queue and so avoids endless output. --- .../soundscape/services/RoutePlayer.kt | 16 ++++++++-------- .../soundscape/services/SoundscapeService.kt | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/services/RoutePlayer.kt b/app/src/main/java/org/scottishtecharmy/soundscape/services/RoutePlayer.kt index 504602f5..8e1688de 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/services/RoutePlayer.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/services/RoutePlayer.kt @@ -152,7 +152,7 @@ class RoutePlayer(val service: SoundscapeService, context: Context) { if ((currentMarker + 1) < route.markers.size) { // We're within 12m of the marker, move on to the next one Log.d(TAG, "Moving to next waypoint ${coroutineContext[Job]}") - moveToNext() + moveToNext(false) } else { // We've reached the end of the route // Announce the end of the route @@ -179,7 +179,7 @@ class RoutePlayer(val service: SoundscapeService, context: Context) { } } - private fun createBeaconAtWaypoint(index: Int) { + private fun createBeaconAtWaypoint(index: Int, userInitiated: Boolean) { currentRouteData?.let { route -> if (index < route.markers.size) { val location = route.markers[index].getLngLatAlt() @@ -206,7 +206,7 @@ class RoutePlayer(val service: SoundscapeService, context: Context) { route.markers[index].name, formatDistanceAndDirection(distance, null, localizedContext)) } - + if(userInitiated) service.audioEngine.clearTextToSpeechQueue() service.speakText( beaconSetText, AudioType.LOCALIZED, location.latitude, location.longitude, 0.0 @@ -229,18 +229,18 @@ class RoutePlayer(val service: SoundscapeService, context: Context) { } fun play() { - createBeaconAtWaypoint(currentMarker) + createBeaconAtWaypoint(currentMarker, true) Log.d(TAG, toString()) } - fun moveToNext() : Boolean { + fun moveToNext(userInitiated: Boolean) : Boolean { currentRouteData?.let { route -> if(route.markers.size > 1) { if ((currentMarker + 1) < route.markers.size) { currentMarker++ _currentRouteFlow.update { it.copy(currentWaypoint = currentMarker) } - createBeaconAtWaypoint(currentMarker) + createBeaconAtWaypoint(currentMarker, userInitiated) } return true } @@ -249,13 +249,13 @@ class RoutePlayer(val service: SoundscapeService, context: Context) { return false } - fun moveToPrevious() : Boolean{ + fun moveToPrevious(userInitiated: Boolean) : Boolean{ currentRouteData?.let { route -> if(route.markers.size > 1) { if (currentMarker > 0) { currentMarker-- _currentRouteFlow.update { it.copy(currentWaypoint = currentMarker) } - createBeaconAtWaypoint(currentMarker) + createBeaconAtWaypoint(currentMarker, userInitiated) return true } } diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/services/SoundscapeService.kt b/app/src/main/java/org/scottishtecharmy/soundscape/services/SoundscapeService.kt index 083a437b..17585aba 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/services/SoundscapeService.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/services/SoundscapeService.kt @@ -663,10 +663,10 @@ class SoundscapeService : MediaSessionService() { routePlayer.stopRoute() } fun routeSkipPrevious(): Boolean { - return routePlayer.moveToPrevious() + return routePlayer.moveToPrevious(true) } fun routeSkipNext(): Boolean { - return routePlayer.moveToNext() + return routePlayer.moveToNext(true) } fun routeMute(): Boolean { if(routePlayer.isPlaying()) { From 1f3167f583ac9ff7302f92624f0be2de84142719 Mon Sep 17 00:00:00 2001 From: Dave Craig Date: Fri, 20 Mar 2026 11:23:41 +0000 Subject: [PATCH 5/6] Update Soundscape description on index.md and include link to iOS version --- ...e_App_Store_Badge_US-UK_RGB_blk_092917.svg | 46 +++++++++++++++ ...ItOnGooglePlay_Badge_Web_color_English.png | Bin 4698 -> 0 bytes ...ItOnGooglePlay_Badge_Web_color_English.svg | 55 ++++++++++++++++++ docs/index.md | 19 +++++- 4 files changed, 117 insertions(+), 3 deletions(-) create mode 100755 docs/Download_on_the_App_Store_Badge_US-UK_RGB_blk_092917.svg delete mode 100644 docs/GetItOnGooglePlay_Badge_Web_color_English.png create mode 100644 docs/GetItOnGooglePlay_Badge_Web_color_English.svg diff --git a/docs/Download_on_the_App_Store_Badge_US-UK_RGB_blk_092917.svg b/docs/Download_on_the_App_Store_Badge_US-UK_RGB_blk_092917.svg new file mode 100755 index 00000000..984e5e99 --- /dev/null +++ b/docs/Download_on_the_App_Store_Badge_US-UK_RGB_blk_092917.svg @@ -0,0 +1,46 @@ + + Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/GetItOnGooglePlay_Badge_Web_color_English.png b/docs/GetItOnGooglePlay_Badge_Web_color_English.png deleted file mode 100644 index 7a06997a57236e18b5bd2a152435911b72371a89..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4698 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 + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index e267c2ab..e2db2744 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,11 +6,24 @@ permalink: / --- # What is Soundscape? -Soundscape is an Android app to help people with visual impairment explore and navigate the world. It does this by describing nearby features using mapping data from OpenStreetMap. +Soundscape is a free, open-source app that uses 3D spatial audio to help people with visual impairments build awareness of their surroundings and navigate more confidently. - Get Soundscape on Google Play Store +Rather than giving turn-by-turn directions, Soundscape acts like a lighthouse for the ears. Through stereo headphones, users hear sounds coming from the direction of real-world places, building a richer mental map of their environment. -The initial release is based on the original [iOS Soundscape app](https://www.scottishtecharmy.org/soundscape). We are now at the point where we can publish openly on the Play Store, though we will be continuing to work on and improve the app based on user feedback. +- **Automatic callouts** - As you walk, the app announces nearby intersections, shops, bus stops, parks, and other points of interest. +- **Audio beacon** - Set a destination and hear a continuous spatial sound pulling you toward it. The tone changes when you're pointing the right way. +- **Location awareness** - Tap a button to hear what's around you in each direction, what road you're on, or what's coming up ahead. +- **Markers and routes** - Save favourite locations and create walking routes between them. The app guides you along the route, automatically moving to the next waypoint as you arrive. +- **Street preview** - Explore an unfamiliar area virtually before visiting, to build confidence and familiarity. + +The app carries on working in the background when the phone in a pocket or bag - no need to hold or look at a screen. It is completely free, with no ads or data collection, and the Android version can work offline using downloaded map regions. + +Soundscape was originally developed on iOS by [Microsoft Research](https://www.microsoft.com/en-us/research/product/soundscape/) and was widely used before being open sourced in 2022. The [Scottish Tech Army](https://www.scottishtecharmy.org) has kept it available on iOS and also rewritten it as an open-source Android app. + + Get Soundscape on Google Play Store + Get Soundscape on Apple App 
+Store # Soundscape support The email address links straight into our help desk system. Please send in any feedback you have, whether it's a feature request or a bug report. You'll receive an automated email in response and we'll get pinged that there's a new ticket to look at and we'll try and answer it as quickly as possible. From ab3bdcf9a2a763e829eefb01ade60da1faf7d3c7 Mon Sep 17 00:00:00 2001 From: Dave Craig Date: Fri, 20 Mar 2026 16:08:07 +0000 Subject: [PATCH 6/6] cancelCallout is always called from user action so clear TTS queue Although cancelCallout was successfully cancelling other hear my surroundings calls it wasn't over-riding auto callouts. This changes ensures that it always takes precedent. --- .../soundscape/services/SoundscapeService.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/services/SoundscapeService.kt b/app/src/main/java/org/scottishtecharmy/soundscape/services/SoundscapeService.kt index 17585aba..955189be 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/services/SoundscapeService.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/services/SoundscapeService.kt @@ -550,10 +550,11 @@ class SoundscapeService : MediaSessionService() { private fun cancelCallout(): Boolean { val wasActive = calloutJob?.isActive == true - if (wasActive) { + if (wasActive) calloutJob?.cancel() - audioEngine.clearTextToSpeechQueue() - } + + // Always clear the TTS queue as there's been a user action that requires a response + audioEngine.clearTextToSpeechQueue() return wasActive }