Add A2DP sink (speaker) support (#2177)

Provide direct connection from BT audio to I2S and PWM audio outputs.
Example included showing play/pause operation.
This commit is contained in:
Earle F. Philhower, III 2024-05-29 14:53:06 -07:00 committed by GitHub
parent ec5e62e533
commit 01e9dc99f2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1561 additions and 68 deletions

View file

@ -0,0 +1,59 @@
// A2DPSink example - Released to the public domain in 2024 by Earle F. Philhower, III
// Hook up a phono plug to GP0 and GP1 (and GND of course...the 1st 3 pins on the PCB)
// Connect wired earbuds up and connect over BT from your phone and play some music.
#include <BluetoothAudio.h>
#include <PWMAudio.h>
PWMAudio pwm;
A2DPSink a2dp;
volatile A2DPSink::PlaybackStatus status = A2DPSink::STOPPED;
void volumeCB(void *param, int pct) {
(void) param;
Serial.printf("Speaker volume changed to %d%%\n", pct);
}
void connectCB(void *param, bool connected) {
(void) param;
if (connected) {
Serial.printf("A2DP connection started to %s\n", bd_addr_to_str(a2dp.getSourceAddress()));
} else {
Serial.printf("A2DP connection stopped\n");
}
}
void playbackCB(void *param, A2DPSink::PlaybackStatus state) {
(void) param;
status = state;
}
void setup() {
Serial.begin(115200);
delay(3000);
Serial.printf("Starting, connect to the PicoW and start playing music\n");
Serial.printf("Use BOOTSEL to pause/resume playback\n");
a2dp.setName("PicoW Boom 00:00:00:00:00:00");
a2dp.setConsumer(new BluetoothAudioConsumerPWM(pwm));
a2dp.onVolume(volumeCB);
a2dp.onConnect(connectCB);
a2dp.onPlaybackStatus(playbackCB);
a2dp.begin();
}
void loop() {
if (BOOTSEL) {
__lockBluetooth();
if (status == A2DPSink::PAUSED) {
a2dp.play();
Serial.printf("Resuming\n");
} else if (status == A2DPSink::PLAYING) {
a2dp.pause();
Serial.printf("Pausing\n");
}
__unlockBluetooth();
while (BOOTSEL);
}
}

View file

@ -4,6 +4,8 @@
#include <BluetoothAudio.h>
#include "raw.h"
A2DPSource a2dp;
int16_t pcm[64 * 2];
uint32_t phase = 0;
volatile uint32_t fr = 32;
@ -39,7 +41,7 @@ void volumeCB(void *param, int pct) {
void connectCB(void *param, bool connected) {
(void) param;
if (connected) {
Serial.printf("A2DP connection started to %s\n", bd_addr_to_str(A2DPSource.getSinkAddress()));
Serial.printf("A2DP connection started to %s\n", bd_addr_to_str(a2dp.getSinkAddress()));
} else {
Serial.printf("A2DP connection stopped\n");
}
@ -67,10 +69,10 @@ void fillPCM() {
void setup() {
delay(2000);
A2DPSource.onAVRCP(avrcpCB);
A2DPSource.onVolume(volumeCB);
A2DPSource.onConnect(connectCB);
A2DPSource.begin();
a2dp.onAVRCP(avrcpCB);
a2dp.onVolume(volumeCB);
a2dp.onConnect(connectCB);
a2dp.begin();
Serial.printf("Starting, press BOOTSEL to pair to first found speaker\n");
Serial.printf("Use the forward button on speaker to change tones\n");
Serial.printf("Use the reverse button on speaker to alternate between tones and Au Claire De La Lune\n");
@ -78,18 +80,18 @@ void setup() {
}
void loop() {
while ((size_t)A2DPSource.availableForWrite() > sizeof(pcm)) {
while ((size_t)a2dp.availableForWrite() > sizeof(pcm)) {
fillPCM();
A2DPSource.write((const uint8_t *)pcm, sizeof(pcm));
a2dp.write((const uint8_t *)pcm, sizeof(pcm));
}
if (BOOTSEL) {
while (BOOTSEL) {
delay(1);
}
A2DPSource.disconnect();
A2DPSource.clearPairing();
a2dp.disconnect();
a2dp.clearPairing();
Serial.printf("Connecting...");
if (A2DPSource.connect()) {
if (a2dp.connect()) {
Serial.printf("Connected!\n");
} else {
Serial.printf("Failed! :(\n");

View file

@ -6,8 +6,14 @@
# Datatypes (KEYWORD1)
#######################################
BluetoothAudio KEYWORD1
BluetoothHCI KEYWORD1
A2DPSource KEYWORD1
BTDeviceInfo KEYWORD1
BluetoothAudioConsumer KEYWORD1
BluetoothAudioConsumerI2S KEYWORD1
BluetoothAudioConsumerPWM KEYWORD1
A2DPSink KEYWORD1
#######################################
# Methods and Functions (KEYWORD2)
@ -15,6 +21,7 @@ BTDeviceInfo KEYWORD1
begin KEYWORD2
end KEYWORD2
setConsumer KEYWORD2
setFrequency KEYWORD2
setName KEYWORD2
setsetBufferSize KEYWORD2
@ -26,14 +33,24 @@ connected KEYWORD2
disconnect KEYWORD2
clearPairing KEYWORD2
connect KEYWORD2
connect KEYWORD2
connect KEYWORD2
play KEYWORD2
stop KEYWORD2
pause KEYWORD2
fastForward KEYWORD2
rewind KEYWORD2
forward KEYWORD2
backward KEYWORD2
volumeUp KEYWORD2
volumeDown KEYWORD2
mute KEYWORD2
onTransmit KEYWORD2
onAVRCP KEYWORD2
onBattery KEYWORD2
onVolume KEYWORD2
onConnect KEYWORD2
onPlaybackStatus KEYWORD2
# BTDeviceInfo
deviceClass KEYWORD2
@ -45,3 +62,6 @@ name KEYWORD2
#######################################
# Constants (LITERAL1)
#######################################
STOPPED LITERAL
PLAYING LITERAL
PAUSED LITERAL

View file

@ -0,0 +1,800 @@
/*
A2DP Sink (Bluetooth audio receiver)
Copyright (c) 2024 Earle F. Philhower, III <earlephilhower@yahoo.com>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "A2DPSink.h"
#include <functional>
#define CCALLBACKNAME _A2DPSINKCB
#include <ctocppcallback.h>
#define PACKETHANDLERCB(class, cbFcn) \
(CCALLBACKNAME<void(uint8_t, uint16_t, uint8_t*, uint16_t), __COUNTER__>::func = std::bind(&class::cbFcn, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), \
static_cast<btstack_packet_handler_t>(CCALLBACKNAME<void(uint8_t, uint16_t, uint8_t*, uint16_t), __COUNTER__ - 1>::callback))
#define L2CAPPACKETHANDLERCB(class, cbFcn) \
(CCALLBACKNAME<void(uint8_t, uint8_t*, uint16_t), __COUNTER__>::func = std::bind(&class::cbFcn, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3), \
static_cast<void (*)(uint8_t, uint8_t *, uint16_t)>(CCALLBACKNAME<void(uint8_t, uint8_t*, uint16_t), __COUNTER__ - 1>::callback))
#define PCMDECODERCB(class, cbFcn) \
(CCALLBACKNAME<void(int16_t*, int, int, int, void*), __COUNTER__>::func = std::bind(&class::cbFcn, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5), \
static_cast<void (*)(int16_t *, int, int, int, void *)>(CCALLBACKNAME<void(int16_t *, int, int, int, void *), __COUNTER__ - 1>::callback))
// Based off of the BlueKitchen A2DP sink demo
/*
Copyright (C) 2023 BlueKitchen GmbH
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
*/
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "btstack.h"
#include "btstack_resample.h"
#include "btstack_ring_buffer.h"
bool A2DPSink::begin() {
if (_running || !_consumer) {
return false;
}
// init protocols
l2cap_init();
sdp_init();
#ifdef ENABLE_BLE
// Initialize LE Security Manager. Needed for cross-transport key derivation
sm_init();
#endif
// Init profiles
a2dp_sink_init();
avrcp_init();
avrcp_controller_init();
avrcp_target_init();
// Configure A2DP Sink
a2dp_sink_register_packet_handler(PACKETHANDLERCB(A2DPSink, a2dp_sink_packet_handler));
a2dp_sink_register_media_handler(L2CAPPACKETHANDLERCB(A2DPSink, handle_l2cap_media_data_packet));
a2dp_sink_stream_endpoint_t * stream_endpoint = &a2dp_sink_stream_endpoint;
avdtp_stream_endpoint_t * local_stream_endpoint = a2dp_sink_create_stream_endpoint(AVDTP_AUDIO,
AVDTP_CODEC_SBC, media_sbc_codec_capabilities, sizeof(media_sbc_codec_capabilities),
stream_endpoint->media_sbc_codec_configuration, sizeof(stream_endpoint->media_sbc_codec_configuration));
if (!local_stream_endpoint) {
DEBUGV("A2DP Source: not enough memory to create local stream endpoint\n");
return false;
}
// - Store stream enpoint's SEP ID, as it is used by A2DP API to identify the stream endpoint
stream_endpoint->a2dp_local_seid = avdtp_local_seid(local_stream_endpoint);
// Configure AVRCP Controller + Target
avrcp_register_packet_handler(PACKETHANDLERCB(A2DPSink, avrcp_packet_handler));
avrcp_controller_register_packet_handler(PACKETHANDLERCB(A2DPSink, avrcp_controller_packet_handler));
avrcp_target_register_packet_handler(PACKETHANDLERCB(A2DPSink, avrcp_target_packet_handler));
// Configure SDP
// - Create and register A2DP Sink service record
memset(sdp_avdtp_sink_service_buffer, 0, sizeof(sdp_avdtp_sink_service_buffer));
a2dp_sink_create_sdp_record(sdp_avdtp_sink_service_buffer, sdp_create_service_record_handle(),
AVDTP_SINK_FEATURE_MASK_HEADPHONE, NULL, NULL);
sdp_register_service(sdp_avdtp_sink_service_buffer);
// - Create AVRCP Controller service record and register it with SDP. We send Category 1 commands to the media player, e.g. play/pause
memset(sdp_avrcp_controller_service_buffer, 0, sizeof(sdp_avrcp_controller_service_buffer));
uint16_t controller_supported_features = 1 << AVRCP_CONTROLLER_SUPPORTED_FEATURE_CATEGORY_PLAYER_OR_RECORDER;
avrcp_controller_create_sdp_record(sdp_avrcp_controller_service_buffer, sdp_create_service_record_handle(),
controller_supported_features, NULL, NULL);
sdp_register_service(sdp_avrcp_controller_service_buffer);
// - Create and register A2DP Sink service record
// - We receive Category 2 commands from the media player, e.g. volume up/down
memset(sdp_avrcp_target_service_buffer, 0, sizeof(sdp_avrcp_target_service_buffer));
uint16_t target_supported_features = 1 << AVRCP_TARGET_SUPPORTED_FEATURE_CATEGORY_MONITOR_OR_AMPLIFIER;
avrcp_target_create_sdp_record(sdp_avrcp_target_service_buffer,
sdp_create_service_record_handle(), target_supported_features, NULL, NULL);
sdp_register_service(sdp_avrcp_target_service_buffer);
// - Create and register Device ID (PnP) service record
memset(device_id_sdp_service_buffer, 0, sizeof(device_id_sdp_service_buffer));
device_id_create_sdp_record(device_id_sdp_service_buffer,
sdp_create_service_record_handle(), DEVICE_ID_VENDOR_ID_SOURCE_BLUETOOTH, BLUETOOTH_COMPANY_ID_BLUEKITCHEN_GMBH, 1, 1);
sdp_register_service(device_id_sdp_service_buffer);
// Configure GAP - discovery / connection
// - Set local name with a template Bluetooth address, that will be automatically
// replaced with an actual address once it is available, i.e. when BTstack boots
// up and starts talking to a Bluetooth module.
if (!_name) {
setName("PicoW A2DP 00:00:00:00:00:00");
}
gap_set_local_name(_name);
// - Allow to show up in Bluetooth inquiry
gap_discoverable_control(1);
// - Set Class of Device - Service Class: Audio, Major Device Class: Audio, Minor: Loudspeaker
gap_set_class_of_device(0x200414);
// - Allow for role switch in general and sniff mode
gap_set_default_link_policy_settings(LM_LINK_POLICY_ENABLE_ROLE_SWITCH | LM_LINK_POLICY_ENABLE_SNIFF_MODE);
// - Allow for role switch on outgoing connections
// - This allows A2DP Source, e.g. smartphone, to become master when we re-connect to it.
gap_set_allow_role_switch(true);
// Register for HCI events
// hci_event_callback_registration.callback = &hci_packet_handler;
// hci_add_event_handler(&hci_event_callback_registration);
_hci.install();
_running = true;
_hci.begin();
return true;
}
bool A2DPSink::disconnect() {
__lockBluetooth();
a2dp_sink_disconnect(a2dp_sink_a2dp_connection.a2dp_cid);
__unlockBluetooth();
if (!_running || !_connected) {
return false;
}
_connected = false;
return true;
}
void A2DPSink::clearPairing() {
disconnect();
__lockBluetooth();
gap_delete_all_link_keys();
__unlockBluetooth();
}
void A2DPSink::playback_handler(int16_t * buffer, uint16_t num_audio_frames) {
// called from lower-layer but guaranteed to be on main thread
if (sbc_frame_size == 0) {
memset(buffer, 0, num_audio_frames * BYTES_PER_FRAME);
return;
}
// first fill from resampled audio
uint32_t bytes_read;
btstack_ring_buffer_read(&decoded_audio_ring_buffer, (uint8_t *) buffer, num_audio_frames * BYTES_PER_FRAME, &bytes_read);
buffer += bytes_read / NUM_CHANNELS;
num_audio_frames -= bytes_read / BYTES_PER_FRAME;
// then start decoding sbc frames using request_* globals
request_buffer = buffer;
request_frames = num_audio_frames;
while (request_frames && btstack_ring_buffer_bytes_available(&sbc_frame_ring_buffer) >= sbc_frame_size) {
// decode frame
uint8_t sbc_frame[MAX_SBC_FRAME_SIZE];
btstack_ring_buffer_read(&sbc_frame_ring_buffer, sbc_frame, sbc_frame_size, &bytes_read);
btstack_sbc_decoder_process_data(&state, 0, sbc_frame, sbc_frame_size);
}
while (request_frames) {
*(request_buffer++) = 0;
*(request_buffer++) = 0;
request_frames--;
}
}
void A2DPSink::handle_pcm_data(int16_t * data, int num_audio_frames, int num_channels, int sample_rate, void * context) {
UNUSED(sample_rate);
UNUSED(context);
UNUSED(num_channels); // must be stereo == 2
// resample into request buffer - add some additional space for resampling
uint32_t resampled_frames = btstack_resample_block(&resample_instance, data, num_audio_frames, output_buffer);
// store data in btstack_audio buffer first
int frames_to_copy = btstack_min(resampled_frames, request_frames);
memcpy(request_buffer, output_buffer, frames_to_copy * BYTES_PER_FRAME);
request_frames -= frames_to_copy;
request_buffer += frames_to_copy * NUM_CHANNELS;
// and rest in ring buffer
int frames_to_store = resampled_frames - frames_to_copy;
if (frames_to_store) {
int status = btstack_ring_buffer_write(&decoded_audio_ring_buffer, (uint8_t *)&output_buffer[frames_to_copy * NUM_CHANNELS], frames_to_store * BYTES_PER_FRAME);
if (status) {
DEBUGV("Error storing samples in PCM ring buffer!!!\n");
}
}
}
int A2DPSink::media_processing_init(BluetoothMediaCodecConfigurationSBC * configuration) {
if (media_initialized) {
return 0;
}
btstack_sbc_decoder_init(&state, mode, PCMDECODERCB(A2DPSink, handle_pcm_data), NULL);
btstack_ring_buffer_init(&sbc_frame_ring_buffer, sbc_frame_storage, sizeof(sbc_frame_storage));
btstack_ring_buffer_init(&decoded_audio_ring_buffer, decoded_audio_storage, sizeof(decoded_audio_storage));
btstack_resample_init(&resample_instance, configuration->num_channels);
// setup audio playback
_consumer->init(NUM_CHANNELS, configuration->sampling_frequency, this);
audio_stream_started = 0;
media_initialized = 1;
return 0;
}
void A2DPSink::media_processing_start(void) {
if (!media_initialized) {
return;
}
// setup audio playback
_consumer->startStream();
audio_stream_started = 1;
}
void A2DPSink::media_processing_pause(void) {
if (!media_initialized) {
return;
}
// stop audio playback
audio_stream_started = 0;
_consumer->stopStream();
// discard pending data
btstack_ring_buffer_reset(&decoded_audio_ring_buffer);
btstack_ring_buffer_reset(&sbc_frame_ring_buffer);
}
void A2DPSink::media_processing_close(void) {
if (!media_initialized) {
return;
}
media_initialized = 0;
audio_stream_started = 0;
sbc_frame_size = 0;
// stop audio playback
_consumer->close();
}
/* @section Handle Media Data Packet
@text Here the audio data, are received through the handle_l2cap_media_data_packet 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.
*/
void A2DPSink::handle_l2cap_media_data_packet(uint8_t seid, uint8_t *packet, uint16_t size) {
UNUSED(seid);
int pos = 0;
avdtp_media_packet_header_t media_header;
if (!read_media_data_header(packet, size, &pos, &media_header)) {
return;
}
avdtp_sbc_codec_header_t sbc_header;
if (!read_sbc_header(packet, size, &pos, &sbc_header)) {
return;
}
int packet_length = size - pos;
uint8_t *packet_begin = packet + pos;
// store sbc frame size for buffer management
sbc_frame_size = packet_length / sbc_header.num_frames;
int status = btstack_ring_buffer_write(&sbc_frame_ring_buffer, packet_begin, packet_length);
if (status != ERROR_CODE_SUCCESS) {
DEBUGV("Error storing samples in SBC ring buffer!!!\n");
}
// decide on audio sync drift based on number of sbc frames in queue
int sbc_frames_in_buffer = btstack_ring_buffer_bytes_available(&sbc_frame_ring_buffer) / sbc_frame_size;
uint32_t resampling_factor;
// nominal factor (fixed-point 2^16) and compensation offset
uint32_t nominal_factor = 0x10000;
uint32_t compensation = 0x00100;
if (sbc_frames_in_buffer < OPTIMAL_FRAMES_MIN) {
resampling_factor = nominal_factor - compensation; // stretch samples
} else if (sbc_frames_in_buffer <= OPTIMAL_FRAMES_MAX) {
resampling_factor = nominal_factor; // nothing to do
} else {
resampling_factor = nominal_factor + compensation; // compress samples
}
btstack_resample_set_factor(&resample_instance, resampling_factor);
// start stream if enough frames buffered
if (!audio_stream_started && sbc_frames_in_buffer >= OPTIMAL_FRAMES_MIN) {
media_processing_start();
}
}
int A2DPSink::read_sbc_header(uint8_t * packet, int size, int * offset, avdtp_sbc_codec_header_t * sbc_header) {
int sbc_header_len = 12; // without crc
int pos = *offset;
if (size - pos < sbc_header_len) {
DEBUGV("Not enough data to read SBC header, expected %d, received %d\n", sbc_header_len, size - pos);
return 0;
}
sbc_header->fragmentation = get_bit16(packet[pos], 7);
sbc_header->starting_packet = get_bit16(packet[pos], 6);
sbc_header->last_packet = get_bit16(packet[pos], 5);
sbc_header->num_frames = packet[pos] & 0x0f;
pos++;
*offset = pos;
return 1;
}
int A2DPSink::read_media_data_header(uint8_t *packet, int size, int *offset, avdtp_media_packet_header_t *media_header) {
int media_header_len = 12; // without crc
int pos = *offset;
if (size - pos < media_header_len) {
DEBUGV("Not enough data to read media packet header, expected %d, received %d\n", media_header_len, size - pos);
return 0;
}
media_header->version = packet[pos] & 0x03;
media_header->padding = get_bit16(packet[pos], 2);
media_header->extension = get_bit16(packet[pos], 3);
media_header->csrc_count = (packet[pos] >> 4) & 0x0F;
pos++;
media_header->marker = get_bit16(packet[pos], 0);
media_header->payload_type = (packet[pos] >> 1) & 0x7F;
pos++;
media_header->sequence_number = big_endian_read_16(packet, pos);
pos += 2;
media_header->timestamp = big_endian_read_32(packet, pos);
pos += 4;
media_header->synchronization_source = big_endian_read_32(packet, pos);
pos += 4;
*offset = pos;
return 1;
}
void A2DPSink::avrcp_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) {
UNUSED(channel);
UNUSED(size);
uint16_t local_cid;
uint8_t status;
bd_addr_t address;
a2dp_sink_avrcp_connection_t * connection = &a2dp_sink_avrcp_connection;
if (packet_type != HCI_EVENT_PACKET) {
return;
}
if (hci_event_packet_get_type(packet) != HCI_EVENT_AVRCP_META) {
return;
}
switch (packet[2]) {
case AVRCP_SUBEVENT_CONNECTION_ESTABLISHED: {
local_cid = avrcp_subevent_connection_established_get_avrcp_cid(packet);
status = avrcp_subevent_connection_established_get_status(packet);
if (status != ERROR_CODE_SUCCESS) {
DEBUGV("AVRCP: Connection failed, status 0x%02x\n", status);
connection->avrcp_cid = 0;
return;
}
connection->avrcp_cid = local_cid;
avrcp_subevent_connection_established_get_bd_addr(packet, address);
DEBUGV("AVRCP: Connected to %s, cid 0x%02x\n", bd_addr_to_str(address), connection->avrcp_cid);
avrcp_target_support_event(connection->avrcp_cid, AVRCP_NOTIFICATION_EVENT_VOLUME_CHANGED);
avrcp_target_support_event(connection->avrcp_cid, AVRCP_NOTIFICATION_EVENT_BATT_STATUS_CHANGED);
avrcp_target_battery_status_changed(connection->avrcp_cid, battery_status);
// query supported events:
avrcp_controller_get_supported_events(connection->avrcp_cid);
return;
}
case AVRCP_SUBEVENT_CONNECTION_RELEASED:
DEBUGV("AVRCP: Channel released: cid 0x%02x\n", avrcp_subevent_connection_released_get_avrcp_cid(packet));
connection->avrcp_cid = 0;
connection->notifications_supported_by_target = 0;
return;
default:
break;
}
}
void A2DPSink::avrcp_controller_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) {
UNUSED(channel);
UNUSED(size);
// helper to print c strings
uint8_t avrcp_subevent_value[256];
uint8_t play_status;
uint8_t event_id;
a2dp_sink_avrcp_connection_t * avrcp_connection = &a2dp_sink_avrcp_connection;
if (packet_type != HCI_EVENT_PACKET) {
return;
}
if (hci_event_packet_get_type(packet) != HCI_EVENT_AVRCP_META) {
return;
}
if (avrcp_connection->avrcp_cid == 0) {
return;
}
memset(avrcp_subevent_value, 0, sizeof(avrcp_subevent_value));
switch (packet[2]) {
case AVRCP_SUBEVENT_GET_CAPABILITY_EVENT_ID:
avrcp_connection->notifications_supported_by_target |= (1 << avrcp_subevent_get_capability_event_id_get_event_id(packet));
break;
case AVRCP_SUBEVENT_GET_CAPABILITY_EVENT_ID_DONE:
DEBUGV("AVRCP Controller: supported notifications by target:\n");
for (event_id = (uint8_t) AVRCP_NOTIFICATION_EVENT_FIRST_INDEX; event_id < (uint8_t) AVRCP_NOTIFICATION_EVENT_LAST_INDEX; event_id++) {
DEBUGV(" - [%s] %s\n",
(avrcp_connection->notifications_supported_by_target & (1 << event_id)) != 0 ? "X" : " ",
avrcp_notification2str((avrcp_notification_event_id_t)event_id));
}
DEBUGV("\n\n");
// automatically enable notifications
avrcp_controller_enable_notification(avrcp_connection->avrcp_cid, AVRCP_NOTIFICATION_EVENT_PLAYBACK_STATUS_CHANGED);
avrcp_controller_enable_notification(avrcp_connection->avrcp_cid, AVRCP_NOTIFICATION_EVENT_NOW_PLAYING_CONTENT_CHANGED);
avrcp_controller_enable_notification(avrcp_connection->avrcp_cid, AVRCP_NOTIFICATION_EVENT_TRACK_CHANGED);
break;
case AVRCP_SUBEVENT_NOTIFICATION_STATE:
event_id = (avrcp_notification_event_id_t)avrcp_subevent_notification_state_get_event_id(packet);
DEBUGV("AVRCP Controller: %s notification registered\n", avrcp_notification2str((avrcp_notification_event_id_t)event_id));
break;
case AVRCP_SUBEVENT_NOTIFICATION_PLAYBACK_POS_CHANGED:
DEBUGV("AVRCP Controller: Playback position changed, position %d ms\n", (unsigned int) avrcp_subevent_notification_playback_pos_changed_get_playback_position_ms(packet));
break;
case AVRCP_SUBEVENT_NOTIFICATION_PLAYBACK_STATUS_CHANGED:
DEBUGV("AVRCP Controller: Playback status changed %s\n", avrcp_play_status2str(avrcp_subevent_notification_playback_status_changed_get_play_status(packet)));
play_status = avrcp_subevent_notification_playback_status_changed_get_play_status(packet);
switch (play_status) {
case AVRCP_PLAYBACK_STATUS_PLAYING:
avrcp_connection->playing = true;
break;
default:
avrcp_connection->playing = false;
break;
}
if (_playbackStatusCB) {
PlaybackStatus status;
switch (play_status) {
case AVRCP_PLAYBACK_STATUS_PLAYING:
status = PLAYING;
break;
case AVRCP_PLAYBACK_STATUS_PAUSED:
status = PAUSED;
break;
default:
status = STOPPED;
break;
}
_playbackStatusCB(_playbackStatusData, status);
}
break;
case AVRCP_SUBEVENT_NOTIFICATION_NOW_PLAYING_CONTENT_CHANGED:
DEBUGV("AVRCP Controller: Playing content changed\n");
break;
case AVRCP_SUBEVENT_NOTIFICATION_TRACK_CHANGED:
DEBUGV("AVRCP Controller: Track changed\n");
break;
case AVRCP_SUBEVENT_NOTIFICATION_AVAILABLE_PLAYERS_CHANGED:
DEBUGV("AVRCP Controller: Available Players Changed\n");
break;
case AVRCP_SUBEVENT_SHUFFLE_AND_REPEAT_MODE: {
uint8_t shuffle_mode = avrcp_subevent_shuffle_and_repeat_mode_get_shuffle_mode(packet);
uint8_t repeat_mode = avrcp_subevent_shuffle_and_repeat_mode_get_repeat_mode(packet);
(void) shuffle_mode;
(void) repeat_mode;
DEBUGV("AVRCP Controller: %s, %s\n", avrcp_shuffle2str(shuffle_mode), avrcp_repeat2str(repeat_mode));
break;
}
case AVRCP_SUBEVENT_NOW_PLAYING_TRACK_INFO:
DEBUGV("AVRCP Controller: Track %d\n", avrcp_subevent_now_playing_track_info_get_track(packet));
break;
case AVRCP_SUBEVENT_NOW_PLAYING_TOTAL_TRACKS_INFO:
DEBUGV("AVRCP Controller: Total Tracks %d\n", avrcp_subevent_now_playing_total_tracks_info_get_total_tracks(packet));
break;
case AVRCP_SUBEVENT_NOW_PLAYING_TITLE_INFO:
if (avrcp_subevent_now_playing_title_info_get_value_len(packet) > 0) {
memcpy(avrcp_subevent_value, avrcp_subevent_now_playing_title_info_get_value(packet), avrcp_subevent_now_playing_title_info_get_value_len(packet));
DEBUGV("AVRCP Controller: Title %s\n", avrcp_subevent_value);
}
break;
case AVRCP_SUBEVENT_NOW_PLAYING_ARTIST_INFO:
if (avrcp_subevent_now_playing_artist_info_get_value_len(packet) > 0) {
memcpy(avrcp_subevent_value, avrcp_subevent_now_playing_artist_info_get_value(packet), avrcp_subevent_now_playing_artist_info_get_value_len(packet));
DEBUGV("AVRCP Controller: Artist %s\n", avrcp_subevent_value);
}
break;
case AVRCP_SUBEVENT_NOW_PLAYING_ALBUM_INFO:
if (avrcp_subevent_now_playing_album_info_get_value_len(packet) > 0) {
memcpy(avrcp_subevent_value, avrcp_subevent_now_playing_album_info_get_value(packet), avrcp_subevent_now_playing_album_info_get_value_len(packet));
DEBUGV("AVRCP Controller: Album %s\n", avrcp_subevent_value);
}
break;
case AVRCP_SUBEVENT_NOW_PLAYING_GENRE_INFO:
if (avrcp_subevent_now_playing_genre_info_get_value_len(packet) > 0) {
memcpy(avrcp_subevent_value, avrcp_subevent_now_playing_genre_info_get_value(packet), avrcp_subevent_now_playing_genre_info_get_value_len(packet));
DEBUGV("AVRCP Controller: Genre %s\n", avrcp_subevent_value);
}
break;
case AVRCP_SUBEVENT_PLAY_STATUS:
DEBUGV("AVRCP Controller: Song length %" PRIu32 " ms, Song position %" PRIu32 " ms, Play status %s\n",
avrcp_subevent_play_status_get_song_length(packet),
avrcp_subevent_play_status_get_song_position(packet),
avrcp_play_status2str(avrcp_subevent_play_status_get_play_status(packet)));
break;
case AVRCP_SUBEVENT_OPERATION_COMPLETE:
DEBUGV("AVRCP Controller: %s complete\n", avrcp_operation2str(avrcp_subevent_operation_complete_get_operation_id(packet)));
break;
case AVRCP_SUBEVENT_OPERATION_START:
DEBUGV("AVRCP Controller: %s start\n", avrcp_operation2str(avrcp_subevent_operation_start_get_operation_id(packet)));
break;
case AVRCP_SUBEVENT_NOTIFICATION_EVENT_TRACK_REACHED_END:
DEBUGV("AVRCP Controller: Track reached end\n");
break;
case AVRCP_SUBEVENT_PLAYER_APPLICATION_VALUE_RESPONSE:
DEBUGV("AVRCP Controller: Set Player App Value %s\n", avrcp_ctype2str(avrcp_subevent_player_application_value_response_get_command_type(packet)));
break;
default:
break;
}
}
void A2DPSink::avrcp_target_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) {
UNUSED(channel);
UNUSED(size);
if (packet_type != HCI_EVENT_PACKET) {
return;
}
if (hci_event_packet_get_type(packet) != HCI_EVENT_AVRCP_META) {
return;
}
uint8_t volume;
char const * button_state;
(void) button_state;
avrcp_operation_id_t operation_id;
switch (packet[2]) {
case AVRCP_SUBEVENT_NOTIFICATION_VOLUME_CHANGED:
volume = avrcp_subevent_notification_volume_changed_get_absolute_volume(packet);
volume_percentage = volume * 100 / 127;
DEBUGV("AVRCP Target : Volume set to %d%% (%d)\n", volume_percentage, volume);
_consumer->setVolume(volume);
break;
case AVRCP_SUBEVENT_OPERATION:
operation_id = (avrcp_operation_id_t)avrcp_subevent_operation_get_operation_id(packet);
button_state = avrcp_subevent_operation_get_button_pressed(packet) > 0 ? "PRESS" : "RELEASE";
DEBUGV("AVRCP Target: operation %s (%s)\n", avrcp_operation2str(operation_id), button_state);
if (_avrcpCB) {
_avrcpCB(_avrcpData, operation_id, avrcp_subevent_operation_get_button_pressed(packet) > 0);
}
switch (operation_id) {
case AVRCP_OPERATION_ID_VOLUME_UP:
DEBUGV("AVRCP Target : VOLUME UP (%s)\n", button_state);
break;
case AVRCP_OPERATION_ID_VOLUME_DOWN:
DEBUGV("AVRCP Target : VOLUME DOWN (%s)\n", button_state);
break;
default:
return;
}
break;
default:
DEBUGV("AVRCP Target : Event 0x%02x is not parsed\n", packet[2]);
break;
}
}
void A2DPSink::a2dp_sink_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) {
UNUSED(channel);
UNUSED(size);
uint8_t status;
uint8_t allocation_method;
if (packet_type != HCI_EVENT_PACKET) {
return;
}
if (hci_event_packet_get_type(packet) != HCI_EVENT_A2DP_META) {
return;
}
a2dp_sink_a2dp_connection_t * a2dp_conn = &a2dp_sink_a2dp_connection;
switch (packet[2]) {
case A2DP_SUBEVENT_SIGNALING_MEDIA_CODEC_OTHER_CONFIGURATION:
DEBUGV("A2DP Sink : Received non SBC codec - not implemented\n");
break;
case A2DP_SUBEVENT_SIGNALING_MEDIA_CODEC_SBC_CONFIGURATION: {
DEBUGV("A2DP Sink : Received SBC codec configuration\n");
a2dp_conn->sbc_configuration.reconfigure = a2dp_subevent_signaling_media_codec_sbc_configuration_get_reconfigure(packet);
a2dp_conn->sbc_configuration.num_channels = a2dp_subevent_signaling_media_codec_sbc_configuration_get_num_channels(packet);
a2dp_conn->sbc_configuration.sampling_frequency = a2dp_subevent_signaling_media_codec_sbc_configuration_get_sampling_frequency(packet);
a2dp_conn->sbc_configuration.block_length = a2dp_subevent_signaling_media_codec_sbc_configuration_get_block_length(packet);
a2dp_conn->sbc_configuration.subbands = a2dp_subevent_signaling_media_codec_sbc_configuration_get_subbands(packet);
a2dp_conn->sbc_configuration.min_bitpool_value = a2dp_subevent_signaling_media_codec_sbc_configuration_get_min_bitpool_value(packet);
a2dp_conn->sbc_configuration.max_bitpool_value = a2dp_subevent_signaling_media_codec_sbc_configuration_get_max_bitpool_value(packet);
allocation_method = a2dp_subevent_signaling_media_codec_sbc_configuration_get_allocation_method(packet);
// Adapt Bluetooth spec definition to SBC Encoder expected input
a2dp_conn->sbc_configuration.allocation_method = (btstack_sbc_allocation_method_t)(allocation_method - 1);
switch (a2dp_subevent_signaling_media_codec_sbc_configuration_get_channel_mode(packet)) {
case AVDTP_CHANNEL_MODE_JOINT_STEREO:
a2dp_conn->sbc_configuration.channel_mode = SBC_CHANNEL_MODE_JOINT_STEREO;
break;
case AVDTP_CHANNEL_MODE_STEREO:
a2dp_conn->sbc_configuration.channel_mode = SBC_CHANNEL_MODE_STEREO;
break;
case AVDTP_CHANNEL_MODE_DUAL_CHANNEL:
a2dp_conn->sbc_configuration.channel_mode = SBC_CHANNEL_MODE_DUAL_CHANNEL;
break;
case AVDTP_CHANNEL_MODE_MONO:
a2dp_conn->sbc_configuration.channel_mode = SBC_CHANNEL_MODE_MONO;
break;
default:
btstack_assert(false);
break;
}
a2dp_conn->sbc_configuration.dump();
break;
}
case A2DP_SUBEVENT_STREAM_ESTABLISHED:
status = a2dp_subevent_stream_established_get_status(packet);
if (status != ERROR_CODE_SUCCESS) {
DEBUGV("A2DP Sink : Streaming connection failed, status 0x%02x\n", status);
break;
}
a2dp_subevent_stream_established_get_bd_addr(packet, a2dp_conn->addr);
a2dp_conn->a2dp_cid = a2dp_subevent_stream_established_get_a2dp_cid(packet);
a2dp_conn->a2dp_local_seid = a2dp_subevent_stream_established_get_local_seid(packet);
a2dp_conn->stream_state = STREAM_STATE_OPEN;
DEBUGV("A2DP Sink : Streaming connection is established, address %s, cid 0x%02x, local seid %d\n",
bd_addr_to_str(a2dp_conn->addr), a2dp_conn->a2dp_cid, a2dp_conn->a2dp_local_seid);
memcpy(_sourceAddress, a2dp_conn->addr, sizeof(_sourceAddress));
break;
case A2DP_SUBEVENT_STREAM_STARTED:
DEBUGV("A2DP Sink : Stream started\n");
a2dp_conn->stream_state = STREAM_STATE_PLAYING;
if (a2dp_conn->sbc_configuration.reconfigure) {
media_processing_close();
}
// prepare media processing
media_processing_init(&a2dp_conn->sbc_configuration);
// audio stream is started when buffer reaches minimal level
_connected = true;
if (_connectCB) {
_connectCB(_connectData, true);
}
break;
case A2DP_SUBEVENT_STREAM_SUSPENDED:
DEBUGV("A2DP Sink : Stream paused\n");
a2dp_conn->stream_state = STREAM_STATE_PAUSED;
media_processing_pause();
break;
case A2DP_SUBEVENT_STREAM_RELEASED:
DEBUGV("A2DP Sink : Stream released\n");
a2dp_conn->stream_state = STREAM_STATE_CLOSED;
media_processing_close();
break;
case A2DP_SUBEVENT_SIGNALING_CONNECTION_RELEASED:
DEBUGV("A2DP Sink : Signaling connection released\n");
a2dp_conn->a2dp_cid = 0;
media_processing_close();
_connected = false;
if (_connectCB) {
_connectCB(_connectData, false);
}
break;
default:
break;
}
}

View file

@ -0,0 +1,293 @@
/*
A2DP Sink (Bluetooth audio receiver)
Copyright (c) 2024 Earle F. Philhower, III <earlephilhower@yahoo.com>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#pragma once
#include <Arduino.h>
#include "BluetoothHCI.h"
#include "BluetoothAudioConsumer.h"
#include "BluetoothMediaConfigurationSBC.h"
#include "btstack.h"
#include "btstack_resample.h"
#include "btstack_ring_buffer.h"
class A2DPSink : public Stream {
public:
A2DPSink() {
}
virtual int available() override {
return 0; // Unreadable, this is output only
}
virtual int read() override {
return 0;
}
virtual int peek() override {
return 0;
}
virtual void flush() override {
}
virtual size_t write(const uint8_t *buffer, size_t size) override {
(void) buffer;
(void) size;
return 0;
}
virtual int availableForWrite() override {
return 0;
}
virtual size_t write(uint8_t s) override {
(void) s;
return 0;
}
bool setName(const char *name) {
if (_running) {
return false;
}
free(_name);
_name = strdup(name);
return true;
}
void onTransmit(void (*cb)(void *), void *cbData = nullptr) {
_transmitCB = cb;
_transmitData = cbData;
}
void onAVRCP(void (*cb)(void *, avrcp_operation_id_t, int), void *cbData = nullptr) {
_avrcpCB = cb;
_avrcpData = cbData;
}
void onBattery(void (*cb)(void *, avrcp_battery_status_t), void *cbData = nullptr) {
_batteryCB = cb;
_batteryData = cbData;
}
void onVolume(void (*cb)(void *, int), void *cbData = nullptr) {
_volumeCB = cb;
_volumeData = cbData;
}
void onConnect(void (*cb)(void *, bool), void *cbData = nullptr) {
_connectCB = cb;
_connectData = cbData;
}
typedef enum { STOPPED, PLAYING, PAUSED } PlaybackStatus;
void onPlaybackStatus(void (*cb)(void *, PlaybackStatus), void *cbData = nullptr) {
_playbackStatusCB = cb;
_playbackStatusData = cbData;
}
const uint8_t *getSourceAddress() {
if (!_connected) {
return nullptr;
} else {
return _sourceAddress;
}
}
void setConsumer(BluetoothAudioConsumer_ *c) {
_consumer = c;
}
bool begin();
bool disconnect();
void clearPairing();
void playback_handler(int16_t * buffer, uint16_t num_audio_frames);
void play() {
if (_connected) {
avrcp_controller_play(a2dp_sink_avrcp_connection.avrcp_cid);
}
}
void stop() {
if (_connected) {
avrcp_controller_stop(a2dp_sink_avrcp_connection.avrcp_cid);
}
}
void pause() {
if (_connected) {
avrcp_controller_pause(a2dp_sink_avrcp_connection.avrcp_cid);
}
}
void fastForward() {
if (_connected) {
avrcp_controller_fast_forward(a2dp_sink_avrcp_connection.avrcp_cid);
}
}
void rewind() {
if (_connected) {
avrcp_controller_rewind(a2dp_sink_avrcp_connection.avrcp_cid);
}
}
void forward() {
if (_connected) {
avrcp_controller_forward(a2dp_sink_avrcp_connection.avrcp_cid);
}
}
void backward() {
if (_connected) {
avrcp_controller_backward(a2dp_sink_avrcp_connection.avrcp_cid);
}
}
void volumeUp() {
if (_connected) {
avrcp_controller_volume_up(a2dp_sink_avrcp_connection.avrcp_cid);
}
}
void volumeDown() {
if (_connected) {
avrcp_controller_volume_down(a2dp_sink_avrcp_connection.avrcp_cid);
}
}
void mute() {
if (_connected) {
avrcp_controller_mute(a2dp_sink_avrcp_connection.avrcp_cid);
}
}
private:
void handle_pcm_data(int16_t * data, int num_audio_frames, int num_channels, int sample_rate, void * context);
int media_processing_init(BluetoothMediaCodecConfigurationSBC * configuration);
void media_processing_start();
void media_processing_pause();
void media_processing_close();
void handle_l2cap_media_data_packet(uint8_t seid, uint8_t *packet, uint16_t size);
int read_sbc_header(uint8_t * packet, int size, int * offset, avdtp_sbc_codec_header_t * sbc_header);
int read_media_data_header(uint8_t *packet, int size, int *offset, avdtp_media_packet_header_t *media_header);
void avrcp_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);
void avrcp_controller_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);
void avrcp_target_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);
void a2dp_sink_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);
BluetoothHCI_ _hci;
BluetoothAudioConsumer_ *_consumer = nullptr;
bool _running = false;
bool _connected = false;
// Callbacks
void (*_transmitCB)(void *) = nullptr;
void *_transmitData;
void (*_avrcpCB)(void *, avrcp_operation_id_t, int) = nullptr;
void *_avrcpData;
void (*_batteryCB)(void *, avrcp_battery_status_t) = nullptr;
void *_batteryData;
void (*_volumeCB)(void *, int) = nullptr;
void *_volumeData;
void (*_connectCB)(void *, bool) = nullptr;
void *_connectData;
void (*_playbackStatusCB)(void *, PlaybackStatus) = nullptr;
void *_playbackStatusData;
char *_name = nullptr;
uint8_t _sourceAddress[6];
enum { NUM_CHANNELS = 2, BYTES_PER_FRAME = (2 * NUM_CHANNELS), MAX_SBC_FRAME_SIZE = 120 };
uint8_t sdp_avdtp_sink_service_buffer[150];
uint8_t sdp_avrcp_target_service_buffer[150];
uint8_t sdp_avrcp_controller_service_buffer[200];
uint8_t device_id_sdp_service_buffer[100];
// we support all configurations with bitpool 2-53
const uint8_t media_sbc_codec_capabilities[4] = {
0xFF,//(AVDTP_SBC_44100 << 4) | AVDTP_SBC_STEREO,
0xFF,//(AVDTP_SBC_BLOCK_LENGTH_16 << 4) | (AVDTP_SBC_SUBBANDS_8 << 2) | AVDTP_SBC_ALLOCATION_METHOD_LOUDNESS,
2, 53
};
// SBC Decoder for WAV file or live playback
btstack_sbc_decoder_state_t state;
btstack_sbc_mode_t mode = SBC_MODE_STANDARD;
// ring buffer for SBC Frames
// below 30: add samples, 30-40: fine, above 40: drop samples
enum { OPTIMAL_FRAMES_MIN = 60, OPTIMAL_FRAMES_MAX = 80, ADDITIONAL_FRAMES = 30 };
uint8_t sbc_frame_storage[(OPTIMAL_FRAMES_MAX + ADDITIONAL_FRAMES) * MAX_SBC_FRAME_SIZE];
btstack_ring_buffer_t sbc_frame_ring_buffer;
unsigned int sbc_frame_size;
// overflow buffer for not fully used sbc frames, with additional frames for resampling
uint8_t decoded_audio_storage[(128 + 16) * BYTES_PER_FRAME];
btstack_ring_buffer_t decoded_audio_ring_buffer;
int media_initialized = 0;
int audio_stream_started;
btstack_resample_t resample_instance;
// temp storage of lower-layer request for audio samples
int16_t * request_buffer;
int request_frames;
// sink state
int volume_percentage = 0;
avrcp_battery_status_t battery_status = AVRCP_BATTERY_STATUS_WARNING;
typedef enum {
STREAM_STATE_CLOSED,
STREAM_STATE_OPEN,
STREAM_STATE_PLAYING,
STREAM_STATE_PAUSED,
} stream_state_t;
typedef struct {
uint8_t a2dp_local_seid;
uint8_t media_sbc_codec_configuration[4];
} a2dp_sink_stream_endpoint_t;
a2dp_sink_stream_endpoint_t a2dp_sink_stream_endpoint;
typedef struct {
bd_addr_t addr;
uint16_t a2dp_cid;
uint8_t a2dp_local_seid;
stream_state_t stream_state;
BluetoothMediaCodecConfigurationSBC sbc_configuration;
} a2dp_sink_a2dp_connection_t;
a2dp_sink_a2dp_connection_t a2dp_sink_a2dp_connection;
typedef struct {
bd_addr_t addr;
uint16_t avrcp_cid;
bool playing;
uint16_t notifications_supported_by_target;
} a2dp_sink_avrcp_connection_t;
a2dp_sink_avrcp_connection_t a2dp_sink_avrcp_connection;
int16_t output_buffer[(128 + 16) * NUM_CHANNELS]; // 16 * 8 * 2
};

View file

@ -73,7 +73,7 @@
static_cast<void(*)(btstack_timer_source_t*)>(_A2DPSOURCECB<void(btstack_timer_source_t*), __COUNTER__ - 1>::callback))
bool A2DPSource_::begin() {
bool A2DPSource::begin() {
if (_running) {
return false;
}
@ -100,7 +100,7 @@ bool A2DPSource_::begin() {
// Initialize A2DP Source
a2dp_source_init();
a2dp_source_register_packet_handler(PACKETHANDLERCB(A2DPSource_, a2dp_source_packet_handler));
a2dp_source_register_packet_handler(PACKETHANDLERCB(A2DPSource, a2dp_source_packet_handler));
// Create stream endpoint
avdtp_stream_endpoint_t * local_stream_endpoint = a2dp_source_create_stream_endpoint(AVDTP_AUDIO, AVDTP_CODEC_SBC, media_sbc_codec_capabilities, sizeof(media_sbc_codec_capabilities), media_sbc_codec_configuration, sizeof(media_sbc_codec_configuration));
@ -115,14 +115,14 @@ bool A2DPSource_::begin() {
// Initialize AVRCP Service
avrcp_init();
avrcp_register_packet_handler(PACKETHANDLERCB(A2DPSource_, avrcp_packet_handler));
avrcp_register_packet_handler(PACKETHANDLERCB(A2DPSource, avrcp_packet_handler));
// Initialize AVRCP Target
avrcp_target_init();
avrcp_target_register_packet_handler(PACKETHANDLERCB(A2DPSource_, avrcp_target_packet_handler));
avrcp_target_register_packet_handler(PACKETHANDLERCB(A2DPSource, avrcp_target_packet_handler));
// Initialize AVRCP Controller
avrcp_controller_init();
avrcp_controller_register_packet_handler(PACKETHANDLERCB(A2DPSource_, avrcp_controller_packet_handler));
avrcp_controller_register_packet_handler(PACKETHANDLERCB(A2DPSource, avrcp_controller_packet_handler));
// Initialize SDP,
sdp_init();
@ -169,7 +169,7 @@ bool A2DPSource_::begin() {
return true;
}
bool A2DPSource_::connect(const uint8_t *addr) {
bool A2DPSource::connect(const uint8_t *addr) {
if (!_running) {
return false;
}
@ -197,7 +197,7 @@ bool A2DPSource_::connect(const uint8_t *addr) {
}
}
bool A2DPSource_::disconnect() {
bool A2DPSource::disconnect() {
__lockBluetooth();
a2dp_source_disconnect(media_tracker.a2dp_cid);
__unlockBluetooth();
@ -208,7 +208,7 @@ bool A2DPSource_::disconnect() {
return true;
}
void A2DPSource_::clearPairing() {
void A2DPSource::clearPairing() {
disconnect();
__lockBluetooth();
gap_delete_all_link_keys();
@ -216,7 +216,7 @@ void A2DPSource_::clearPairing() {
}
// from Print (see notes on write() methods below)
size_t A2DPSource_::write(const uint8_t *buffer, size_t size) {
size_t A2DPSource::write(const uint8_t *buffer, size_t size) {
size_t count = 0;
size /= 2;
__lockBluetooth();
@ -248,7 +248,7 @@ size_t A2DPSource_::write(const uint8_t *buffer, size_t size) {
return count;
}
int A2DPSource_::availableForWrite() {
int A2DPSource::availableForWrite() {
int avail = 0;
__lockBluetooth();
if (_pcmWriter == _pcmReader) {
@ -262,31 +262,19 @@ int A2DPSource_::availableForWrite() {
return avail;
}
void A2DPSource_::dump_sbc_configuration(media_codec_configuration_sbc_t * configuration) {
(void) configuration;
DEBUGV("Received media codec configuration:\n");
DEBUGV(" - num_channels: %d\n", configuration->num_channels);
DEBUGV(" - sampling_frequency: %d\n", configuration->sampling_frequency);
DEBUGV(" - channel_mode: %d\n", configuration->channel_mode);
DEBUGV(" - block_length: %d\n", configuration->block_length);
DEBUGV(" - subbands: %d\n", configuration->subbands);
DEBUGV(" - allocation_method: %d\n", configuration->allocation_method);
DEBUGV(" - bitpool_value [%d, %d] \n", configuration->min_bitpool_value, configuration->max_bitpool_value);
}
void A2DPSource_::a2dp_timer_start(a2dp_media_sending_context_t * context) {
void A2DPSource::a2dp_timer_start(a2dp_media_sending_context_t * context) {
context->max_media_payload_size = btstack_min(a2dp_max_media_payload_size(context->a2dp_cid, context->local_seid), SBC_STORAGE_SIZE);
context->sbc_storage_count = 0;
context->sbc_ready_to_send = 0;
context->streaming = 1;
btstack_run_loop_remove_timer(&context->audio_timer);
btstack_run_loop_set_timer_handler(&context->audio_timer, TIMEOUTHANDLERCB(A2DPSource_, a2dp_audio_timeout_handler));
btstack_run_loop_set_timer_handler(&context->audio_timer, TIMEOUTHANDLERCB(A2DPSource, a2dp_audio_timeout_handler));
btstack_run_loop_set_timer_context(&context->audio_timer, context);
btstack_run_loop_set_timer(&context->audio_timer, AUDIO_TIMEOUT_MS);
btstack_run_loop_add_timer(&context->audio_timer);
}
void A2DPSource_::a2dp_timer_stop(a2dp_media_sending_context_t * context) {
void A2DPSource::a2dp_timer_stop(a2dp_media_sending_context_t * context) {
context->time_audio_data_sent = 0;
context->acc_num_missed_samples = 0;
context->samples_ready = 0;
@ -296,7 +284,7 @@ void A2DPSource_::a2dp_timer_stop(a2dp_media_sending_context_t * context) {
btstack_run_loop_remove_timer(&context->audio_timer);
}
int A2DPSource_::a2dp_fill_sbc_audio_buffer(a2dp_media_sending_context_t * context) {
int A2DPSource::a2dp_fill_sbc_audio_buffer(a2dp_media_sending_context_t * context) {
// perform sbc encoding
int total_num_bytes_read = 0;
unsigned int num_audio_samples_per_sbc_buffer = btstack_sbc_encoder_num_audio_frames();
@ -327,7 +315,7 @@ int A2DPSource_::a2dp_fill_sbc_audio_buffer(a2dp_media_sending_context_t * conte
return total_num_bytes_read;
}
void A2DPSource_::a2dp_audio_timeout_handler(btstack_timer_source_t * timer) {
void A2DPSource::a2dp_audio_timeout_handler(btstack_timer_source_t * timer) {
a2dp_media_sending_context_t * context = (a2dp_media_sending_context_t *) btstack_run_loop_get_timer_context(timer);
btstack_run_loop_set_timer(&context->audio_timer, AUDIO_TIMEOUT_MS);
btstack_run_loop_add_timer(&context->audio_timer);
@ -361,7 +349,7 @@ void A2DPSource_::a2dp_audio_timeout_handler(btstack_timer_source_t * timer) {
}
}
void A2DPSource_::a2dp_send_media_packet() {
void A2DPSource::a2dp_send_media_packet() {
int num_bytes_in_frame = btstack_sbc_encoder_sbc_buffer_length();
int bytes_in_storage = media_tracker.sbc_storage_count;
uint8_t num_sbc_frames = bytes_in_storage / num_bytes_in_frame;
@ -382,7 +370,7 @@ void A2DPSource_::a2dp_send_media_packet() {
}
}
void A2DPSource_::a2dp_source_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) {
void A2DPSource::a2dp_source_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) {
UNUSED(channel);
UNUSED(size);
uint8_t status;
@ -460,7 +448,7 @@ void A2DPSource_::a2dp_source_packet_handler(uint8_t packet_type, uint16_t chann
btstack_assert(false);
break;
}
dump_sbc_configuration(&sbc_configuration);
sbc_configuration.dump();
btstack_sbc_encoder_init(&sbc_encoder_state, SBC_MODE_STANDARD,
sbc_configuration.block_length, sbc_configuration.subbands,
@ -586,7 +574,7 @@ void A2DPSource_::a2dp_source_packet_handler(uint8_t packet_type, uint16_t chann
}
}
void A2DPSource_::avrcp_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) {
void A2DPSource::avrcp_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) {
UNUSED(channel);
UNUSED(size);
bd_addr_t event_addr;
@ -637,7 +625,7 @@ void A2DPSource_::avrcp_packet_handler(uint8_t packet_type, uint16_t channel, ui
}
}
void A2DPSource_::avrcp_target_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) {
void A2DPSource::avrcp_target_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) {
UNUSED(channel);
UNUSED(size);
uint8_t status = ERROR_CODE_SUCCESS;
@ -695,7 +683,7 @@ void A2DPSource_::avrcp_target_packet_handler(uint8_t packet_type, uint16_t chan
}
}
void A2DPSource_::avrcp_controller_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) {
void A2DPSource::avrcp_controller_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) {
UNUSED(channel);
UNUSED(size);
@ -732,5 +720,3 @@ void A2DPSource_::avrcp_controller_packet_handler(uint8_t packet_type, uint16_t
break;
}
}
A2DPSource_ A2DPSource;

View file

@ -18,16 +18,19 @@
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#pragma once
#include <Arduino.h>
#include "BluetoothHCI.h"
#include "BluetoothMediaConfigurationSBC.h"
#include <functional>
#include <list>
#include <memory>
class A2DPSource_ : public Stream {
class A2DPSource : public Stream {
public:
A2DPSource_() {
A2DPSource() {
}
bool setFrequency(uint32_t rate) {
@ -190,25 +193,12 @@ private:
uint8_t volume;
} a2dp_media_sending_context_t;
typedef struct {
int reconfigure;
int num_channels;
int sampling_frequency;
int block_length;
int subbands;
int min_bitpool_value;
int max_bitpool_value;
btstack_sbc_channel_mode_t channel_mode;
btstack_sbc_allocation_method_t allocation_method;
} media_codec_configuration_sbc_t;
uint8_t sdp_a2dp_source_service_buffer[150];
uint8_t sdp_avrcp_target_service_buffer[200];
uint8_t sdp_avrcp_controller_service_buffer[200];
uint8_t device_id_sdp_service_buffer[100];
media_codec_configuration_sbc_t sbc_configuration;
BluetoothMediaCodecConfigurationSBC sbc_configuration;
btstack_sbc_encoder_state_t sbc_encoder_state;
a2dp_media_sending_context_t media_tracker;
@ -221,8 +211,6 @@ private:
} avrcp_play_status_info_t;
avrcp_play_status_info_t play_info;
void dump_sbc_configuration(media_codec_configuration_sbc_t * configuration);
void a2dp_timer_start(a2dp_media_sending_context_t * context);
void a2dp_timer_stop(a2dp_media_sending_context_t * context);
@ -247,7 +235,4 @@ private:
void avrcp_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);
void avrcp_target_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);
void avrcp_controller_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);
};
extern A2DPSource_ A2DPSource;

View file

@ -1,3 +1,8 @@
#include "BluetoothDevice.h"
#include "BluetoothHCI.h"
#include "BluetoothMediaConfigurationSBC.h"
#include "A2DPSource.h"
#include "A2DPSink.h"
#include "BluetoothAudioConsumer.h"
#include "BluetoothAudioConsumerPWM.h"
#include "BluetoothAudioConsumerI2S.h"

View file

@ -0,0 +1,45 @@
/*
Bluetooth A2DP audio stream consumer
Copyright (c) 2024 Earle F. Philhower, III <earlephilhower@yahoo.com>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#pragma once
class A2DPSink;
class BluetoothAudioConsumer_ {
public:
BluetoothAudioConsumer_() {
_state = STATE_OFF;
}
virtual ~BluetoothAudioConsumer_() {
/* noop */
}
virtual bool init(uint8_t channels, uint32_t samplerate, A2DPSink *a2dpSink) = 0;
virtual void setVolume(uint8_t gain) = 0;
virtual void startStream() = 0;
virtual void stopStream() = 0;
virtual void close() = 0;
protected:
typedef enum { STATE_OFF = 0, STATE_INITIALIZED, STATE_STREAMING } State;
A2DPSink *_a2dpSink;
State _state;
uint8_t _gain;
int _channels;
int _samplerate;
};

View file

@ -0,0 +1,84 @@
/*
Bluetooth A2DP audio stream consumer - PWMAudio
Copyright (c) 2024 Earle F. Philhower, III <earlephilhower@yahoo.com>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "BluetoothAudioConsumerI2S.h"
#include "A2DPSink.h"
#include <functional>
#define CCALLBACKNAME _BTA2DPI2S
#include <ctocppcallback.h>
#define NOPARAMCB(class, cbFcn) \
(CCALLBACKNAME<void(void), __COUNTER__>::func = std::bind(&class::cbFcn, this), \
static_cast<void (*)(void)>(CCALLBACKNAME<void(void), __COUNTER__ - 1>::callback))
bool BluetoothAudioConsumerI2S::init(uint8_t channels, uint32_t samplerate, A2DPSink *a2dpSink) {
_channels = channels;
_samplerate = samplerate;
_a2dpSink = a2dpSink;
_i2s->setBuffers(16, 64);
_i2s->onTransmit(NOPARAMCB(BluetoothAudioConsumerI2S, fill));
if (_i2s->begin(samplerate)) {
_state = STATE_INITIALIZED;
_gain = 64;
return true;
}
return false;
}
void BluetoothAudioConsumerI2S::setVolume(uint8_t gain) {
_gain = gain;
}
void BluetoothAudioConsumerI2S::startStream() {
if (_state != STATE_INITIALIZED) {
return;
}
_state = STATE_STREAMING;
}
void BluetoothAudioConsumerI2S::stopStream() {
if (_state != STATE_STREAMING) {
return;
}
_state = STATE_INITIALIZED;
}
void BluetoothAudioConsumerI2S::close() {
if (_state == STATE_STREAMING) {
stopStream();
}
_state = STATE_OFF;
_i2s->end();
}
void BluetoothAudioConsumerI2S::fill() {
int num_samples = _i2s->availableForWrite() / 2;
int16_t buff[32 * 2];
while (num_samples > 63) {
_a2dpSink->playback_handler((int16_t *) buff, 32);
num_samples -= 64;
for (int i = 0; i < 64; i++) {
int32_t tmp = buff[i];
tmp *= _gain;
tmp >>= 8;
_i2s->write((int16_t)tmp);
}
}
}

View file

@ -0,0 +1,41 @@
/*
Bluetooth A2DP audio stream consumer - I2S
Copyright (c) 2024 Earle F. Philhower, III <earlephilhower@yahoo.com>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#pragma once
#include <I2S.h>
#include "BluetoothAudioConsumer.h"
class BluetoothAudioConsumerI2S : public BluetoothAudioConsumer_ {
public:
BluetoothAudioConsumerI2S(I2S &i2s) : BluetoothAudioConsumer_() {
_i2s = &i2s;
}
virtual bool init(uint8_t channels, uint32_t samplerate, A2DPSink *a2dpSink) override;
virtual void setVolume(uint8_t gain) override;
virtual void startStream() override;
virtual void stopStream() override;
virtual void close() override;
private:
I2S *_i2s;
void fill();
};

View file

@ -0,0 +1,85 @@
/*
Bluetooth A2DP audio stream consumer - PWMAudio
Copyright (c) 2024 Earle F. Philhower, III <earlephilhower@yahoo.com>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "BluetoothAudioConsumerPWM.h"
#include "A2DPSink.h"
#include <functional>
#define CCALLBACKNAME _BTA2DPPWM
#include <ctocppcallback.h>
#define NOPARAMCB(class, cbFcn) \
(CCALLBACKNAME<void(void), __COUNTER__>::func = std::bind(&class::cbFcn, this), \
static_cast<void (*)(void)>(CCALLBACKNAME<void(void), __COUNTER__ - 1>::callback))
bool BluetoothAudioConsumerPWM::init(uint8_t channels, uint32_t samplerate, A2DPSink *a2dpSink) {
_channels = channels;
_samplerate = samplerate;
_a2dpSink = a2dpSink;
_pwm->setStereo(channels == 2);
_pwm->setBuffers(16, 64);
_pwm->onTransmit(NOPARAMCB(BluetoothAudioConsumerPWM, fill));
if (_pwm->begin(samplerate)) {
_state = STATE_INITIALIZED;
_gain = 64;
return true;
}
return false;
}
void BluetoothAudioConsumerPWM::setVolume(uint8_t gain) {
_gain = gain;
}
void BluetoothAudioConsumerPWM::startStream() {
if (_state != STATE_INITIALIZED) {
return;
}
_state = STATE_STREAMING;
}
void BluetoothAudioConsumerPWM::stopStream() {
if (_state != STATE_STREAMING) {
return;
}
_state = STATE_INITIALIZED;
}
void BluetoothAudioConsumerPWM::close() {
if (_state == STATE_STREAMING) {
stopStream();
}
_state = STATE_OFF;
_pwm->end();
}
void BluetoothAudioConsumerPWM::fill() {
int num_samples = _pwm->availableForWrite() / 2;
int16_t buff[32 * 2];
while (num_samples > 63) {
_a2dpSink->playback_handler((int16_t *) buff, 32);
num_samples -= 64;
for (int i = 0; i < 64; i++) {
int32_t tmp = buff[i];
tmp *= _gain;
tmp >>= 8;
_pwm->write((int16_t)tmp);
}
}
}

View file

@ -0,0 +1,41 @@
/*
Bluetooth A2DP audio stream consumer - PWMAudio
Copyright (c) 2024 Earle F. Philhower, III <earlephilhower@yahoo.com>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#pragma once
#include <PWMAudio.h>
#include "BluetoothAudioConsumer.h"
class BluetoothAudioConsumerPWM : public BluetoothAudioConsumer_ {
public:
BluetoothAudioConsumerPWM(PWMAudio &pwm) : BluetoothAudioConsumer_() {
_pwm = &pwm;
}
virtual bool init(uint8_t channels, uint32_t samplerate, A2DPSink *a2dpSink) override;
virtual void setVolume(uint8_t gain) override;
virtual void startStream() override;
virtual void stopStream() override;
virtual void close() override;
private:
PWMAudio *_pwm;
void fill();
};

View file

@ -0,0 +1,47 @@
/*
A1DP Source (Bluetooth audio sender)
Copyright (c) 2024 Earle F. Philhower, III <earlephilhower@yahoo.com>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#pragma once
#include <Arduino.h>
class BluetoothMediaCodecConfigurationSBC {
public:
uint8_t reconfigure;
uint8_t num_channels;
uint16_t sampling_frequency;
uint8_t block_length;
uint8_t subbands;
uint8_t min_bitpool_value;
uint8_t max_bitpool_value;
btstack_sbc_channel_mode_t channel_mode;
btstack_sbc_allocation_method_t allocation_method;
void dump() {
DEBUGV(" - num_channels: %d\n", num_channels);
DEBUGV(" - sampling_frequency: %d\n", sampling_frequency);
DEBUGV(" - channel_mode: %d\n", channel_mode);
DEBUGV(" - block_length: %d\n", block_length);
DEBUGV(" - subbands: %d\n", subbands);
DEBUGV(" - allocation_method: %d\n", allocation_method);
DEBUGV(" - bitpool_value [%d, %d] \n", min_bitpool_value, max_bitpool_value);
DEBUGV("\n");
}
};