: 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