diff --git a/README.md b/README.md index 93ef3b5cc72..0f37f39ead9 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,15 @@ # Friend: Open-Source AI Wearable with 24h+ on single charge -| Assembled | Disassembled | -| :-------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------: | -| | | - + + + + + + + + + +
AssembledDisassembled
AssembledDisassembled
[![Discord Follow](https://dcbadge.vercel.app/api/server/kEXXsnb5b3?style=flat)](https://discord.gg/kEXXsnb5b3)   [![License: GPLv3](https://img.shields.io/badge/license-GPLv3-blue)](https://opensource.org/license/agpl-v3) @@ -67,7 +73,7 @@ There are 2 different apps in these repositories located in different branches a ## Getting Started -Follow these steps to get started with your Friend. Note, we tested everything on a mac + iphone. Currently trying to make it work for Android +Follow these steps to get started with your Friend. ### Install the app @@ -80,7 +86,7 @@ Follow these steps to get started with your Friend. Note, we tested everything o - For AppWithWearable, open file api_calls.dart located in `AppWithWearable/lib/backend/api_requests ` Find "Whisper" and instead of "key", provide your own api-key for openai whisper for transcriptions to work - ![CleanShot 2024-03-25 at 21 58 42](https://github.com/BasedHardware/Friend/assets/43514161/d0fb89d2-07fd-44e3-8563-68f938bb2319) + CleanShot 2024-03-25 at 21 58 42 - For AppStandalone, update variables in in .env.template file @@ -95,44 +101,44 @@ Follow these steps to get started with your Friend. Note, we tested everything o - Don't have the device? [Clone this Flutterflow Project ](https://app.flutterflow.io/project/friend-0x9u40) - Have the wearable device? [Copy this Flutterflow Project](https://app.flutterflow.io/project/friend-share-19bk3d) -### Install Firmware +# Install Firmware -1. [Download Arduino](https://www.arduino.cc/en/software) -2. Run `cd src/BluetoothDeviceDriver ` in your home repository and Open Arduino .ino file, go to "Settings" and paste these 2 links in additional Boards Manager URLs +Follow these steps to install the firmware: - ``` - https://adafruit.github.io/arduino-board-index/package_adafruit_index.json - https://files.seeedstudio.com/arduino/package_seeeduino_boards_index.json - ``` +1. Set up nRF Connect by following the tutorial in this video: [https://youtu.be/EAJdOqsL9m8](https://youtu.be/EAJdOqsL9m8?feature=shared) + +2. In the nRF Connect Extension inside your VS Code, click "Open an existing application" and open the `firmware` folder from the root of this repo. + + VS Code Extension + +3. In the application panel of the extension, click the "Add Build Configuration" icon. - ![IMAGE 2024-03-24 19:44:35](https://github.com/BasedHardware/friend/assets/43514161/f08cf422-8d30-4ffa-b61c-0e8ee4a0e685) + Add Build Configuration -3. Go to Boards Manager and download these 2 Boards +4. Choose the board as "xiao_ble_sense" and select the configuration as "prj.conf". Then, click "Build Configuration". - ![IMAGE 2024-03-24 19:46:49](https://github.com/BasedHardware/friend/assets/43514161/9c85a0c4-ee73-42ba-a75b-3f8fafa81cbe) + Build Settings -4. Connect NRF52840 board via USB cable to your computer -5. Go to Tools > Board > - ![IMAGE 2024-03-24 19:50:42](https://github.com/BasedHardware/friend/assets/43514161/065e794f-6e20-4f91-a6bf-1b43a5a3614e) - and select "Seeed nRF52 mbed-enabled Boards (you need board that has Sense) +5. Once the build succeeds, you will find the `zephyr.uf2` file in the `firmware/build/zephyr` directory. - Also select Port (should be something that contains USB...) - ![IMAGE 2024-03-24 19:55:07](https://github.com/BasedHardware/friend/assets/43514161/0719de62-b58f-4ceb-85e2-d288916375c9) +6. Double-click on the reset button of the device. The device will appear on your computer as a disk. Drag and drop the `zephyr.uf2` file into it. + + > **Note:** On a Mac, you might see an error message after dropping the file, indicating that the process did not complete. This is just a Mac-specific error; the firmware is successfully uploaded. -6. Go to Sketch => Include Library => Add .zip library and upload a library which you should download [from here](https://github.com/Seeed-Studio/Seeed_Arduino_Mic), make sure you download the `Seeed_Arduino_Mic` repository itself as a .zip file and not use the .zip from its releases section. -7. Install Arduino BLE and Seeed Arduino LSM6DS3 libraries which can be found in Arduino's menu -8. Click "Upload" and then open Serial Monitor to see logs + Pinout + +That's it! You have successfully installed the firmware on your device. ### Testing Audio Recording on Your Computer Follow these steps to test audio recording on your computer using a Python script: -1. Open your terminal and navigate to your home directory. +1. Open your terminal and navigate to the project's root directory. -2. Change to the "src" folder: +2. Change to the "test" folder: ``` - cd src + cd test ``` 3. Install the required Python modules: @@ -147,37 +153,7 @@ Follow these steps to test audio recording on your computer using a Python scrip python local_laptop_client.py ``` - This script will list the available audio devices and their corresponding IDs. - -5. Copy the ID of the desired audio device. - -6. Open the `local_laptop_client.py` file in a text editor and locate the following line: - - ```python - DEVICE_ID = "564A72F4-4552-8CE8-719D-8D5CB2E5D43D" - ``` - - Replace `"564A72F4-4552-8CE8-719D-8D5CB2E5D43D"` with the ID you copied in step 5. - -7. Save the changes to the `local_laptop_client.py` file. - -8. Run the script again: - - ``` - python local_laptop_client.py - ``` - - or - - ``` - python3 local_laptop_client.py - ``` - -9. You can now control the audio recording: - - Double-tap the device to start recording. - - Double-tap the device again to stop recording. - -The recorded audio files will be stored periodically in the `src/recordings` directory. +The recorded audio files will be stored periodically in the `test/recordings` directory. That's it! You have now set up and tested audio recording on your computer. @@ -185,25 +161,25 @@ That's it! You have now set up and tested audio recording on your computer. **Step 0:** Make sure you have bought everything from the buying guide above - +Components **Step 1:** You need to design the case using 3D printer. Find .stl file [here](https://github.com/BasedHardware/Friend/blob/main/3d-printing%20designs/Cover%20%2B%20Case.stl). If you don't know how to do it, send this file to someone who has a 3d printer **Step 2:** Solder everything together like on the picture below. using a soldering kit. Don't have it? buy [this one for $9](https://a.co/d/0XdthUV) -![CleanShot 2024-03-28 at 17 01 53](https://github.com/BasedHardware/Friend/assets/43514161/c254668c-1662-412f-8b2c-05a97fb68419) +Soldering - +Soldered **Step 3:** Fit everything in the case. Biggest hole is for the usb port. In my example, I put the battery first, then the board and then the switch, however it's not an ideal design. If you will figure out a better solution, please contribute! - +Assembled **Step 4:** Use hot glue to attach the lid to the case. You can also use a scotch tape first for testing purposes. Last, on the USB-port side, you'll find 2 small round holes. This is where the thread should go through. - +Lid Congratulations! you now have a fully working and assembled device! @@ -236,4 +212,4 @@ Friend is available under dual licensing options: ### Choosing Your License - If you wish to contribute to or use Friend in open-source projects, you are free to do so under the terms of the GPL, as detailed in the LICENSE file. -- If you require a commercial license for your project or enterprise, please contact us at [team@whomane.com](mailto:team@whomane.com) to discuss your needs and obtain licensing information. +- If you require a commercial license for your project or enterprise, please contact us at [team@whomane.com](mailto:team@whomane.com) to discuss your needs and obtain licensing information. \ No newline at end of file diff --git a/Screenshots/addbuild.png b/Screenshots/addbuild.png new file mode 100644 index 00000000000..a12f6215161 Binary files /dev/null and b/Screenshots/addbuild.png differ diff --git a/Screenshots/build_settings.png b/Screenshots/build_settings.png new file mode 100644 index 00000000000..b8cde502188 Binary files /dev/null and b/Screenshots/build_settings.png differ diff --git a/Screenshots/pinout.jpg b/Screenshots/pinout.jpg new file mode 100644 index 00000000000..295969b901f Binary files /dev/null and b/Screenshots/pinout.jpg differ diff --git a/Screenshots/vscode_extension.png b/Screenshots/vscode_extension.png new file mode 100644 index 00000000000..1cdcd331c76 Binary files /dev/null and b/Screenshots/vscode_extension.png differ diff --git a/firmware/CMakeLists.txt b/firmware/CMakeLists.txt new file mode 100644 index 00000000000..dfe7aa159c8 --- /dev/null +++ b/firmware/CMakeLists.txt @@ -0,0 +1,161 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(nrf52840_nordic) +enable_language(C ASM) + +target_sources(app PRIVATE + src/main.c + src/transport.c + src/mic.c + src/led.c + src/audio.c + src/codec.c + src/opus-1.2.1/A2NLSF.c + src/opus-1.2.1/CNG.c + src/opus-1.2.1/HP_variable_cutoff.c + src/opus-1.2.1/LPC_analysis_filter.c + src/opus-1.2.1/LPC_fit.c + src/opus-1.2.1/LPC_inv_pred_gain.c + src/opus-1.2.1/LP_variable_cutoff.c + src/opus-1.2.1/LTP_analysis_filter_FIX.c + src/opus-1.2.1/LTP_scale_ctrl_FIX.c + src/opus-1.2.1/NLSF2A.c + src/opus-1.2.1/NLSF_VQ.c + src/opus-1.2.1/NLSF_VQ_weights_laroia.c + src/opus-1.2.1/NLSF_decode.c + src/opus-1.2.1/NLSF_del_dec_quant.c + src/opus-1.2.1/NLSF_encode.c + src/opus-1.2.1/NLSF_stabilize.c + src/opus-1.2.1/NLSF_unpack.c + src/opus-1.2.1/NSQ.c + src/opus-1.2.1/NSQ_del_dec.c + src/opus-1.2.1/PLC.c + src/opus-1.2.1/VAD.c + src/opus-1.2.1/VQ_WMat_EC.c + src/opus-1.2.1/ana_filt_bank_1.c + src/opus-1.2.1/analysis.c + src/opus-1.2.1/apply_sine_window_FIX.c + src/opus-1.2.1/autocorr_FIX.c + src/opus-1.2.1/bands.c + src/opus-1.2.1/biquad_alt.c + src/opus-1.2.1/burg_modified_FIX.c + src/opus-1.2.1/bwexpander.c + src/opus-1.2.1/bwexpander_32.c + src/opus-1.2.1/celt.c + src/opus-1.2.1/celt_decoder.c + src/opus-1.2.1/celt_encoder.c + src/opus-1.2.1/celt_lpc.c + src/opus-1.2.1/arm/celt_pitch_xcorr_arm_gcc.s + src/opus-1.2.1/check_control_input.c + src/opus-1.2.1/code_signs.c + src/opus-1.2.1/control_SNR.c + src/opus-1.2.1/control_audio_bandwidth.c + src/opus-1.2.1/control_codec.c + src/opus-1.2.1/corrMatrix_FIX.c + src/opus-1.2.1/cwrs.c + src/opus-1.2.1/debug.c + src/opus-1.2.1/dec_API.c + src/opus-1.2.1/decode_core.c + src/opus-1.2.1/decode_frame.c + src/opus-1.2.1/decode_indices.c + src/opus-1.2.1/decode_parameters.c + src/opus-1.2.1/decode_pitch.c + src/opus-1.2.1/decode_pulses.c + src/opus-1.2.1/decoder_set_fs.c + src/opus-1.2.1/enc_API.c + src/opus-1.2.1/encode_frame_FIX.c + src/opus-1.2.1/encode_indices.c + src/opus-1.2.1/encode_pulses.c + src/opus-1.2.1/entcode.c + src/opus-1.2.1/entdec.c + src/opus-1.2.1/entenc.c + src/opus-1.2.1/find_LPC_FIX.c + src/opus-1.2.1/find_LTP_FIX.c + src/opus-1.2.1/find_pitch_lags_FIX.c + src/opus-1.2.1/find_pred_coefs_FIX.c + src/opus-1.2.1/gain_quant.c + src/opus-1.2.1/init_decoder.c + src/opus-1.2.1/init_encoder.c + src/opus-1.2.1/inner_prod_aligned.c + src/opus-1.2.1/interpolate.c + src/opus-1.2.1/k2a_FIX.c + src/opus-1.2.1/k2a_Q16_FIX.c + src/opus-1.2.1/kiss_fft.c + src/opus-1.2.1/laplace.c + src/opus-1.2.1/lin2log.c + src/opus-1.2.1/log2lin.c + src/opus-1.2.1/mathops.c + src/opus-1.2.1/mdct.c + src/opus-1.2.1/mlp.c + src/opus-1.2.1/mlp_data.c + src/opus-1.2.1/modes.c + src/opus-1.2.1/noise_shape_analysis_FIX.c + src/opus-1.2.1/opus.c + src/opus-1.2.1/opus_decoder.c + src/opus-1.2.1/opus_encoder.c + src/opus-1.2.1/opus_multistream.c + src/opus-1.2.1/opus_multistream_decoder.c + src/opus-1.2.1/opus_multistream_encoder.c + src/opus-1.2.1/pitch.c + src/opus-1.2.1/pitch_analysis_core_FIX.c + src/opus-1.2.1/pitch_est_tables.c + src/opus-1.2.1/process_NLSFs.c + src/opus-1.2.1/process_gains_FIX.c + src/opus-1.2.1/quant_LTP_gains.c + src/opus-1.2.1/quant_bands.c + src/opus-1.2.1/rate.c + src/opus-1.2.1/regularize_correlations_FIX.c + src/opus-1.2.1/repacketizer.c + src/opus-1.2.1/resampler.c + src/opus-1.2.1/resampler_down2.c + src/opus-1.2.1/resampler_down2_3.c + src/opus-1.2.1/resampler_private_AR2.c + src/opus-1.2.1/resampler_private_IIR_FIR.c + src/opus-1.2.1/resampler_private_down_FIR.c + src/opus-1.2.1/resampler_private_up2_HQ.c + src/opus-1.2.1/resampler_rom.c + src/opus-1.2.1/residual_energy16_FIX.c + src/opus-1.2.1/residual_energy_FIX.c + src/opus-1.2.1/schur64_FIX.c + src/opus-1.2.1/schur_FIX.c + src/opus-1.2.1/shell_coder.c + src/opus-1.2.1/sigm_Q15.c + src/opus-1.2.1/sort.c + src/opus-1.2.1/stereo_LR_to_MS.c + src/opus-1.2.1/stereo_MS_to_LR.c + src/opus-1.2.1/stereo_decode_pred.c + src/opus-1.2.1/stereo_encode_pred.c + src/opus-1.2.1/stereo_find_predictor.c + src/opus-1.2.1/stereo_quant_pred.c + src/opus-1.2.1/sum_sqr_shift.c + src/opus-1.2.1/table_LSF_cos.c + src/opus-1.2.1/tables_LTP.c + src/opus-1.2.1/tables_NLSF_CB_NB_MB.c + src/opus-1.2.1/tables_NLSF_CB_WB.c + src/opus-1.2.1/tables_gain.c + src/opus-1.2.1/tables_other.c + src/opus-1.2.1/tables_pitch_lag.c + src/opus-1.2.1/tables_pulses_per_block.c + src/opus-1.2.1/vector_ops_FIX.c + src/opus-1.2.1/vq.c + src/opus-1.2.1/warped_autocorrelation_FIX.c + src/opus-1.2.1/arm/celt_pitch_xcorr_arm_gcc.s +) + +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DARM_MATH_CM4") +# set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DVAR_ARRAYS") +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DOPUS_ARM_ASM") +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DOPUS_ARM_INLINE_ASM") +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DOPUS_ARM_INLINE_EDSP") +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DOPUS_ARM_INLINE_MEDIA") +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DOPUS_ARM_MAY_HAVE_EDSP") +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DOPUS_ARM_PRESUME_EDSP") +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DOPUS_BUILD") +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DUSE_ALLOCA") +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DFIXED_POINT -DDISABLE_FLOAT_API") +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DHAVE_CONFIG_H") +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DHAVE_ALLOCA_H") +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsingle-precision-constant") # A lot of constants are written as doubles +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DHAVE_LRINT -DHAVE_LRINTF") \ No newline at end of file diff --git a/firmware/client.py b/firmware/client.py index d3ed244f1b2..97b8f2dbfff 100644 --- a/firmware/client.py +++ b/firmware/client.py @@ -9,11 +9,11 @@ import struct from scipy.signal import stft, istft -DEVICE_NAME = "Super" +DEVICE_NAME = "Friend" SERVICE_UUID = "19B10000-E8F2-537E-4F6C-D104768A1214" CHARACTERISTIC_UUID = "19B10001-E8F2-537E-4F6C-D104768A1214" -CODEC = "mulaw" # "pcm" or "mulaw" +CODEC = "pcm" # "pcm" or "mulaw" SAMPLE_RATE = 8000 # Sample rate for the audio SAMPLE_WIDTH = 2 # 16-bit audio CHANNELS = 1 # Mono audio diff --git a/firmware/prj.conf b/firmware/prj.conf index 1147ea4866b..5db4e9ea606 100644 --- a/firmware/prj.conf +++ b/firmware/prj.conf @@ -11,7 +11,7 @@ CONFIG_NRFX_PDM=y CONFIG_BT=y CONFIG_BT_PERIPHERAL=y -CONFIG_BT_DEVICE_NAME="Super" +CONFIG_BT_DEVICE_NAME="Friend" CONFIG_BT_MAX_CONN=1 CONFIG_BT_MAX_PAIRED=1 CONFIG_BT_DEVICE_APPEARANCE=22 diff --git a/firmware/src/transport.c b/firmware/src/transport.c index 353e34a8465..bffbaeb0b39 100644 --- a/firmware/src/transport.c +++ b/firmware/src/transport.c @@ -37,7 +37,7 @@ static struct bt_gatt_service audio_service = BT_GATT_SERVICE(attrs); // Advertisement data static const struct bt_data bt_ad[] = { BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)), - BT_DATA(BT_DATA_NAME_COMPLETE, "Super", sizeof("Super") - 1), + BT_DATA(BT_DATA_NAME_COMPLETE, "Friend", sizeof("Friend") - 1), }; // Scan response data diff --git a/src/BLE_Audio_Stream_NRF52840 b/src/BLE_Audio_Stream_NRF52840 deleted file mode 160000 index b668822272a..00000000000 --- a/src/BLE_Audio_Stream_NRF52840 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b668822272ade958bf57b1aad576aea262782c49 diff --git a/src/BluetoothDeviceDriver/BluetoothDeviceDriver.ino b/src/BluetoothDeviceDriver/BluetoothDeviceDriver.ino deleted file mode 100644 index 67c9f0de35f..00000000000 --- a/src/BluetoothDeviceDriver/BluetoothDeviceDriver.ino +++ /dev/null @@ -1,176 +0,0 @@ -#include -#include // make sure to use the library "Seeed arduino LSM6DS3" -#include - -// Settings -#define DEBUG 1 // Enable pin pulse during ISR -#define SAMPLES 4000 // Changed to 4000 -#define CHUNK_SIZE 200 -#define BUFFER_SIZE (SAMPLES * 4) // Doubled the buffer size to accommodate continuous recording -#define int2Pin PIN_LSM6DS3TR_C_INT1 -//#define DOUBLE_TAP_ENABLED // This enables double tap and usage of IMU - -mic_config_t mic_config{ - .channel_cnt = 1, - .sampling_rate = 16000, // Keep the sampling rate at 16000 - .buf_size = 16000, // Use the larger buffer size - .debug_pin = LED_BUILTIN // Toggles each DAC ISR (if DEBUG is set to 1) -}; - -LSM6DS3 gyro(I2C_MODE, 0x6A); // gyro init -NRF52840_ADC_Class Mic(&mic_config); - -uint16_t recording_buf[BUFFER_SIZE]; -volatile uint32_t recording_idx = 0; -volatile uint32_t read_idx = 0; -volatile bool recording = false; -bool isConnected = false; - -uint8_t tapCount = 0; // Amount of received taps -uint8_t prevTapCount = 0; // Tap Counter from last loop - -BLEService audioService("19B10000-E8F2-537E-4F6C-D104768A1214"); -BLECharacteristic audioCharacteristic("19B10001-E8F2-537E-4F6C-D104768A1214", BLERead | BLENotify, CHUNK_SIZE * sizeof(int16_t)); - -void setup() { - pinMode(int2Pin, INPUT); - pinMode(LED_BUILTIN, OUTPUT); - pinMode(LEDR, OUTPUT); - pinMode(LED_BLUE, OUTPUT); - pinMode(LED_GREEN, OUTPUT); - Serial.begin(115200); - - unsigned long startMillis = millis(); - while (!Serial && millis() - startMillis < 500) { - delay(10); // Short delay to prevent hanging in a tight loop - } - - Mic.set_callback(audio_rec_callback); - if (!Mic.begin()) { - Serial.println("Mic initialization failed"); - setLedRGB(true, false, false); - while (1) - ; - } - Serial.println("Mic initialization done."); - setLedRGB(false, true, false); - - if (!BLE.begin()) { - Serial.println("Starting Bluetooth® Low Energy module failed!"); - setLedRGB(true, false, false); - while (1) - ; - } -#ifdef DOUBLE_TAP_ENABLED - if (gyro.begin() != 0) { - Serial.println("gyro error"); - setLedRGB(true, false, false); - while (1) - ; - } - Serial.println("Gyro initialization done."); - - setupDoubleTap(); - - attachInterrupt(digitalPinToInterrupt(int2Pin), tapCallback, RISING); -#endif - setLedRGB(false, true, false); - BLE.setLocalName("AudioRecorder"); - - BLE.setAdvertisedService(audioService); - audioService.addCharacteristic(audioCharacteristic); - BLE.addService(audioService); - BLE.advertise(); - - // Print device address - Serial.print("Device Address: "); - Serial.println(BLE.address()); - - Serial.println("BLE Audio Recorder"); -} - -void loop() { - BLEDevice central = BLE.central(); - - if (central && !isConnected) { - Serial.print("Connected to central: "); - Serial.println(central.address()); - isConnected = true; - Serial.println("Type 'rec' to start recording"); - } - -#ifdef DOUBLE_TAP_ENABLED - if (tapCount > prevTapCount) { - Serial.println("Double tapped!"); - recording = !recording; // Toggle the recording state - - if (recording) { - setLedRGB(true, false, true); // Purple color for recording - Serial.println("Recording started"); - } else { - setLedRGB(false, true, false); // Green color for stopped - Serial.println("Recording stopped"); - } - } - - prevTapCount = tapCount; -#endif - - if (Serial.available()) { - String resp = Serial.readStringUntil('\n'); - if (resp == "rec" && !recording) { - recording = true; - setLedRGB(false, false, true); - Serial.println("Recording started"); - } else if (resp == "stop" && recording) { - recording = false; - setLedRGB(false, true, false); - Serial.println("Recording stopped"); - } - } - - if (recording) { - uint32_t available_samples = (recording_idx + BUFFER_SIZE - read_idx) % BUFFER_SIZE; - if (available_samples >= CHUNK_SIZE) { -#ifdef DEBUG - Serial.println("Sending " + String(available_samples) + " samples"); -#endif - uint16_t chunk[CHUNK_SIZE]; - for (int i = 0; i < CHUNK_SIZE; i++) { - chunk[i] = recording_buf[(read_idx + i) % BUFFER_SIZE]; - } - audioCharacteristic.writeValue(chunk, sizeof(chunk)); - read_idx = (read_idx + CHUNK_SIZE) % BUFFER_SIZE; - delay(20); - } - } -} - -static void audio_rec_callback(uint16_t *buf, uint32_t buf_len) { - if (recording) { - for (uint32_t i = 0; i < buf_len; i += 4) { - recording_buf[recording_idx] = buf[i]; - recording_idx = (recording_idx + 1) % BUFFER_SIZE; - } - } -} - -void setupDoubleTap() { - // Double Tap Config - gyro.writeRegister(LSM6DS3_ACC_GYRO_CTRL1_XL, 0x60); //* Acc = 416Hz (High-Performance mode)// Turn on the accelerometer - gyro.writeRegister(LSM6DS3_ACC_GYRO_TAP_CFG1, 0x8E); // INTERRUPTS_ENABLE, SLOPE_FDS// Enable interrupts and tap detection on X, Y, Z-axis - gyro.writeRegister(LSM6DS3_ACC_GYRO_TAP_THS_6D, 0x85); // Set tap threshold 8C - gyro.writeRegister(LSM6DS3_ACC_GYRO_INT_DUR2, 0x7F); // Set Duration, Quiet and Shock time windows 7F - gyro.writeRegister(LSM6DS3_ACC_GYRO_WAKE_UP_THS, 0x80); // Single & double-tap enabled (SINGLE_DOUBLE_TAP = 1) - gyro.writeRegister(LSM6DS3_ACC_GYRO_MD1_CFG, 0x08); // Double-tap interrupt driven to INT1 pin -} - -void tapCallback() { - tapCount++; -} - -void setLedRGB(bool red, bool green, bool blue) { - digitalWrite(LEDB, blue ? LOW : HIGH); // Blue ON when blue is true - digitalWrite(LEDG, green ? LOW : HIGH); // Green ON when green is true - digitalWrite(LEDR, red ? LOW : HIGH); // Red ON when red is true -} \ No newline at end of file diff --git a/src/requirements.txt b/src/requirements.txt deleted file mode 100644 index ce84222e812..00000000000 --- a/src/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -bleak==0.21.1 -numpy==1.26.4 -scipy==1.12.0 \ No newline at end of file diff --git a/src/local_laptop_client.py b/test/local_laptop_client.py similarity index 53% rename from src/local_laptop_client.py rename to test/local_laptop_client.py index bf91521365f..97b8f2dbfff 100644 --- a/src/local_laptop_client.py +++ b/test/local_laptop_client.py @@ -1,30 +1,49 @@ import asyncio import bleak -from bleak import BleakClient, BleakScanner +from bleak import BleakClient import wave from datetime import datetime import numpy as np import time import os +import struct from scipy.signal import stft, istft -DEVICE_ID = "564A72F4-4552-8CE8-719D-8D5CB2E5D43D" # NOTE: You will have to update this ID with your devices bluetooth id +DEVICE_NAME = "Friend" SERVICE_UUID = "19B10000-E8F2-537E-4F6C-D104768A1214" CHARACTERISTIC_UUID = "19B10001-E8F2-537E-4F6C-D104768A1214" -SAMPLE_RATE = 4000 # Sample rate for the audio +CODEC = "pcm" # "pcm" or "mulaw" +SAMPLE_RATE = 8000 # Sample rate for the audio SAMPLE_WIDTH = 2 # 16-bit audio CHANNELS = 1 # Mono audio -CAPTURE_TIME = 30 # Time to capture audio in seconds +CAPTURE_TIME = 10 # Time to capture audio in seconds + +def ulaw2linear(ulaw_byte): + """Convert a µ-law byte to a 16-bit linear PCM value.""" + EXPONENT_LUT = [0, 132, 396, 924, 1980, 4092, 8316, 16764] + ulaw_byte = ~ulaw_byte + sign = (ulaw_byte & 0x80) + exponent = (ulaw_byte >> 4) & 0x07 + mantissa = ulaw_byte & 0x0F + sample = EXPONENT_LUT[exponent] + (mantissa << (exponent + 3)) + if sign != 0: + sample = -sample + + return sample + +def ulaw_bytes_to_pcm16(ulaw_data): + """Convert a sequence of µ-law encoded bytes to a list of 16-bit PCM values.""" + return [ulaw2linear(byte) for byte in ulaw_data] async def main(): print("Discovering AudioRecorder...") - devices = await BleakScanner.discover(timeout=2.0) + devices = await bleak.discover(timeout=2.0) audio_recorder = None for device in devices: if device.name: print(device.name, device.address) - if device.address == DEVICE_ID: + if device.name == DEVICE_NAME: audio_recorder = device break @@ -34,21 +53,48 @@ async def main(): def handle_ble_disconnect(client): print("Disconnected from AudioRecorder") + # def ulawToLinear(ulawByte): + # ulawByte = ~ulawByte & 0xFF # Invert and mask to 8-bit + # sign = (ulawByte & 0x80) + # exponent = (ulawByte >> 4) & 0x07 + # mantissa = ulawByte & 0x0F + # sample = (0x80 + (mantissa << 4)) << max(0, exponent - 1) + # sample -= 0x84 << 2 # Adjusting back the bias used during encoding + + # if sign == 0: + # sample = -sample + + # # Adjust output to fit into 16-bit PCM range + # return sample & 0xFFFF + def filter_audio_data(audio_data): - audio_data = np.frombuffer(audio_data, dtype=np.uint16) - audio_data -= 32768 - scaling_factor = 2*32768 / (max(0, np.max(audio_data)) - min(0, np.min(audio_data))) - return (audio_data * scaling_factor).astype(np.int16) + + if CODEC == "mulaw": + # pcm16_samples = audioop.ulaw2lin(audio_data, 2) + # pcm16_samples = struct.unpack('<' + 'h' * (len(pcm16_samples) // 2), pcm16_samples) + # print(pcm16_samples) + pcm16_samples = ulaw_bytes_to_pcm16(audio_data) + audio_data = np.array(pcm16_samples, dtype=np.int16) + + if CODEC == "pcm": + audio_data = audio_data[:len(audio_data) - len(audio_data) % 2] + audio_data = np.frombuffer(audio_data, dtype=np.int16) + + # Normalize + # scaling_factor = 2*32768 / (max(0, np.max(audio_data)) - min(0, np.min(audio_data))) + # return (audio_data * scaling_factor).astype(np.int16) + return audio_data - def export_audio_data(filtered_audio_data, file_extension): + def export_audio_data(filtered_audio_data, raw_file, file_extension): recordings_dir = "recordings" if not os.path.exists(recordings_dir): os.makedirs(recordings_dir) filename = os.path.join(recordings_dir, datetime.now().strftime("%H-%M-%S-%f") + file_extension) print(filename) + # filename = os.path.join(recordings_dir, "recording" + file_extension) if file_extension == ".txt": with open(filename, "w") as file: - file.write(str(list(filtered_audio_data))) + file.write(str(list(raw_file))) else: # Directly use the filename with wave.open for .wav files with wave.open(filename, "wb") as wav_file: @@ -64,8 +110,8 @@ async def process_audio(audio_data): return filtered_audio_data = filter_audio_data(audio_data) - export_audio_data(filtered_audio_data, ".wav") - export_audio_data(filtered_audio_data, ".txt") + export_audio_data(filtered_audio_data, audio_data, ".wav") + export_audio_data(filtered_audio_data, audio_data, ".txt") pass async with BleakClient(audio_recorder.address, services=[SERVICE_UUID], disconnect_callback=handle_ble_disconnect) as client: @@ -78,9 +124,9 @@ async def process_audio(audio_data): # end_signal = b"\xFF" def handle_audio_data(sender, data): - print("---handle_audio_data---") - print(f"Received {len(data)} bytes at {time.time()}") - audio_data.extend(data) + # print("---handle_audio_data---") + # print(f"Received {len(data)} bytes at {time.time()}") + audio_data.extend(data[3:]) # if data == [end_signal]: # print(f"End signal received after {len(audio_data)} bytes") @@ -95,6 +141,7 @@ async def record_audio(): async def record_and_process(): while True: await record_audio() + print(len(audio_data)) asyncio.ensure_future(process_audio(audio_data.copy())) audio_data.clear()