diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..f3b46a42 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.github/workflows/build-pico-sdk.yml b/.github/workflows/build-pico-sdk.yml index ab6c6310..25ec50ed 100644 --- a/.github/workflows/build-pico-sdk.yml +++ b/.github/workflows/build-pico-sdk.yml @@ -20,6 +20,8 @@ jobs: board: pico - name: pico-w-blink-sdk board: pico_w + - name: harmony + board: pico_w swift: [swift-DEVELOPMENT-SNAPSHOT-2024-12-04-a] steps: @@ -41,8 +43,8 @@ jobs: - name: Install GNU ARM toolchain run: | ARCH=`uname -m` - curl -sL https://developer.arm.com/-/media/Files/downloads/gnu/13.3.rel1/binrel/arm-gnu-toolchain-13.3.rel1-$ARCH-arm-none-eabi.tar.xz -O - tar xf arm-gnu-toolchain-13.3.rel1-$ARCH-arm-none-eabi.tar.xz + curl -sL https://developer.arm.com/-/media/Files/downloads/gnu/14.2.rel1/binrel/arm-gnu-toolchain-14.2.rel1-$ARCH-arm-none-eabi.tar.xz -O + tar xf arm-gnu-toolchain-14.2.rel1-$ARCH-arm-none-eabi.tar.xz - name: Install ${{ matrix.swift }} run: | @@ -60,12 +62,20 @@ jobs: git submodule update --init --recursive cd .. + - name: Clone Pico Extras + run: | + git clone https://github.com/raspberrypi/pico-extras.git + cd pico-extras + git submodule update --init --recursive + cd .. + - name: Set Pico environment variables run: | ARCH=`uname -m` echo "PICO_BOARD=${{ matrix.example.board }}" >> $GITHUB_ENV echo "PICO_SDK_PATH=`pwd`/pico-sdk" >> $GITHUB_ENV - echo "PICO_TOOLCHAIN_PATH=`pwd`/arm-gnu-toolchain-13.3.rel1-$ARCH-arm-none-eabi" >> $GITHUB_ENV + echo "PICO_EXTRAS_PATH=`pwd`/pico-extras" >> $GITHUB_ENV + echo "PICO_TOOLCHAIN_PATH=`pwd`/arm-gnu-toolchain-14.2.rel1-$ARCH-arm-none-eabi" >> $GITHUB_ENV - name: Build ${{ matrix.example.name }} run: | diff --git a/.swiftformatignore b/.swiftformatignore new file mode 100644 index 00000000..c91272e4 --- /dev/null +++ b/.swiftformatignore @@ -0,0 +1 @@ +./harmony/* diff --git a/README.md b/README.md index 0a1ce898..63f325dd 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Each example in this repository contains build and deployment instructions, howe | [pico-blink-sdk](./pico-blink-sdk) | Raspberry Pi Pico, Pico 2 | Pico SDK | Blink an LED repeatedly with Swift & the Pico SDK. | | | [pico-blink](./pico-blink) | Raspberry Pi Pico | None | Blink an LED repeatedly. | | | [pico-w-blink-sdk](./pico-w-blink-sdk) | Raspberry Pi Pico W | Pico SDK | Blink an LED to signal 'SOS' in Morse code repeatedly with Swift & the Pico SDK. | | +| [harmony](./harmony) | Raspberry Pi Pico W | Pico SDK | A bluetooth speaker and ferrofluidic music visualizer. Firmware, Electrical, and Mechanical designs fully available. | | | [pico2-neopixel](./pico2-neopixel) | Raspberry Pi Pico 2 | None | Control Neopixel LEDs using the RP2350 PIO. | | | [rpi4b-blink](./rpi4b-blink) | Raspberry Pi 4B | None | Blink the Pi's status green LED repeatedly using Swift MMIO. | | | [rpi5-blink](./rpi5-blink) | Raspberry Pi 5 | None | Blink the Pi's status green LED repeatedly with Swift MMIO. | | diff --git a/harmony/.vscode/settings.json b/harmony/.vscode/settings.json new file mode 100644 index 00000000..3416ca02 --- /dev/null +++ b/harmony/.vscode/settings.json @@ -0,0 +1,16 @@ +{ + "swift.path": "/Users/rauhul/Library/Developer/Toolchains/swift-DEVELOPMENT-SNAPSHOT-2025-01-05-a.xctoolchain/usr/bin", + "swift.swiftEnvironmentVariables": { + "DEVELOPER_DIR": "/Applications/Xcode.app/" + }, + "cmake.environment": { + "TOOLCHAINS": "org.swift.62202501051a", + "PICO_BOARD": "pico_w", + "PICO_PLATFORM": "rp2040", + "PICO_SDK_PATH": "/Volumes/Developer/org.swift/swift-embedded-examples/harmony/pico-sdk", + "PICO_TOOLCHAIN_PATH": "/Volumes/Developer/org.swift/swift-embedded-examples/harmony/arm-gnu-toolchain-14.2.rel1-darwin-arm64-arm-none-eabi", + "PICO_EXTRAS_PATH": "/Volumes/Developer/org.swift/swift-embedded-examples/harmony/pico-extras", + }, + "cmake.generator": "Ninja", + "swift.disableAutoResolve": true +} diff --git a/harmony/.vscode/tasks.json b/harmony/.vscode/tasks.json new file mode 100644 index 00000000..2fcc2f12 --- /dev/null +++ b/harmony/.vscode/tasks.json @@ -0,0 +1,34 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "cmake", + "label": "CMake: build", + "command": "build", + "targets": [ + "all" + ], + "group": { + "kind": "build", + "isDefault": true + }, + "presentation": { + "clear": true + } + }, + { + "label": "deploy", + "type": "shell", + "command": "cp build/app.uf2 /Volumes/RPI-RP2/", + "group": { + "kind": "build", + "isDefault": false + }, + "presentation": { + "clear": true, + "reveal": "always", + "panel": "shared" + } + } + ] +} \ No newline at end of file diff --git a/harmony/ACKNOWLEDGEMENTS.md b/harmony/ACKNOWLEDGEMENTS.md new file mode 100644 index 00000000..eb99c525 --- /dev/null +++ b/harmony/ACKNOWLEDGEMENTS.md @@ -0,0 +1,277 @@ + +# Acknowledgements + +Harmony is built on top of wonderful software provided by: + +## Pico-SDK + +Copyright 2020 (c) 2020 Raspberry Pi (Trading) Ltd. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +## Pico-Extras + +Copyright 2020 (c) 2020 Raspberry Pi (Trading) Ltd. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +## BTStack + +Copyright (C) 2009 BlueKitchen GmbH +All rights reserved + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holders nor the names of contributors may + be used to endorse or promote products derived from this software without + specific prior written permission. + +4. Any redistribution, use, or modification is done solely for personal benefit + and not for any commercial purpose or for monetary gain. + +THIS SOFTWARE IS PROVIDED BY BLUEKITCHEN GMBH AND CONTRIBUTORS ``AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL BLUEKITCHEN GMBH OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Please inquire about commercial licensing options at +contact@bluekitchen-gmbh.com + +## CMSIS-DSP + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/harmony/BillOfMaterials.md b/harmony/BillOfMaterials.md new file mode 100644 index 00000000..29bc5de3 --- /dev/null +++ b/harmony/BillOfMaterials.md @@ -0,0 +1,20 @@ +| **Item** | **Link** | **QTY** | +| ------------------------------- | ------------------------------------------------------------ | ------- | +| **Raspberry Pi Pico W** | https://www.adafruit.com/product/5544 | 1 | +| **12V Power Supply** | https://www.adafruit.com/product/4880 | 1 | +| **5V Step-Down Converter** | https://www.adafruit.com/product/1466 | 1 | +| **Barrel Jack** | https://www.digikey.com/en/products/detail/tensility-international-corp/54-00063/6206244 | 1 | +| **Power Switch** | https://www.digikey.com/en/products/detail/e-switch/RR3112ABLKBLKNFF0/1589375 | 1 | +| **Speakers** | https://www.adafruit.com/product/1314 | 2 | +| **Amplifier** | https://www.adafruit.com/product/1752 | 1 | +| **Digital to Analog Converter** | https://www.adafruit.com/product/3678 | 1 | +| **Opto Coupler** | 4N35 | 1 | +| **Diodes** | 1N4001, RL207 | 1 | +| **Power Mosfet** | IRFZ44N | 1 | +| **Permanent Magnets** | https://www.kjmagnetics.com/d41-neodymium-disc-magnet?srsltid=AfmBOoqX5QSYfH9Br4u6VB6d0PGNB2tcBJuptexQ-pBq5WYRsY6VNTj8 | 6 | +| **Electromagnet** | https://www.adafruit.com/product/3875 | 1 | +| **Ferrofluid** | https://www.amazon.com/dp/B0DBW2NH7X/ | 1 | +| **Volume Encoder** | https://www.digikey.com/en/products/detail/bourns-inc/PEC11R-4025F-S0024/4699199 | 1 | +| **Volume Knob** | https://www.digikey.com/en/products/detail/kilo-international/OEDNI-63-2-7/5970329 | 1 | +| **Buttons** | https://www.adafruit.com/product/1505 | 3 | +| **LED Strip** | https://www.adafruit.com/product/1506 | 1 | diff --git a/harmony/CMakeLists.txt b/harmony/CMakeLists.txt new file mode 100644 index 00000000..d53dbd08 --- /dev/null +++ b/harmony/CMakeLists.txt @@ -0,0 +1,212 @@ +cmake_minimum_required(VERSION 3.29) +include($ENV{PICO_SDK_PATH}/external/pico_sdk_import.cmake) +include($ENV{PICO_EXTRAS_PATH}/external/pico_extras_import.cmake) + +project(app) +pico_sdk_init() + +set(CMSISCORE "$ENV{PICO_SDK_PATH}/src/rp2_common/cmsis/stub/CMSIS/Core") +set(DISABLEFLOAT16 ON) +include(FetchContent) +FetchContent_Declare(cmsisdsp + GIT_REPOSITORY https://github.com/ARM-software/CMSIS-DSP.git + GIT_TAG "v1.16.2" +) +FetchContent_MakeAvailable(cmsisdsp) + + + +add_executable(app) + +pico_generate_pio_header(app ${CMAKE_CURRENT_LIST_DIR}/Sources/PIOPrograms/I2S.pio) +pico_generate_pio_header(app ${CMAKE_CURRENT_LIST_DIR}/Sources/PIOPrograms/QuadratureEncoder.pio) +pico_generate_pio_header(app ${CMAKE_CURRENT_LIST_DIR}/Sources/PIOPrograms/WS2812.pio) + +# Which Pico SDK libraries are we using. This also automatically sets up header +# search paths (via target_include_directories / INTERFACE_INCLUDE_DIRECTORIES). +target_link_libraries(app + pico_cyw43_arch_lwip_threadsafe_background + pico_multicore + pico_stdlib + + pico_btstack_ble + pico_btstack_classic + pico_btstack_sbc_common + pico_btstack_sbc_decoder + pico_btstack_sbc_encoder + pico_btstack_cyw43 + + # FIXME: remove + pico_audio + hardware_adc + hardware_dma + hardware_i2c + hardware_pio + hardware_irq +) + +# Make our "config headers" (e.g. lwipopts.h) discoverable by libraries themselves. +target_include_directories(app PRIVATE + "${CMAKE_CURRENT_LIST_DIR}/include" + "${CMAKE_CURRENT_LIST_DIR}/dsp" + "${CMAKE_CURRENT_LIST_DIR}/platform" + "${cmsisdsp_SOURCE_DIR}/Include" +) + +# Uncomment to debug lwIP. +# target_compile_definitions(app PRIVATE PICO_DEBUG_MALLOC=1) +# target_compile_definitions(pico_standard_link INTERFACE "LWIP_DEBUG=1") +# target_compile_definitions(pico_standard_link INTERFACE "WANT_HCI_DUMP=1") +target_compile_definitions(app PRIVATE "PICO_AUDIO_I2S_DATA_PIN=26") +target_compile_definitions(app PRIVATE "PICO_AUDIO_I2S_CLOCK_PIN_BASE=27") + +# Gather compile definitions from all dependencies +set_property(GLOBAL PROPERTY visited_targets "") +set_property(GLOBAL PROPERTY compilerdefs_list "") + +function(gather_compile_definitions_recursive target) + # Get the current value of visited_targets + get_property(visited_targets GLOBAL PROPERTY visited_targets) + + # make sure we don't visit the same target twice + # and that we don't visit the special generator expressions + if (${target} MATCHES "\\\$<" OR ${target} MATCHES "::@" OR ${target} IN_LIST visited_targets) + return() + endif() + + # Append the target to visited_targets + list(APPEND visited_targets ${target}) + set_property(GLOBAL PROPERTY visited_targets "${visited_targets}") + + # Get the current value of compilerdefs_list + get_property(compilerdefs_list GLOBAL PROPERTY compilerdefs_list) + + get_target_property(target_definitions ${target} INTERFACE_COMPILE_DEFINITIONS) + if (target_definitions) + # Append the target definitions to compilerdefs_list + list(APPEND compilerdefs_list ${target_definitions}) + set_property(GLOBAL PROPERTY compilerdefs_list "${compilerdefs_list}") + endif() + + get_target_property(target_linked_libs ${target} INTERFACE_LINK_LIBRARIES) + if (target_linked_libs) + foreach(linked_target ${target_linked_libs}) + # Recursively gather compile definitions from dependencies + gather_compile_definitions_recursive(${linked_target}) + endforeach() + endif() +endfunction() + +gather_compile_definitions_recursive(app) + +add_dependencies(app CMSISDSP) +target_link_libraries(app + ${cmsisdsp_BINARY_DIR}/Source/libCMSISDSP.a +) + +target_include_directories(app PRIVATE +# ${CMAKE_CURRENT_LIST_DIR}/dsp +# ${CMAKE_CURRENT_LIST_DIR}/platform + ${cmsisdsp_SOURCE_DIR}/Include + ${PICO_SDK_PATH}/src/rp2_common/cmsis/stub/CMSIS/Core/Include +) + + +get_property(COMPILE_DEFINITIONS GLOBAL PROPERTY compilerdefs_list) + +# Parse compiler definitions into a format that swiftc can understand +list(REMOVE_DUPLICATES COMPILE_DEFINITIONS) +list(PREPEND COMPILE_DEFINITIONS "") # adds a semicolon at the beginning +string(REPLACE "$" "$" COMPILE_DEFINITIONS "${COMPILE_DEFINITIONS}") +string(REPLACE ";" " -Xcc -D" COMPILE_DEFINITIONS "${COMPILE_DEFINITIONS} ") +message("COMPILE_DEFINITIONS: ${COMPILE_DEFINITIONS}") + +get_target_property(var pico_standard_link INTERFACE_COMPILE_OPTIONS) +set_target_properties(pico_standard_link PROPERTIES INTERFACE_COMPILE_OPTIONS "") + +# Compute -Xcc flags to set up the C and C++ header search paths for Swift (for +# the bridging header). +set(SWIFT_INCLUDES) +foreach(dir ${CMAKE_C_IMPLICIT_INCLUDE_DIRECTORIES}) + string(CONCAT SWIFT_INCLUDES ${SWIFT_INCLUDES} "-Xcc ") + string(CONCAT SWIFT_INCLUDES ${SWIFT_INCLUDES} "-I${dir} ") +endforeach() +foreach(dir ${CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES}) + string(CONCAT SWIFT_INCLUDES ${SWIFT_INCLUDES} "-Xcc ") + string(CONCAT SWIFT_INCLUDES ${SWIFT_INCLUDES} "-I${dir} ") +endforeach() + +# Swift compiler flags to build in Embedded Swift mode, optimize for size, +# choose the right ISA, ABI, etc. +target_compile_options(app PUBLIC "$<$:SHELL: + -target armv6m-none-none-eabi + -Xfrontend -function-sections -wmo -parse-as-library -Osize + -enable-experimental-feature Embedded + -enable-experimental-feature Extern + -enable-experimental-feature Span + -enable-experimental-feature SymbolLinkageMarkers + + -assert-config Debug + + -Xcc -mfloat-abi=soft -Xcc -fshort-enums + -Xcc -D__APPLE_CC__ + + -Xcc -I${CMAKE_CURRENT_LIST_DIR}/include + + -pch-output-dir /tmp + -Xfrontend -enable-single-module-llvm-emission + + ${SWIFT_INCLUDES} + ${COMPILE_DEFINITIONS} + + -import-bridging-header ${CMAKE_CURRENT_LIST_DIR}/Sources/Application/BridgingHeader.h + >") + +# Enable Swift support in CMake, force Whole Module builds (required by Embedded +# Swift), and use "CMAKE_Swift_COMPILER_WORKS" to skip the trial compilations +# which don't (yet) correctly work when cross-compiling. +set(CMAKE_Swift_COMPILER_WORKS YES) +set(CMAKE_Swift_COMPILATION_MODE_DEFAULT wholemodule) +set(CMAKE_Swift_COMPILATION_MODE wholemodule) +enable_language(Swift) + +# Don't link via the Swift driver, it doesn't understand some GNU linker +# arguments, and it's not necessary for Embedded Swift. +set_property(TARGET app PROPERTY LINKER_LANGUAGE C) + +# List of Swift and C source files to build. +target_sources(app + PRIVATE + Sources/Application/Button.swift + Sources/Application/ButtonTimes.swift + Sources/Application/LEDStrip.swift + Sources/Application/Logging.swift + Sources/Application/Main.swift + Sources/Application/QuadratureEncoder.swift + Sources/Application/Stubs.swift + + Sources/Audio/AudioAnalyzer.swift + Sources/Audio/AudioBuffer.swift + Sources/Audio/AudioBufferTransport.swift + Sources/Audio/AudioEngine.swift + Sources/Audio/AudioI2S.swift + Sources/Audio/AudioPico.swift + Sources/Audio/MAX9744.swift + Sources/Audio/Resampler.swift + Sources/Audio/Ring.swift + Sources/Audio/RingBuffer.swift + Sources/Audio/SpinLock.swift + Sources/Audio/TPA2016D2.swift + + Sources/Bluetooth/A2DP.swift + Sources/Bluetooth/AVRCP.swift + Sources/Bluetooth/HCI.swift + Sources/Bluetooth/SBC.swift + Sources/Bluetooth/SDP.swift + + Sources/PIOPrograms/I2S.pio + Sources/PIOPrograms/QuadratureEncoder.pio + Sources/PIOPrograms/WS2812.pio +) + +pico_add_extra_outputs(app) diff --git a/harmony/README.md b/harmony/README.md new file mode 100644 index 00000000..02d60270 --- /dev/null +++ b/harmony/README.md @@ -0,0 +1,112 @@ +# Harmony + +> [!NOTE] +> This README is still under construction. + +Harmony is a Bluetooth speaker and Ferrofluidic music visualizer. + +## Table of Contents + +- [Overview](#overview) +- [Features](#features) +- [Bill of Materials](#bill-of-materials) +- [Compiling the firmware](#compiling-the-firmware) +- [Flashing and running](#flashing-and-running) +- [Monitoring UART](#monitoring-uart) +- [Hardware Setup](#hardware-setup) +- [Software Architecture](#software-architecture) + +## Overview + +Harmony combines Bluetooth audio streaming with the visual effects of a ferrofluid display reacting to the music's rhythm and bass. + +> [!WARNING] +> This project involves power electronics which can be dangerous. Take proper +> safety precautions and consult a qualified professional if unsure. + +## Features + +- Bluetooth audio streaming (using the SBC codec) +- Ferrofluid visualization synchronized with music +- Volume and playback controls +- Customizable LED lighting effects + +## Bill of Materials + +A detailed Bill of Materials (BOM) can be found in `BillOfMaterials.md`. This document lists all the necessary components for building Harmony. + +## Compiling the firmware + +The firmware for Harmony is built using CMake and requires the Raspberry Pi Pico SDK. + +1. Ensure you have the Pico SDK set up on your system. See the official Raspberry Pi Pico documentation for instructions: [https://www.raspberrypi.com/documentation/pico/getting-started/](https://www.raspberrypi.com/documentation/pico/getting-started/) + +2. Clone the swift-embedded-examples repository (if not already done): + ```bash + git clone https://github.com/apple/swift-embedded-examples.git + cd harmony + ``` + +3. Set the necessary environment variables: + ```bash + export TOOLCHAINS='' # e.g., gcc-arm-none-eabi + export PICO_BOARD=pico_w + export PICO_SDK_PATH='' # e.g., ../pico-sdk + export PICO_EXTRAS_PATH='' # e.g., ../pico-extras + export PICO_TOOLCHAIN_PATH='' # e.g., /usr/bin + ``` + +4. Generate the build files using CMake: + ```bash + cmake -B build -G Ninja . + ``` + +5. Build the firmware: + ```bash + cmake --build build + ``` + +## Flashing and running + +To flash the compiled firmware onto the Raspberry Pi Pico, you'll need OpenOCD. + +1. Connect your Pico to your computer using a USB cable and put it into BOOTSEL mode by holding the BOOTSEL button while plugging it in. + +2. Run OpenOCD with the appropriate configuration files: + ```bash + openocd -f interface/cmsis-dap.cfg -f target/rp2040.cfg -c "adapter speed 5000" -c "program build/app.elf verify reset exit" + ``` + +## Monitoring UART + +For debugging and monitoring, you can connect to the Pico's UART using a serial terminal program like `screen`. + +1. Identify the serial port connected to your Pico. You can usually find it using `ls /dev/cu.usbmodem*` on macOS or by checking the Device Manager on Windows. + +2. Connect to the UART using `screen`: + ```bash + screen /dev/cu.usbmodem<...> 115200 + ``` + (Replace `<...>` with the correct port identifier) + +## Hardware Setup + +> [!NOTE] +> This README is still under construction. + +## Software Architecture + +> [!NOTE] +> This README is still under construction. + +The firmware is designed with a modular architecture for efficient audio processing and visualization control. + +- **Bluetooth Handlers:** Manage the Bluetooth connection and incoming audio stream using the SBC codec. +- **SBC Ring Buffer:** Stores the decoded SBC audio data for further processing. +- **Audio Decoder:** Decodes the SBC encoded audio stream into PCM (Pulse Code Modulation) format. +- **PCM Ring Buffer:** Stores the decoded PCM audio data. +- **Audio Analyzer:** Analyzes the PCM audio data to extract relevant information like amplitude and frequency. This data is used to control the ferrofluid display. +- **Audio Driver:** Sends the PCM audio data to the DAC (Digital-to-Analog Converter). +- **DAC:** Converts the digital audio signal to an analog signal. +- **Amplifier:** Amplifies the analog audio signal to drive the speaker. +- **Electromagnet Driver:** Controls the electromagnet based on the analyzed audio data, creating the ferrofluid movements. diff --git a/harmony/Sources/Application/BridgingHeader.h b/harmony/Sources/Application/BridgingHeader.h new file mode 100644 index 00000000..74f7f34a --- /dev/null +++ b/harmony/Sources/Application/BridgingHeader.h @@ -0,0 +1,69 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#pragma once + +// C Stdlib +#include + +// Pico SDK +#include +#include +#include + +#include +#include +#include +#include +#include + +// Bluetooth SDK +#include +#include +#include +#include + +// PIO Programs +#include "I2S.pio.h" +#include "QuadratureEncoder.pio.h" +#include "WS2812.pio.h" + +#undef ISB +#include "arm_math.h" + +// Shims +static inline pio_hw_t* _pio0(void) { return pio0; } +static inline pio_hw_t* _pio1(void) { return pio1; } +static inline int32_t _errno() { return errno; } + +static const q15_t _samples_512[512] = { + // 10000 Hz + // 0, 32419, 9435, -29673, -18071, 24413, 25176, -17086, -30149, 8311, 32568, 1166, -32228, -10546, 29159, 19033, -23619, -25907, 16079, 30587, -7177, -32676, -2332, 31997, 11644, -28608, -19971, 22796, 26605, -15052, -30986, 6034, 32742, 3494, -31725, -12728, 28021, 20883, -21943, -27269, 14006, 31346, -4883, -32767, -4652, 31413, 13795, -27398, -21769, 21062, 27899, -12943, -31666, 3726, 32751, 5805, -31061, -14845, 26741, 22627, -20155, -28493, 11862, 31946, -2565, -32692, -6949, 30670, 15876, -26049, -23457, 19222, 29052, -10767, -32185, 1400, 32593, 8085, -30240, -16886, 25325, 24257, -18265, -29573, 9658, 32384, -233, -32452, -9211, 29771, 17876, -24568, -25026, 17285, 30057, -8537, -32541, -933, 32270, 10325, -29265, -18842, 23781, 25763, -16282, -30502, 7405, 32658, 2099, -32047, -11426, 28721, 19785, -22963, -26468, 15259, 30909, -6263, -32732, -3262, 31783, 12512, -28141, -20703, 22116, 27139, -14217, -31277, 5114, 32766, 4421, -31479, -13583, 27525, 21594, -21241, -27776, 13157, 31605, -3958, -32757, -5575, 31135, 14636, -26875, -22458, 20339, 28377, -12080, -31893, 2797, 32707, 6721, -30751, -15671, 26190, 23293, -19411, -28943, 10987, 32141, -1633, -32616, -7859, 30329, 16686, -25472, -24099, 18458, 29472, -9881, -32347, 466, 32483, 8987, -29868, -17679, 24722, 24875, -17483, -29963, 8762, 32513, 700, -32309, -10103, 29369, 18651, -23941, -25619, 16484, 30416, -7632, -32638, -1866, 32094, 11207, -28833, -19598, 23129, 26330, -15466, -30831, 6492, 32721, 3030, -31839, -12296, 28260, 20521, -22287, -27008, 14427, 31207, -5344, -32762, -4190, 31543, 13370, -27651, -21418, 21418, 27651, -13370, -31543, 4190, 32762, 5344, -31207, -14427, 27008, 22287, -20521, -28260, 12296, 31839, -3030, -32721, -6492, 30831, 15466, -26330, -23129, 19598, 28833, -11207, -32094, 1866, 32638, 7632, -30416, -16484, 25619, 23941, -18651, -29369, 10103, 32309, -700, -32513, -8762, 29963, 17483, -24875, -24722, 17679, 29868, -8987, -32483, -466, 32347, 9881, -29472, -18458, 24099, 25472, -16686, -30329, 7859, 32616, 1633, -32141, -10987, 28943, 19411, -23293, -26190, 15671, 30751, -6721, -32707, -2797, 31893, 12080, -28377, -20339, 22458, 26875, -14636, -31135, 5575, 32757, 3958, -31605, -13157, 27776, 21241, -21594, -27525, 13583, 31479, -4421, -32766, -5114, 31277, 14217, -27139, -22116, 20703, 28141, -12512, -31783, 3262, 32732, 6263, -30909, -15259, 26468, 22963, -19785, -28721, 11426, 32047, -2099, -32658, -7405, 30502, 16282, -25763, -23781, 18842, 29265, -10325, -32270, 933, 32541, 8537, -30057, -17285, 25026, 24568, -17876, -29771, 9211, 32452, 233, -32384, -9658, 29573, 18265, -24257, -25325, 16886, 30240, -8085, -32593, -1400, 32185, 10767, -29052, -19222, 23457, 26049, -15876, -30670, 6949, 32692, 2565, -31946, -11862, 28493, 20155, -22627, -26741, 14845, 31061, -5805, -32751, -3726, 31666, 12943, -27899, -21062, 21769, 27398, -13795, -31413, 4652, 32767, 4883, -31346, -14006, 27269, 21943, -20883, -28021, 12728, 31725, -3494, -32742, -6034, 30986, 15052, -26605, -22796, 19971, 28608, -11644, -31997, 2332, 32676, 7177, -30587, -16079, 25907, 23619, -19033, -29159, 10546, 32228, -1166, -32568, -8311, 30149, 17086, -25176, -24413, 18071, 29673, -9435, -32419, 0, 32419, 9435, -29673, -18071, 24413, 25176, -17086, -30149, 8311, 32568, 1166, -32228, -10546, 29159, 19033, -23619, -25907, 16079, 30587, -7177, -32676, -2332, 31997, 11644, -28608, -19971, 22796, 26605, -15052, -30986, 6034, 32742, 3494, -31725, -12728, 28021, 20883, -21943, -27269, 14006, 31346, -4883, -32767, -4652, 31413, 13795, -27398, -21769, 21062, 27899, -12943, -31666, 3726, 32751, 5805, -31061, -14845, 26741, 22627, -20155, -28493, 11862, 31946, -2565, -32692, -6949, 30670, 15876, -26049, -23457 + // 100 Hz + // 0, 466, 933, 1400, 1866, 2332, 2797, 3262, 3726, 4190, 4652, 5114, 5575, 6034, 6492, 6949, 7405, 7859, 8311, 8762, 9211, 9658, 10103, 10546, 10987, 11426, 11862, 12296, 12728, 13157, 13583, 14006, 14427, 14845, 15259, 15671, 16079, 16484, 16886, 17285, 17679, 18071, 18458, 18842, 19222, 19598, 19971, 20339, 20703, 21062, 21418, 21769, 22116, 22458, 22796, 23129, 23457, 23781, 24099, 24413, 24722, 25026, 25325, 25619, 25907, 26190, 26468, 26741, 27008, 27269, 27525, 27776, 28021, 28260, 28493, 28721, 28943, 29159, 29369, 29573, 29771, 29963, 30149, 30329, 30502, 30670, 30831, 30986, 31135, 31277, 31413, 31543, 31666, 31783, 31893, 31997, 32094, 32185, 32270, 32347, 32419, 32483, 32541, 32593, 32638, 32676, 32707, 32732, 32751, 32762, 32767, 32766, 32757, 32742, 32721, 32692, 32658, 32616, 32568, 32513, 32452, 32384, 32309, 32228, 32141, 32047, 31946, 31839, 31725, 31605, 31479, 31346, 31207, 31061, 30909, 30751, 30587, 30416, 30240, 30057, 29868, 29673, 29472, 29265, 29052, 28833, 28608, 28377, 28141, 27899, 27651, 27398, 27139, 26875, 26605, 26330, 26049, 25763, 25472, 25176, 24875, 24568, 24257, 23941, 23619, 23293, 22963, 22627, 22287, 21943, 21594, 21241, 20883, 20521, 20155, 19785, 19411, 19033, 18651, 18265, 17876, 17483, 17086, 16686, 16282, 15876, 15466, 15052, 14636, 14217, 13795, 13370, 12943, 12512, 12080, 11644, 11207, 10767, 10325, 9881, 9435, 8987, 8537, 8085, 7632, 7177, 6721, 6263, 5805, 5344, 4883, 4421, 3958, 3494, 3030, 2565, 2099, 1633, 1166, 700, 233, -233, -700, -1166, -1633, -2099, -2565, -3030, -3494, -3958, -4421, -4883, -5344, -5805, -6263, -6721, -7177, -7632, -8085, -8537, -8987, -9435, -9881, -10325, -10767, -11207, -11644, -12080, -12512, -12943, -13370, -13795, -14217, -14636, -15052, -15466, -15876, -16282, -16686, -17086, -17483, -17876, -18265, -18651, -19033, -19411, -19785, -20155, -20521, -20883, -21241, -21594, -21943, -22287, -22627, -22963, -23293, -23619, -23941, -24257, -24568, -24875, -25176, -25472, -25763, -26049, -26330, -26605, -26875, -27139, -27398, -27651, -27899, -28141, -28377, -28608, -28833, -29052, -29265, -29472, -29673, -29868, -30057, -30240, -30416, -30587, -30751, -30909, -31061, -31207, -31346, -31479, -31605, -31725, -31839, -31946, -32047, -32141, -32228, -32309, -32384, -32452, -32513, -32568, -32616, -32658, -32692, -32721, -32742, -32757, -32766, -32767, -32762, -32751, -32732, -32707, -32676, -32638, -32593, -32541, -32483, -32419, -32347, -32270, -32185, -32094, -31997, -31893, -31783, -31666, -31543, -31413, -31277, -31135, -30986, -30831, -30670, -30502, -30329, -30149, -29963, -29771, -29573, -29369, -29159, -28943, -28721, -28493, -28260, -28021, -27776, -27525, -27269, -27008, -26741, -26468, -26190, -25907, -25619, -25325, -25026, -24722, -24413, -24099, -23781, -23457, -23129, -22796, -22458, -22116, -21769, -21418, -21062, -20703, -20339, -19971, -19598, -19222, -18842, -18458, -18071, -17679, -17285, -16886, -16484, -16079, -15671, -15259, -14845, -14427, -14006, -13583, -13157, -12728, -12296, -11862, -11426, -10987, -10546, -10103, -9658, -9211, -8762, -8311, -7859, -7405, -6949, -6492, -6034, -5575, -5114, -4652, -4190, -3726, -3262, -2797, -2332, -1866, -1400, -933, -466, 0, 466, 933, 1400, 1866, 2332, 2797, 3262, 3726, 4190, 4652, 5114, 5575, 6034, 6492, 6949, 7405, 7859, 8311, 8762, 9211, 9658, 10103, 10546, 10987, 11426, 11862, 12296, 12728, 13157, 13583, 14006, 14427, 14845, 15259, 15671, 16079, 16484, 16886, 17285, 17679, 18071, 18458, 18842, 19222, 19598, 19971, 20339, 20703, 21062, 21418, 21769, 22116, 22458, 22796, 23129, 23457, 23781, 24099, 24413, 24722, 25026, 25325, 25619, 25907, 26190, 26468, 26741, 27008, 27269, 27525 + // Random + // -23267, -28627, -22857, 28240, 13800, 30726, -24151, 26079, -13476, -12798, 13052, 13852, -22637, -6154, -3815, -12039, 25243, -1585, -19939, -8784, -28265, -26459, -11751, 11175, 29945, 6392, 2017, -26312, 705, 3937, 15719, 15254, 2106, -19522, -11782, 14779, 17287, -573, -4131, 1598, -17359, -2321, 29406, 10983, -1820, 10484, 2547, 21179, -8994, -2079, -27106, 10036, 24048, 20759, -22663, 30870, -23019, -6484, 10645, 28446, 5694, 29587, 2716, 28330, -31127, -24224, 11747, 4653, 25607, 2060, -13584, 18776, 3286, 4279, -23640, -19751, -29165, -26461, 1009, -26, 24934, 1737, 25904, 13741, 26638, -9855, 24739, 1151, 1288, 27717, -14507, 31321, 14853, 30335, 6779, -25100, -31174, 21443, -26019, 10443, -16230, -7792, -17010, 22850, 26999, -22520, -3965, -28776, 20422, 32717, -13509, 615, 3751, 14471, -31442, 331, -4401, -13910, -9235, -22272, -20351, -18042, -12559, -26528, -21608, -10967, -10773, 7318, 29416, 11987, -18385, 18446, -7565, -1826, 5991, -1501, -18115, 11245, -26602, 26281, -27812, 606, 10535, -24038, -9512, -28585, 17743, -23902, -5358, 14428, -6620, -11902, 29990, -1351, 229, 505, -20824, 2948, -10728, -8917, -30529, -8611, 10386, 8405, 29667, 1259, -933, 27763, 15110, -7538, 2579, -2260, 16781, 3587, 987, 10675, -12646, -26337, -15515, 10036, -10923, 19627, 23472, -19409, -25964, -15233, -12738, -17699, 21449, 12398, 31055, 3204, 10120, -5165, 28072, 3534, 8925, 8718, 12006, -14307, 3727, -2169, -9054, -32745, -27455, -2414, -30904, -27071, 29227, -1711, -32026, 16670, 4041, 11990, -6825, 19509, -10102, -5291, 14789, 13031, 27966, 3079, -30755, 18652, 8718, 17518, 27673, -21518, 26194, -7598, 8682, -30879, -26492, 25542, -683, 29625, -18786, -11302, 14478, -25560, 2884, 16250, -17512, -5658, 10514, -32180, 19268, 13426, -17484, 15865, 12636, 5169, -8869, 24014, 26874, 21372, -31006, 1717, 28024, 1712, 24846, 28640, 14211, 38, -32053, -23226, 4101, 10121, 17271, 67, 20471, -265, 17592, -17786, -14468, 836, 24620, -10300, -31533, 18124, -27177, 8895, -14010, 3583, 4616, -370, -1692, -24560, 7943, -15712, -548, 22854, 6585, -6472, -3219, -5611, -6953, 27266, 32132, -9528, 27921, -5179, 25555, -1361, 7012, 20560, -9756, 20686, -28590, -9269, 23420, -6441, -26236, -26378, -16303, -4482, -32143, 879, 28814, -15829, 13154, -32500, 10497, -15844, 27537, 12933, 24326, -8289, -25445, 5613, 3783, 19422, 13656, -28388, 10940, 23522, -8935, -10820, -23230, -6261, 8161, 16840, -23319, -14576, -20202, 9909, 14492, 17965, 830, 2347, -11266, -8371, -9564, -4490, 10044, -1236, -14600, 27197, 31869, 9030, 28833, 27173, -31152, 4385, 31150, 24565, -17190, 15066, 31896, -18756, 20622, -3244, -11415, 28206, 824, 11787, 17574, -32303, 23499, -23228, 24135, 29557, 11106, -12688, -1300, 16113, 4289, 8128, -10194, 8058, -24081, -13334, -22492, -20062, -21175, -31592, 11336, 26112, -14411, 29755, 8660, -19490, -11577, 17292, -8281, -2277, 3554, 4593, -2709, -5323, -8179, 10204, -17385, 18261, -301, 12645, -14775, -12287, -16381, 29835, -25494, 17111, 17776, 19593, 2789, 17686, -30471, -23637, -29774, -16137, 14721, -22862, 4031, -29039, -22289, 11175, 10518, -15373, 695, -23720, -24736, 14425, -9407, -25318, 2501, -5415, -21451, -20293, -19032, 27457, 28601, -11034, -9415, -8598, -30687, -26219, 31028, 9152, -20955, -30113, 31446, -23025, -14905, 384, -738, -871, 43, -5814, -6575, -12151, 13081, 14489, -3276, -24880, -27900, 5863, 7411, -21359, -26944, -9252, 15265, -12092, -8907, 17623, -13430, -3138, 23092, 10217, 13241, 1005, 11791, -12903, -12487, -13885, -8748, -7247, -12551, 1132, 16750, 29330, 9297, -22940, -2912, 7094, -3066, 20032, -13050, -26769, -32399, -23202, 13843, 18265 + // Mixed content + 0, 20065, 20774, 14478, 7238, -8053, -16118, -4051, 7534, 6075, 3660, -2261, -17285, -20472, -3071, 12656, 19351, 24507, 16764, -5807, -17284, -11343, -5570, 1128, 11683, 7992, -8598, -12980, -4950, 2096, 14516, 28344, 21538, 414, -11404, -16217, -18132, -4858, 13563, 13604, 3724, 32, -4475, -6513, 8151, 23989, 20258, 8508, -1704, -17249, -25082, -10049, 8615, 14091, 16355, 14067, -790, -9122, 1929, 12576, 13845, 15030, 7381, -13670, -23561, -13069, -737, 10830, 25231, 24491, 6096, -4544, -2850, -1845, 4676, 16574, 12215, -5964, -14129, -13027, -9898, 6065, 27202, 28380, 14823, 5084, -5222, -13918, -4550, 11428, 11365, 3816, -424, -9456, -14133, 1869, 21552, 25413, 22684, 15389, -4598, -19263, -11696, 681, 5865, 12445, 12521, -2447, -10764, -435, 10586, 17554, 26412, 21788, -1044, -16380, -15529, -11888, -1615, 16659, 20446, 7035, -458, -336, -1161, 7875, 23559, 21483, 4371, -7233, -15614, -21255, -8114, 14640, 21499, 16945, 13025, 2104, -8840, -766, 13847, 14491, 9531, 3725, -12102, -23332, -11514, 7094, 16637, 24423, 24493, 6412, -9454, -6591, -251, 3352, 11817, 11634, -5689, -16853, -11022, -2732, 8793, 26686, 29578, 11646, -3250, -9050, -14188, -8191, 9209, 13344, 2263, -4056, -7008, -10299, 1348, 22278, 26522, 16272, 6387, -8487, -23100, -16775, 1463, 8630, 9698, 10245, -523, -11784, -3252, 12037, 16701, 18293, 14505, -5708, -23916, -20524, -9316, -5, 14238, 20734, 7077, -5948, -4177, -816, 3751, 15842, 16850, -1764, -16792, -19304, -19174, -8705, 14023, 23853, 14256, 5119, -2037, -11764, -8128, 8128, 11764, 2037, -5119, -14256, -23853, -14023, 8705, 19174, 19304, 16792, 1764, -16850, -15842, -3751, 816, 4177, 5948, -7077, -20734, -14238, 5, 9316, 20524, 23916, 5708, -14505, -18293, -16701, -12037, 3252, 11784, 523, -10245, -9698, -8630, -1463, 16775, 23100, 8487, -6387, -16272, -26522, -22278, -1348, 10299, 7008, 4056, -2263, -13344, -9209, 8191, 14188, 9050, 3250, -11646, -29578, -26686, -8793, 2732, 11022, 16853, 5689, -11634, -11817, -3352, 251, 6591, 9454, -6412, -24493, -24423, -16637, -7094, 11514, 23332, 12102, -3725, -9531, -14491, -13847, 766, 8840, -2104, -13025, -16945, -21499, -14640, 8114, 21255, 15614, 7233, -4371, -21483, -23559, -7875, 1161, 336, 458, -7035, -20446, -16659, 1615, 11888, 15529, 16380, 1044, -21788, -26412, -17554, -10586, 435, 10764, 2447, -12521, -12445, -5865, -681, 11696, 19263, 4598, -15389, -22684, -25413, -21552, -1869, 14133, 9456, 424, -3816, -11365, -11428, 4550, 13918, 5222, -5084, -14823, -28380, -27202, -6065, 9898, 13027, 14129, 5964, -12215, -16574, -4676, 1845, 2850, 4544, -6096, -24491, -25231, -10830, 737, 13069, 23561, 13670, -7381, -15030, -13845, -12576, -1929, 9122, 790, -14067, -16355, -14091, -8615, 10049, 25082, 17249, 1704, -8508, -20258, -23989, -8151, 6513, 4475, -32, -3724, -13604, -13563, 4858, 18132, 16217, 11404, -414, -21538, -28344, -14516, -2096, 4950, 12980, 8598, -7992, -11683, -1128, 5570, 11343, 17284, 5807, -16764, -24507, -19351, -12656, 3071, 20472, 17285, 2261, -3660, -6075, -7534, 4051, 16118, 8053, -7238, -14478, -20774, -20065, 0, 20065, 20774, 14478, 7238, -8053, -16118, -4051, 7534, 6075, 3660, -2261, -17285, -20472, -3071, 12656, 19351, 24507, 16764, -5807, -17284, -11343, -5570, 1128, 11683, 7992, -8598, -12980, -4950, 2096, 14516, 28344, 21538, 414, -11404, -16217, -18132, -4858, 13563, 13604, 3724, 32, -4475, -6513, 8151, 23989, 20258, 8508, -1704, -17249, -25082, -10049, 8615, 14091, 16355, 14067, -790, -9122, 1929, 12576, 13845, 15030, 7381, -13670, -23561, -13069, -737, 10830, 25231, 24491, 6096 +}; + +const q15_t* samples_512(void) { + return _samples_512; +} + +static const q15_t _samples_64[64] = { + 0, 16209, 4717, -14836, -9035, 12206, 12588, -8543, -15074, 4155, 16284, 583, -16114, -5273, 14579, 9516, -11809, -12953, 8039, 15293, -3588, -16338, -1166, 15998, 5822, -14304, -9985, 11398, 13302, -7526, -15493, 3017, 16371, 1747, -15862, -6364, 14010, 10441, -10971, -13634, 7003, 15673, -2441, -16383, -2326, 15706, 6897, -13699, -10884, 10531, 13949, -6471, -15833, 1863, 16375, 2902, -15530, -7422, 13370, 11313, -10077, -14246, 5931, 15973 +}; + +const q15_t* samples_64(void) { + return _samples_64; +} + diff --git a/harmony/Sources/Application/Button.swift b/harmony/Sources/Application/Button.swift new file mode 100644 index 00000000..03867b54 --- /dev/null +++ b/harmony/Sources/Application/Button.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +struct Button: ~Copyable { + var pin: UInt32 + + init(pin: UInt32, onPress callback: @convention(c) (UInt32, UInt32) -> Void) { + self.pin = pin + + gpio_init(pin) + gpio_set_dir(pin, false) // input + gpio_pull_up(pin) // pull up the pin + gpio_set_irq_enabled_with_callback( + pin, UInt32(GPIO_IRQ_EDGE_FALL.rawValue), true, callback) + } + + deinit { + gpio_deinit(self.pin) + } +} + diff --git a/harmony/Sources/Application/ButtonTimes.swift b/harmony/Sources/Application/ButtonTimes.swift new file mode 100644 index 00000000..63d53b47 --- /dev/null +++ b/harmony/Sources/Application/ButtonTimes.swift @@ -0,0 +1,42 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +struct ButtonTimes { + typealias Times = ( + UInt32, UInt32, UInt32, UInt32, + UInt32, UInt32, UInt32, UInt32, + UInt32, UInt32, UInt32, UInt32) + static let count = 12 + static let rawSize = MemoryLayout.size + static let reboundSize = Self.count * MemoryLayout.size + + private var times: Times = + (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + + subscript(_ index: some FixedWidthInteger) -> UInt32 { + get { + precondition(Self.rawSize == Self.reboundSize) + return withUnsafePointer(to: self.times) { + $0.withMemoryRebound(to: UInt32.self, capacity: Self.count) { + $0[Int(index)] + } + } + } + set { + precondition(Self.rawSize == Self.reboundSize) + return withUnsafeMutablePointer(to: &self.times) { + $0.withMemoryRebound(to: UInt32.self, capacity: Self.count) { + $0[Int(index)] = newValue + } + } + } + } +} diff --git a/harmony/Sources/Application/LEDStrip.swift b/harmony/Sources/Application/LEDStrip.swift new file mode 100644 index 00000000..f8f058b5 --- /dev/null +++ b/harmony/Sources/Application/LEDStrip.swift @@ -0,0 +1,92 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +struct LEDStrip: ~Copyable { + var dataPin: UInt32 + var ledCount: Int + + var pio: UInt32 + var pioSm: UInt32 + var pioHw: PIO + var pioOffset: UInt32 + + init( + dataPin: UInt32, + ledCount: Int, + // FIXME: change to `PIO` + pio: UInt32, + pioSm: UInt32 + ) { + self.dataPin = dataPin + self.ledCount = ledCount + self.pio = pio + self.pioSm = pioSm + self.pioHw = + switch self.pio { + case 0: _pio0() + case 1: _pio1() + default: fatalError("Invalid PIO index") + } + self.pioOffset = 0 + + pio_gpio_init(self.pioHw, self.dataPin) + + // FIXME: lower quadrature_encoder_program_init max_step_rate + pio_sm_claim(self.pioHw, self.pioSm) + self.pioOffset = withUnsafePointer(to: ws2812_program) { + UInt32(pio_add_program(self.pioHw, $0)) + } + ws2812_program_init(self.pioHw, self.pioSm, self.pioOffset, self.dataPin, 800000, false) + } + + deinit { + withUnsafePointer(to: ws2812_program) { + pio_remove_program_and_unclaim_sm($0, self.pioHw, self.pioSm, self.pioOffset) + } + + gpio_deinit(self.dataPin) + } +} + +extension LEDStrip { + // mutating func putPixel(pixelGRB: UInt32) { + // pio_sm_put_blocking(self.pioHw, self.pioSm, pixelGRB << 8) + // } + + // func urgb_u32(_ r: UInt8, _ g: UInt8, _ b: UInt8) -> UInt32 { + // (UInt32(r) << 8) | (UInt32(g) << 16) | UInt32(b) + // } + + // mutating func patternSnakes(t: UInt32) { + // for i in 0..> 1)) % 64 + // if x < 10 { + // putPixel(pixelGRB: urgb_u32(0xff, 0, 0)) + // } else if x >= 15 && x < 25 { + // putPixel(pixelGRB: urgb_u32(0, 0xff, 0)) + // } else if x >= 30 && x < 40 { + // putPixel(pixelGRB: urgb_u32(0, 0, 0xff)) + // } else { + // putPixel(pixelGRB: 0) + // } + // } + // } + + mutating func setColor(red: UInt8, green: UInt8, blue: UInt8) { + for _ in 0.. StreamingSerialMessage, + terminator: StaticString = "\n" +) { + _ = message() + SerialPrinter().write(terminator) +} + +/// An implementation of `CharacterPrinter` that calls `putchar` to write to serial. +struct SerialPrinter: CharacterPrinter { + func write(rawByte: UInt8) { + _ = putchar(CInt(rawByte)) + } + + func write(contentsOf: Self) { + // Don't need to handle nested SerialPrinter objects: they will have + // already written out to serial. + } +} + +// String interpolation objects cast to this type will be streamed +// to serial via calls to `putchar`. +typealias StreamingSerialMessage = StreamingMessage + +/// This file provides functionality for logging interpolated strings without +/// requiring construction of String types (which are not available in Embedded +/// Swift). + +/// Types that implement `Loggable` are able to be logged using the +/// `StreamingInterpolation` mechanisms. +/// +/// The `write` function should write a human-readable instance of the object to +/// the passed-in `Printer` type: +/// +/// struct MyType: Loggable { +/// func write(to printer: Printer) { +/// printer.write("A static string") +/// printer.write(42) +/// printer.write(anyLoggableItem) +/// printer.write("A more \(complex) string \(interpolation)") +/// } +/// } +protocol Loggable: ~Copyable { + func write(to: Printer) +} + +/// A type that supports printing individual characters. +/// +/// Characters can either be streamed directly out to a log (e.g. stdout), or +/// buffered any manually written out by the user. +protocol CharacterPrinter { + /// Initialize a new instance. + /// + /// Unfortunately, Swift calls this from within the compiler's generated + /// code, with a fresh object created each time string interpolation is + /// used, and no chance to have any instance variables. + init() + + /// Write a single byte to output. + func write(rawByte: UInt8) + + /// Write the contents of the given `CharacterPrinter` to this + /// `CharacterPrinter`. + /// + /// This method is required to be implemented so that implementors of the + /// `Loggable` protocol can themselves use String Interpolation. + /// + /// Implementations of `CharacterPrinter` that don't buffer anything (for + /// example, if they just forward characters directly to `stdout`) need not + /// do anything here. However, implementations of `CharacterPrinter` that + /// are attempting to buffer the text will need to append the contents of + /// the child `CharacterPrinter` into the parent `CharacterPrinter`. + func write(contentsOf: Self) +} + +// Convenience functions for writing basic objects to a CharacterPrinter. +// +// Most types should either just implement `Loggable` (preferred), or implement +// their type as an overload to `StreamingInterpolation.appendInterpolation` +// directly (for generic types or types requiring additional parameters). +extension CharacterPrinter { + /// Write the given object that implements the `Loggable` interface to the + /// printer. + func write(_ value: some Loggable) { + value.write(to: self) + } + + /// Write an integer to the printer. + func write(_ value: some FixedWidthInteger) { + value.write(to: self) + } + + /// Write the given buffer to the printer. + /// + /// This function will print the entire buffer, including NUL bytes and + /// anything following them. To print NUL-terminated strings, see the + /// overload `write(nulTerminated)`. + func write(contentsOf buffer: UnsafeBufferPointer) { + self.write(contentsOf: UnsafeRawBufferPointer(buffer)) + } + + /// Write the given buffer to the printer. + /// + /// This function will print the entire buffer, including NUL bytes and + /// anything following them. To print NUL-terminated strings, see the + /// overload `write(nulTerminated)`. + @inline(never) // avoid aggressive inlining of non-perf-sensitive code + func write(contentsOf buffer: UnsafeRawBufferPointer) { + for c in buffer { + self.write(rawByte: c) + } + } + + /// Write a NULL-terminated (C style) string to the printer. + @inline(never) // avoid aggressive inlining of non-perf-sensitive code + func write(nullTerminated value: UnsafeBufferPointer) { + for c in value { + if c == 0 { + break + } + self.write(rawByte: UInt8(c)) + } + } + + // Write the given interpolated string to this character printer. + // + // This allows implementations of `Loggable` to themselves use interpolated + // strings: + // + // ``` + // class MyClass: Loggable { + // func write(printer: P) { + // printer.write("hello, \(self.world)") + // } + // } + // ``` + @_disfavoredOverload + func write(_ value: @autoclosure () -> StreamingMessage) { + self.write(contentsOf: value().printer) + } +} + +// Loggable implementation for various types. +extension Bool: Loggable { + func write(to printer: Printer) { + if self { + printer.write("true") + } else { + printer.write("false") + } + } +} + +extension StaticString: Loggable { + func write(to printer: Printer) { + self.withUTF8Buffer { + printer.write(contentsOf: $0) + } + } +} + +extension UnsafeRawBufferPointer: Loggable { + func write(to printer: Printer) { + let base = UInt(bitPattern: self.baseAddress) + printer.write("\(hex: base), count: \(self.count)") + } +} + +extension UnsafeMutableRawBufferPointer: Loggable { + func write(to printer: Printer) { + printer.write(UnsafeRawBufferPointer(self)) + } +} + +extension CharacterPrinter { + // Write the given UInt64 to a CharacterPrinter. + // + // We use this function so that all integer sizes can reuse the same version of + // the code in the compiled binary. + @inline(never) + fileprivate func write(value: UInt64, isNegative: Bool, radix: Int) { + precondition(radix == 10 || radix == 16) + + // Special case for zero, which otherwise would have no digits printed for + // it in the algorithm below. + if value == 0 { + if radix == 16 { + self.write("0x0") + } else { + self.write("0") + } + return + } + + // Convert the given digit to its ASCII code. + func _ascii(_ digit: UInt8) -> UInt8 { + if digit < 10 { + UInt8(("0" as Unicode.Scalar).value) + digit + } else { + UInt8(("a" as Unicode.Scalar).value) + (digit - 10) + } + } + + // Render to a temporary buffer. + // + // Worst case: 64-bit type and radix 10, requires `ceil(log_10(2**64)) == 20` + // characters to render. We use another for the negative sign, and another two + // for the `0x` prefix on base 16. + withUnsafeTemporaryAllocation(byteCount: 32, alignment: 1) { buffer in + var index = buffer.count - 1 + var value = value + while value != 0 { + let (quotient, remainder) = value.quotientAndRemainder( + dividingBy: UInt64(radix)) + buffer[index] = _ascii(UInt8(truncatingIfNeeded: remainder)) + index -= 1 + value = quotient + } + if radix == 16 { + buffer[index - 1] = UInt8(("0" as Unicode.Scalar).value) + buffer[index - 0] = UInt8(("x" as Unicode.Scalar).value) + index -= 2 + } + if isNegative { + buffer[index] = UInt8(("-" as Unicode.Scalar).value) + index -= 1 + } + let start = index + 1 + let end = buffer.count - 1 + let count = end - start + 1 + self.write( + contentsOf: + UnsafeBufferPointer( + start: buffer.baseAddress?.advanced(by: start).assumingMemoryBound( + to: UInt8.self), count: count)) + } + } +} + +// Functionality to write FixedWidthInteger types to a CharacterPrinter. +extension FixedWidthInteger { + // Write a FixedWidthInteger to the given CharacterPrinter in the given radix. + func write(to printer: some CharacterPrinter, radix: Int = 10) { + precondition(radix == 10 || radix == 16) + precondition(Self.bitWidth <= 64) + + let isNegative = Self.isSigned && self < (0 as Self) + let value = self.magnitude + printer.write(value: UInt64(value), isNegative: isNegative, radix: radix) + } +} + +// Write a `StringInterpolation` to the given `CharacterPrinter` type. +// +// That is, given a type `Printer` implementing the protocol +// `CharacterPrinter`, we will convert types used in string interpolations into +// a form that can be written out to the Printer. +// +// See Swift's documentation on `StringInterpolationProtocol` for details. +struct StreamingInterpolation: StringInterpolationProtocol +{ + typealias StringLiteralType = StaticString + var printer: P = P() + + init(literalCapacity: Int, interpolationCount: Int) {} + + // Write a string literal. + mutating func appendLiteral(_ literal: StaticString) { + printer.write(literal) + } + + // Write a StaticString interpolated variable. + mutating func appendInterpolation( + _ value: StaticString + ) { + printer.write(value) + } + + // Write a basic integer type (Int16, UInt64, etc). + mutating func appendInterpolation( + _ value: some FixedWidthInteger + ) { + printer.write(value) + } +} + +extension StreamingInterpolation { + mutating func appendInterpolation(cString pointer: UnsafePointer?) { + guard var pointer else { + self.printer.write("nil") + return + } + while pointer.pointee != 0 { + self.printer.write(rawByte: pointer.pointee) + pointer = pointer.advanced(by: 1) + } + } + + mutating func appendInterpolation(cString pointer: UnsafePointer?) { + guard var pointer else { + self.printer.write("nil") + return + } + while pointer.pointee != 0 { + self.printer.write(rawByte: UInt8(pointer.pointee)) + pointer = pointer.advanced(by: 1) + } + } + + mutating func appendInterpolation(cString buffer: UnsafeBufferPointer?) + { + guard let buffer else { + self.printer.write("nil") + return + } + for byte in buffer { + guard byte != 0 else { break } + self.printer.write(rawByte: byte) + } + } + +} + +extension StreamingInterpolation { + // Write a basic integer type in hex. + // + // This can be used by writing `"the value in hex is \(hex: value)."`. + mutating func appendInterpolation( + hex value: @autoclosure () -> Word + ) { + value().write(to: printer, radix: 16) + } + + // Write an object conforming to the `Loggable` protocol. + mutating func appendInterpolation( + _ value: @autoclosure () -> some Loggable + ) { + printer.write(value()) + } + + // Write a pointer's value. + mutating func appendInterpolation(_ value: some _Pointer) { + appendInterpolation(hex: UInt(bitPattern: value)) + } + + // Write a generic `UnsafeBufferPointer` value to the stream. + mutating func appendInterpolation( + _ value: @autoclosure () -> UnsafeBufferPointer + ) { + appendInterpolation(hex: UInt(bitPattern: value().baseAddress)) + appendInterpolation(", count: ") + appendInterpolation(value().count) + } + + // Write a generic `UnsafeMutableBufferPointer` value to the stream. + mutating func appendInterpolation( + _ value: @autoclosure () -> UnsafeMutableBufferPointer + ) { + appendInterpolation(UnsafeBufferPointer(value())) + } + +} + +// Initiates a StringInterpolation. +// +// See Swift's documentation on `StringInterpolationProtocol` for details. +struct StreamingMessage: ExpressibleByStringInterpolation { + typealias StringInterpolation = StreamingInterpolation

+ + init(printer: P, stringInterpolation: StreamingInterpolation

) { + self.interpolation = stringInterpolation + } + + init(stringInterpolation: StreamingInterpolation

) { + self.interpolation = stringInterpolation + } + + init(stringLiteral value: StaticString) { + self.interpolation = StreamingInterpolation( + literalCapacity: 0, interpolationCount: 0) + self.interpolation.appendLiteral(value) + } + + var printer: P { interpolation.printer } + + private var interpolation: StreamingInterpolation

+} diff --git a/harmony/Sources/Application/Main.swift b/harmony/Sources/Application/Main.swift new file mode 100644 index 00000000..23eb9897 --- /dev/null +++ b/harmony/Sources/Application/Main.swift @@ -0,0 +1,387 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +// swift-format-ignore: AlwaysUseLowerCamelCase, NeverForceUnwrap + +struct A2DPStreamEndpoint { + var a2dp_local_seid: UInt8 = 0 + var media_sbc_codec_configuration: (UInt8, UInt8, UInt8, UInt8) = (0, 0, 0, 0) +} + +var hci_event_callback_registration = btstack_packet_callback_registration_t() +var stream_endpoint = A2DPStreamEndpoint() + +// we support all configurations with bitpool 2-53 +var media_sbc_codec_capabilities: (UInt8, UInt8, UInt8, UInt8) = ( + //(AVDTP_SBC_44100 << 4) | AVDTP_SBC_STEREO, + 0xFF, + //(AVDTP_SBC_BLOCK_LENGTH_16 << 4) | (AVDTP_SBC_SUBBANDS_8 << 2) | AVDTP_SBC_ALLOCATION_METHOD_LOUDNESS, + 0xFF, + 2, 53 +) + +// FIXME: use Vector +let sdp_avdtp_sink_service_buffer = UnsafeMutableRawBufferPointer.allocate( + byteCount: 150, + alignment: MemoryLayout.alignment) +// FIXME: use Vector +let sdp_avrcp_target_service_buffer = UnsafeMutableRawBufferPointer.allocate( + byteCount: 150, + alignment: MemoryLayout.alignment) +// FIXME: use Vector +let sdp_avrcp_controller_service_buffer = + UnsafeMutableRawBufferPointer.allocate( + byteCount: 200, + alignment: MemoryLayout.alignment) +// FIXME: use Vector +let device_id_sdp_service_buffer = UnsafeMutableRawBufferPointer.allocate( + byteCount: 100, + alignment: MemoryLayout.alignment) + +// FIXME: use `Vector` +func buttonCallback(pin: UInt32, event: UInt32) { + guard event & UInt32(GPIO_IRQ_EDGE_FALL.rawValue) != 0 else { return } + Application.shared.buttonPressed(pin: pin) +} + +let BUFFER_SAMPLE_CAPACITY = 512 + + +let LED_STRIP_LED_COUNT = 20 + +let MUTE_BUTTON_PIN: UInt32 = 6 +let ROTARY_ENCODER_A_PIN : UInt32 = 7 +let ROTARY_ENCODER_B_PIN: UInt32 = 8 +let PLAY_PAUSE_BUTTON_PIN: UInt32 = 9 +let PREVIOUS_BUTTON_PIN: UInt32 = 10 +let NEXT_BUTTON_PIN: UInt32 = 11 +let LED_STRIP_PIN: UInt32 = 17 +let EM_DRIVE_PIN: UInt32 = 18 + +let WIRELESS_LED_PIN = UInt32(CYW43_WL_GPIO_LED_PIN) + +struct Application: ~Copyable { + var audioEngine = AudioEngine() + + var audioAnalyzer = AudioAnalyzer() + + let wirelessLedBlinkPeriodMs: UInt32 = 1000 + var wirelessLedBlinkTimer = btstack_timer_source_t() + var wirelessLedBlinkState = false + + let ledStripUpdatePeriodMs: UInt32 = 1000 + var ledStripUpdateTimer = btstack_timer_source_t() + var ledStrip = LEDStrip( + dataPin: LED_STRIP_PIN, + ledCount: LED_STRIP_LED_COUNT, + pio: 0, + pioSm: 1) + + let volumeKnobSamplerPeriodMs: UInt32 = 100 + var volumeKnobSamplerTimer = btstack_timer_source_t() + var volumeKnob = QuadratureEncoder( + pinA: ROTARY_ENCODER_A_PIN, + pinB: ROTARY_ENCODER_B_PIN, + pio: 1, + pioSm: 0) + + var previousPressTimes = ButtonTimes() + + var muteButton = Button( + pin: MUTE_BUTTON_PIN, + onPress: buttonCallback) + var nextButton = Button( + pin: NEXT_BUTTON_PIN, + onPress: buttonCallback) + var playPauseButton = Button( + pin: PLAY_PAUSE_BUTTON_PIN, + onPress: buttonCallback) + var previousButton = Button( + pin: PREVIOUS_BUTTON_PIN, + onPress: buttonCallback) + + mutating func run() { + stdio_init_all() + i2c_init() + + multicore_launch_core1 { + log("core1_main") + Application.shared.audioAnalyzer.run() + } + + log("Hello!") + log("sys clock running at \(clock_get_hz(clk_sys)) Hz") + log("Initializing cyw43_driver") + precondition(cyw43_arch_init() == 0, "cyw43_arch_init failed") + wirelessLedBlink(count: 2) + + gpio_init(EM_DRIVE_PIN) + gpio_set_dir(EM_DRIVE_PIN, true) + + var sdp = ServiceDiscoveryProtocol() + _setup_demo(&sdp) + + + // turn on! + log("Starting BTstack ...") + hci_power_control(HCI_POWER_ON) + wirelessLedBlink(count: 2) + log("[main] Started, starting btstack run loop") + + btstack_run_loop_set_timer_handler(&self.volumeKnobSamplerTimer) { timer in + guard let timer else { return } + Application.shared.volumeKnobSamplerHandler(&timer.pointee) + } + btstack_run_loop_set_timer(&self.volumeKnobSamplerTimer, volumeKnobSamplerPeriodMs) + btstack_run_loop_add_timer(&self.volumeKnobSamplerTimer) + + btstack_run_loop_set_timer_handler(&self.ledStripUpdateTimer) { timer in + guard let timer else { return } + Application.shared.ledStripUpdateHandler(&timer.pointee) + } + btstack_run_loop_set_timer(&self.ledStripUpdateTimer, ledStripUpdatePeriodMs) + btstack_run_loop_add_timer(&self.ledStripUpdateTimer) + + btstack_run_loop_set_timer_handler(&self.wirelessLedBlinkTimer) { timer in + guard let timer else { return } + Application.shared.wirelessLedBlinkHandler(&timer.pointee) + } + btstack_run_loop_set_timer(&self.wirelessLedBlinkTimer, wirelessLedBlinkPeriodMs) + btstack_run_loop_add_timer(&self.wirelessLedBlinkTimer) + + + btstack_run_loop_execute() // btstack_run_loop_execute never returns + _ = sdp // make sure SDP lives until the runloop exits + } +} + +// Timer handlers +extension Application { + mutating func volumeKnobSamplerHandler(_ timer: inout btstack_timer_source_t) { + let scaleFactor: Int32 = 5 + audioEngine.adjustVolume(by: Application.shared.volumeKnob.delta() * scaleFactor) + btstack_run_loop_set_timer(&timer, volumeKnobSamplerPeriodMs) + btstack_run_loop_add_timer(&timer) + } + + mutating func ledStripUpdateHandler(_ timer: inout btstack_timer_source_t) { + ledStrip.setColor( + red: .random(in: 0...255), + green: .random(in: 0...255), + blue: .random(in: 0...255)) + btstack_run_loop_set_timer(&timer, ledStripUpdatePeriodMs) + btstack_run_loop_add_timer(&timer) + } + + mutating func wirelessLedBlinkHandler(_ timer: inout btstack_timer_source_t) { + self.wirelessLedBlinkState.toggle() + cyw43_arch_gpio_put(WIRELESS_LED_PIN, self.wirelessLedBlinkState) + btstack_run_loop_set_timer(&timer, wirelessLedBlinkPeriodMs) + btstack_run_loop_add_timer(&timer) + } +} + +// Button press callbacks +extension Application { + // FIXME: use `time_us_64` + // This is a particularly large debounce time + static let buttonDebounceTimeMs = 150 + mutating func buttonPressed(pin: UInt32) { + let currentTime = to_ms_since_boot(get_absolute_time()) + guard currentTime - previousPressTimes[pin] > Self.buttonDebounceTimeMs else { + log("soft debounce \(pin)") + return + } + previousPressTimes[pin] = currentTime + + switch pin { + case MUTE_BUTTON_PIN: + self.toggleMute() + case NEXT_BUTTON_PIN: + self.nextTrack() + case PLAY_PAUSE_BUTTON_PIN: + self.playPauseTrack() + case PREVIOUS_BUTTON_PIN: + self.previousTrack() + default: + // ignore + break + } + } + + mutating func toggleMute() { + self.audioEngine.toggleMute() + } + + mutating func nextTrack() { + log("avrcp_controller_forward") + avrcp_controller_forward(avrcp_connection.avrcp_cid) + } + + mutating func playPauseTrack() { + // FIXME: this state management is almost certainly wrong + if audioEngine.running { + log("avrcp_controller_stop") + avrcp_controller_pause(avrcp_connection.avrcp_cid) + } else { + log("avrcp_controller_play") + avrcp_controller_play(avrcp_connection.avrcp_cid) + } + } + + mutating func previousTrack() { + log("avrcp_controller_backward") + avrcp_controller_backward(avrcp_connection.avrcp_cid) + } +} + +extension Application { + func wirelessLedBlink(count: UInt32) { + for _ in 0.. Int32 { + let value = quadrature_encoder_get_count(self.pioHw, self.pioSm) + self.previousValue = value + return value + } + + mutating func delta() -> Int32 { + let value = quadrature_encoder_get_count(self.pioHw, self.pioSm) + // NOTE: Thanks to two's complement arithmetic `delta`` will always be + // correct even when `value`` wraps around `Int32.max` / `Int32.min`. + let delta = value &- self.previousValue + self.previousValue = value + return delta + } +} diff --git a/harmony/Sources/Application/Stubs.swift b/harmony/Sources/Application/Stubs.swift new file mode 100644 index 00000000..b6e8deb0 --- /dev/null +++ b/harmony/Sources/Application/Stubs.swift @@ -0,0 +1,39 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +// Embedded Swift currently requires posix_memalign, but the C libraries in the +// Pico SDK do not provide it. Let's implement it and forward the calls to +// aligned_alloc(3). +@_cdecl("posix_memalign") +public func posix_memalign( + memptr: UnsafeMutablePointer, + alignment: size_t, + size: size_t +) -> Int32 { + if let allocation = aligned_alloc(alignment, size) { + memptr.pointee = allocation + return 0 + } + return _errno() +} + +// FIXME: document +@_cdecl("swift_isEscapingClosureAtFileLocation") +func swift_isEscapingClosureAtFileLocation( + object: UnsafeRawPointer, + filename: UnsafePointer, + filenameLength: Int32, + line: Int32, + column: Int32, + type: UInt +) -> Bool { + false +} diff --git a/harmony/Sources/Audio/AudioAnalyzer.swift b/harmony/Sources/Audio/AudioAnalyzer.swift new file mode 100644 index 00000000..1939fd3f --- /dev/null +++ b/harmony/Sources/Audio/AudioAnalyzer.swift @@ -0,0 +1,94 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +struct AnalyzedAudioBuffer: ~Copyable { + var enableMagnet: Bool + var buffer: AudioBuffer +} + +struct AudioAnalyzer: ~Copyable { + // FIXME: add soft limit for time magnet can be enabled + + var fft_instance: arm_rfft_instance_q15 + // Used for both sample input and fft magnitude output + var dataBuffer0: UnsafeMutableBufferPointer + // Used for fft output + var dataBuffer1: UnsafeMutableBufferPointer + + init() { + let audioBufferCapacity = BUFFER_SAMPLE_CAPACITY + let fftOutputBufferCapacity = BUFFER_SAMPLE_CAPACITY * 2 // real + complex + + self.fft_instance = arm_rfft_instance_q15() + self.dataBuffer0 = .allocate(capacity: audioBufferCapacity) + self.dataBuffer1 = .allocate(capacity: fftOutputBufferCapacity) + // IMPORTANT: `bitReverseFlag` must be set. I don't understand why based on + // the documentation + arm_rfft_init_q15(&fft_instance, UInt32(audioBufferCapacity), 0, 1) + } + + deinit { + self.dataBuffer1.deallocate() + self.dataBuffer0.deallocate() + } + + mutating func run() { + while true { + guard let buffer = Application.shared.audioEngine.buffers.popFullBuffer() else { continue } + + /// Copy data from buffer to dataBuffer0 (because the fft will modify the data) + precondition(self.dataBuffer0.update(from: buffer.storage).index == self.dataBuffer0.count) + // Perform the fft using the data in dataBuffer0 and store the result in dataBuffer1 + arm_rfft_q15(&self.fft_instance, self.dataBuffer0.baseAddress, self.dataBuffer1.baseAddress) + // Calculate the magnitude of the fft output in dataBuffer1 and store the result in dataBuffer0 + arm_cmplx_mag_q15(self.dataBuffer1.baseAddress, self.dataBuffer0.baseAddress, UInt32(self.dataBuffer0.count)) + + // NOTE: This is probably wrong becasue buffer.storage is stereo data + + // Given we take an fft of audio data at 44100 Hz with a window of 512 + // samples, each output bin of the fft represents a 172 Hz range + // (44100 Hz / 2 / 512 = ~172Hz) + + // 1 Khz + let lowend = + self.dataBuffer0[00] + + self.dataBuffer0[01] + + self.dataBuffer0[02] + + self.dataBuffer0[03] + + self.dataBuffer0[04] + + self.dataBuffer0[05] + + self.dataBuffer0[06] + + self.dataBuffer0[07] + + self.dataBuffer0[08] + + self.dataBuffer0[09] + + self.dataBuffer0[10] + + self.dataBuffer0[10] + + self.dataBuffer0[11] + + self.dataBuffer0[12] + + self.dataBuffer0[13] + + self.dataBuffer0[14] + + self.dataBuffer0[15] + + self.dataBuffer0[16] + + self.dataBuffer0[17] + + self.dataBuffer0[18] + + self.dataBuffer0[19] + + + + let avg = lowend / 20 + + let analyzedBuffer = AnalyzedAudioBuffer( + enableMagnet: avg > (1 << 8), + buffer: buffer) + Application.shared.audioEngine.buffers.pushAnalyzedBuffer(analyzedBuffer) + } + } +} diff --git a/harmony/Sources/Audio/AudioBuffer.swift b/harmony/Sources/Audio/AudioBuffer.swift new file mode 100644 index 00000000..49431b52 --- /dev/null +++ b/harmony/Sources/Audio/AudioBuffer.swift @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +struct AudioBuffer: ~Copyable { + // FIXME: Raw + var storage: UnsafeMutableBufferPointer + var capacity: Int { self.storage.count } + var count: Int + + init(capacity: Int) { + self.storage = .allocate(capacity: capacity) + self.storage.initialize(repeating: 0) + // FIXME: don't assume filled. + self.count = capacity + } + + deinit { + self.storage.deallocate() + } +} diff --git a/harmony/Sources/Audio/AudioBufferTransport.swift b/harmony/Sources/Audio/AudioBufferTransport.swift new file mode 100644 index 00000000..907637d3 --- /dev/null +++ b/harmony/Sources/Audio/AudioBufferTransport.swift @@ -0,0 +1,54 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +struct AudioBufferTransport: ~Copyable { + var emptyBuffers: Ring + var fullBuffers: Ring + var analyzedBuffers: Ring + + init(bufferCount: Int, bufferCapacity: Int) { + // Ring buffer needs one extra slot to distinguish between empty and full. + self.emptyBuffers = Ring(capacity: bufferCount + 1) + self.fullBuffers = Ring(capacity: bufferCount + 1) + self.analyzedBuffers = Ring(capacity: bufferCount + 1) + + for _ in 0.. AudioBuffer? { + self.emptyBuffers.pop() + } + + mutating func pushFullBuffer(_ buffer: consuming AudioBuffer) { + self.fullBuffers.push(buffer) + } + + mutating func popFullBuffer() -> AudioBuffer? { + self.fullBuffers.pop() + } + + mutating func pushAnalyzedBuffer(_ buffer: consuming AnalyzedAudioBuffer) { + self.analyzedBuffers.push(buffer) + } + + mutating func popAnalyzedBuffer() -> AnalyzedAudioBuffer? { + self.analyzedBuffers.pop() + } +} diff --git a/harmony/Sources/Audio/AudioEngine.swift b/harmony/Sources/Audio/AudioEngine.swift new file mode 100644 index 00000000..660b2367 --- /dev/null +++ b/harmony/Sources/Audio/AudioEngine.swift @@ -0,0 +1,120 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +struct AudioEngine: ~Copyable { + var running: Bool + var mute: Bool + var volume: UInt8 + var rawVolume: UInt8 + + var audio_pico: AudioPico + var audio_i2s: AudioI2S + var buffers: AudioBufferTransport + var amp: MAX9744 + + init() { + self.running = false + self.mute = false + self.volume = 0 + self.rawVolume = 30 + + self.audio_pico = AudioPico() + self.audio_i2s = AudioI2S( + data_pin: PICO_AUDIO_I2S_DATA_PIN, + clock_pin_base: PICO_AUDIO_I2S_CLOCK_PIN_BASE, + pio: 0, + pio_sm: 0, + // FIXME: Dont claim on each `media_processing_init`?? + dma_channel: UInt32(dma_claim_unused_channel(true))) + self.buffers = AudioBufferTransport(bufferCount: 8, bufferCapacity: BUFFER_SAMPLE_CAPACITY) + self.amp = MAX9744(i2c: i2c0_inst) + + self.set(volume: 0) + } +} + +extension AudioEngine { + mutating func `init`(_ configuration: MediaCodecConfigurationSBC) { + log(#function) + SBCDecoder.configure(mode: SBC_MODE_STANDARD) + + // setup audio playback + // FIXME: update channel count in resampler + // FIXME: update output sample-rate + + self.audio_i2s.update_pio_frequency( + UInt32(configuration.sampling_frequency)) + + self.running = false + } + + mutating func toggleMute() { + self.mute.toggle() + if self.mute { + self.amp.set(rawVolume: 0) + } else { + self.amp.set(rawVolume: rawVolume) + } + } + + mutating func set(volume: UInt8) { + guard self.volume != volume else { return } + self.volume = volume + // FIXME: + avrcp_target_volume_changed(avrcp_connection.avrcp_cid, volume >> 1) + + // Map volume (0-255) to gain (0-63) + let rawVolume = UInt8((UInt32(volume) * 63) / 255) + guard self.rawVolume != rawVolume else { return } + self.rawVolume = rawVolume + + guard !self.mute else { return } + self.amp.set(rawVolume: rawVolume) + } + + mutating func adjustVolume(by delta: Int32) { + guard delta != 0 else { return } + let volume = Int32(self.volume) + delta + let clamped = UInt8(clamping: volume) + log("Adjust volume by \(delta) to \(clamped)") + self.set(volume: clamped) + } + + mutating func start() { + guard !self.running else { return } + guard self.audio_pico.sbc_frames.count >= OPTIMAL_FRAMES_MIN else { return } + log(#function) + // start audio playback + self.audio_pico.start_stream() + self.audio_i2s.enable(true) + self.running = true + } + + mutating func pause() { + guard self.running else { return } + log(#function) + self.close() + } + + mutating func close() { + log(#function) + + // stop audio playback + self.running = false + self.audio_pico.stop_stream() + self.audio_i2s.enable(false) + + // discard pending data + self.audio_pico.decoded_audio.clear() + self.audio_pico.sbc_frame_size = 0 + self.audio_pico.sbc_frames.clear() + } +} diff --git a/harmony/Sources/Audio/AudioI2S.swift b/harmony/Sources/Audio/AudioI2S.swift new file mode 100644 index 00000000..52e4b96d --- /dev/null +++ b/harmony/Sources/Audio/AudioI2S.swift @@ -0,0 +1,174 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +var zero: UInt32 = 0 + +// FIXME: #define __time_critical_func(func_name) __not_in_flash_func(func_name) +// irq handler for DMA +@_cdecl("audio_i2s_dma_irq_handler") +func audio_i2s_dma_irq_handler() { + Application.shared.audioEngine.audio_i2s.handle_dma_irq() +} + +struct AudioI2S: ~Copyable { + var enabled: Bool + var freq: UInt32 + var playing_buffer: AudioBuffer? + + var pio: UInt32 + var pio_sm: UInt32 + var dma_channel: UInt32 + var pioHw: PIO + + init( + data_pin: UInt32, + clock_pin_base: UInt32, + pio: UInt32, + pio_sm: UInt32, + // FIXME: dma_channel is already claimed + dma_channel: UInt32, + ) { + self.enabled = false + self.freq = 0 + + self.pio = pio + self.pio_sm = pio_sm + self.dma_channel = dma_channel + + let gpioFunc: gpio_function_rp2040 + switch pio { + case 0: + self.pioHw = _pio0() + gpioFunc = GPIO_FUNC_PIO0 + case 1: + self.pioHw = _pio1() + gpioFunc = GPIO_FUNC_PIO1 + default: + fatalError("Invalid PIO index") + } + + gpio_set_function(data_pin, gpioFunc) + gpio_set_function(clock_pin_base, gpioFunc) + gpio_set_function(clock_pin_base + 1, gpioFunc) + + pio_sm_claim(self.pioHw, self.pio_sm) + + let offset = withUnsafePointer(to: audio_i2s_program) { + pio_add_program(self.pioHw, $0) + } + + audio_i2s_program_init( + self.pioHw, self.pio_sm, UInt32(offset), data_pin, clock_pin_base) + + __mem_fence_release() + + var dma_config = dma_channel_get_default_config(dma_channel) + + channel_config_set_dreq( + &dma_config, + UInt32(DREQ_PIO0_TX0.rawValue) + self.pio_sm) + + channel_config_set_transfer_data_size(&dma_config, i2s_dma_configure_size) + dma_channel_configure( + dma_channel, + &dma_config, + // FIXME: .advanced(by: Int(self.pio_sm)) + self.pioHw.pointer(to: \.txf), // dest + nil, // src + 0, // count + false) // trigger + + irq_add_shared_handler( + UInt32(DMA_IRQ_0.rawValue) + PICO_AUDIO_I2S_DMA_IRQ, + audio_i2s_dma_irq_handler, + UInt8(PICO_SHARED_IRQ_HANDLER_DEFAULT_ORDER_PRIORITY)) + dma_irqn_set_channel_enabled(PICO_AUDIO_I2S_DMA_IRQ, dma_channel, true) + } + + mutating func enable(_ enable: Bool) { + guard self.enabled != enable else { return } + self.enabled = enable + + irq_set_enabled(UInt32(DMA_IRQ_0.rawValue) + PICO_AUDIO_I2S_DMA_IRQ, enable) + + if enable { + self.audio_start_dma_transfer() + } else { + // if there was a buffer in flight, it will not be freed by DMA IRQ, + // let's do it manually + self.audio_finish_dma_transfer() + gpio_put(EM_DRIVE_PIN, false) + } + + pio_sm_set_enabled(self.pioHw, self.pio_sm, enable) + } + + mutating func update_pio_frequency(_ sample_freq: UInt32?) { + guard let sample_freq = sample_freq else { return } + guard sample_freq != self.freq else { return } + + let system_clock_frequency = clock_get_hz(clk_sys) + precondition(system_clock_frequency < 0x4000_0000) + // avoid arithmetic overflow + let divider = system_clock_frequency * 4 / sample_freq + precondition(divider < 0x1000000) + pio_sm_set_clkdiv_int_frac( + self.pioHw, self.pio_sm, UInt16(divider >> 8), UInt8(divider & 0xff)) + self.freq = sample_freq + } + + mutating func handle_dma_irq() { + guard dma_irqn_get_channel_status(PICO_AUDIO_I2S_DMA_IRQ, self.dma_channel) + else { return } + dma_irqn_acknowledge_channel(PICO_AUDIO_I2S_DMA_IRQ, self.dma_channel) + + // free the buffer we just finished + self.audio_finish_dma_transfer() + self.audio_start_dma_transfer() + } + + mutating func audio_start_dma_transfer() { + precondition(self.playing_buffer == nil) + + // FIXME: support dynamic frequency shifting + + if let ab = Application.shared.audioEngine.buffers.popAnalyzedBuffer() { + gpio_put(EM_DRIVE_PIN, ab.enableMagnet) + + let ab = ab.buffer + let buf = UnsafeMutableRawBufferPointer(ab.storage) + self.playing_buffer = consume ab + + var c = dma_get_channel_config(self.dma_channel) + channel_config_set_read_increment(&c, true) + dma_channel_set_config(self.dma_channel, &c, false) + dma_channel_transfer_from_buffer_now( + self.dma_channel, + buf.baseAddress, + // FIXME: using capacity instead of ab count + UInt32(buf.count) / 4) + } else { + gpio_put(EM_DRIVE_PIN, false) + log("buffer pool low") + // just play some silence + var c = dma_get_channel_config(self.dma_channel) + channel_config_set_read_increment(&c, false) + dma_channel_set_config(self.dma_channel, &c, false) + dma_channel_transfer_from_buffer_now( + self.dma_channel, &zero, PICO_AUDIO_I2S_SILENCE_BUFFER_SAMPLE_LENGTH) + } + } + + mutating func audio_finish_dma_transfer() { + guard let playingBuffer = self.playing_buffer.take() else { return } + Application.shared.audioEngine.buffers.pushEmptyBuffer(playingBuffer) + } +} diff --git a/harmony/Sources/Audio/AudioPico.swift b/harmony/Sources/Audio/AudioPico.swift new file mode 100644 index 00000000..ef540daf --- /dev/null +++ b/harmony/Sources/Audio/AudioPico.swift @@ -0,0 +1,181 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +extension UnsafeMutableBufferPointer where Element: ~Copyable { + func split(at index: Self.Index) -> (Self, Self) { + (self.extracting(.. + var sbc_frames: RingBuffer + var sbc_frames_in_buffer: Int { + guard sbc_frame_size > 0 else { return 0 } + return self.sbc_frames.count / self.sbc_frame_size + } + + // overflow buffer for not fully used sbc frames, with additional frames for resampling + let decoded_audio_buffer: UnsafeMutableBufferPointer + var decoded_audio: RingBuffer + + init() { + let CHANNELS_PER_FRAME = 2 + let capacity = (128 + 16) * CHANNELS_PER_FRAME + + self.fill_timer = btstack_timer_source_t() + self.resampler = Resampler(channels: CHANNELS_PER_FRAME) + + self.sbc_frame_size = 0 + self.sbc_frame_buffer = UnsafeMutableBufferPointer.allocate( + capacity: MAX_SBC_FRAME_SIZE) + self.sbc_frames = RingBuffer( + capacity: (OPTIMAL_FRAMES_MAX + ADDITIONAL_FRAMES) * MAX_SBC_FRAME_SIZE) + + self.decoded_audio_buffer = .allocate(capacity: capacity) + self.decoded_audio = RingBuffer(capacity: capacity) + } + + mutating func enqueue(sbc_frames: UnsafeMutableBufferPointer, frame_size: Int) + { + self.sbc_frame_size = frame_size + if !self.sbc_frames.write(contentsOf: sbc_frames) { + log("Error: SBC frame buffer overflow") + } + self.updateResamplingFactor() + } + + mutating func updateResamplingFactor() { + let nominal_factor: UInt32 = 0x10000 + let compensation: UInt32 = 0x00100 + + let resampling_factor = + switch self.sbc_frames_in_buffer { + case ..) { + // called from lower-layer but guaranteed to be on main thread + guard self.sbc_frame_size != 0 else { + log("Frame size is 0") + buffer.update(repeating: 0) + return + } + + // first fill from resampled audio + let samplesReadCount = self.decoded_audio.read(into: buffer) + var buffer = buffer.extracting(samplesReadCount...) + + // then start decoding sbc frames into the buffer + while buffer.count > 0, self.sbc_frames.count > self.sbc_frame_size { + // decode frame + let elementsRead = self.sbc_frames.read( + into: self.sbc_frame_buffer, count: self.sbc_frame_size) + precondition( + elementsRead == self.sbc_frame_size, "sbc frame size mismatch") + + SBCDecoder.decode_signed_16( + mode: SBC_MODE_STANDARD, + packet_status_flag: 0, + buffer: UnsafeRawBufferPointer(self.sbc_frame_buffer) + ) { samples, num_channels, sample_rate in + precondition(num_channels == 2, "must be stereo") + + // Resample audio to compensate for the amount of buffered SBC frames + let samples = self.resampler.resample( + samples: .init(samples), + usingTemporaryBuffer: self.decoded_audio_buffer) + + // Store samples in buffer first and excess in the ring buffer. + let (samples_to_copy, samples_to_store) = samples.split( + at: min(samples.count, buffer.count)) + let samplesCopiedCount = buffer.moveUpdate( + fromContentsOf: samples_to_copy) + buffer = buffer.extracting(samplesCopiedCount...) + if !self.decoded_audio.write(contentsOf: samples_to_store) { + log("ERROR: PCM ring buffer full!") + } + } + } + } + + mutating func fill_timer( + _ timer: UnsafeMutablePointer? + ) { + // refill + self.fill_buffers() + + // re-set timer + btstack_run_loop_set_timer(timer, UInt32(DRIVER_POLL_INTERVAL_MS)) + btstack_run_loop_add_timer(timer) + } + + mutating func start_stream() { + // pre-fill buffers + self.fill_buffers() + + // start timer + // FIXME: Use ctx + // NOTE: hardcoded to `Self` because the timer callback has no context + // argument which can be used to pass `self` + btstack_run_loop_set_timer_handler( + &self.fill_timer, { Application.shared.audioEngine.audio_pico.fill_timer($0) }) + btstack_run_loop_set_timer_context(&self.fill_timer, nil) + btstack_run_loop_set_timer( + &self.fill_timer, UInt32(DRIVER_POLL_INTERVAL_MS)) + btstack_run_loop_add_timer(&self.fill_timer) + } + + mutating func stop_stream() { + // stop timer + btstack_run_loop_remove_timer(&self.fill_timer) + } +} diff --git a/harmony/Sources/Audio/MAX9744.swift b/harmony/Sources/Audio/MAX9744.swift new file mode 100644 index 00000000..6c969508 --- /dev/null +++ b/harmony/Sources/Audio/MAX9744.swift @@ -0,0 +1,105 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +struct MAX9744: ~Copyable { + static let i2cAddress: UInt8 = 0x4B // 7 bit address + static let absoluteVolumeControlRegisterAddress: UInt8 = 0x0 + + static let modulationControlRegisterAddress: UInt8 = 0x1 + static let filterlessModulationBitPattern: UInt8 = 0x0 + static let pwmModulationBitPattern: UInt8 = 0x1 + + static let incrementalVolumeControlRegisterAddress: UInt8 = 0x3 + static let increaseVolumeBitPattern: UInt8 = 0x4 + static let decreaseVolumeBitPattern: UInt8 = 0x5 + + var i2c: i2c_inst_t + + init(i2c: i2c_inst_t) { + self.i2c = i2c + } +} + +extension MAX9744 { + static func validAddress(_ address: UInt8) -> Bool { + switch address { + case Self.absoluteVolumeControlRegisterAddress: true + case Self.filterlessModulationBitPattern: true + case Self.incrementalVolumeControlRegisterAddress: true + default: false + } + } + + mutating func write(address: UInt8, value: UInt8) { + precondition(Self.validAddress(address)) + var data = (address << 6) | value + log("attempting to write \(hex: data)") + let size = MemoryLayout.size(ofValue: data) + let result = i2c_write_blocking( + &self.i2c, + Self.i2cAddress, + &data, + size, + false) + precondition(result == size, "I2C write failed") + } + + mutating func read(address: UInt8) -> UInt8 { + precondition(Self.validAddress(address)) + var data = address << 6 + let size = MemoryLayout.size(ofValue: data) + let readResult = i2c_read_blocking( + &self.i2c, + Self.i2cAddress, + &data, + size, + false) + precondition(readResult == size, "I2C read failed") + return data + } +} + +extension MAX9744 { + /// 6 bit value ranging from 0 (mute) to 63 (+ 9.5 dB) + mutating func set(rawVolume: UInt8) { + precondition(0 <= rawVolume && rawVolume <= 63) + self.write( + address: Self.absoluteVolumeControlRegisterAddress, + value: rawVolume) + } + + enum ModulationMode { + case filterless + case pwm + } + + mutating func set(moduluationMode: ModulationMode) { + let modulationBitPattern = switch moduluationMode { + case .filterless: Self.filterlessModulationBitPattern + case .pwm: Self.pwmModulationBitPattern + } + self.write( + address: Self.modulationControlRegisterAddress, + value: modulationBitPattern) + } + + mutating func increaseVolume() { + self.write( + address: Self.incrementalVolumeControlRegisterAddress, + value: Self.increaseVolumeBitPattern) + } + + mutating func decreaseVolume() { + self.write( + address: Self.incrementalVolumeControlRegisterAddress, + value: Self.decreaseVolumeBitPattern) + } +} diff --git a/harmony/Sources/Audio/Resampler.swift b/harmony/Sources/Audio/Resampler.swift new file mode 100644 index 00000000..ef4cd8bb --- /dev/null +++ b/harmony/Sources/Audio/Resampler.swift @@ -0,0 +1,52 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +struct Resampler: ~Copyable { + var channels: Int + var context: btstack_resample_t + + init(channels: Int) { + self.channels = channels + self.context = btstack_resample_t() + btstack_resample_init(&self.context, Int32(channels)) + } + + mutating func set(channels: Int) { + self.channels = channels + btstack_resample_init(&self.context, Int32(channels)) + } + + mutating func set(factor: UInt32) { + btstack_resample_set_factor(&self.context, factor) + } + + /// Resamples the given samples using the previously set resampling factor. + /// + /// Returns a slice of the temporary buffer that contains the resampled audio. + mutating func resample( + samples: UnsafeBufferPointer, + usingTemporaryBuffer buffer: UnsafeMutableBufferPointer + ) -> UnsafeMutableBufferPointer { + precondition(samples.count.isMultiple(of: self.channels)) + + // FIXME: understand why this is not `samples.count / self.channels` + // The documentation just calls this parameter `numFrames` which implies + // the sample count should be divided by the channel count. + let inputFrameCount = samples.count + let resampledFrameCount = btstack_resample_block( + &self.context, + samples.baseAddress, + UInt32(inputFrameCount), + buffer.baseAddress) + let resampledSampleCount = Int(resampledFrameCount) * self.channels + return buffer.extracting(..: ~Copyable { + // FIMXE: Use an inline allocation like `Vector` + var storage: UnsafeMutableBufferPointer + var readerIndex: Int + var writerIndex: Int + + init(capacity: Int) { + self.storage = .allocate(capacity: capacity) + self.readerIndex = 0 + self.writerIndex = 0 + } + + deinit { + var readerIndex = self.readerIndex + while self.readerIndex != self.writerIndex { + self.storage.deinitializeElement(at: readerIndex) + readerIndex = (readerIndex + 1) % self.storage.count + } + // FIXME: why can't we use a mutating method here? + // while _ = self.pop() { } + self.storage.deallocate() + } +} + +extension Ring where Element: ~Copyable { + mutating func push(_ element: consuming Element) { + let nextWriterIndex = (self.writerIndex + 1) % self.storage.count + guard nextWriterIndex != self.readerIndex else { fatalError("Overflow") } + self.storage.initializeElement(at: self.writerIndex, to: element) + __dsb() // Make sure the element is written before updating the index + self.writerIndex = nextWriterIndex + } + + mutating func pop() -> Element? { + guard self.readerIndex != self.writerIndex else { return nil } + let element = self.storage.moveElement(from: self.readerIndex) + __dsb() // Make sure the element is read before updating the index + self.readerIndex = (self.readerIndex + 1) % self.storage.count + return element + } +} diff --git a/harmony/Sources/Audio/RingBuffer.swift b/harmony/Sources/Audio/RingBuffer.swift new file mode 100644 index 00000000..11a30d0e --- /dev/null +++ b/harmony/Sources/Audio/RingBuffer.swift @@ -0,0 +1,126 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +// FIXME: RingBuffer +struct RingBuffer: ~Copyable { + // FIMXE: Use an inline allocation like `Vector` + var storage: UnsafeMutableBufferPointer + var count: Int + var readerIndex: Int + var writerIndex: Int + + init(capacity: Int) { + self.storage = .allocate(capacity: capacity) + self.readerIndex = 0 + self.writerIndex = 0 + self.count = 0 + } + + deinit { + self.storage.deallocate() + } +} + +extension RingBuffer { + var capacity: Int { self.storage.count } + var availableCapacity: Int { self.capacity - self.count } + var isEmpty: Bool { self.count == 0 } + var isFull: Bool { self.count == self.capacity } +} + +extension RingBuffer { + mutating func clear() { + // Forget about the contents of `storage`, this is safe because + // `Element` is `BitwiseCopyable`. + self.count = 0 + self.readerIndex = 0 + self.writerIndex = 0 + } + + mutating func read( + into buffer: UnsafeMutableBufferPointer, + count: Int? = nil + ) -> Int { + let elementsToRead = min(buffer.count, count ?? Int.max, self.count) + + // Reading 0 elements is a no-op. + guard elementsToRead > 0 else { return elementsToRead } + + // Read the initial elements from the end of the ring buffer. + let elementsUntilEnd = self.capacity - self.readerIndex + let elementsToReadFirstHalf = min(elementsUntilEnd, elementsToRead) + buffer.baseAddress!.update( + from: self.storage.baseAddress! + self.readerIndex, + count: elementsToReadFirstHalf) + self.readerIndex += elementsToReadFirstHalf + + // Update the reader index to wrap if needed. + if self.readerIndex == self.capacity { + self.readerIndex = 0 + } + + // Read the remaining elements from the beginning of the ring buffer. + let elementsToReadSecondHalf = elementsToRead - elementsToReadFirstHalf + precondition(elementsToReadSecondHalf >= 0) + (buffer.baseAddress! + elementsToReadFirstHalf).update( + from: self.storage.baseAddress! + self.readerIndex, + count: elementsToReadSecondHalf) + self.readerIndex += elementsToReadSecondHalf + + // Update bookkeeping with the new count. + self.count -= elementsToRead + + return elementsToRead + } + + mutating func write( + contentsOf buffer: UnsafeMutableBufferPointer + ) -> Bool { + self.write(contentsOf: UnsafeBufferPointer(buffer)) + } + + mutating func write( + contentsOf buffer: UnsafeBufferPointer + ) -> Bool { + let elementsToWrite = buffer.count + + // Writing 0 elements is a no-op. + guard elementsToWrite > 0 else { return true } + // Writing more than the available capacity is an error. + guard elementsToWrite <= self.availableCapacity else { return false } + + // Write the initial elements to the end of the ring buffer. + let elementsUntilEnd = self.capacity - self.writerIndex + let elementsToWriteFirstHalf = min(elementsUntilEnd, elementsToWrite) + (self.storage.baseAddress! + self.writerIndex).update( + from: buffer.baseAddress!, + count: elementsToWriteFirstHalf) + self.writerIndex += elementsToWriteFirstHalf + + // Update the writer index to wrap if needed. + if self.writerIndex == self.capacity { + self.writerIndex = 0 + } + + // Write the remaining elements to the beginning of the ring buffer. + let elementsToWriteSecondHalf = elementsToWrite - elementsToWriteFirstHalf + precondition(elementsToWriteSecondHalf >= 0) + (self.storage.baseAddress! + self.writerIndex).update( + from: buffer.baseAddress! + elementsToWriteFirstHalf, + count: elementsToWriteSecondHalf) + self.writerIndex += elementsToWriteSecondHalf + + // Update bookkeeping with the new count. + self.count += elementsToWrite + + return true + } +} diff --git a/harmony/Sources/Audio/SpinLock.swift b/harmony/Sources/Audio/SpinLock.swift new file mode 100644 index 00000000..8212f04f --- /dev/null +++ b/harmony/Sources/Audio/SpinLock.swift @@ -0,0 +1,38 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +struct SpinLock: ~Copyable { + var _lock: UnsafeMutablePointer + var value: Value + + init(index: Int, initialValue: consuming Value) { + self._lock = spin_lock_init(UInt32(index)) + self.value = initialValue + } +} + +extension SpinLock where Value: ~Copyable { + func lock() -> UInt32 { + spin_lock_blocking(self._lock) + } + + func unlock(irq_mask: UInt32) { + spin_unlock(self._lock, irq_mask) + } + + mutating func withLock( + _ body: (inout Value) throws(Error) -> Result + ) throws(Error) -> Result where Result: ~Copyable { + let irq_mask = self.lock() + defer { self.unlock(irq_mask: irq_mask) } + return try body(&self.value) + } +} diff --git a/harmony/Sources/Audio/TPA2016D2.swift b/harmony/Sources/Audio/TPA2016D2.swift new file mode 100644 index 00000000..59b56640 --- /dev/null +++ b/harmony/Sources/Audio/TPA2016D2.swift @@ -0,0 +1,118 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +func i2c_init() { + i2c_init(&i2c0_inst, 100 * 1000) // 400kHz "Fast Mode" + gpio_set_function(UInt32(PICO_DEFAULT_I2C_SDA_PIN), GPIO_FUNC_I2C) + gpio_set_function(UInt32(PICO_DEFAULT_I2C_SCL_PIN), GPIO_FUNC_I2C) + gpio_pull_up(UInt32(PICO_DEFAULT_I2C_SDA_PIN)) + gpio_pull_up(UInt32(PICO_DEFAULT_I2C_SCL_PIN)) + + // I2C reserves some addresses for special purposes. We exclude these from the scan. + // These are any addresses of the form 000 0xxx or 111 1xxx + func reserved_addr(_ addr: UInt8) -> Bool{ + return (addr & 0x78) == 0 || (addr & 0x78) == 0x78 + } + + log("\nI2C Bus Scan") + log(" 0 1 2 3 4 5 6 7 8 9 A B C D E F") + for addr in UInt8(0) ..< (1 << 7) { + if addr.isMultiple(of: 16) { + log("\(addr >> 4) ", terminator: "") + } + + // Perform a 1-byte dummy read from the probe address. If a slave + // acknowledges this address, the function returns the number of bytes + // transferred. If the address byte is ignored, the function returns + // -1. + + // Skip over any reserved addresses. + var rxdata: UInt8 = 0 + let ret = if reserved_addr(addr) { + Int32(PICO_ERROR_GENERIC.rawValue) + } else { + i2c_read_blocking(&i2c0_inst, addr, &rxdata, 1, false) + } + + log(ret < 0 ? "." : "@", terminator: addr % 16 == 15 ? "\n" : " ") + } + log("Done.\n") +} + +struct TPA2016D2: ~Copyable { + static let address: UInt8 = 0x58 // 7 bit address + static let IC_FUNCTION_CONTROL: UInt8 = 0x1 + static let AGC_ATTACK_CONTROL: UInt8 = 0x2 + static let AGC_RELEASE_CONTROL: UInt8 = 0x3 + static let AGC_HOLD_TIME_CONTROL: UInt8 = 0x4 + static let AGC_FIXED_GAIN_CONTROL: UInt8 = 0x5 + static let AGC_CONTROL_0: UInt8 = 0x6 + static let AGC_CONTROL_1: UInt8 = 0x7 + + var i2c: i2c_inst_t + + init(i2c: i2c_inst_t) { + self.i2c = i2c + + for r in UInt8(0x1) ... 0x7 { + log("Register \(hex: r); read \(hex: self.read(address: r))") + } + + // Immediately configure the amp to our desired defaults. + // Disable AGC (Automatic Gain Control). + self.write(address: Self.AGC_CONTROL_1, value: 0x0) + // Disable Output Limiter + self.write(address: Self.AGC_CONTROL_0, value: 1 << 7) + // Set the attack time to the fastest setting (0.1067 ms per step) + self.write(address: Self.AGC_ATTACK_CONTROL, value: 1) + // Set the release time to the fastest setting (0.0137 s per step) + self.write(address: Self.AGC_RELEASE_CONTROL, value: 1) + // Disable the hold time entirely + self.write(address: Self.AGC_HOLD_TIME_CONTROL, value: 0) + } +} + +extension TPA2016D2 { + mutating func write(address: UInt8, value: UInt8) { + var combined: UInt16 = (UInt16(value) << 8) | UInt16(address) + let result = i2c_write_blocking( + &self.i2c, + Self.address, + &combined, + MemoryLayout.size(ofValue: combined), + false) + precondition(result == 2, "I2C write failed") + // log("Register \(hex: address); wrote \(hex: value) - read \(hex: self.read(address: address))") + } + + mutating func read(address: UInt8) -> UInt8 { + var data = address + let writeResult = i2c_write_blocking(&self.i2c, Self.address, &data, 1, false) + precondition(writeResult == 1, "I2C write failed") + let readResult = i2c_read_blocking(&self.i2c, Self.address, &data, 1, false) + precondition(readResult == 1, "I2C read failed") + return data + } +} + +extension TPA2016D2 { + // scale from 0 to 255 + mutating func set(gain: UInt8) { + precondition(0 <= gain && gain <= 30) + self.write(address: Self.AGC_FIXED_GAIN_CONTROL, value: gain) + } + + mutating func mute(_ mute: Bool) { + var value = self.read(address: Self.IC_FUNCTION_CONTROL) + value = mute ? value | (1 << 5) : value & ~(1 << 5) + self.write(address: Self.IC_FUNCTION_CONTROL, value: value) + } +} diff --git a/harmony/Sources/Audio/Timer.swift b/harmony/Sources/Audio/Timer.swift new file mode 100644 index 00000000..acad420a --- /dev/null +++ b/harmony/Sources/Audio/Timer.swift @@ -0,0 +1,20 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +// struct Timer: ~Copyable, ~Escapable { +// var context: UnsafePointer + +// init(context: borrowing Context) dependsOn(context) { +// withUnsafePointerToInstance(context) { context in +// self.context = context +// } +// } +// } \ No newline at end of file diff --git a/harmony/Sources/Bluetooth/A2DP.swift b/harmony/Sources/Bluetooth/A2DP.swift new file mode 100644 index 00000000..ebb257f6 --- /dev/null +++ b/harmony/Sources/Bluetooth/A2DP.swift @@ -0,0 +1,300 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +// Advanced Audio Distribution Profile + +struct MediaCodecConfigurationSBC { + var reconfigure: UInt8 + var num_channels: UInt8 + var sampling_frequency: UInt16 + var block_length: UInt8 + var subbands: UInt8 + var min_bitpool_value: UInt8 + var max_bitpool_value: UInt8 + var channel_mode: btstack_sbc_channel_mode_t + var allocation_method: btstack_sbc_allocation_method_t + + init() { + self.reconfigure = 0 + self.num_channels = 0 + self.sampling_frequency = 0 + self.block_length = 0 + self.subbands = 0 + self.min_bitpool_value = 0 + self.max_bitpool_value = 0 + self.channel_mode = SBC_CHANNEL_MODE_MONO + self.allocation_method = SBC_ALLOCATION_METHOD_LOUDNESS + } + + func dump() { + log( + """ + - num_channels: \(self.num_channels) + - sampling_frequency: \(self.sampling_frequency) + - channel_mode: \(self.channel_mode.rawValue) + - block_length: \(self.block_length) + - subbands: \(self.subbands) + - allocation_method: \(self.allocation_method.rawValue) + - bitpool_value [\(self.min_bitpool_value), \(self.max_bitpool_value)] + """) + } +} + +enum StreamState { + case closed + case open + case playing + case paused +} + +struct A2DPConnection { + static var shared = Self() + + var addr: bd_addr_t = (0, 0, 0, 0, 0, 0) + var a2dp_cid: UInt16 = 0 + var a2dp_local_seid: UInt8 = 0 + var stream_state: StreamState = .closed + var sbc_configuration: MediaCodecConfigurationSBC = .init() +} + +@_cdecl("a2dp_sink_packet_handler") +func a2dp_sink_packet_handler( + packet_type: UInt8, + channel: UInt16, + packet: UnsafeMutablePointer?, + size: UInt16 +) { + guard packet_type == HCI_EVENT_PACKET else { return } + guard hci_event_packet_get_type(packet) == HCI_EVENT_A2DP_META else { return } + + let subevent = packet?[2] + switch subevent { + case UInt8(A2DP_SUBEVENT_SIGNALING_MEDIA_CODEC_OTHER_CONFIGURATION): + log("A2DP Sink : Received non SBC codec - not implemented") + + case UInt8(A2DP_SUBEVENT_SIGNALING_MEDIA_CODEC_SBC_CONFIGURATION): + log("A2DP Sink : Received SBC codec configuration") + A2DPConnection.shared.sbc_configuration.reconfigure = + a2dp_subevent_signaling_media_codec_sbc_configuration_get_reconfigure( + packet) + A2DPConnection.shared.sbc_configuration.num_channels = + a2dp_subevent_signaling_media_codec_sbc_configuration_get_num_channels( + packet) + A2DPConnection.shared.sbc_configuration.sampling_frequency = + a2dp_subevent_signaling_media_codec_sbc_configuration_get_sampling_frequency( + packet) + A2DPConnection.shared.sbc_configuration.block_length = + a2dp_subevent_signaling_media_codec_sbc_configuration_get_block_length( + packet) + A2DPConnection.shared.sbc_configuration.subbands = + a2dp_subevent_signaling_media_codec_sbc_configuration_get_subbands(packet) + A2DPConnection.shared.sbc_configuration.min_bitpool_value = + a2dp_subevent_signaling_media_codec_sbc_configuration_get_min_bitpool_value( + packet) + A2DPConnection.shared.sbc_configuration.max_bitpool_value = + a2dp_subevent_signaling_media_codec_sbc_configuration_get_max_bitpool_value( + packet) + + let allocation_method = + a2dp_subevent_signaling_media_codec_sbc_configuration_get_allocation_method( + packet) + + // Adapt Bluetooth spec definition to SBC Encoder expected input + A2DPConnection.shared.sbc_configuration.allocation_method = + (btstack_sbc_allocation_method_t)(allocation_method - 1) + + switch avdtp_channel_mode_t( + a2dp_subevent_signaling_media_codec_sbc_configuration_get_channel_mode( + packet)) + { + case AVDTP_CHANNEL_MODE_JOINT_STEREO: + A2DPConnection.shared.sbc_configuration.channel_mode = + SBC_CHANNEL_MODE_JOINT_STEREO + case AVDTP_CHANNEL_MODE_STEREO: + A2DPConnection.shared.sbc_configuration.channel_mode = + SBC_CHANNEL_MODE_STEREO + case AVDTP_CHANNEL_MODE_DUAL_CHANNEL: + A2DPConnection.shared.sbc_configuration.channel_mode = + SBC_CHANNEL_MODE_DUAL_CHANNEL + case AVDTP_CHANNEL_MODE_MONO: + A2DPConnection.shared.sbc_configuration.channel_mode = + SBC_CHANNEL_MODE_MONO + default: + fatalError() + } + A2DPConnection.shared.sbc_configuration.dump() + + case UInt8(A2DP_SUBEVENT_STREAM_ESTABLISHED): + let status = a2dp_subevent_stream_established_get_status(packet) + guard status == ERROR_CODE_SUCCESS else { + log( + "A2DP Sink : Streaming connection failed, status \(hex: status)" + ) + return + } + + a2dp_subevent_stream_established_get_bd_addr( + packet, &A2DPConnection.shared.addr) + A2DPConnection.shared.a2dp_cid = + a2dp_subevent_stream_established_get_a2dp_cid(packet) + A2DPConnection.shared.a2dp_local_seid = + a2dp_subevent_stream_established_get_local_seid(packet) + A2DPConnection.shared.stream_state = .open + + log( + "A2DP Sink : Streaming connection is established, address \(cString: bd_addr_to_str(&A2DPConnection.shared.addr)), cid \(hex: A2DPConnection.shared.a2dp_cid), local seid \(A2DPConnection.shared.a2dp_local_seid)" + ) + + #if ENABLE_AVDTP_ACCEPTOR_EXPLICIT_START_STREAM_CONFIRMATION + case UInt8(A2DP_SUBEVENT_START_STREAM_REQUESTED): + log( + "A2DP Sink : Explicit Accept to start stream, local_seid %d\n", + a2dp_subevent_start_stream_requested_get_local_seid(packet)) + a2dp_sink_start_stream_accept(a2dp_cid, a2dp_local_seid) + #endif + + case UInt8(A2DP_SUBEVENT_STREAM_STARTED): + log("A2DP Sink : Stream started") + A2DPConnection.shared.stream_state = .playing + if A2DPConnection.shared.sbc_configuration.reconfigure != 0 { + Application.shared.audioEngine.close() + } + // prepare media processing + // audio playback starts when buffer reaches minimal level + Application.shared.audioEngine.`init`(A2DPConnection.shared.sbc_configuration) + + case UInt8(A2DP_SUBEVENT_STREAM_SUSPENDED): + log("A2DP Sink : Stream paused") + A2DPConnection.shared.stream_state = .paused + Application.shared.audioEngine.pause() + + case UInt8(A2DP_SUBEVENT_STREAM_RELEASED): + log("A2DP Sink : Stream released") + A2DPConnection.shared.stream_state = .closed + Application.shared.audioEngine.close() + + case UInt8(A2DP_SUBEVENT_SIGNALING_CONNECTION_RELEASED): + log("A2DP Sink : Signaling connection released") + A2DPConnection.shared.a2dp_cid = 0 + Application.shared.audioEngine.close() + + default: + log("AVRCP Sink : Event \(hex: subevent ?? 0xff) is not parsed") + } +} + +/* @section Handle Media Data Packet + * + * @text Here the audio data, are received through the a2dp_sink_media_handler callback. + * Currently, only the SBC media codec is supported. Hence, the media data consists of the media packet header and the SBC packet. + * The SBC frame will be stored in a ring buffer for later processing (instead of decoding it to PCM right away which would require a much larger buffer). + * If the audio stream wasn't started already and there are enough SBC frames in the ring buffer, start playback. + */ + +func read_media_data_header( + _ packet: UnsafeMutablePointer?, + _ size: Int32, + _ offset: UnsafeMutablePointer, + _ media_header: UnsafeMutablePointer +) -> Bool { + guard let packet else { return false } + let media_header_len: Int32 = 12 // without crc + var pos = Int(offset.pointee) + + if size - Int32(pos) < media_header_len { + log( + "Not enough data to read media packet header, expected \(media_header_len), received \(size-Int32(pos))" + ) + return false + } + + media_header.pointee.version = packet[pos] & 0x03 + media_header.pointee.padding = UInt8(get_bit16(UInt16(packet[pos]), 2)) + media_header.pointee.extension = UInt8(get_bit16(UInt16(packet[pos]), 3)) + media_header.pointee.csrc_count = (packet[pos] >> 4) & 0x0F + pos += 1 + + media_header.pointee.marker = UInt8(get_bit16(UInt16(packet[pos]), 0)) + media_header.pointee.payload_type = (packet[pos] >> 1) & 0x7F + pos += 1 + + media_header.pointee.sequence_number = UInt16( + big_endian_read_16(packet, Int32(pos))) + pos += 2 + + media_header.pointee.timestamp = big_endian_read_32(packet, Int32(pos)) + pos += 4 + + media_header.pointee.synchronization_source = big_endian_read_32( + packet, Int32(pos)) + pos += 4 + offset.pointee = Int32(pos) + return true +} + +func read_sbc_header( + _ packet: UnsafeMutablePointer?, + _ size: Int32, + _ offset: UnsafeMutablePointer, + _ sbc_header: UnsafeMutablePointer +) -> Bool { + guard let packet else { return false } + let sbc_header_len: Int32 = 12 // without crc + var pos: Int32 = offset.pointee + + if size - pos < sbc_header_len { + log( + "Not enough data to read SBC header, expected \(sbc_header_len), received \(size-pos)" + ) + return false + } + + sbc_header.pointee.fragmentation = UInt8( + get_bit16(UInt16(packet[Int(pos)]), 7)) + sbc_header.pointee.starting_packet = UInt8( + get_bit16(UInt16(packet[Int(pos)]), 6)) + sbc_header.pointee.last_packet = UInt8(get_bit16(UInt16(packet[Int(pos)]), 5)) + sbc_header.pointee.num_frames = UInt8(packet[Int(pos)] & 0x0f) + pos += 1 + offset.pointee = pos + return true +} + +@_cdecl("a2dp_sink_media_handler") +func a2dp_sink_media_handler( + seid: UInt8, + packet: UnsafeMutablePointer?, + size: UInt16 +) { + var pos: Int32 = 0 + + var media_header = avdtp_media_packet_header_t() + guard read_media_data_header(packet, Int32(size), &pos, &media_header) else { + log("Failed to read media data header") + return + } + + var sbc_header = avdtp_sbc_codec_header_t() + guard read_sbc_header(packet, Int32(size), &pos, &sbc_header) else { + log("Failed to read SBC header") + return + } + + let packet_length = UInt32(size) - UInt32(pos) + let packet_begin = packet?.advanced(by: Int(pos)) + let sbc_frame_size = Int(packet_length / UInt32(sbc_header.num_frames)) + + let packetBuffer = UnsafeMutableBufferPointer( + start: packet_begin, count: Int(packet_length)) + Application.shared.audioEngine.audio_pico.enqueue( + sbc_frames: packetBuffer, frame_size: sbc_frame_size) + Application.shared.audioEngine.start() +} diff --git a/harmony/Sources/Bluetooth/AVRCP.swift b/harmony/Sources/Bluetooth/AVRCP.swift new file mode 100644 index 00000000..6f5bc44a --- /dev/null +++ b/harmony/Sources/Bluetooth/AVRCP.swift @@ -0,0 +1,286 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +// Audio/Video Remote Control Profile + +struct AVRCPConnection { + var addr: bd_addr_t + var avrcp_cid: UInt16 + var playing: Bool + var notifications_supported_by_target: UInt16 +} + +var avrcp_connection = AVRCPConnection( + addr: (0, 0, 0, 0, 0, 0), + avrcp_cid: 0, + playing: false, + notifications_supported_by_target: 0) + +@_cdecl("avrcp_packet_handler") +func avrcp_packet_handler( + packet_type: UInt8, + channel: UInt16, + packet: UnsafeMutablePointer?, + size: UInt16 +) { + guard packet_type == HCI_EVENT_PACKET else { return } + guard hci_event_packet_get_type(packet) == HCI_EVENT_AVRCP_META else { + return + } + + let subevent = packet?[2] + switch subevent { + case UInt8(AVRCP_SUBEVENT_CONNECTION_ESTABLISHED): + log("AVRCP_SUBEVENT_CONNECTION_ESTABLISHED") + let local_cid = avrcp_subevent_connection_established_get_avrcp_cid(packet) + let status = avrcp_subevent_connection_established_get_status(packet) + + if status != ERROR_CODE_SUCCESS { + log("AVRCP: Connection failed, status \(hex: status)") + avrcp_connection.avrcp_cid = 0 + return + } + + avrcp_connection.avrcp_cid = local_cid + var address: bd_addr_t = (0, 0, 0, 0, 0, 0) + avrcp_subevent_connection_established_get_bd_addr(packet, &address) + log( + "AVRCP: Connected to \(cString: bd_addr_to_str(&address)), cid \(hex: avrcp_connection.avrcp_cid)" + ) + + avrcp_target_support_event( + avrcp_connection.avrcp_cid, AVRCP_NOTIFICATION_EVENT_VOLUME_CHANGED) + avrcp_target_support_event( + avrcp_connection.avrcp_cid, AVRCP_NOTIFICATION_EVENT_BATT_STATUS_CHANGED) + let battery_status = AVRCP_BATTERY_STATUS_WARNING + avrcp_target_battery_status_changed( + avrcp_connection.avrcp_cid, battery_status) + + // query supported events: + avrcp_controller_get_supported_events(avrcp_connection.avrcp_cid) + + case UInt8(AVRCP_SUBEVENT_CONNECTION_RELEASED): + log("AVRCP_SUBEVENT_CONNECTION_RELEASED") + log( + "AVRCP: Channel released: cid \(hex: avrcp_subevent_connection_released_get_avrcp_cid(packet))" + ) + avrcp_connection.avrcp_cid = 0 + avrcp_connection.notifications_supported_by_target = 0 + + default: + log("AVRCP: Event \(hex: subevent ?? 0xff) is not parsed") + } +} + +@_cdecl("avrcp_controller_packet_handler") +func avrcp_controller_packet_handler( + packet_type: UInt8, + channel: UInt16, + packet: UnsafeMutablePointer?, + size: UInt16 +) { + guard packet_type == HCI_EVENT_PACKET else { return } + guard hci_event_packet_get_type(packet) == HCI_EVENT_AVRCP_META else { + return + } + guard avrcp_connection.avrcp_cid != 0 else { return } + + let subevent = packet?[2] + switch subevent { + case UInt8(AVRCP_SUBEVENT_GET_CAPABILITY_EVENT_ID): + avrcp_connection.notifications_supported_by_target |= + (1 << avrcp_subevent_get_capability_event_id_get_event_id(packet)) + + case UInt8(AVRCP_SUBEVENT_GET_CAPABILITY_EVENT_ID_DONE): + log("AVRCP Controller: supported notifications by target:") + for event_id in UInt8( + AVRCP_NOTIFICATION_EVENT_FIRST_INDEX.rawValue).. 0 else { break } + let avrcp_subevent_value = UnsafeBufferPointer( + start: avrcp_subevent_now_playing_title_info_get_value(packet), + count: Int(count)) + log("AVRCP Controller: Title \(cString: avrcp_subevent_value)") + + case UInt8(AVRCP_SUBEVENT_NOW_PLAYING_ARTIST_INFO): + let count = avrcp_subevent_now_playing_artist_info_get_value_len(packet) + guard count > 0 else { break } + let avrcp_subevent_value = UnsafeBufferPointer( + start: avrcp_subevent_now_playing_artist_info_get_value(packet), + count: Int(count)) + log("AVRCP Controller: Artist \(cString: avrcp_subevent_value)") + + case UInt8(AVRCP_SUBEVENT_NOW_PLAYING_ALBUM_INFO): + let count = avrcp_subevent_now_playing_album_info_get_value_len(packet) + guard count > 0 else { break } + let avrcp_subevent_value = UnsafeBufferPointer( + start: avrcp_subevent_now_playing_album_info_get_value(packet), + count: Int(count)) + log("AVRCP Controller: Album \(cString: avrcp_subevent_value)") + + case UInt8(AVRCP_SUBEVENT_NOW_PLAYING_GENRE_INFO): + let count = avrcp_subevent_now_playing_genre_info_get_value_len(packet) + guard count > 0 else { break } + let avrcp_subevent_value = UnsafeBufferPointer( + start: avrcp_subevent_now_playing_genre_info_get_value(packet), + count: Int(count)) + log("AVRCP Controller: Genre \(cString: avrcp_subevent_value)") + + case UInt8(AVRCP_SUBEVENT_PLAY_STATUS): + let songLength = avrcp_subevent_play_status_get_song_length(packet) + let songPosition = avrcp_subevent_play_status_get_song_position(packet) + let playStatus = avrcp_play_status2str( + avrcp_subevent_play_status_get_play_status(packet)) + log( + "AVRCP Controller: Song length \(songLength) ms, Song position \(songPosition) ms, Play status \(cString: playStatus)" + ) + + case UInt8(AVRCP_SUBEVENT_OPERATION_COMPLETE): + let operationId = avrcp_operation2str( + avrcp_subevent_operation_complete_get_operation_id(packet)) + log("AVRCP Controller: \(cString: operationId) complete") + + case UInt8(AVRCP_SUBEVENT_OPERATION_START): + let operationId = avrcp_operation2str( + avrcp_subevent_operation_start_get_operation_id(packet)) + log("AVRCP Controller: \(cString: operationId) start") + + case UInt8(AVRCP_SUBEVENT_NOTIFICATION_EVENT_TRACK_REACHED_END): + log("AVRCP Controller: Track reached end") + + case UInt8(AVRCP_SUBEVENT_PLAYER_APPLICATION_VALUE_RESPONSE): + let commandType = avrcp_ctype2str( + avrcp_subevent_player_application_value_response_get_command_type(packet)) + log("AVRCP Controller: Set Player App Value \(cString: commandType)") + + default: + break + } +} + +@_cdecl("avrcp_target_packet_handler") +func avrcp_target_packet_handler( + packet_type: UInt8, + channel: UInt16, + packet: UnsafeMutablePointer?, + size: UInt16 +) { + guard packet_type == HCI_EVENT_PACKET else { return } + guard hci_event_packet_get_type(packet) == HCI_EVENT_AVRCP_META else { + return + } + + let subevent = packet?[2] + switch subevent { + case UInt8(AVRCP_SUBEVENT_NOTIFICATION_VOLUME_CHANGED): + let volume = avrcp_subevent_notification_volume_changed_get_absolute_volume( + packet) + log("AVRCP Target : Volume set to [\(volume) / 127]") + Application.shared.audioEngine.set(volume: volume << 1) + + case UInt8(AVRCP_SUBEVENT_OPERATION): + let operation_id = avrcp_operation_id_t( + avrcp_subevent_operation_get_operation_id(packet)) + let button_state: StaticString = + avrcp_subevent_operation_get_button_pressed(packet) > 0 + ? "PRESS" : "RELEASE" + switch operation_id { + case AVRCP_OPERATION_ID_VOLUME_UP: + log("AVRCP Target : VOLUME UP (\(button_state))") + case AVRCP_OPERATION_ID_VOLUME_DOWN: + log("AVRCP Target : VOLUME DOWN (\(button_state))") + default: + return + } + + default: + log("AVRCP Target : Event \(hex: subevent ?? 0xff) is not parsed") + } +} diff --git a/harmony/Sources/Bluetooth/HCI.swift b/harmony/Sources/Bluetooth/HCI.swift new file mode 100644 index 00000000..e025e281 --- /dev/null +++ b/harmony/Sources/Bluetooth/HCI.swift @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +// Host Controller Interface + +@_cdecl("hci_packet_handler") +func hci_packet_handler( + packet_type: UInt8, + channel: UInt16, + packet: UnsafeMutablePointer?, + size: UInt16 +) { + guard packet_type == HCI_EVENT_PACKET else { return } + guard hci_event_packet_get_type(packet) == HCI_EVENT_PIN_CODE_REQUEST else { + return + } + + var address: bd_addr_t = (0, 0, 0, 0, 0, 0) + log("Pin code request - using '0000'") + hci_event_pin_code_request_get_bd_addr(packet, &address) + gap_pin_code_response(&address, "0000") +} diff --git a/harmony/Sources/Bluetooth/SBC.swift b/harmony/Sources/Bluetooth/SBC.swift new file mode 100644 index 00000000..2b9a3093 --- /dev/null +++ b/harmony/Sources/Bluetooth/SBC.swift @@ -0,0 +1,61 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +enum SBCDecoder { + typealias Callback = ( + _ data: UnsafeMutableBufferPointer, + _ num_channels: Int32, + _ sample_rate: Int32 + ) -> Void + + static var context = btstack_sbc_decoder_bluedroid_t() + static var instance: UnsafePointer? = nil + static var callback: Callback? = nil + + static func configure(mode: btstack_sbc_mode_t) { + self.instance = btstack_sbc_decoder_bluedroid_init_instance(&context) + + func decode_callback( + _ data: UnsafeMutablePointer?, + _ num_samples: Int32, + _ num_channels: Int32, + _ sample_rate: Int32, + _ context: UnsafeMutableRawPointer? + ) { + let data = UnsafeMutableBufferPointer( + start: data, count: Int(num_samples)) + Self.callback?(data, num_channels, sample_rate) + } + + instance?.pointee.configure(&context, mode, decode_callback, nil) + } + + static func decode_signed_16( + mode: btstack_sbc_mode_t, + packet_status_flag: UInt8, + buffer: UnsafeRawBufferPointer, + callback: Callback + ) { + guard let instance = Self.instance else { + preconditionFailure("Must call configure prior to decode_signed_16") + } + + return withoutActuallyEscaping(callback) { + Self.callback = $0 + instance.pointee.decode_signed_16( + &Self.context, + packet_status_flag, + buffer.baseAddress, + UInt16(buffer.count)) + Self.callback = nil + } + } +} diff --git a/harmony/Sources/Bluetooth/SDP.swift b/harmony/Sources/Bluetooth/SDP.swift new file mode 100644 index 00000000..d7d37fac --- /dev/null +++ b/harmony/Sources/Bluetooth/SDP.swift @@ -0,0 +1,50 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +// Service Discovery Protocol + +struct ServiceDiscoveryProtocol: ~Copyable { + typealias ServiceRecord = UnsafePointer + typealias ServiceRecordHandle = UInt32 + + init() { + sdp_init() + } + + deinit { + sdp_deinit() + } + + mutating func registerService(record: ServiceRecord) { + precondition(sdp_register_service(record) == 0) + } + + mutating func registerService(record: UnsafeMutableRawBufferPointer) { + precondition(de_get_len(record.baseAddress) <= record.count) + precondition(sdp_register_service(record.baseAddress) == 0) + } + + mutating func unregisterService(handle: ServiceRecordHandle) { + sdp_unregister_service(handle) + } + + mutating func getServiceRecordHandle(for record: ServiceRecord) -> ServiceRecordHandle { + sdp_get_service_record_handle(record) + } + + mutating func makeServiceRecordHandle() -> ServiceRecordHandle { + sdp_create_service_record_handle() + } + + mutating func getServiceRecord(for handle: ServiceRecordHandle) -> ServiceRecord? { + ServiceRecord(sdp_get_record_for_handle(handle)) + } +} diff --git a/harmony/Sources/PIOPrograms/I2S.pio b/harmony/Sources/PIOPrograms/I2S.pio new file mode 100644 index 00000000..7b9ab6ec --- /dev/null +++ b/harmony/Sources/PIOPrograms/I2S.pio @@ -0,0 +1,64 @@ +; +; Copyright (c) 2020 Raspberry Pi (Trading) Ltd. +; +; SPDX-License-Identifier: BSD-3-Clause +; + +; Transmit a mono or stereo I2S audio stream as stereo +; This is 16 bits per sample; can be altered by modifying the "set" params, +; or made programmable by replacing "set x" with "mov x, y" and using Y as a config register. +; +; Autopull must be enabled, with threshold set to 32. +; Since I2S is MSB-first, shift direction should be to left. +; Hence the format of the FIFO word is: +; +; | 31 : 16 | 15 : 0 | +; | sample ws=0 | sample ws=1 | +; +; Data is output at 1 bit per clock. Use clock divider to adjust frequency. +; Fractional divider will probably be needed to get correct bit clock period, +; but for common syslck freqs this should still give a constant word select period. +; +; One output pin is used for the data output. +; Two side-set pins are used. Bit 0 is clock, bit 1 is word select. + +; Send 16 bit words to the PIO for mono, 32 bit words for stereo + +.program audio_i2s +.side_set 2 + + ; /--- LRCLK + ; |/-- BCLK +bitloop1: ; || + out pins, 1 side 0b10 + jmp x-- bitloop1 side 0b11 + out pins, 1 side 0b00 + set x, 14 side 0b01 + +bitloop0: + out pins, 1 side 0b00 + jmp x-- bitloop0 side 0b01 + out pins, 1 side 0b10 +public entry_point: + set x, 14 side 0b11 + +% c-sdk { + +static inline void audio_i2s_program_init(PIO pio, uint sm, uint offset, uint data_pin, uint clock_pin_base) { + pio_sm_config sm_config = audio_i2s_program_get_default_config(offset); + + sm_config_set_out_pins(&sm_config, data_pin, 1); + sm_config_set_sideset_pins(&sm_config, clock_pin_base); + sm_config_set_out_shift(&sm_config, false, true, 32); + sm_config_set_fifo_join(&sm_config, PIO_FIFO_JOIN_TX); + + pio_sm_init(pio, sm, offset, &sm_config); + + uint pin_mask = (1u << data_pin) | (3u << clock_pin_base); + pio_sm_set_pindirs_with_mask(pio, sm, pin_mask, pin_mask); + pio_sm_set_pins(pio, sm, 0); // clear pins + + pio_sm_exec(pio, sm, pio_encode_jmp(offset + audio_i2s_offset_entry_point)); +} + +%} diff --git a/harmony/Sources/PIOPrograms/QuadratureEncoder.pio b/harmony/Sources/PIOPrograms/QuadratureEncoder.pio new file mode 100644 index 00000000..37ed3948 --- /dev/null +++ b/harmony/Sources/PIOPrograms/QuadratureEncoder.pio @@ -0,0 +1,148 @@ + +// FROM: https://github.com/raspberrypi/pico-examples/blob/master/pio/quadrature_encoder/quadrature_encoder.pio + +; +; Copyright (c) 2023 Raspberry Pi (Trading) Ltd. +; +; SPDX-License-Identifier: BSD-3-Clause +; +.pio_version 0 // only requires PIO version 0 + +.program quadrature_encoder + +; the code must be loaded at address 0, because it uses computed jumps +.origin 0 + + +; the code works by running a loop that continuously shifts the 2 phase pins into +; ISR and looks at the lower 4 bits to do a computed jump to an instruction that +; does the proper "do nothing" | "increment" | "decrement" action for that pin +; state change (or no change) + +; ISR holds the last state of the 2 pins during most of the code. The Y register +; keeps the current encoder count and is incremented / decremented according to +; the steps sampled + +; the program keeps trying to write the current count to the RX FIFO without +; blocking. To read the current count, the user code must drain the FIFO first +; and wait for a fresh sample (takes ~4 SM cycles on average). The worst case +; sampling loop takes 10 cycles, so this program is able to read step rates up +; to sysclk / 10 (e.g., sysclk 125MHz, max step rate = 12.5 Msteps/sec) + +; 00 state + JMP update ; read 00 + JMP decrement ; read 01 + JMP increment ; read 10 + JMP update ; read 11 + +; 01 state + JMP increment ; read 00 + JMP update ; read 01 + JMP update ; read 10 + JMP decrement ; read 11 + +; 10 state + JMP decrement ; read 00 + JMP update ; read 01 + JMP update ; read 10 + JMP increment ; read 11 + +; to reduce code size, the last 2 states are implemented in place and become the +; target for the other jumps + +; 11 state + JMP update ; read 00 + JMP increment ; read 01 +decrement: + ; note: the target of this instruction must be the next address, so that + ; the effect of the instruction does not depend on the value of Y. The + ; same is true for the "JMP X--" below. Basically "JMP Y--, " + ; is just a pure "decrement Y" instruction, with no other side effects + JMP Y--, update ; read 10 + + ; this is where the main loop starts +.wrap_target +update: + MOV ISR, Y ; read 11 + PUSH noblock + +sample_pins: + ; we shift into ISR the last state of the 2 input pins (now in OSR) and + ; the new state of the 2 pins, thus producing the 4 bit target for the + ; computed jump into the correct action for this state. Both the PUSH + ; above and the OUT below zero out the other bits in ISR + OUT ISR, 2 + IN PINS, 2 + + ; save the state in the OSR, so that we can use ISR for other purposes + MOV OSR, ISR + ; jump to the correct state machine action + MOV PC, ISR + + ; the PIO does not have a increment instruction, so to do that we do a + ; negate, decrement, negate sequence +increment: + MOV Y, ~Y + JMP Y--, increment_cont +increment_cont: + MOV Y, ~Y +.wrap ; the .wrap here avoids one jump instruction and saves a cycle too + + + +% c-sdk { + +#include "hardware/clocks.h" +#include "hardware/gpio.h" + +// max_step_rate is used to lower the clock of the state machine to save power +// if the application doesn't require a very high sampling rate. Passing zero +// will set the clock to the maximum + +static inline void quadrature_encoder_program_init(PIO pio, uint sm, uint pin, int max_step_rate) +{ + pio_sm_set_consecutive_pindirs(pio, sm, pin, 2, false); + pio_gpio_init(pio, pin); + pio_gpio_init(pio, pin + 1); + + gpio_pull_up(pin); + gpio_pull_up(pin + 1); + + pio_sm_config c = quadrature_encoder_program_get_default_config(0); + + sm_config_set_in_pins(&c, pin); // for WAIT, IN + sm_config_set_jmp_pin(&c, pin); // for JMP + // shift to left, autopull disabled + sm_config_set_in_shift(&c, false, false, 32); + // don't join FIFO's + sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_NONE); + + // passing "0" as the sample frequency, + if (max_step_rate == 0) { + sm_config_set_clkdiv(&c, 1.0); + } else { + // one state machine loop takes at most 10 cycles + float div = (float)clock_get_hz(clk_sys) / (10 * max_step_rate); + sm_config_set_clkdiv(&c, div); + } + + pio_sm_init(pio, sm, 0, &c); + pio_sm_set_enabled(pio, sm, true); +} + +static inline int32_t quadrature_encoder_get_count(PIO pio, uint sm) +{ + uint ret; + int n; + + // if the FIFO has N entries, we fetch them to drain the FIFO, + // plus one entry which will be guaranteed to not be stale + n = pio_sm_get_rx_fifo_level(pio, sm) + 1; + while (n > 0) { + ret = pio_sm_get_blocking(pio, sm); + n--; + } + return ret; +} + +%} \ No newline at end of file diff --git a/harmony/Sources/PIOPrograms/WS2812.pio b/harmony/Sources/PIOPrograms/WS2812.pio new file mode 100644 index 00000000..839ce5f2 --- /dev/null +++ b/harmony/Sources/PIOPrograms/WS2812.pio @@ -0,0 +1,49 @@ +; +; Copyright (c) 2020 Raspberry Pi (Trading) Ltd. +; +; SPDX-License-Identifier: BSD-3-Clause +; +.pio_version 0 // only requires PIO version 0 + +.program ws2812 +.side_set 1 + +; The following constants are selected for broad compatibility with WS2812, +; WS2812B, and SK6812 LEDs. Other constants may support higher bandwidths for +; specific LEDs, such as (7,10,8) for WS2812B LEDs. + +.define public T1 3 +.define public T2 3 +.define public T3 4 + +.wrap_target +bitloop: + out x, 1 side 0 [T3 - 1] ; Side-set still takes place when instruction stalls + jmp !x do_zero side 1 [T1 - 1] ; Branch on the bit we shifted out. Positive pulse +do_one: + jmp bitloop side 1 [T2 - 1] ; Continue driving high, for a long pulse +do_zero: + nop side 0 [T2 - 1] ; Or drive low, for a short pulse +.wrap + +% c-sdk { +#include "hardware/clocks.h" + +static inline void ws2812_program_init(PIO pio, uint sm, uint offset, uint pin, float freq, bool rgbw) { + + pio_gpio_init(pio, pin); + pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true); + + pio_sm_config c = ws2812_program_get_default_config(offset); + sm_config_set_sideset_pins(&c, pin); + sm_config_set_out_shift(&c, false, true, rgbw ? 32 : 24); + sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX); + + int cycles_per_bit = ws2812_T1 + ws2812_T2 + ws2812_T3; + float div = clock_get_hz(clk_sys) / (freq * cycles_per_bit); + sm_config_set_clkdiv(&c, div); + + pio_sm_init(pio, sm, offset, &c); + pio_sm_set_enabled(pio, sm, true); +} +%} diff --git a/harmony/Tests/AudioTests/RingBufferTests.swift b/harmony/Tests/AudioTests/RingBufferTests.swift new file mode 100644 index 00000000..70e119e9 --- /dev/null +++ b/harmony/Tests/AudioTests/RingBufferTests.swift @@ -0,0 +1,177 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors. +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest + +@testable import Core + +extension RingBuffer { + mutating func write(contentsOf array: [Element]) -> Bool { + array.withUnsafeBufferPointer { + self.write(contentsOf: $0) + } + } + + mutating func read(into array: inout [Element], count: Int? = nil) -> Int { + array.withUnsafeMutableBufferPointer { buffer in + self.read(into: buffer, count: count) + } + } + + func assertState( + count: Int, + readerIndex: Int, + writerIndex: Int, + file: StaticString = #filePath, + line: UInt = #line + ) { + XCTAssertEqual( + self.availableCapacity, self.capacity - count, + "incorrect availableCapacity", file: file, line: line) + XCTAssertEqual( + self.isEmpty, (count == 0), "incorrect isEmpty", file: file, line: line) + XCTAssertEqual( + self.isFull, (count == self.capacity), "incorrect isFull", file: file, + line: line) + XCTAssertEqual(self.count, count, "incorrect count", file: file, line: line) + XCTAssertEqual( + self.readerIndex, readerIndex, "incorrect readerIndex", file: file, + line: line) + XCTAssertEqual( + self.writerIndex, writerIndex, "incorrect writerIndex", file: file, + line: line) + if self.isEmpty || self.isFull { + XCTAssertEqual(self.readerIndex, self.writerIndex, file: file, line: line) + } + } +} + +final class RingBufferTests: XCTestCase { + func testInitialization() { + let ringBuffer = RingBuffer(capacity: 10) + XCTAssertEqual(ringBuffer.capacity, 10) + ringBuffer.assertState(count: 0, readerIndex: 0, writerIndex: 0) + } + + func testCapacityAndAvailableCapacity() { + var ringBuffer = RingBuffer(capacity: 5) + ringBuffer.assertState(count: 0, readerIndex: 0, writerIndex: 0) + + XCTAssertTrue(ringBuffer.write(contentsOf: [1, 2, 3])) + ringBuffer.assertState(count: 3, readerIndex: 0, writerIndex: 3) + + XCTAssertTrue(ringBuffer.write(contentsOf: [4, 5])) + ringBuffer.assertState(count: 5, readerIndex: 0, writerIndex: 0) + } + + func testWriteAndRead() { + var ringBuffer = RingBuffer(capacity: 5) + + // Write data to the buffer + XCTAssertTrue(ringBuffer.write(contentsOf: [1, 2, 3])) + ringBuffer.assertState(count: 3, readerIndex: 0, writerIndex: 3) + + // Attempt to read from the buffer + var readBuffer = Array(repeating: 0, count: 3) + let readCount = ringBuffer.read(into: &readBuffer) + XCTAssertEqual(readCount, 3) + XCTAssertEqual(readBuffer, [1, 2, 3]) + ringBuffer.assertState(count: 0, readerIndex: 3, writerIndex: 3) + } + + func testOverwriteBehavior() { + var ringBuffer = RingBuffer(capacity: 3) + + // Fill buffer to capacity + XCTAssertTrue(ringBuffer.write(contentsOf: [1, 2, 3])) + ringBuffer.assertState(count: 3, readerIndex: 0, writerIndex: 0) + + // Attempt to overwrite when full + XCTAssertFalse(ringBuffer.write(contentsOf: [4])) + ringBuffer.assertState(count: 3, readerIndex: 0, writerIndex: 0) + + // Read and check if the buffer remains unaltered + var readBuffer = Array(repeating: 0, count: 3) + let readCount = ringBuffer.read(into: &readBuffer) + XCTAssertEqual(readCount, 3) + XCTAssertEqual(readBuffer, [1, 2, 3]) + ringBuffer.assertState(count: 0, readerIndex: 0, writerIndex: 0) + } + + func testClearBuffer() { + var ringBuffer = RingBuffer(capacity: 5) + XCTAssertTrue(ringBuffer.write(contentsOf: [1, 2, 3])) + ringBuffer.assertState(count: 3, readerIndex: 0, writerIndex: 3) + + ringBuffer.clear() + ringBuffer.assertState(count: 0, readerIndex: 0, writerIndex: 0) + } + + func testWrappingBehavior() { + var ringBuffer = RingBuffer(capacity: 5) + + // Step 1: Write some data to fill part of the buffer + XCTAssertTrue(ringBuffer.write(contentsOf: [1, 2, 3])) + ringBuffer.assertState(count: 3, readerIndex: 0, writerIndex: 3) + + // Step 2: Read some data, advancing the reader index + var readBuffer = Array(repeating: 0, count: 2) + let readCount = ringBuffer.read(into: &readBuffer) + XCTAssertEqual(readCount, 2) + XCTAssertEqual(readBuffer, [1, 2]) + ringBuffer.assertState(count: 1, readerIndex: 2, writerIndex: 3) + + // Step 3: Write more data, causing the writer index to wrap around + XCTAssertTrue(ringBuffer.write(contentsOf: [4, 5, 6])) + ringBuffer.assertState(count: 4, readerIndex: 2, writerIndex: 1) + + // Step 4: Read remaining data to verify the wrap-around behavior + readBuffer = Array(repeating: 0, count: 4) + let totalReadCount = ringBuffer.read(into: &readBuffer) + XCTAssertEqual(totalReadCount, 4) + XCTAssertEqual(readBuffer, [3, 4, 5, 6]) + ringBuffer.assertState(count: 0, readerIndex: 1, writerIndex: 1) + } + + func testWrappingWriteOverflowAndWrappingReadUnderflow() { + var ringBuffer = RingBuffer(capacity: 5) + + // Step 1: Fill the buffer almost to capacity + XCTAssertTrue(ringBuffer.write(contentsOf: [1, 2, 3])) + ringBuffer.assertState(count: 3, readerIndex: 0, writerIndex: 3) + + // Step 2: Read some data to advance the reader index + var readBuffer = Array(repeating: 0, count: 2) + let readCount = ringBuffer.read(into: &readBuffer) + XCTAssertEqual(readCount, 2) + XCTAssertEqual(readBuffer, [1, 2]) + ringBuffer.assertState(count: 1, readerIndex: 2, writerIndex: 3) + + // Step 3: Write more data to cause the writer index to wrap around + XCTAssertTrue(ringBuffer.write(contentsOf: [4, 5, 6])) + // Writer wraps around + ringBuffer.assertState(count: 4, readerIndex: 2, writerIndex: 1) + + // Step 4: Attempt a write that overflows (fails due to lack of capacity) + XCTAssertFalse(ringBuffer.write(contentsOf: [7, 8, 9])) + // State remains unchanged + ringBuffer.assertState(count: 4, readerIndex: 2, writerIndex: 1) + + // Step 5: Read more data than available to test wrapping underflow + readBuffer = Array(repeating: 0, count: 5) + let underflowReadCount = ringBuffer.read(into: &readBuffer) + XCTAssertEqual(underflowReadCount, 4) // Should only read 4 elements + // Validate read data + XCTAssertEqual(readBuffer.prefix(underflowReadCount), [3, 4, 5, 6]) + // Reader wraps around + ringBuffer.assertState(count: 0, readerIndex: 1, writerIndex: 1) + } +} diff --git a/harmony/assets/harmony.jpeg b/harmony/assets/harmony.jpeg new file mode 100644 index 00000000..c8ff0dab Binary files /dev/null and b/harmony/assets/harmony.jpeg differ diff --git a/harmony/include/btstack_config.h b/harmony/include/btstack_config.h new file mode 100644 index 00000000..1b969e1b --- /dev/null +++ b/harmony/include/btstack_config.h @@ -0,0 +1,89 @@ +#ifndef _PICO_BTSTACK_BTSTACK_CONFIG_H +#define _PICO_BTSTACK_BTSTACK_CONFIG_H + +// BTstack features that can be enabled +#define ENABLE_LOG_INFO +#define ENABLE_LOG_ERROR +#define ENABLE_PRINTF_HEXDUMP +#define ENABLE_SCO_OVER_HCI + +#ifdef ENABLE_BLE +#define ENABLE_GATT_CLIENT_PAIRING +#define ENABLE_L2CAP_LE_CREDIT_BASED_FLOW_CONTROL_MODE +#define ENABLE_LE_CENTRAL +#define ENABLE_LE_DATA_LENGTH_EXTENSION +#define ENABLE_LE_PERIPHERAL +#define ENABLE_LE_PRIVACY_ADDRESS_RESOLUTION +#define ENABLE_LE_SECURE_CONNECTIONS +#endif + +#ifdef ENABLE_CLASSIC +#define ENABLE_L2CAP_ENHANCED_RETRANSMISSION_MODE +#define ENABLE_GOEP_L2CAP +#endif + +#if defined (ENABLE_CLASSIC) && defined(ENABLE_BLE) +#define ENABLE_CROSS_TRANSPORT_KEY_DERIVATION +#endif + +// BTstack configuration. buffers, sizes, ... +#define HCI_OUTGOING_PRE_BUFFER_SIZE 4 +#define HCI_ACL_PAYLOAD_SIZE (1691 + 4) +#define HCI_ACL_CHUNK_SIZE_ALIGNMENT 4 +#define MAX_NR_AVDTP_CONNECTIONS 1 +#define MAX_NR_AVDTP_STREAM_ENDPOINTS 1 +#define MAX_NR_AVRCP_CONNECTIONS 2 +#define MAX_NR_BNEP_CHANNELS 1 +#define MAX_NR_BNEP_SERVICES 1 +#define MAX_NR_BTSTACK_LINK_KEY_DB_MEMORY_ENTRIES 2 +#define MAX_NR_GATT_CLIENTS 1 +#define MAX_NR_HCI_CONNECTIONS 2 +#define MAX_NR_HID_HOST_CONNECTIONS 1 +#define MAX_NR_HIDS_CLIENTS 1 +#define MAX_NR_HFP_CONNECTIONS 1 +#define MAX_NR_L2CAP_CHANNELS 4 +#define MAX_NR_L2CAP_SERVICES 3 +#define MAX_NR_RFCOMM_CHANNELS 1 +#define MAX_NR_RFCOMM_MULTIPLEXERS 1 +#define MAX_NR_RFCOMM_SERVICES 1 +#define MAX_NR_SERVICE_RECORD_ITEMS 4 +#define MAX_NR_SM_LOOKUP_ENTRIES 3 +#define MAX_NR_WHITELIST_ENTRIES 16 +#define MAX_NR_LE_DEVICE_DB_ENTRIES 16 + +// Limit number of ACL/SCO Buffer to use by stack to avoid cyw43 shared bus overrun +#define MAX_NR_CONTROLLER_ACL_BUFFERS 3 +#define MAX_NR_CONTROLLER_SCO_PACKETS 3 + +// Enable and configure HCI Controller to Host Flow Control to avoid cyw43 shared bus overrun +#define ENABLE_HCI_CONTROLLER_TO_HOST_FLOW_CONTROL +#define HCI_HOST_ACL_PACKET_LEN 1024 +#define HCI_HOST_ACL_PACKET_NUM 3 +#define HCI_HOST_SCO_PACKET_LEN 120 +#define HCI_HOST_SCO_PACKET_NUM 3 + +// Link Key DB and LE Device DB using TLV on top of Flash Sector interface +#define NVM_NUM_DEVICE_DB_ENTRIES 16 +#define NVM_NUM_LINK_KEYS 16 + +// We don't give btstack a malloc, so use a fixed-size ATT DB. +#define MAX_ATT_DB_SIZE 512 + +// BTstack HAL configuration +#define HAVE_EMBEDDED_TIME_MS + +// map btstack_assert onto Pico SDK assert() +#define HAVE_ASSERT + +// Some USB dongles take longer to respond to HCI reset (e.g. BCM20702A). +#define HCI_RESET_RESEND_TIMEOUT_MS 1000 + +#define ENABLE_SOFTWARE_AES128 +#define ENABLE_MICRO_ECC_FOR_LE_SECURE_CONNECTIONS + +#define HAVE_BTSTACK_STDIN + +// To get the audio demos working even with HCI dump at 115200, this truncates long ACL packets +//#define HCI_DUMP_STDOUT_MAX_SIZE_ACL 100 + +#endif // _PICO_BTSTACK_BTSTACK_CONFIG_H diff --git a/harmony/include/lwipopts.h b/harmony/include/lwipopts.h new file mode 100644 index 00000000..fefe1e4b --- /dev/null +++ b/harmony/include/lwipopts.h @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#ifndef _LWIPOPTS_H +#define _LWIPOPTS_H + +#define NO_SYS 1 +#define LWIP_SOCKET 0 +#define LWIP_NETCONN 0 + +// Watch out: Without this, lwip fails to initialize and crashes inside +// memp_init_pool due to misaligned memory access (the fallback is "1"). +#define MEM_ALIGNMENT 4 + +#endif