Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[submodule "3rd-party/lhdcv5-decoder"]
path = 3rd-party/lhdcv5-decoder
url = https://github.com/WillyBilly06/LHDC-V5-Decoder.git
[submodule "3rd-party/lhdcv5-encoder"]
path = 3rd-party/lhdcv5-encoder
url = https://github.com/WillyBilly06/LHDC-V5-Encoder.git
[submodule "3rd-party/portaudio"]
path = 3rd-party/portaudio
url = https://github.com/PortAudio/portaudio.git
1 change: 1 addition & 0 deletions 3rd-party/lhdcv5-decoder
Submodule lhdcv5-decoder added at 63d564
1 change: 1 addition & 0 deletions 3rd-party/lhdcv5-encoder
Submodule lhdcv5-encoder added at 3f9d19
1 change: 1 addition & 0 deletions 3rd-party/portaudio
Submodule portaudio added at a88021
515 changes: 450 additions & 65 deletions chipset/intel/btstack_chipset_intel_firmware.c

Large diffs are not rendered by default.

125 changes: 90 additions & 35 deletions platform/posix/btstack_audio_portaudio.c
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@


#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "btstack_debug.h"
#include "btstack_audio.h"
Expand All @@ -47,16 +48,20 @@
#ifdef HAVE_PORTAUDIO

#define PA_SAMPLE_TYPE paInt16
#define NUM_FRAMES_PER_PA_BUFFER 512
#define NUM_OUTPUT_BUFFERS 5
#define NUM_FRAMES_PER_PA_BUFFER 128
#define MAX_OUTPUT_FRAMES_PER_PA_BUFFER 512
#define NUM_OUTPUT_BUFFERS 4
#define NUM_INPUT_BUFFERS 5
#define DRIVER_POLL_INTERVAL_MS 5
#define DRIVER_POLL_INTERVAL_MS 1

#ifndef MAX_NR_AUDIO_CHANNELS
#define MAX_NR_AUDIO_CHANNELS 2
#endif

#include <portaudio.h>
#ifdef _WIN32
#include <pa_win_wasapi.h>
#endif

// config
static int num_channels_sink;
Expand Down Expand Up @@ -86,10 +91,15 @@ static void (*playback_callback)(int16_t * buffer, uint16_t num_samples, const b
static void (*recording_callback)(const int16_t * buffer, uint16_t num_samples, const btstack_audio_context_t * context);

// output buffer
static int16_t output_buffer_storage[NUM_OUTPUT_BUFFERS * NUM_FRAMES_PER_PA_BUFFER * MAX_NR_AUDIO_CHANNELS];
static int16_t output_buffer_storage[NUM_OUTPUT_BUFFERS * MAX_OUTPUT_FRAMES_PER_PA_BUFFER * MAX_NR_AUDIO_CHANNELS];
static int16_t * output_buffers[NUM_OUTPUT_BUFFERS];
static int16_t output_discard_buffer[MAX_OUTPUT_FRAMES_PER_PA_BUFFER * MAX_NR_AUDIO_CHANNELS];
static int output_buffer_to_play;
static int output_buffer_to_fill;
static uint16_t sink_frames_per_buffer = NUM_FRAMES_PER_PA_BUFFER;
/* Monotonic SPSC counters avoid the modulo-index full-lap ambiguity. */
static volatile uint32_t output_buffers_consumed;
static uint32_t output_buffers_refilled;

// input buffer
static int16_t input_buffer_storage[NUM_INPUT_BUFFERS * NUM_FRAMES_PER_PA_BUFFER * MAX_NR_AUDIO_CHANNELS];
Expand Down Expand Up @@ -120,33 +130,37 @@ static int portaudio_callback_sink( const void * inputBuffer
(void) frames_per_buffer;
(void) inputBuffer;

uint32_t play_seq = output_buffers_consumed;
uint32_t play_index = play_seq % NUM_OUTPUT_BUFFERS;

// get microsecond timestamp
btstack_time_us_t time_us = (uint32_t) (uint64_t) (timeInfo->outputBufferDacTime * 1000000);
sink_playback_audio_contexts[output_buffer_to_play].timestamp = time_us;
sink_playback_audio_contexts[play_index].timestamp = time_us;

// simplified volume control
uint16_t index;
int16_t * from_buffer = output_buffers[output_buffer_to_play];
int16_t * from_buffer = output_buffers[play_index];
int16_t * to_buffer = (int16_t *) outputBuffer;
btstack_assert(frames_per_buffer == NUM_FRAMES_PER_PA_BUFFER);
btstack_assert(frames_per_buffer == sink_frames_per_buffer);

#if 0
// up to 8 right shifts
int right_shift = 8 - btstack_min(8, ((sink_volume + 15) / 16));
for (index = 0; index < (NUM_FRAMES_PER_PA_BUFFER * num_channels_sink); index++){
for (index = 0; index < (sink_frames_per_buffer * num_channels_sink); index++){
*to_buffer++ = (*from_buffer++) >> right_shift;
}
#else
// multiply with volume ^ 4
int16_t x2 = sink_volume * sink_volume;
int16_t x4 = (x2 * x2) >> 14;
for (index = 0; index < (NUM_FRAMES_PER_PA_BUFFER * num_channels_sink); index++){
for (index = 0; index < (sink_frames_per_buffer * num_channels_sink); index++){
*to_buffer++ = ((*from_buffer++) * x4) >> 14;
}
#endif

// next
output_buffer_to_play = (output_buffer_to_play + 1 ) % NUM_OUTPUT_BUFFERS;
// Publish one consumed staging buffer to the BTstack run-loop thread.
output_buffers_consumed = play_seq + 1;
output_buffer_to_play = (int)((play_seq + 1) % NUM_OUTPUT_BUFFERS);

return 0;
}
Expand Down Expand Up @@ -179,17 +193,29 @@ static int portaudio_callback_source( const void * inputBuff
}

static void driver_timer_handler_sink(btstack_timer_source_t * ts){
uint32_t consumed = output_buffers_consumed;
uint32_t lag = consumed - output_buffers_refilled;

/* Keep latency bounded. If the run loop missed more playback periods than
* the staging ring can represent, consume/drop the stale PCM now instead
* of leaving it queued and letting latency grow without bound. */
if (lag > NUM_OUTPUT_BUFFERS) {
uint32_t stale = lag - NUM_OUTPUT_BUFFERS;
while (stale--) {
(*playback_callback)(output_discard_buffer, sink_frames_per_buffer, NULL);
output_buffers_refilled++;
}
}

// playback buffer ready to fill
while (output_buffer_to_play != output_buffer_to_fill){
(*playback_callback)(output_buffers[output_buffer_to_fill], NUM_FRAMES_PER_PA_BUFFER,
&sink_playback_audio_contexts[output_buffer_to_fill]);

// next
output_buffer_to_fill = (output_buffer_to_fill + 1 ) % NUM_OUTPUT_BUFFERS;
while (output_buffers_refilled != consumed) {
uint32_t fill_index = output_buffers_refilled % NUM_OUTPUT_BUFFERS;
(*playback_callback)(output_buffers[fill_index], sink_frames_per_buffer,
&sink_playback_audio_contexts[fill_index]);
output_buffers_refilled++;
output_buffer_to_fill = (int)(output_buffers_refilled % NUM_OUTPUT_BUFFERS);
consumed = output_buffers_consumed;
}

// re-set timer
btstack_run_loop_set_timer(ts, DRIVER_POLL_INTERVAL_MS);
btstack_run_loop_add_timer(ts);
}
Expand Down Expand Up @@ -224,8 +250,12 @@ static int btstack_audio_portaudio_sink_init(
num_channels_sink = channels;
num_bytes_per_sample_sink = 2 * channels;

sink_frames_per_buffer = NUM_FRAMES_PER_PA_BUFFER;
if (samplerate >= 192000) sink_frames_per_buffer = 512;
else if (samplerate >= 88200) sink_frames_per_buffer = 256;

for (int i=0;i<NUM_OUTPUT_BUFFERS;i++){
output_buffers[i] = &output_buffer_storage[i * NUM_FRAMES_PER_PA_BUFFER * MAX_NR_AUDIO_CHANNELS];
output_buffers[i] = &output_buffer_storage[i * MAX_OUTPUT_FRAMES_PER_PA_BUFFER * MAX_NR_AUDIO_CHANNELS];
}

/* -- initialize PortAudio -- */
Expand Down Expand Up @@ -253,17 +283,26 @@ static int btstack_audio_portaudio_sink_init(
}
}

/* -- use default device otherwise -- */
/* -- prefer the Windows WASAPI default endpoint for low latency -- */
if (device_index < 0){
device_index = Pa_GetDefaultOutputDevice();
output_device_info = Pa_GetDeviceInfo(device_index );
PaHostApiIndex wasapi_index = Pa_HostApiTypeIdToHostApiIndex(paWASAPI);
if (wasapi_index >= 0) {
const PaHostApiInfo *wasapi_info = Pa_GetHostApiInfo(wasapi_index);
if (wasapi_info != NULL && wasapi_info->defaultOutputDevice != paNoDevice) {
device_index = wasapi_info->defaultOutputDevice;
}
}
if (device_index < 0) device_index = Pa_GetDefaultOutputDevice();
output_device_info = Pa_GetDeviceInfo(device_index);
}
const PaHostApiInfo *selected_host = Pa_GetHostApiInfo(output_device_info->hostApi);

/* -- setup output -- */
PaStreamParameters output_parameters;
output_parameters.device = device_index;
output_parameters.channelCount = channels;
output_parameters.sampleFormat = PA_SAMPLE_TYPE;
output_parameters.suggestedLatency = output_device_info->defaultHighOutputLatency;
output_parameters.suggestedLatency = output_device_info->defaultLowOutputLatency;
output_parameters.hostApiSpecificStreamInfo = NULL;

log_info("PortAudio: sink device: %s", output_device_info->name);
Expand All @@ -275,10 +314,25 @@ static int btstack_audio_portaudio_sink_init(
NULL,
&output_parameters,
samplerate,
NUM_FRAMES_PER_PA_BUFFER,
paClipOff, /* we won't output out of range samples so don't bother clipping them */
portaudio_callback_sink, /* use callback */
NULL );
sink_frames_per_buffer,
paClipOff,
portaudio_callback_sink,
NULL );
#ifdef _WIN32
if (err == paInvalidSampleRate && selected_host != NULL && selected_host->type == paWASAPI) {
PaWasapiStreamInfo wasapi_info;
memset(&wasapi_info, 0, sizeof(wasapi_info));
wasapi_info.size = sizeof(wasapi_info);
wasapi_info.hostApiType = paWASAPI;
wasapi_info.version = 1;
wasapi_info.flags = paWinWasapiAutoConvert;
output_parameters.hostApiSpecificStreamInfo = &wasapi_info;
log_info("PortAudio: shared WASAPI rejected %u Hz, retrying with auto-convert", (unsigned)samplerate);
err = Pa_OpenStream(&stream_sink, NULL, &output_parameters, samplerate,
sink_frames_per_buffer, paClipOff,
portaudio_callback_sink, NULL);
}
#endif

if (err != paNoError){
log_error("Portudio: error initializing portaudio: \"%s\"\n", Pa_GetErrorText(err));
Expand All @@ -287,11 +341,10 @@ static int btstack_audio_portaudio_sink_init(
log_info("PortAudio: sink stream created");

const PaStreamInfo * stream_info = Pa_GetStreamInfo(stream_sink);

// verify latency
uint32_t latency_samples = (uint32_t) (stream_info->outputLatency * samplerate);
uint32_t buffer_samples = NUM_FRAMES_PER_PA_BUFFER * NUM_OUTPUT_BUFFERS;
int buffers_needed = (latency_samples + NUM_FRAMES_PER_PA_BUFFER - 1) / NUM_FRAMES_PER_PA_BUFFER;
uint32_t buffer_samples = sink_frames_per_buffer * NUM_OUTPUT_BUFFERS;
int buffers_needed = (latency_samples + sink_frames_per_buffer - 1) / sink_frames_per_buffer;
log_info("PortAudio: output latency of %f requires %u buffers, %u buffers available\n",
stream_info->outputLatency, buffers_needed, NUM_OUTPUT_BUFFERS);
UNUSED(buffers_needed);
Expand Down Expand Up @@ -418,13 +471,15 @@ static void btstack_audio_portaudio_sink_start_stream(void){

if (!playback_callback) return;

// fill buffers once
// Pre-fill the entire staging ring before PortAudio starts consuming it.
uint8_t i;
for (i=0;i<NUM_OUTPUT_BUFFERS-1;i++){
(*playback_callback)(&output_buffer_storage[i * NUM_FRAMES_PER_PA_BUFFER * MAX_NR_AUDIO_CHANNELS], NUM_FRAMES_PER_PA_BUFFER, 0);
for (i=0;i<NUM_OUTPUT_BUFFERS;i++){
(*playback_callback)(output_buffers[i], sink_frames_per_buffer, 0);
}
output_buffers_consumed = 0;
output_buffers_refilled = 0;
output_buffer_to_play = 0;
output_buffer_to_fill = NUM_OUTPUT_BUFFERS-1;
output_buffer_to_fill = 0;

/* -- start stream -- */
PaError err = Pa_StartStream(stream_sink);
Expand Down
60 changes: 48 additions & 12 deletions port/windows-winusb-intel/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ file(GLOB SOURCES_CSR "../../chipset/csr/*.c")
file(GLOB SOURCES_INTEL "../../chipset/intel/*.c")
file(GLOB SOURCES_ZEPHYR "../../chipset/zephyr/*.c")
file(GLOB SOURCES_LC3_GOOGLE "../../3rd-party/lc3-google/src/*.c")
file(GLOB SOURCES_PORT "*.c")
file(GLOB SOURCES_PORT "*.c" "../../platform/posix/btstack_audio_portaudio.c")
file(GLOB SOURCES_YXML "../../3rd-party/yxml/yxml.c")

file(GLOB SOURCES_BLE_OFF "../../src/ble/le_device_db_memory.c")
Expand Down Expand Up @@ -85,21 +85,41 @@ list(SORT SOURCES)
# create static lib
add_library(btstack STATIC ${SOURCES})

# pkgconfig
find_package(PkgConfig QUIET)
# LHDC V5 portable codec libraries
file(GLOB SOURCES_LHDCV5_DECODER "../../3rd-party/lhdcv5-decoder/decoder/*.c")
add_library(lhdcv5_decoder STATIC ${SOURCES_LHDCV5_DECODER})
target_include_directories(lhdcv5_decoder PUBLIC "${BTSTACK_ROOT}/3rd-party/lhdcv5-decoder/decoder")
target_compile_definitions(lhdcv5_decoder PRIVATE LHDC_HOST_BUILD)
if(MSVC)
target_compile_options(lhdcv5_decoder PRIVATE /FI"${BTSTACK_ROOT}/port/windows-winusb/lhdcv5_msvc_compat.h")
endif()

file(GLOB SOURCES_LHDCV5_ENCODER "../../3rd-party/lhdcv5-encoder/src/*.c")
add_library(lhdcv5_encoder STATIC ${SOURCES_LHDCV5_ENCODER})
target_include_directories(lhdcv5_encoder PUBLIC "${BTSTACK_ROOT}/3rd-party/lhdcv5-encoder/include" PRIVATE "${BTSTACK_ROOT}/3rd-party/lhdcv5-encoder/src")

# portaudio
if (PkgConfig_FOUND)
# PortAudio: use system pkg-config when available, otherwise build the vendored copy.
find_package(PkgConfig QUIET)
if(PkgConfig_FOUND)
pkg_check_modules(PORTAUDIO portaudio-2.0)
if(PORTAUDIO_FOUND)
message("HAVE_PORTAUDIO")
target_include_directories(btstack PUBLIC ${PORTAUDIO_INCLUDE_DIRS})
target_link_directories(btstack PUBLIC ${PORTAUDIO_LIBRARY_DIRS})
target_link_libraries(btstack ${PORTAUDIO_LIBRARIES})
add_compile_definitions(HAVE_PORTAUDIO)
endif()
endif()

if(PORTAUDIO_FOUND)
message("HAVE_PORTAUDIO (pkg-config)")
target_include_directories(btstack PUBLIC ${PORTAUDIO_INCLUDE_DIRS})
target_link_directories(btstack PUBLIC ${PORTAUDIO_LIBRARY_DIRS})
target_link_libraries(btstack ${PORTAUDIO_LIBRARIES})
else()
message("HAVE_PORTAUDIO (vendored)")
set(PA_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(PA_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(PA_BUILD_TESTS OFF CACHE BOOL "" FORCE)
add_subdirectory("${BTSTACK_ROOT}/3rd-party/portaudio" "${CMAKE_CURRENT_BINARY_DIR}/portaudio")
target_include_directories(btstack PUBLIC "${BTSTACK_ROOT}/3rd-party/portaudio/include")
target_link_libraries(btstack portaudio)
endif()
add_compile_definitions(HAVE_PORTAUDIO)

# get list of examples, skipping mesh_node_demo
include(../../example/CMakeLists.txt)
set (EXAMPLES ${EXAMPLES_GENERAL} ${EXAMPLES_CLASSIC_ONLY} ${EXAMPLES_LE_ONLY} ${EXAMPLES_DUAL_MODE})
Expand Down Expand Up @@ -127,3 +147,19 @@ foreach(EXAMPLE ${EXAMPLES})
add_executable(${EXAMPLE} ${SOURCES_EXAMPLE})
target_link_libraries(${EXAMPLE} btstack setupapi winusb)
endforeach(EXAMPLE)

# Windows Intel A2DP Sink with LHDC V5 software decoder
add_executable(a2dp_sink_lhdcv5_demo "${BTSTACK_ROOT}/port/windows-winusb/a2dp_sink_lhdcv5_demo.c")
target_link_libraries(a2dp_sink_lhdcv5_demo btstack lhdcv5_decoder setupapi winusb)

if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/firmware/ibt-1040-0041.sfi" AND
EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/firmware/ibt-1040-0041.ddc")
add_custom_command(TARGET a2dp_sink_lhdcv5_demo POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:a2dp_sink_lhdcv5_demo>/firmware"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CMAKE_CURRENT_SOURCE_DIR}/firmware/ibt-1040-0041.sfi"
"$<TARGET_FILE_DIR:a2dp_sink_lhdcv5_demo>/firmware/ibt-1040-0041.sfi"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CMAKE_CURRENT_SOURCE_DIR}/firmware/ibt-1040-0041.ddc"
"$<TARGET_FILE_DIR:a2dp_sink_lhdcv5_demo>/firmware/ibt-1040-0041.ddc")
endif()
7 changes: 6 additions & 1 deletion port/windows-winusb-intel/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,12 @@ int main(int argc, const char * argv[]){

// setup USB Transport
transport = hci_transport_usb_instance();
btstack_chipset_intel_download_firmware(hci_transport_usb_instance(), &intel_firmware_done);
// Intel AX211 Bluetooth controller used by this PC. The generic WinUSB
// transport does not include modern Intel IDs in its built-in allowlist.
hci_transport_usb_add_device(0x8087, 0x0033);
btstack_chipset_intel_set_firmware_path("firmware");
printf("Intel firmware directory: firmware\n");
btstack_chipset_intel_download_firmware(transport, &intel_firmware_done);

// go
btstack_run_loop_execute();
Expand Down
Loading