@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.
Files changed (278) hide show
  1. package/README.md +278 -0
  2. package/android/build.gradle +167 -0
  3. package/android/lc3Lib/CMakeLists.txt +6 -0
  4. package/android/lc3Lib/build.gradle +109 -0
  5. package/android/lc3Lib/proguard-rules.pro +33 -0
  6. package/android/lc3Lib/src/main/AndroidManifest.xml +2 -0
  7. package/android/lc3Lib/src/main/cpp/CMakeLists.txt +8 -0
  8. package/android/lc3Lib/src/main/cpp/google_opus_stuff/CMakeLists.txt +30 -0
  9. package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/CMakeLists.txt +7 -0
  10. package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/ogg_opus_encoder.cc +96 -0
  11. package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/ogg_opus_encoder.h +53 -0
  12. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/Makefile.am +6 -0
  13. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/config_types.h +26 -0
  14. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/config_types.h.in +26 -0
  15. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/ogg.h +209 -0
  16. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/os_types.h +158 -0
  17. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus.h +981 -0
  18. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_custom.h +342 -0
  19. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_defines.h +799 -0
  20. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_multistream.h +660 -0
  21. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_projection.h +568 -0
  22. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_types.h +166 -0
  23. package/android/lc3Lib/src/main/cpp/google_opus_stuff/ogg_opus_encoder.cc +268 -0
  24. package/android/lc3Lib/src/main/cpp/google_opus_stuff/ogg_opus_encoder.h +115 -0
  25. package/android/lc3Lib/src/main/cpp/google_opus_stuff/opus_tools/opus_header.h +59 -0
  26. package/android/lc3Lib/src/main/cpp/liblc3/CMakeLists.txt +38 -0
  27. package/android/lc3Lib/src/main/cpp/liblc3/include/lc3.h +309 -0
  28. package/android/lc3Lib/src/main/cpp/liblc3/include/lc3_private.h +162 -0
  29. package/android/lc3Lib/src/main/cpp/liblc3/include/rnnoise.h +114 -0
  30. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/CMakeLists.txt +19 -0
  31. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/attdet.c +92 -0
  32. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/attdet.h +44 -0
  33. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bits.c +375 -0
  34. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bits.h +315 -0
  35. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bwdet.c +129 -0
  36. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bwdet.h +69 -0
  37. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/common.h +148 -0
  38. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/energy.c +70 -0
  39. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/energy.h +43 -0
  40. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/fastmath.h +158 -0
  41. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/lc3.c +702 -0
  42. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf.c +893 -0
  43. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf.h +111 -0
  44. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf_arm.h +506 -0
  45. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf_neon.h +281 -0
  46. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/makefile.mk +35 -0
  47. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct.c +452 -0
  48. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct.h +57 -0
  49. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct_neon.h +296 -0
  50. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/meson.build +46 -0
  51. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/plc.c +61 -0
  52. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/plc.h +57 -0
  53. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/sns.c +880 -0
  54. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/sns.h +103 -0
  55. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/spec.c +904 -0
  56. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/spec.h +119 -0
  57. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tables.c +3457 -0
  58. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tables.h +94 -0
  59. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tns.c +457 -0
  60. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tns.h +99 -0
  61. package/android/lc3Lib/src/main/cpp/liblc3/liblc3.cpp +159 -0
  62. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/_kiss_fft_guts.h +182 -0
  63. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/arch.h +261 -0
  64. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/celt_lpc.c +279 -0
  65. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/celt_lpc.h +59 -0
  66. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/common.h +48 -0
  67. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/compile.sh +3 -0
  68. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/denoise.c +646 -0
  69. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/kiss_fft.c +601 -0
  70. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/kiss_fft.h +203 -0
  71. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/opus_types.h +159 -0
  72. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/pitch.c +526 -0
  73. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/pitch.h +149 -0
  74. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn.c +178 -0
  75. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn.h +69 -0
  76. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_data.c +11051 -0
  77. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_data.h +34 -0
  78. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_reader.c +168 -0
  79. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_train.py +66 -0
  80. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/tansig_table.h +45 -0
  81. package/android/lc3Lib/src/main/java/com/mentra/lc3Lib/Lc3Cpp.java +38 -0
  82. package/android/lc3Lib/third_party/CMakeLists.txt +23 -0
  83. package/android/lc3Lib/third_party/CMakeLists_libogg.txt +18 -0
  84. package/android/lc3Lib/third_party/CMakeLists_libopus.txt +18 -0
  85. package/android/lc3Lib/third_party/CMakeLists_opus-tools.txt +23 -0
  86. package/android/lc3Lib/third_party/opus_tools/src/AUTHORS +5 -0
  87. package/android/lc3Lib/third_party/opus_tools/src/CMakeLists.txt +4 -0
  88. package/android/lc3Lib/third_party/opus_tools/src/COPYING +371 -0
  89. package/android/lc3Lib/third_party/opus_tools/src/ChangeLog +3 -0
  90. package/android/lc3Lib/third_party/opus_tools/src/Makefile.am +42 -0
  91. package/android/lc3Lib/third_party/opus_tools/src/Makefile.unix +23 -0
  92. package/android/lc3Lib/third_party/opus_tools/src/NEWS +0 -0
  93. package/android/lc3Lib/third_party/opus_tools/src/README +0 -0
  94. package/android/lc3Lib/third_party/opus_tools/src/autogen.sh +115 -0
  95. package/android/lc3Lib/third_party/opus_tools/src/configure.ac +293 -0
  96. package/android/lc3Lib/third_party/opus_tools/src/src/arch.h +239 -0
  97. package/android/lc3Lib/third_party/opus_tools/src/src/audio-in.c +1046 -0
  98. package/android/lc3Lib/third_party/opus_tools/src/src/diag_range.c +245 -0
  99. package/android/lc3Lib/third_party/opus_tools/src/src/diag_range.h +28 -0
  100. package/android/lc3Lib/third_party/opus_tools/src/src/info_opus.c +320 -0
  101. package/android/lc3Lib/third_party/opus_tools/src/src/info_opus.h +51 -0
  102. package/android/lc3Lib/third_party/opus_tools/src/src/lpc.c +157 -0
  103. package/android/lc3Lib/third_party/opus_tools/src/src/lpc.h +27 -0
  104. package/android/lc3Lib/third_party/opus_tools/src/src/opus_header.c +286 -0
  105. package/android/lc3Lib/third_party/opus_tools/src/src/opus_header.h +59 -0
  106. package/android/lc3Lib/third_party/opus_tools/src/src/opusdec.c +884 -0
  107. package/android/lc3Lib/third_party/opus_tools/src/src/opusenc.c +1021 -0
  108. package/android/lc3Lib/third_party/opus_tools/src/src/opusenc.h +101 -0
  109. package/android/lc3Lib/third_party/opus_tools/src/src/opusinfo.c +639 -0
  110. package/android/lc3Lib/third_party/opus_tools/src/src/opusinfo.h +51 -0
  111. package/android/lc3Lib/third_party/opus_tools/src/src/os_support.h +167 -0
  112. package/android/lc3Lib/third_party/opus_tools/src/src/resample.c +1137 -0
  113. package/android/lc3Lib/third_party/opus_tools/src/src/speex_resampler.h +344 -0
  114. package/android/lc3Lib/third_party/opus_tools/src/src/stack_alloc.h +115 -0
  115. package/android/lc3Lib/third_party/opus_tools/src/src/wav_io.c +125 -0
  116. package/android/lc3Lib/third_party/opus_tools/src/src/wav_io.h +62 -0
  117. package/android/lc3Lib/third_party/opus_tools/src/src/wave_out.c +223 -0
  118. package/android/lc3Lib/third_party/opus_tools/src/src/wave_out.h +60 -0
  119. package/android/settings.gradle +1 -0
  120. package/android/src/main/AndroidManifest.xml +40 -0
  121. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +528 -0
  122. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +610 -0
  123. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +1616 -0
  124. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +314 -0
  125. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothModels.kt +1710 -0
  126. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +554 -0
  127. package/android/src/main/java/com/mentra/bluetoothsdk/ObservableStore.kt +68 -0
  128. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +158 -0
  129. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +934 -0
  130. package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +175 -0
  131. package/android/src/main/java/com/mentra/bluetoothsdk/services/PhoneMic.kt +1109 -0
  132. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +3974 -0
  133. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +3551 -0
  134. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +1327 -0
  135. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +6959 -0
  136. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1655 -0
  137. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +53675 -0
  138. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +202 -0
  139. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +224 -0
  140. package/android/src/main/java/com/mentra/bluetoothsdk/stt/STTTools.kt +340 -0
  141. package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +465 -0
  142. package/android/src/main/java/com/mentra/bluetoothsdk/stt/VadGateSpeechPolicy.kt +221 -0
  143. package/android/src/main/java/com/mentra/bluetoothsdk/utils/AES.java +106 -0
  144. package/android/src/main/java/com/mentra/bluetoothsdk/utils/AudioSessionMonitor.kt +189 -0
  145. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BitmapJavaUtils.java +169 -0
  146. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +179 -0
  147. package/android/src/main/java/com/mentra/bluetoothsdk/utils/Constants.kt +44 -0
  148. package/android/src/main/java/com/mentra/bluetoothsdk/utils/G1Text.kt +436 -0
  149. package/android/src/main/java/com/mentra/bluetoothsdk/utils/IncidentLogBleRelayNaming.java +29 -0
  150. package/android/src/main/java/com/mentra/bluetoothsdk/utils/IncidentLogBleUploadService.java +76 -0
  151. package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +791 -0
  152. package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +186 -0
  153. package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +589 -0
  154. package/android/src/main/java/com/mentra/bluetoothsdk/utils/PhoneAudioMonitor.kt +303 -0
  155. package/android/src/main/java/com/mentra/bluetoothsdk/utils/SmartGlassesConnectionState.java +9 -0
  156. package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/ByteUtilAudioPlayer.java +655 -0
  157. package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/Lc3Player.java +441 -0
  158. package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/PCMAudioPlayer.java +431 -0
  159. package/android/src/main/res/values/strings.xml +4 -0
  160. package/app.plugin.js +1 -0
  161. package/build/BluetoothSdk.types.d.ts +498 -0
  162. package/build/BluetoothSdk.types.d.ts.map +1 -0
  163. package/build/BluetoothSdk.types.js +23 -0
  164. package/build/BluetoothSdk.types.js.map +1 -0
  165. package/build/BluetoothSdkModule.d.ts +71 -0
  166. package/build/BluetoothSdkModule.d.ts.map +1 -0
  167. package/build/BluetoothSdkModule.js +106 -0
  168. package/build/BluetoothSdkModule.js.map +1 -0
  169. package/build/index.d.ts +3 -0
  170. package/build/index.d.ts.map +1 -0
  171. package/build/index.js +5 -0
  172. package/build/index.js.map +1 -0
  173. package/expo-module.config.json +9 -0
  174. package/ios/BluetoothSdkModule.swift +665 -0
  175. package/ios/MentraBluetoothSDK.podspec +77 -0
  176. package/ios/Packages/CoreObjC/CoreObjC.xcodeproj/project.pbxproj +213 -0
  177. package/ios/Packages/CoreObjC/PcmConverter.h +22 -0
  178. package/ios/Packages/CoreObjC/PcmConverter.m +266 -0
  179. package/ios/Packages/CoreObjC/attdet.c +92 -0
  180. package/ios/Packages/CoreObjC/attdet.h +44 -0
  181. package/ios/Packages/CoreObjC/bits.c +375 -0
  182. package/ios/Packages/CoreObjC/bits.h +315 -0
  183. package/ios/Packages/CoreObjC/bwdet.c +129 -0
  184. package/ios/Packages/CoreObjC/bwdet.h +69 -0
  185. package/ios/Packages/CoreObjC/common.h +151 -0
  186. package/ios/Packages/CoreObjC/energy.c +70 -0
  187. package/ios/Packages/CoreObjC/energy.h +43 -0
  188. package/ios/Packages/CoreObjC/fastmath.h +158 -0
  189. package/ios/Packages/CoreObjC/lc3.c +704 -0
  190. package/ios/Packages/CoreObjC/lc3.h +313 -0
  191. package/ios/Packages/CoreObjC/lc3_cpp.h +283 -0
  192. package/ios/Packages/CoreObjC/lc3_private.h +163 -0
  193. package/ios/Packages/CoreObjC/ltpf.c +905 -0
  194. package/ios/Packages/CoreObjC/ltpf.h +111 -0
  195. package/ios/Packages/CoreObjC/ltpf_arm.h +506 -0
  196. package/ios/Packages/CoreObjC/ltpf_neon.h +281 -0
  197. package/ios/Packages/CoreObjC/makefile.mk +35 -0
  198. package/ios/Packages/CoreObjC/mdct.c +469 -0
  199. package/ios/Packages/CoreObjC/mdct.h +57 -0
  200. package/ios/Packages/CoreObjC/mdct_neon.h +296 -0
  201. package/ios/Packages/CoreObjC/meson.build +61 -0
  202. package/ios/Packages/CoreObjC/plc.c +61 -0
  203. package/ios/Packages/CoreObjC/plc.h +57 -0
  204. package/ios/Packages/CoreObjC/rnnoise.h +114 -0
  205. package/ios/Packages/CoreObjC/sns.c +880 -0
  206. package/ios/Packages/CoreObjC/sns.h +103 -0
  207. package/ios/Packages/CoreObjC/spec.c +907 -0
  208. package/ios/Packages/CoreObjC/spec.h +119 -0
  209. package/ios/Packages/CoreObjC/tables.c +3457 -0
  210. package/ios/Packages/CoreObjC/tables.h +94 -0
  211. package/ios/Packages/CoreObjC/tns.c +457 -0
  212. package/ios/Packages/CoreObjC/tns.h +99 -0
  213. package/ios/Packages/SherpaOnnx/Model/joiner.onnx +0 -0
  214. package/ios/Packages/SherpaOnnx/Model/tokens.txt +502 -0
  215. package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +1659 -0
  216. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/cargs.h +162 -0
  217. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/c-api.h +1852 -0
  218. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/cxx-api.h +674 -0
  219. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +44 -0
  220. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
  221. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/sherpa-onnx.a +0 -0
  222. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/libsherpa-onnx.a +0 -0
  223. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/sherpa-onnx.a +0 -0
  224. package/ios/Packages/VAD/Common/VAD/VADQuality.swift +41 -0
  225. package/ios/Packages/VAD/Common/VAD/VADState.swift +26 -0
  226. package/ios/Packages/VAD/Common/VAD/VADStrategy.swift +29 -0
  227. package/ios/Packages/VAD/Common/VAD/VADType.swift +14 -0
  228. package/ios/Packages/VAD/Data/Configuration.swift +68 -0
  229. package/ios/Packages/VAD/Data/FrameSize.swift +39 -0
  230. package/ios/Packages/VAD/Data/Record.swift +13 -0
  231. package/ios/Packages/VAD/Data/Result.swift +22 -0
  232. package/ios/Packages/VAD/Data/SampleRate.swift +48 -0
  233. package/ios/Packages/VAD/Silero/Model/silero_vad.onnx +0 -0
  234. package/ios/Packages/VAD/Silero/SileroVAD.swift +284 -0
  235. package/ios/Packages/VAD/Silero/SileroVADStrategy.swift +64 -0
  236. package/ios/Packages/libbz2/module.modulemap +5 -0
  237. package/ios/Packages/libbz2/shim.h +1 -0
  238. package/ios/Source/Bridge.swift +353 -0
  239. package/ios/Source/Bridging-Header.h +16 -0
  240. package/ios/Source/DeviceManager.swift +1478 -0
  241. package/ios/Source/DeviceStore.swift +295 -0
  242. package/ios/Source/MentraBluetoothSDK.swift +2439 -0
  243. package/ios/Source/ObservableStore.swift +88 -0
  244. package/ios/Source/PrivacyInfo.xcprivacy +23 -0
  245. package/ios/Source/controllers/ControllerManager.swift +187 -0
  246. package/ios/Source/controllers/R1.swift +827 -0
  247. package/ios/Source/services/PhoneMic.swift +662 -0
  248. package/ios/Source/sgcs/Frame.swift +651 -0
  249. package/ios/Source/sgcs/G1.swift +2505 -0
  250. package/ios/Source/sgcs/G2.swift +3730 -0
  251. package/ios/Source/sgcs/Mach1.swift +543 -0
  252. package/ios/Source/sgcs/MentraLive.swift +4708 -0
  253. package/ios/Source/sgcs/MentraNex.swift +2479 -0
  254. package/ios/Source/sgcs/SGCManager.swift +243 -0
  255. package/ios/Source/sgcs/Simulated.swift +270 -0
  256. package/ios/Source/sgcs/mentraos_ble.pb.swift +3811 -0
  257. package/ios/Source/stt/STTTools.swift +154 -0
  258. package/ios/Source/stt/SherpaOnnxTranscriber.swift +401 -0
  259. package/ios/Source/utils/AudioSessionMonitor.swift +283 -0
  260. package/ios/Source/utils/Constants.swift +74 -0
  261. package/ios/Source/utils/Enums.swift +95 -0
  262. package/ios/Source/utils/G1Text.swift +2067 -0
  263. package/ios/Source/utils/JSCExperiment.swift +241 -0
  264. package/ios/Source/utils/MemoryMonitor.swift +44 -0
  265. package/ios/Source/utils/MessageChunker.swift +164 -0
  266. package/ios/Source/utils/Models.swift +135 -0
  267. package/ios/Source/utils/PhoneAudioMonitor.swift +226 -0
  268. package/ios/Source/utils/TarBz2Extractor.swift +206 -0
  269. package/package.json +78 -0
  270. package/plugin/build/index.d.ts +6 -0
  271. package/plugin/build/index.js +12 -0
  272. package/plugin/build/withAndroid.d.ts +4 -0
  273. package/plugin/build/withAndroid.js +80 -0
  274. package/plugin/build/withIos.d.ts +4 -0
  275. package/plugin/build/withIos.js +64 -0
  276. package/src/BluetoothSdk.types.ts +581 -0
  277. package/src/BluetoothSdkModule.ts +259 -0
  278. 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
+ }