Skip to content

Commit 5ba2a94

Browse files
Functional improvements v3.1.1.1
1 parent d8de718 commit 5ba2a94

11 files changed

Lines changed: 150 additions & 38 deletions

File tree

app/src/main/cpp/hark_audio_engine.cpp

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ bool HarkAudioEngine::start(int32_t sampleRate, int32_t framesPerBurst) {
2323
mEnvelope = 0.0f;
2424
mPrevInput = 0.0f;
2525
mPrevOutput = 0.0f;
26+
mIsBuffering = true;
2627

27-
uint32_t fifoCapacity = static_cast<uint32_t>(framesPerBurst) * 8;
28+
uint32_t fifoCapacity = std::max(static_cast<uint32_t>(framesPerBurst) * 16, static_cast<uint32_t>(4096));
2829
mFifoBuffer = std::make_unique<oboe::FifoBuffer>(sizeof(float), fifoCapacity);
2930

3031
// Whisper.cpp usually expects 16kHz audio.
@@ -145,6 +146,17 @@ oboe::DataCallbackResult HarkAudioEngine::onAudioReady(
145146
return oboe::DataCallbackResult::Continue;
146147
}
147148

149+
// Pre-buffering logic to prevent snapping from minor clock drift/jitter
150+
uint32_t framesAvailable = mFifoBuffer->getFullFramesAvailable();
151+
if (mIsBuffering) {
152+
if (framesAvailable >= static_cast<uint32_t>(numFrames * 4)) {
153+
mIsBuffering = false;
154+
} else {
155+
std::fill_n(outputData, numFrames, 0.0f);
156+
return oboe::DataCallbackResult::Continue;
157+
}
158+
}
159+
148160
int32_t framesRead = mFifoBuffer->read(outputData, numFrames);
149161

150162
float currentGain = mGain.load(std::memory_order_acquire);
@@ -169,19 +181,22 @@ oboe::DataCallbackResult HarkAudioEngine::onAudioReady(
169181
// 3. MIXER
170182
float mixedSignal = primarySignal + ambientSignal;
171183

172-
// 4. SAFETY: Limiter
184+
// 4. SAFETY: Dynamics Processing
173185
if (dynamicsEnabled) {
174186
outputData[i] = applySoftKneeLimiter(mixedSignal);
175187
} else {
176-
outputData[i] = std::clamp(mixedSignal, -1.0f, 1.0f);
188+
outputData[i] = std::clamp(mixedSignal, -1.1f, 1.1f);
177189
}
178190
}
179191

180192
if (framesRead < numFrames) {
193+
// We ran out of frames! Enter buffering mode to avoid repeated snaps
181194
std::fill_n(outputData + framesRead, numFrames - framesRead, 0.0f);
195+
mIsBuffering = true;
182196
}
183197
} else {
184198
std::fill_n(outputData, numFrames, 0.0f);
199+
mIsBuffering = true;
185200
}
186201
}
187202
return oboe::DataCallbackResult::Continue;
@@ -224,24 +239,37 @@ float HarkAudioEngine::applySpeechEnhancement(float input) {
224239
}
225240

226241
float HarkAudioEngine::applySoftKneeLimiter(float input) {
227-
const float threshold = 0.8f;
228-
const float kneeWidth = 0.2f;
229-
const float attack = 0.01f;
230-
const float release = 0.1f;
242+
const float threshold = 0.75f; // Slightly lower for safety
243+
const float kneeWidth = 0.15f;
244+
245+
// Attack must be fast to catch peaks. Release must be slow to prevent "hissing/pumping" distortion.
246+
// At 48kHz, 0.05 is ~0.4ms attack. 0.0005 is ~40ms release.
247+
const float attack = 0.05f;
248+
const float release = 0.0005f;
249+
231250
float absInput = std::abs(input);
232251

252+
// Envelope follower
233253
if (absInput > mEnvelope) {
234254
mEnvelope = absInput * attack + mEnvelope * (1.0f - attack);
235255
} else {
236256
mEnvelope = absInput * release + mEnvelope * (1.0f - release);
237257
}
238258

239259
if (mEnvelope <= threshold - kneeWidth / 2.0f) return input;
240-
if (mEnvelope >= threshold + kneeWidth / 2.0f) return (input > 0) ? threshold : -threshold;
241260

242-
float diff = mEnvelope - (threshold - kneeWidth / 2.0f);
243-
float reduction = (diff * diff) / (2.0f * kneeWidth);
244-
return input * (1.0f - reduction / mEnvelope);
261+
// Proper gain reduction instead of hard-clipping the waveform
262+
float targetGain = 1.0f;
263+
if (mEnvelope >= threshold + kneeWidth / 2.0f) {
264+
targetGain = threshold / mEnvelope;
265+
} else {
266+
// Soft knee region
267+
float diff = mEnvelope - (threshold - kneeWidth / 2.0f);
268+
float reduction = (diff * diff) / (2.0f * kneeWidth);
269+
targetGain = (mEnvelope - reduction) / mEnvelope;
270+
}
271+
272+
return input * targetGain;
245273
}
246274

247275
void HarkAudioEngine::onErrorAfterClose(oboe::AudioStream *audioStream, oboe::Result error) {

app/src/main/cpp/hark_audio_engine.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class HarkAudioEngine : public oboe::AudioStreamDataCallback, public oboe::Audio
5959
float mEnvelope = 0.0f;
6060
float mPrevInput = 0.0f;
6161
float mPrevOutput = 0.0f;
62+
bool mIsBuffering = true;
6263

6364
std::mutex mStreamLock;
6465

app/src/main/java/com/thivyanstudios/hark/audio/model/AudioProcessingConfig.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ data class AudioProcessingConfig(
44
val microphoneGain: Float = 1.0f,
55
val ambientGain: Float = 0.0f,
66
val noiseSuppressionEnabled: Boolean = false,
7-
val dynamicsProcessingEnabled: Boolean = false
7+
val dynamicsProcessingEnabled: Boolean = false,
8+
val preferExternalMic: Boolean = false
89
)

app/src/main/java/com/thivyanstudios/hark/data/UserPreferencesRepository.kt

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class UserPreferencesRepository @Inject constructor(
2929
private object PreferenceKeys {
3030
val HAPTIC_FEEDBACK_ENABLED = booleanPreferencesKey("haptic_feedback_enabled")
3131
val KEEP_SCREEN_ON = booleanPreferencesKey("keep_screen_on")
32-
val DISABLE_HEARING_AID_PRIORITY = booleanPreferencesKey("disable_hearing_aid_priority")
32+
val ENABLE_BLUETOOTH_HEADSET_SUPPORT = booleanPreferencesKey("enable_bluetooth_headset_support")
3333
val MICROPHONE_GAIN = floatPreferencesKey("microphone_gain")
3434
val NOISE_SUPPRESSION_ENABLED = booleanPreferencesKey("noise_suppression_enabled")
3535
val DYNAMICS_PROCESSING_ENABLED = booleanPreferencesKey("dynamics_processing_enabled")
@@ -41,6 +41,7 @@ class UserPreferencesRepository @Inject constructor(
4141
val SILENCE_THRESHOLD = floatPreferencesKey("silence_threshold")
4242
val TRANSCRIPTION_FONT_SIZE = floatPreferencesKey("transcription_font_size")
4343
val SELECTED_MODEL_ID = androidx.datastore.preferences.core.stringPreferencesKey("selected_model_id")
44+
val PREFER_EXTERNAL_MIC = booleanPreferencesKey("prefer_external_mic")
4445
}
4546

4647
val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
@@ -56,7 +57,7 @@ class UserPreferencesRepository @Inject constructor(
5657
UserPreferences(
5758
hapticFeedbackEnabled = preferences[PreferenceKeys.HAPTIC_FEEDBACK_ENABLED] ?: false,
5859
keepScreenOn = preferences[PreferenceKeys.KEEP_SCREEN_ON] ?: false,
59-
disableHearingAidPriority = preferences[PreferenceKeys.DISABLE_HEARING_AID_PRIORITY] ?: false,
60+
enableBluetoothHeadsetSupport = preferences[PreferenceKeys.ENABLE_BLUETOOTH_HEADSET_SUPPORT] ?: false,
6061
microphoneGain = preferences[PreferenceKeys.MICROPHONE_GAIN] ?: Constants.Preferences.DEFAULT_GAIN,
6162
noiseSuppressionEnabled = preferences[PreferenceKeys.NOISE_SUPPRESSION_ENABLED] ?: false,
6263
dynamicsProcessingEnabled = preferences[PreferenceKeys.DYNAMICS_PROCESSING_ENABLED] ?: false,
@@ -67,7 +68,8 @@ class UserPreferencesRepository @Inject constructor(
6768
whisperTranslate = preferences[PreferenceKeys.WHISPER_TRANSLATE] ?: false,
6869
silenceThreshold = preferences[PreferenceKeys.SILENCE_THRESHOLD] ?: 0.005f,
6970
transcriptionFontSize = preferences[PreferenceKeys.TRANSCRIPTION_FONT_SIZE] ?: 22f,
70-
selectedModelId = preferences[PreferenceKeys.SELECTED_MODEL_ID] ?: "ggml-base-q8_0"
71+
selectedModelId = preferences[PreferenceKeys.SELECTED_MODEL_ID] ?: "ggml-base-q8_0",
72+
preferExternalMic = preferences[PreferenceKeys.PREFER_EXTERNAL_MIC] ?: false
7173
)
7274
}
7375
.flowOn(ioDispatcher)
@@ -80,8 +82,8 @@ class UserPreferencesRepository @Inject constructor(
8082
it[PreferenceKeys.KEEP_SCREEN_ON] = isEnabled
8183
}
8284

83-
suspend fun setDisableHearingAidPriority(isEnabled: Boolean) = update {
84-
it[PreferenceKeys.DISABLE_HEARING_AID_PRIORITY] = isEnabled
85+
suspend fun setEnableBluetoothHeadsetSupport(isEnabled: Boolean) = update {
86+
it[PreferenceKeys.ENABLE_BLUETOOTH_HEADSET_SUPPORT] = isEnabled
8587
}
8688

8789
suspend fun setMicrophoneGain(gain: Float) = update {
@@ -128,6 +130,10 @@ class UserPreferencesRepository @Inject constructor(
128130
it[PreferenceKeys.SELECTED_MODEL_ID] = id
129131
}
130132

133+
suspend fun setPreferExternalMic(enabled: Boolean) = update {
134+
it[PreferenceKeys.PREFER_EXTERNAL_MIC] = enabled
135+
}
136+
131137
private suspend fun update(action: (MutablePreferences) -> Unit) {
132138
try {
133139
dataStore.edit { action(it) }

app/src/main/java/com/thivyanstudios/hark/data/model/UserPreferences.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import com.thivyanstudios.hark.util.Constants
55
data class UserPreferences(
66
val hapticFeedbackEnabled: Boolean = false,
77
val keepScreenOn: Boolean = false,
8-
val disableHearingAidPriority: Boolean = false,
8+
val enableBluetoothHeadsetSupport: Boolean = false,
99
val microphoneGain: Float = Constants.Preferences.DEFAULT_GAIN,
1010
val noiseSuppressionEnabled: Boolean = false,
1111
val dynamicsProcessingEnabled: Boolean = false,
@@ -16,5 +16,6 @@ data class UserPreferences(
1616
val whisperTranslate: Boolean = false,
1717
val silenceThreshold: Float = 0.005f,
1818
val transcriptionFontSize: Float = 22f,
19-
val selectedModelId: String = "ggml-base-q8_0"
19+
val selectedModelId: String = "ggml-base-q8_0",
20+
val preferExternalMic: Boolean = false
2021
)

app/src/main/java/com/thivyanstudios/hark/service/AudioStreamingService.kt

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ class AudioStreamingService : Service(), AudioStreamingController {
8282
lateinit var ioDispatcher: CoroutineDispatcher
8383

8484
private lateinit var serviceScope: CoroutineScope
85-
private var disableHearingAidPriority = false
85+
private var enableBluetoothHeadsetSupport = false
86+
private var preferExternalMic = false
8687
private var lastTranscriptModeEnabled: Boolean? = null
8788
private var lastSelectedModelId: String? = null
8889

@@ -132,7 +133,7 @@ class AudioStreamingService : Service(), AudioStreamingController {
132133
}
133134

134135
private fun isCompatibleDevice(type: Int): Boolean {
135-
return if (disableHearingAidPriority) {
136+
return if (enableBluetoothHeadsetSupport) {
136137
when (type) {
137138
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
138139
AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
@@ -178,10 +179,19 @@ class AudioStreamingService : Service(), AudioStreamingController {
178179
userPreferencesRepository.userPreferencesFlow
179180
.distinctUntilChanged()
180181
.onEach { prefs ->
181-
val newDisablePriority = prefs.disableHearingAidPriority
182-
if (newDisablePriority != disableHearingAidPriority) {
183-
HarkLog.i(TAG, "Hearing aid priority preference changed: $newDisablePriority")
184-
disablePriorityChange(newDisablePriority)
182+
val newEnableSupport = prefs.enableBluetoothHeadsetSupport
183+
if (newEnableSupport != enableBluetoothHeadsetSupport) {
184+
HarkLog.i(TAG, "Bluetooth headset support preference changed: $newEnableSupport")
185+
bluetoothSupportChange(newEnableSupport)
186+
}
187+
188+
val newPreferExternalMic = prefs.preferExternalMic
189+
if (newPreferExternalMic != preferExternalMic) {
190+
HarkLog.i(TAG, "Prefer external mic preference changed: $newPreferExternalMic")
191+
preferExternalMic = newPreferExternalMic
192+
if (_isStreaming.value) {
193+
updateAudioRouting()
194+
}
185195
}
186196

187197
val newTranscriptMode = prefs.transcriptModeEnabled
@@ -246,8 +256,8 @@ class AudioStreamingService : Service(), AudioStreamingController {
246256
updateHearingAidStatus()
247257
}
248258

249-
private fun disablePriorityChange(newDisablePriority: Boolean) {
250-
disableHearingAidPriority = newDisablePriority
259+
private fun bluetoothSupportChange(newEnableSupport: Boolean) {
260+
enableBluetoothHeadsetSupport = newEnableSupport
251261
if (_isStreaming.value) {
252262
stopStreaming()
253263
}
@@ -300,6 +310,7 @@ class AudioStreamingService : Service(), AudioStreamingController {
300310
withContext(ioDispatcher) {
301311
// Initialize Whisper before starting engine
302312
prepareWhisper()
313+
updateAudioRouting()
303314
audioEngine.start()
304315
startTranscriptionLoop()
305316
startLevelMonitoringLoop()
@@ -496,6 +507,7 @@ class AudioStreamingService : Service(), AudioStreamingController {
496507

497508
HarkLog.i(TAG, "Stopping streaming")
498509
_isStreaming.value = false
510+
clearAudioRouting()
499511
abandonAudioFocus()
500512

501513
fullTranscript.setLength(0)
@@ -557,6 +569,52 @@ class AudioStreamingService : Service(), AudioStreamingController {
557569
}
558570
}
559571

572+
private fun updateAudioRouting() {
573+
val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
574+
if (preferExternalMic) {
575+
// Set audio mode to IN_COMMUNICATION to help routing
576+
audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
577+
578+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
579+
val devices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
580+
val btMic = devices.find {
581+
it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO ||
582+
it.type == AudioDeviceInfo.TYPE_BLE_HEADSET
583+
}
584+
if (btMic != null) {
585+
HarkLog.i(TAG, "Setting communication device to: ${btMic.productName}")
586+
audioManager.setCommunicationDevice(btMic)
587+
} else {
588+
HarkLog.w(TAG, "External mic preferred but no BT mic found")
589+
}
590+
} else {
591+
HarkLog.i(TAG, "Starting Bluetooth SCO")
592+
@Suppress("DEPRECATION")
593+
audioManager.startBluetoothSco()
594+
@Suppress("DEPRECATION")
595+
audioManager.isBluetoothScoOn = true
596+
}
597+
} else {
598+
clearAudioRouting()
599+
}
600+
}
601+
602+
private fun clearAudioRouting() {
603+
val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
604+
audioManager.mode = AudioManager.MODE_NORMAL
605+
606+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
607+
audioManager.clearCommunicationDevice()
608+
} else {
609+
@Suppress("DEPRECATION")
610+
if (audioManager.isBluetoothScoOn) {
611+
HarkLog.i(TAG, "Stopping Bluetooth SCO")
612+
audioManager.stopBluetoothSco()
613+
audioManager.isBluetoothScoOn = false
614+
}
615+
}
616+
}
617+
560618
override fun onDestroy() {
561619
HarkLog.i(TAG, "onDestroy")
562620
stopStreaming()

app/src/main/java/com/thivyanstudios/hark/ui/screens/SettingsScreen.kt

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -225,10 +225,18 @@ fun SettingsScreen(
225225
// Section: Connectivity
226226
SettingsGroup(title = "Connectivity") {
227227
SettingsSwitchRow(
228-
text = stringResource(R.string.settings_disable_hearing_aid_priority),
229-
icon = Icons.Default.Hearing,
230-
checked = uiState.disableHearingAidPriority,
231-
onCheckedChange = settingsViewModel::setDisableHearingAidPriority,
228+
text = stringResource(R.string.settings_enable_bluetooth_headset_support),
229+
icon = Icons.Default.BluetoothAudio,
230+
checked = uiState.enableBluetoothHeadsetSupport,
231+
onCheckedChange = settingsViewModel::setEnableBluetoothHeadsetSupport,
232+
hapticFeedbackEnabled = uiState.hapticFeedbackEnabled
233+
)
234+
235+
SettingsSwitchRow(
236+
text = stringResource(R.string.settings_prefer_external_mic),
237+
icon = Icons.Default.BluetoothAudio,
238+
checked = uiState.preferExternalMic,
239+
onCheckedChange = settingsViewModel::setPreferExternalMic,
232240
hapticFeedbackEnabled = uiState.hapticFeedbackEnabled
233241
)
234242

app/src/main/java/com/thivyanstudios/hark/ui/viewmodel/SettingsUiState.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ data class SettingsUiState(
66
val versionName: String = "",
77
val hapticFeedbackEnabled: Boolean = false,
88
val keepScreenOn: Boolean = false,
9-
val disableHearingAidPriority: Boolean = false,
9+
val enableBluetoothHeadsetSupport: Boolean = false,
10+
val preferExternalMic: Boolean = false,
1011
val microphoneGain: Float = Constants.Preferences.DEFAULT_GAIN,
1112
val noiseSuppressionEnabled: Boolean = false,
1213
val dynamicsProcessingEnabled: Boolean = false,

app/src/main/java/com/thivyanstudios/hark/ui/viewmodel/SettingsViewModel.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,8 @@ class SettingsViewModel @Inject constructor(
147147
versionName = version,
148148
hapticFeedbackEnabled = prefs.hapticFeedbackEnabled,
149149
keepScreenOn = prefs.keepScreenOn,
150-
disableHearingAidPriority = prefs.disableHearingAidPriority,
150+
enableBluetoothHeadsetSupport = prefs.enableBluetoothHeadsetSupport,
151+
preferExternalMic = prefs.preferExternalMic,
151152
microphoneGain = prefs.microphoneGain,
152153
noiseSuppressionEnabled = prefs.noiseSuppressionEnabled,
153154
dynamicsProcessingEnabled = prefs.dynamicsProcessingEnabled,
@@ -185,9 +186,15 @@ class SettingsViewModel @Inject constructor(
185186
}
186187
}
187188

188-
fun setDisableHearingAidPriority(isEnabled: Boolean) {
189+
fun setEnableBluetoothHeadsetSupport(isEnabled: Boolean) {
189190
viewModelScope.launch(ioDispatcher) {
190-
userPreferencesRepository.setDisableHearingAidPriority(isEnabled)
191+
userPreferencesRepository.setEnableBluetoothHeadsetSupport(isEnabled)
192+
}
193+
}
194+
195+
fun setPreferExternalMic(isEnabled: Boolean) {
196+
viewModelScope.launch(ioDispatcher) {
197+
userPreferencesRepository.setPreferExternalMic(isEnabled)
191198
}
192199
}
193200

app/src/main/res/values/strings.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
<string name="settings_haptic_feedback">Haptic feedback</string>
99
<string name="settings_dark_mode">Use dark mode</string>
1010
<string name="settings_keep_screen_on">Keep screen on when in foreground</string>
11-
<string name="settings_disable_hearing_aid_priority">Disable Hearing Aid priority</string>
11+
<string name="settings_enable_bluetooth_headset_support">Enable Bluetooth Headset Support</string>
12+
<string name="settings_prefer_external_mic">Prefer Bluetooth Microphone</string>
1213
<string name="settings_microphone_gain">Microphone gain</string>
1314
<string name="settings_support_kofi">Support me on Ko-fi</string>
1415
<string name="gain_db_format">%1$s dB</string>

0 commit comments

Comments
 (0)