@mentra/bluetooth-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +278 -0
- package/android/build.gradle +167 -0
- package/android/lc3Lib/CMakeLists.txt +6 -0
- package/android/lc3Lib/build.gradle +109 -0
- package/android/lc3Lib/proguard-rules.pro +33 -0
- package/android/lc3Lib/src/main/AndroidManifest.xml +2 -0
- package/android/lc3Lib/src/main/cpp/CMakeLists.txt +8 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/CMakeLists.txt +30 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/CMakeLists.txt +7 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/ogg_opus_encoder.cc +96 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/ogg_opus_encoder.h +53 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/Makefile.am +6 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/config_types.h +26 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/config_types.h.in +26 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/ogg.h +209 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/os_types.h +158 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus.h +981 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_custom.h +342 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_defines.h +799 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_multistream.h +660 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_projection.h +568 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_types.h +166 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/ogg_opus_encoder.cc +268 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/ogg_opus_encoder.h +115 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/opus_tools/opus_header.h +59 -0
- package/android/lc3Lib/src/main/cpp/liblc3/CMakeLists.txt +38 -0
- package/android/lc3Lib/src/main/cpp/liblc3/include/lc3.h +309 -0
- package/android/lc3Lib/src/main/cpp/liblc3/include/lc3_private.h +162 -0
- package/android/lc3Lib/src/main/cpp/liblc3/include/rnnoise.h +114 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/CMakeLists.txt +19 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/attdet.c +92 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/attdet.h +44 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bits.c +375 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bits.h +315 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bwdet.c +129 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bwdet.h +69 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/common.h +148 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/energy.c +70 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/energy.h +43 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/fastmath.h +158 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/lc3.c +702 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf.c +893 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf.h +111 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf_arm.h +506 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf_neon.h +281 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/makefile.mk +35 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct.c +452 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct.h +57 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct_neon.h +296 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/meson.build +46 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/plc.c +61 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/plc.h +57 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/sns.c +880 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/sns.h +103 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/spec.c +904 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/spec.h +119 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tables.c +3457 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tables.h +94 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tns.c +457 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tns.h +99 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3.cpp +159 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/_kiss_fft_guts.h +182 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/arch.h +261 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/celt_lpc.c +279 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/celt_lpc.h +59 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/common.h +48 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/compile.sh +3 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/denoise.c +646 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/kiss_fft.c +601 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/kiss_fft.h +203 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/opus_types.h +159 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/pitch.c +526 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/pitch.h +149 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn.c +178 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn.h +69 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_data.c +11051 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_data.h +34 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_reader.c +168 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_train.py +66 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/tansig_table.h +45 -0
- package/android/lc3Lib/src/main/java/com/mentra/lc3Lib/Lc3Cpp.java +38 -0
- package/android/lc3Lib/third_party/CMakeLists.txt +23 -0
- package/android/lc3Lib/third_party/CMakeLists_libogg.txt +18 -0
- package/android/lc3Lib/third_party/CMakeLists_libopus.txt +18 -0
- package/android/lc3Lib/third_party/CMakeLists_opus-tools.txt +23 -0
- package/android/lc3Lib/third_party/opus_tools/src/AUTHORS +5 -0
- package/android/lc3Lib/third_party/opus_tools/src/CMakeLists.txt +4 -0
- package/android/lc3Lib/third_party/opus_tools/src/COPYING +371 -0
- package/android/lc3Lib/third_party/opus_tools/src/ChangeLog +3 -0
- package/android/lc3Lib/third_party/opus_tools/src/Makefile.am +42 -0
- package/android/lc3Lib/third_party/opus_tools/src/Makefile.unix +23 -0
- package/android/lc3Lib/third_party/opus_tools/src/NEWS +0 -0
- package/android/lc3Lib/third_party/opus_tools/src/README +0 -0
- package/android/lc3Lib/third_party/opus_tools/src/autogen.sh +115 -0
- package/android/lc3Lib/third_party/opus_tools/src/configure.ac +293 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/arch.h +239 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/audio-in.c +1046 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/diag_range.c +245 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/diag_range.h +28 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/info_opus.c +320 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/info_opus.h +51 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/lpc.c +157 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/lpc.h +27 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opus_header.c +286 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opus_header.h +59 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusdec.c +884 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusenc.c +1021 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusenc.h +101 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusinfo.c +639 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusinfo.h +51 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/os_support.h +167 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/resample.c +1137 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/speex_resampler.h +344 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/stack_alloc.h +115 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/wav_io.c +125 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/wav_io.h +62 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/wave_out.c +223 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/wave_out.h +60 -0
- package/android/settings.gradle +1 -0
- package/android/src/main/AndroidManifest.xml +40 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +528 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +610 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +1616 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +314 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothModels.kt +1710 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +554 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/ObservableStore.kt +68 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +158 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +934 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +175 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/PhoneMic.kt +1109 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +3974 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +3551 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +1327 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +6959 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1655 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +53675 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +202 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +224 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/STTTools.kt +340 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +465 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/VadGateSpeechPolicy.kt +221 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/AES.java +106 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/AudioSessionMonitor.kt +189 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BitmapJavaUtils.java +169 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +179 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/Constants.kt +44 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/G1Text.kt +436 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/IncidentLogBleRelayNaming.java +29 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/IncidentLogBleUploadService.java +76 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +791 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +186 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +589 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/PhoneAudioMonitor.kt +303 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/SmartGlassesConnectionState.java +9 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/ByteUtilAudioPlayer.java +655 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/Lc3Player.java +441 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/PCMAudioPlayer.java +431 -0
- package/android/src/main/res/values/strings.xml +4 -0
- package/app.plugin.js +1 -0
- package/build/BluetoothSdk.types.d.ts +498 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -0
- package/build/BluetoothSdk.types.js +23 -0
- package/build/BluetoothSdk.types.js.map +1 -0
- package/build/BluetoothSdkModule.d.ts +71 -0
- package/build/BluetoothSdkModule.d.ts.map +1 -0
- package/build/BluetoothSdkModule.js +106 -0
- package/build/BluetoothSdkModule.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +5 -0
- package/build/index.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/BluetoothSdkModule.swift +665 -0
- package/ios/MentraBluetoothSDK.podspec +77 -0
- package/ios/Packages/CoreObjC/CoreObjC.xcodeproj/project.pbxproj +213 -0
- package/ios/Packages/CoreObjC/PcmConverter.h +22 -0
- package/ios/Packages/CoreObjC/PcmConverter.m +266 -0
- package/ios/Packages/CoreObjC/attdet.c +92 -0
- package/ios/Packages/CoreObjC/attdet.h +44 -0
- package/ios/Packages/CoreObjC/bits.c +375 -0
- package/ios/Packages/CoreObjC/bits.h +315 -0
- package/ios/Packages/CoreObjC/bwdet.c +129 -0
- package/ios/Packages/CoreObjC/bwdet.h +69 -0
- package/ios/Packages/CoreObjC/common.h +151 -0
- package/ios/Packages/CoreObjC/energy.c +70 -0
- package/ios/Packages/CoreObjC/energy.h +43 -0
- package/ios/Packages/CoreObjC/fastmath.h +158 -0
- package/ios/Packages/CoreObjC/lc3.c +704 -0
- package/ios/Packages/CoreObjC/lc3.h +313 -0
- package/ios/Packages/CoreObjC/lc3_cpp.h +283 -0
- package/ios/Packages/CoreObjC/lc3_private.h +163 -0
- package/ios/Packages/CoreObjC/ltpf.c +905 -0
- package/ios/Packages/CoreObjC/ltpf.h +111 -0
- package/ios/Packages/CoreObjC/ltpf_arm.h +506 -0
- package/ios/Packages/CoreObjC/ltpf_neon.h +281 -0
- package/ios/Packages/CoreObjC/makefile.mk +35 -0
- package/ios/Packages/CoreObjC/mdct.c +469 -0
- package/ios/Packages/CoreObjC/mdct.h +57 -0
- package/ios/Packages/CoreObjC/mdct_neon.h +296 -0
- package/ios/Packages/CoreObjC/meson.build +61 -0
- package/ios/Packages/CoreObjC/plc.c +61 -0
- package/ios/Packages/CoreObjC/plc.h +57 -0
- package/ios/Packages/CoreObjC/rnnoise.h +114 -0
- package/ios/Packages/CoreObjC/sns.c +880 -0
- package/ios/Packages/CoreObjC/sns.h +103 -0
- package/ios/Packages/CoreObjC/spec.c +907 -0
- package/ios/Packages/CoreObjC/spec.h +119 -0
- package/ios/Packages/CoreObjC/tables.c +3457 -0
- package/ios/Packages/CoreObjC/tables.h +94 -0
- package/ios/Packages/CoreObjC/tns.c +457 -0
- package/ios/Packages/CoreObjC/tns.h +99 -0
- package/ios/Packages/SherpaOnnx/Model/joiner.onnx +0 -0
- package/ios/Packages/SherpaOnnx/Model/tokens.txt +502 -0
- package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +1659 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/cargs.h +162 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/c-api.h +1852 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/cxx-api.h +674 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +44 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/sherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/libsherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/sherpa-onnx.a +0 -0
- package/ios/Packages/VAD/Common/VAD/VADQuality.swift +41 -0
- package/ios/Packages/VAD/Common/VAD/VADState.swift +26 -0
- package/ios/Packages/VAD/Common/VAD/VADStrategy.swift +29 -0
- package/ios/Packages/VAD/Common/VAD/VADType.swift +14 -0
- package/ios/Packages/VAD/Data/Configuration.swift +68 -0
- package/ios/Packages/VAD/Data/FrameSize.swift +39 -0
- package/ios/Packages/VAD/Data/Record.swift +13 -0
- package/ios/Packages/VAD/Data/Result.swift +22 -0
- package/ios/Packages/VAD/Data/SampleRate.swift +48 -0
- package/ios/Packages/VAD/Silero/Model/silero_vad.onnx +0 -0
- package/ios/Packages/VAD/Silero/SileroVAD.swift +284 -0
- package/ios/Packages/VAD/Silero/SileroVADStrategy.swift +64 -0
- package/ios/Packages/libbz2/module.modulemap +5 -0
- package/ios/Packages/libbz2/shim.h +1 -0
- package/ios/Source/Bridge.swift +353 -0
- package/ios/Source/Bridging-Header.h +16 -0
- package/ios/Source/DeviceManager.swift +1478 -0
- package/ios/Source/DeviceStore.swift +295 -0
- package/ios/Source/MentraBluetoothSDK.swift +2439 -0
- package/ios/Source/ObservableStore.swift +88 -0
- package/ios/Source/PrivacyInfo.xcprivacy +23 -0
- package/ios/Source/controllers/ControllerManager.swift +187 -0
- package/ios/Source/controllers/R1.swift +827 -0
- package/ios/Source/services/PhoneMic.swift +662 -0
- package/ios/Source/sgcs/Frame.swift +651 -0
- package/ios/Source/sgcs/G1.swift +2505 -0
- package/ios/Source/sgcs/G2.swift +3730 -0
- package/ios/Source/sgcs/Mach1.swift +543 -0
- package/ios/Source/sgcs/MentraLive.swift +4708 -0
- package/ios/Source/sgcs/MentraNex.swift +2479 -0
- package/ios/Source/sgcs/SGCManager.swift +243 -0
- package/ios/Source/sgcs/Simulated.swift +270 -0
- package/ios/Source/sgcs/mentraos_ble.pb.swift +3811 -0
- package/ios/Source/stt/STTTools.swift +154 -0
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +401 -0
- package/ios/Source/utils/AudioSessionMonitor.swift +283 -0
- package/ios/Source/utils/Constants.swift +74 -0
- package/ios/Source/utils/Enums.swift +95 -0
- package/ios/Source/utils/G1Text.swift +2067 -0
- package/ios/Source/utils/JSCExperiment.swift +241 -0
- package/ios/Source/utils/MemoryMonitor.swift +44 -0
- package/ios/Source/utils/MessageChunker.swift +164 -0
- package/ios/Source/utils/Models.swift +135 -0
- package/ios/Source/utils/PhoneAudioMonitor.swift +226 -0
- package/ios/Source/utils/TarBz2Extractor.swift +206 -0
- package/package.json +78 -0
- package/plugin/build/index.d.ts +6 -0
- package/plugin/build/index.js +12 -0
- package/plugin/build/withAndroid.d.ts +4 -0
- package/plugin/build/withAndroid.js +80 -0
- package/plugin/build/withIos.d.ts +4 -0
- package/plugin/build/withIos.js +64 -0
- package/src/BluetoothSdk.types.ts +581 -0
- package/src/BluetoothSdkModule.ts +259 -0
- package/src/index.ts +4 -0
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
//
|
|
2
|
+
// PhoneMic.swift
|
|
3
|
+
// MentraOS_Manager
|
|
4
|
+
//
|
|
5
|
+
// Created on 3/8/25.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import AVFoundation
|
|
9
|
+
import Combine
|
|
10
|
+
import Foundation
|
|
11
|
+
|
|
12
|
+
@MainActor
|
|
13
|
+
class PhoneMic {
|
|
14
|
+
static let shared = PhoneMic()
|
|
15
|
+
|
|
16
|
+
/// Audio recording components
|
|
17
|
+
private var audioEngine: AVAudioEngine?
|
|
18
|
+
private var audioSession: AVAudioSession?
|
|
19
|
+
|
|
20
|
+
/// Recording state - tracked via boolean to avoid EXC_BAD_ACCESS crash
|
|
21
|
+
/// when AVAudioEngine becomes invalid during audio route changes.
|
|
22
|
+
/// See: MENTRA-OS-14P
|
|
23
|
+
private var _isRecording = false
|
|
24
|
+
var isRecording: Bool {
|
|
25
|
+
_isRecording
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private var currentMicMode: String = ""
|
|
29
|
+
private var cancellables = Set<AnyCancellable>()
|
|
30
|
+
|
|
31
|
+
// MARK: - Initialization
|
|
32
|
+
|
|
33
|
+
init() {
|
|
34
|
+
// Set up audio session notification to handle route changes
|
|
35
|
+
NotificationCenter.default.addObserver(
|
|
36
|
+
self,
|
|
37
|
+
selector: #selector(handleRouteChange),
|
|
38
|
+
name: AVAudioSession.routeChangeNotification,
|
|
39
|
+
object: nil
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
NotificationCenter.default.addObserver(
|
|
43
|
+
self,
|
|
44
|
+
selector: #selector(handleInterruption),
|
|
45
|
+
name: AVAudioSession.interruptionNotification,
|
|
46
|
+
object: nil
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
deinit {
|
|
51
|
+
NotificationCenter.default.removeObserver(self)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// MARK: - Public Methods
|
|
55
|
+
|
|
56
|
+
/// Check (but don't request) microphone permissions
|
|
57
|
+
/// Permissions are requested by React Native UI, not directly by Swift
|
|
58
|
+
func requestPermissions() async -> Bool {
|
|
59
|
+
// Instead of requesting permissions directly, we just check the current status
|
|
60
|
+
// This maintains compatibility with existing code that calls this method
|
|
61
|
+
return checkPermissions()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Check if microphone permissions have been granted
|
|
65
|
+
func checkPermissions() -> Bool {
|
|
66
|
+
return AVAudioSession.sharedInstance().recordPermission == .granted
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// Get a list of available audio input devices
|
|
70
|
+
func getAvailableInputDevices() -> [String: String] {
|
|
71
|
+
var deviceInfo = [String: String]()
|
|
72
|
+
|
|
73
|
+
// Get current route inputs
|
|
74
|
+
let currentRoute = AVAudioSession.sharedInstance().currentRoute
|
|
75
|
+
for input in currentRoute.inputs {
|
|
76
|
+
deviceInfo[input.uid] = input.portName
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Also check available inputs which may include disconnected but paired devices
|
|
80
|
+
if let availableInputs = AVAudioSession.sharedInstance().availableInputs {
|
|
81
|
+
for input in availableInputs {
|
|
82
|
+
deviceInfo[input.uid] = input.portName
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return deviceInfo
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// Manually set AirPods or another specific device as preferred input
|
|
90
|
+
func setPreferredInputDevice(named deviceName: String) -> Bool {
|
|
91
|
+
guard let availableInputs = AVAudioSession.sharedInstance().availableInputs else {
|
|
92
|
+
Bridge.log("No available inputs found")
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Find input containing the specified name (case insensitive)
|
|
97
|
+
guard
|
|
98
|
+
let preferredInput = availableInputs.first(where: {
|
|
99
|
+
$0.portName.range(of: deviceName, options: .caseInsensitive) != nil
|
|
100
|
+
})
|
|
101
|
+
else {
|
|
102
|
+
Bridge.log("No input device found containing name: \(deviceName)")
|
|
103
|
+
return false
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
do {
|
|
107
|
+
try AVAudioSession.sharedInstance().setPreferredInput(preferredInput)
|
|
108
|
+
Bridge.log("Successfully set preferred input to: \(preferredInput.portName)")
|
|
109
|
+
return true
|
|
110
|
+
} catch {
|
|
111
|
+
Bridge.log("Failed to set preferred input: \(error)")
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// Check if currently recording with a specific mode
|
|
117
|
+
func isRecordingWithMode(_ mode: String) -> Bool {
|
|
118
|
+
return isRecording && currentMicMode == mode
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Start recording with a specific microphone mode
|
|
122
|
+
/// - Parameter mode: One of MicTypes constants (PHONE_INTERNAL, BT_CLASSIC, BT)
|
|
123
|
+
/// - Returns: true if successfully started recording, false otherwise
|
|
124
|
+
func startMode(_ mode: String) -> Bool {
|
|
125
|
+
// Check if already recording with this mode
|
|
126
|
+
if isRecordingWithMode(mode) {
|
|
127
|
+
return true
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// If recording with a different mode, stop first
|
|
131
|
+
if isRecording {
|
|
132
|
+
Bridge.log(
|
|
133
|
+
"MIC: Already recording with different mode (\(currentMicMode)), stopping first"
|
|
134
|
+
)
|
|
135
|
+
// stopRecording()
|
|
136
|
+
// Brief delay to ensure clean stop
|
|
137
|
+
// Thread.sleep(forTimeInterval: 0.05)
|
|
138
|
+
return false
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Check permissions
|
|
142
|
+
guard checkPermissions() else {
|
|
143
|
+
Bridge.log("MIC: Microphone permissions not granted")
|
|
144
|
+
return false
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Start recording based on mode
|
|
148
|
+
switch mode {
|
|
149
|
+
case MicTypes.PHONE_INTERNAL:
|
|
150
|
+
Bridge.log("MIC: Starting phone internal mic")
|
|
151
|
+
return startRecordingPhoneInternal()
|
|
152
|
+
|
|
153
|
+
// case MicTypes.BT_CLASSIC:
|
|
154
|
+
// Bridge.log("MIC: Starting Bluetooth Classic (SCO)")
|
|
155
|
+
// guard isBluetoothScoAvailable() else {
|
|
156
|
+
// Bridge.log("MIC: Bluetooth SCO not available")
|
|
157
|
+
// return false
|
|
158
|
+
// }
|
|
159
|
+
// return startRecordingBtClassic()
|
|
160
|
+
|
|
161
|
+
case MicTypes.BT:
|
|
162
|
+
Bridge.log("MIC: Starting high-quality Bluetooth mic")
|
|
163
|
+
guard isHighQualityBluetoothAvailable() else {
|
|
164
|
+
Bridge.log("MIC: High-quality Bluetooth not available")
|
|
165
|
+
return false
|
|
166
|
+
}
|
|
167
|
+
return startRecordingBtHighQuality()
|
|
168
|
+
|
|
169
|
+
default:
|
|
170
|
+
Bridge.log("MIC: Unknown mic type: \(mode)")
|
|
171
|
+
return false
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/// Stop recording if currently recording with specified mode
|
|
176
|
+
func stopMode(_ mode: String) -> Bool {
|
|
177
|
+
if isRecordingWithMode(mode) {
|
|
178
|
+
stopRecording()
|
|
179
|
+
return true
|
|
180
|
+
}
|
|
181
|
+
return false
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// MARK: - Private Mode-Specific Recording Methods
|
|
185
|
+
|
|
186
|
+
private func startRecordingPhoneInternal() -> Bool {
|
|
187
|
+
do {
|
|
188
|
+
let session = AVAudioSession.sharedInstance()
|
|
189
|
+
|
|
190
|
+
// Configure for built-in mic only
|
|
191
|
+
try session.setCategory(
|
|
192
|
+
.playAndRecord,
|
|
193
|
+
mode: .default,
|
|
194
|
+
options: [ /* .allowBluetooth, */ .defaultToSpeaker, .mixWithOthers, .allowBluetoothA2DP]
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
// Override the output to use Bluetooth (AirPods) for speaker
|
|
198
|
+
try session.overrideOutputAudioPort(.none)
|
|
199
|
+
|
|
200
|
+
// Try to set built-in mic as preferred input
|
|
201
|
+
if let availableInputs = session.availableInputs {
|
|
202
|
+
let builtInMic = availableInputs.first { input in
|
|
203
|
+
input.portType == .builtInMic
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if let builtInMic = builtInMic {
|
|
207
|
+
try session.setPreferredInput(builtInMic)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
try session.setActive(true, options: .notifyOthersOnDeactivation)
|
|
212
|
+
|
|
213
|
+
let success = startRecordingInternal()
|
|
214
|
+
if success {
|
|
215
|
+
currentMicMode = MicTypes.PHONE_INTERNAL
|
|
216
|
+
}
|
|
217
|
+
return success
|
|
218
|
+
|
|
219
|
+
} catch {
|
|
220
|
+
Bridge.log("MIC: Phone internal recording failed: \(error)")
|
|
221
|
+
return false
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private func startRecordingBtClassic() -> Bool {
|
|
226
|
+
do {
|
|
227
|
+
let session = AVAudioSession.sharedInstance()
|
|
228
|
+
|
|
229
|
+
// Configure for Bluetooth SCO
|
|
230
|
+
try session.setCategory(
|
|
231
|
+
.playAndRecord,
|
|
232
|
+
mode: .voiceChat,
|
|
233
|
+
options: [.allowBluetooth, .mixWithOthers]
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
// Try to set Bluetooth HFP as preferred input
|
|
237
|
+
if let availableInputs = session.availableInputs {
|
|
238
|
+
let bluetoothHFP = availableInputs.first { input in
|
|
239
|
+
input.portType == .bluetoothHFP
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if let bluetoothHFP = bluetoothHFP {
|
|
243
|
+
try session.setPreferredInput(bluetoothHFP)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
try session.setActive(true, options: .notifyOthersOnDeactivation)
|
|
248
|
+
|
|
249
|
+
let success = startRecordingInternal()
|
|
250
|
+
if success {
|
|
251
|
+
currentMicMode = MicTypes.BT_CLASSIC
|
|
252
|
+
}
|
|
253
|
+
return success
|
|
254
|
+
|
|
255
|
+
} catch {
|
|
256
|
+
Bridge.log("MIC: BT Classic recording failed: \(error)")
|
|
257
|
+
return false
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private func startRecordingBtHighQuality() -> Bool {
|
|
262
|
+
do {
|
|
263
|
+
let session = AVAudioSession.sharedInstance()
|
|
264
|
+
|
|
265
|
+
// Configure for high-quality Bluetooth audio (A2DP-like)
|
|
266
|
+
try session.setCategory(
|
|
267
|
+
.playAndRecord,
|
|
268
|
+
mode: .default,
|
|
269
|
+
options: [.allowBluetooth, .allowBluetoothA2DP, .mixWithOthers]
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
// Try to set Bluetooth A2DP as preferred input
|
|
273
|
+
if let availableInputs = session.availableInputs {
|
|
274
|
+
let bluetoothA2DP = availableInputs.first { input in
|
|
275
|
+
input.portType == .bluetoothA2DP || input.portType == .bluetoothLE
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if let bluetoothA2DP = bluetoothA2DP {
|
|
279
|
+
try session.setPreferredInput(bluetoothA2DP)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
try session.setActive(true, options: .notifyOthersOnDeactivation)
|
|
284
|
+
|
|
285
|
+
let success = startRecordingInternal()
|
|
286
|
+
if success {
|
|
287
|
+
currentMicMode = MicTypes.BT
|
|
288
|
+
}
|
|
289
|
+
return success
|
|
290
|
+
|
|
291
|
+
} catch {
|
|
292
|
+
Bridge.log("MIC: BT high-quality recording failed: \(error)")
|
|
293
|
+
return false
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// MARK: - Bluetooth Availability Checks
|
|
298
|
+
|
|
299
|
+
private func isBluetoothScoAvailable() -> Bool {
|
|
300
|
+
guard let availableInputs = AVAudioSession.sharedInstance().availableInputs else {
|
|
301
|
+
return false
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return availableInputs.contains { input in
|
|
305
|
+
input.portType == .bluetoothHFP
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private func isHighQualityBluetoothAvailable() -> Bool {
|
|
310
|
+
guard let availableInputs = AVAudioSession.sharedInstance().availableInputs else {
|
|
311
|
+
return false
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return availableInputs.contains { input in
|
|
315
|
+
input.portType == .bluetoothA2DP || input.portType == .bluetoothLE
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
@objc private func handleInterruption(notification: Notification) {
|
|
320
|
+
guard let userInfo = notification.userInfo,
|
|
321
|
+
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
|
|
322
|
+
let type = AVAudioSession.InterruptionType(rawValue: typeValue)
|
|
323
|
+
else {
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
switch type {
|
|
328
|
+
case .began:
|
|
329
|
+
Bridge.log("Audio session interrupted - another app took control")
|
|
330
|
+
// Phone call started - the system has stopped our audio engine.
|
|
331
|
+
// Reset _isRecording so we can restart when interruption ends.
|
|
332
|
+
if _isRecording {
|
|
333
|
+
_isRecording = false
|
|
334
|
+
currentMicMode = ""
|
|
335
|
+
DeviceManager.shared.onInterruption(began: true)
|
|
336
|
+
}
|
|
337
|
+
case .ended:
|
|
338
|
+
Bridge.log("Audio session interruption ended")
|
|
339
|
+
if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
|
|
340
|
+
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
|
|
341
|
+
if options.contains(.shouldResume) {
|
|
342
|
+
DeviceManager.shared.onInterruption(began: false)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
@unknown default:
|
|
346
|
+
break
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/// Handle audio route changes (e.g. when connecting/disconnecting AirPods)
|
|
351
|
+
@objc private func handleRouteChange(notification: Notification) {
|
|
352
|
+
guard let userInfo = notification.userInfo,
|
|
353
|
+
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
|
|
354
|
+
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue)
|
|
355
|
+
else {
|
|
356
|
+
return
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
Bridge.log("MIC: handleRouteChange: \(reason)")
|
|
360
|
+
DeviceManager.shared.onRouteChange(
|
|
361
|
+
reason: reason, availableInputs: audioSession?.availableInputs ?? []
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
// // If we're recording and the audio route changed (e.g., AirPods connected/disconnected)
|
|
365
|
+
// if isRecording {
|
|
366
|
+
// switch reason {
|
|
367
|
+
// case .newDeviceAvailable, .oldDeviceUnavailable:
|
|
368
|
+
// // Restart recording to use the new input device
|
|
369
|
+
// stopRecording()
|
|
370
|
+
// _ = startRecording()
|
|
371
|
+
// default:
|
|
372
|
+
// break
|
|
373
|
+
// }
|
|
374
|
+
// }
|
|
375
|
+
|
|
376
|
+
// Log the current audio route
|
|
377
|
+
logCurrentAudioRoute()
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/// Log the current audio input/output route for debugging
|
|
381
|
+
private func logCurrentAudioRoute() {
|
|
382
|
+
let currentRoute = AVAudioSession.sharedInstance().currentRoute
|
|
383
|
+
var routeDescription = "Current audio route:\n"
|
|
384
|
+
|
|
385
|
+
// Log inputs
|
|
386
|
+
if currentRoute.inputs.isEmpty {
|
|
387
|
+
routeDescription += "- No input ports\n"
|
|
388
|
+
} else {
|
|
389
|
+
for (index, port) in currentRoute.inputs.enumerated() {
|
|
390
|
+
routeDescription +=
|
|
391
|
+
"- Input \(index + 1): \(port.portName) (type: \(port.portType.rawValue))\n"
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Log outputs
|
|
396
|
+
if currentRoute.outputs.isEmpty {
|
|
397
|
+
routeDescription += "- No output ports"
|
|
398
|
+
} else {
|
|
399
|
+
for (index, port) in currentRoute.outputs.enumerated() {
|
|
400
|
+
routeDescription +=
|
|
401
|
+
"- Output \(index + 1): \(port.portName) (type: \(port.portType.rawValue))"
|
|
402
|
+
if index < currentRoute.outputs.count - 1 {
|
|
403
|
+
routeDescription += "\n"
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// CoreCommsService.log(routeDescription)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// MARK: - Private Helpers
|
|
412
|
+
|
|
413
|
+
/// Extract Int16 data from a converted buffer
|
|
414
|
+
private func extractInt16Data(from buffer: AVAudioPCMBuffer) -> Data {
|
|
415
|
+
let channelCount = Int(buffer.format.channelCount)
|
|
416
|
+
let frameCount = Int(buffer.frameLength)
|
|
417
|
+
let data = NSMutableData()
|
|
418
|
+
|
|
419
|
+
// Safely get int16 data (won't be nil if buffer is in Int16 format)
|
|
420
|
+
guard let int16Data = buffer.int16ChannelData else {
|
|
421
|
+
Bridge.log("Error: Buffer does not contain int16 data")
|
|
422
|
+
return Data()
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
let channels = UnsafeBufferPointer(start: int16Data, count: channelCount)
|
|
426
|
+
|
|
427
|
+
// Extract each sample
|
|
428
|
+
for frame in 0 ..< frameCount {
|
|
429
|
+
for channel in 0 ..< channelCount {
|
|
430
|
+
var sample = channels[channel][frame]
|
|
431
|
+
data.append(&sample, length: 2)
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return data as Data
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/// Start recording from the available microphone (built-in, Bluetooth, AirPods, etc.)
|
|
439
|
+
func startRecording() -> Bool {
|
|
440
|
+
// Ensure we're not already recording
|
|
441
|
+
if isRecording {
|
|
442
|
+
// Core.log("MIC: Microphone is already ON!")
|
|
443
|
+
return true
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Clean up any existing engine (shouldn't happen if _isRecording is accurate, but be safe)
|
|
447
|
+
if let existingEngine = audioEngine {
|
|
448
|
+
_isRecording = false
|
|
449
|
+
existingEngine.stop()
|
|
450
|
+
audioEngine = nil
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Check permissions first
|
|
454
|
+
guard checkPermissions() else {
|
|
455
|
+
Bridge.log("MIC: Microphone permissions not granted")
|
|
456
|
+
return false
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Set up audio session BEFORE creating the engine
|
|
460
|
+
audioSession = AVAudioSession.sharedInstance()
|
|
461
|
+
do {
|
|
462
|
+
try audioSession?.setCategory(
|
|
463
|
+
.playAndRecord,
|
|
464
|
+
mode: .default,
|
|
465
|
+
options: [.allowBluetooth, .defaultToSpeaker, .mixWithOthers]
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
// Set preferred input if available
|
|
469
|
+
if let availableInputs = audioSession?.availableInputs, !availableInputs.isEmpty {
|
|
470
|
+
let preferredInput =
|
|
471
|
+
availableInputs.first { input in
|
|
472
|
+
input.portType == .bluetoothHFP || input.portType == .bluetoothA2DP
|
|
473
|
+
} ?? availableInputs.first
|
|
474
|
+
|
|
475
|
+
try audioSession?.setPreferredInput(preferredInput)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Activate the session BEFORE creating the engine
|
|
479
|
+
try audioSession?.setActive(true, options: .notifyOthersOnDeactivation)
|
|
480
|
+
} catch {
|
|
481
|
+
Bridge.log("MIC: Failed to set up audio session: \(error)")
|
|
482
|
+
return false
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return startRecordingInternal()
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/// Internal recording logic shared by all recording modes
|
|
489
|
+
private func startRecordingInternal() -> Bool {
|
|
490
|
+
// check if we're in the background:
|
|
491
|
+
let appState = UIApplication.shared.applicationState
|
|
492
|
+
if appState == .background {
|
|
493
|
+
Bridge.log("MIC: App is in background, cannot start recording")
|
|
494
|
+
return false
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if let existingEngine = audioEngine {
|
|
498
|
+
_isRecording = false
|
|
499
|
+
existingEngine.inputNode.removeTap(onBus: 0)
|
|
500
|
+
existingEngine.stop()
|
|
501
|
+
audioEngine = nil
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
let session = AVAudioSession.sharedInstance()
|
|
505
|
+
guard let availableInputs = session.availableInputs, !availableInputs.isEmpty else {
|
|
506
|
+
Bridge.log("MIC: No audio inputs available, cannot start recording")
|
|
507
|
+
return false
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// NOW create the audio engine:
|
|
511
|
+
audioEngine = AVAudioEngine()
|
|
512
|
+
|
|
513
|
+
// Safely get the input node
|
|
514
|
+
guard let engine = audioEngine else {
|
|
515
|
+
Bridge.log("MIC: Failed to create audio engine")
|
|
516
|
+
return false
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// The engine must have an input node, but let's be safe
|
|
520
|
+
let inputNode = engine.inputNode
|
|
521
|
+
|
|
522
|
+
// Verify the node is valid before accessing its properties
|
|
523
|
+
guard inputNode.engine != nil else {
|
|
524
|
+
Bridge.log("MIC: Input node is not properly attached to engine")
|
|
525
|
+
audioEngine = nil
|
|
526
|
+
return false
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Check if the node has inputs available
|
|
530
|
+
guard inputNode.numberOfInputs > 0 else {
|
|
531
|
+
Bridge.log("MIC: Input node has no available inputs")
|
|
532
|
+
audioEngine = nil
|
|
533
|
+
return false
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Get the native input format - typically 48kHz floating point samples
|
|
537
|
+
// let inputFormat = inputNode.inputFormat(forBus: 0)
|
|
538
|
+
let inputFormat = inputNode.outputFormat(forBus: 0)
|
|
539
|
+
Bridge.log("MIC: Input format: \(inputFormat)")
|
|
540
|
+
|
|
541
|
+
// Set up a converter node if you need 16-bit PCM
|
|
542
|
+
let converter = AVAudioConverter(
|
|
543
|
+
from: inputFormat,
|
|
544
|
+
to: AVAudioFormat(
|
|
545
|
+
commonFormat: .pcmFormatInt16,
|
|
546
|
+
sampleRate: 16000,
|
|
547
|
+
channels: 1,
|
|
548
|
+
interleaved: false
|
|
549
|
+
)!
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
guard let converter = converter else {
|
|
553
|
+
Bridge.log("MIC: converter is nil")
|
|
554
|
+
// audioEngine = nil
|
|
555
|
+
return false
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Remove any existing tap before installing new one (prevents crash if tap
|
|
559
|
+
// already exists from previous engine). This is safe even if no tap exists.
|
|
560
|
+
// See: MENTRA-OS-YM, MENTRA-OS-137
|
|
561
|
+
inputNode.removeTap(onBus: 0)
|
|
562
|
+
|
|
563
|
+
// Buffer size of 1024 at 48kHz = ~21ms of audio
|
|
564
|
+
// After downsampling to 16kHz = ~341 samples = ~2 LC3 frames (160 samples each)
|
|
565
|
+
// Larger buffers reduce callback frequency and timing jitter which can cause audio blips
|
|
566
|
+
inputNode.installTap(onBus: 0, bufferSize: 1024, format: nil) {
|
|
567
|
+
[weak self] buffer, _ in
|
|
568
|
+
guard let self = self else { return }
|
|
569
|
+
|
|
570
|
+
let frameCount = Int(buffer.frameLength)
|
|
571
|
+
|
|
572
|
+
// Calculate the correct output buffer capacity based on sample rate conversion
|
|
573
|
+
// For downsampling from inputFormat.sampleRate to 16000 Hz
|
|
574
|
+
let outputCapacity = AVAudioFrameCount(
|
|
575
|
+
Double(frameCount) * (16000.0 / inputFormat.sampleRate)
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
// Create a 16-bit PCM data buffer with adjusted capacity
|
|
579
|
+
let convertedBuffer = AVAudioPCMBuffer(
|
|
580
|
+
pcmFormat: converter.outputFormat,
|
|
581
|
+
frameCapacity: outputCapacity
|
|
582
|
+
)!
|
|
583
|
+
|
|
584
|
+
var error: NSError? = nil
|
|
585
|
+
let status = converter.convert(
|
|
586
|
+
to: convertedBuffer,
|
|
587
|
+
error: &error,
|
|
588
|
+
withInputFrom: { _, outStatus in
|
|
589
|
+
outStatus.pointee = .haveData
|
|
590
|
+
return buffer
|
|
591
|
+
}
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
guard status == .haveData && error == nil else {
|
|
595
|
+
Bridge.log(
|
|
596
|
+
"MIC: Error converting audio buffer: \(error?.localizedDescription ?? "unknown")"
|
|
597
|
+
)
|
|
598
|
+
return
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
let pcmData = self.extractInt16Data(from: convertedBuffer)
|
|
602
|
+
DeviceManager.shared.handlePcm(pcmData)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Start the audio engine
|
|
606
|
+
do {
|
|
607
|
+
try audioEngine?.start()
|
|
608
|
+
_isRecording = true
|
|
609
|
+
Bridge.log("MIC: Started recording from: \(getActiveInputDevice() ?? "Unknown device")")
|
|
610
|
+
return true
|
|
611
|
+
} catch {
|
|
612
|
+
Bridge.log("MIC: Failed to start audio engine: \(error)")
|
|
613
|
+
return false
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/// Get the currently active input device name
|
|
618
|
+
func getActiveInputDevice() -> String? {
|
|
619
|
+
let currentRoute = AVAudioSession.sharedInstance().currentRoute
|
|
620
|
+
return currentRoute.inputs.first?.portName
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/// Stop recording from the microphone
|
|
624
|
+
func stopRecording() {
|
|
625
|
+
guard _isRecording else {
|
|
626
|
+
return
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Set state FIRST before touching engine to prevent crash if
|
|
630
|
+
// route change triggers isRecording check mid-cleanup
|
|
631
|
+
_isRecording = false
|
|
632
|
+
currentMicMode = ""
|
|
633
|
+
|
|
634
|
+
// Capture engine reference before cleanup to avoid race conditions
|
|
635
|
+
let engine = audioEngine
|
|
636
|
+
audioEngine = nil
|
|
637
|
+
|
|
638
|
+
// Remove the tap and stop the engine safely
|
|
639
|
+
// Accessing inputNode can crash if engine is in an invalid state (e.g., during
|
|
640
|
+
// audio route changes), so we guard against this. See: MENTRA-OS-17X
|
|
641
|
+
if let engine = engine {
|
|
642
|
+
// Only access inputNode if engine is still attached
|
|
643
|
+
if engine.inputNode.engine != nil {
|
|
644
|
+
engine.inputNode.removeTap(onBus: 0)
|
|
645
|
+
}
|
|
646
|
+
engine.stop()
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Clean up
|
|
650
|
+
try? audioSession?.setActive(false)
|
|
651
|
+
audioSession = nil
|
|
652
|
+
|
|
653
|
+
Bridge.log("MIC: Stopped recording")
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// MARK: - Cleanup
|
|
657
|
+
|
|
658
|
+
func cleanup() {
|
|
659
|
+
NotificationCenter.default.removeObserver(self)
|
|
660
|
+
stopRecording()
|
|
661
|
+
}
|
|
662
|
+
}
|