This squashed commit comprises a long series of improvements, bug fixes, optimizations, and new feature additions:
- 48,000 frames per second operation, - 48k lossless stereo, 5.1 and 7.1 surround sound - Multichannel and multi rate operation on ALSA, PipeWire, PulseAudio, FreeBSD, stdout and unix pipe output backends. Automatic, flexible and controllable output format (rate, sample format and channel count) selection. Full FFmpeg integration to support transcoding, resampling, and new audio formats. Better operation on lower powered devices down to e.g. Raspberry Pi B. Reduced Docker image sizes with a slimmed-down FFmpeg library. Enhanced timestamp handling for better synchronization. Improved the sync error calculation. A new "vernier" resampling and interpolation method for low-power CPUs. Bug fixes and minor enhancements. Note -- there are many breaking changes from previous versions of Shairport Sync!
This commit is contained in:
+534
-171
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Asynchronous PulseAudio Backend. This file is part of Shairport Sync.
|
||||
* Copyright (c) Mike Brady 2017-2023
|
||||
* PulseAudio Backend. This file is part of Shairport Sync.
|
||||
* Copyright (c) Mike Brady 2017--2025
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
@@ -36,12 +36,20 @@
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
// note -- these are hacked and hardwired into this code.
|
||||
#define FORMAT PA_SAMPLE_S16NE
|
||||
#define RATE 44100
|
||||
typedef struct {
|
||||
pa_sample_format_t pa_format;
|
||||
sps_format_t sps_format;
|
||||
unsigned int bytes_per_sample;
|
||||
} pa_sps_t;
|
||||
|
||||
// Four seconds buffer -- should be plenty
|
||||
#define buffer_allocation RATE * 4 * 2 * 2
|
||||
// these are the only formats that audio_pw will ever allow itself to be configured with
|
||||
static pa_sps_t format_lookup[] = {{PA_SAMPLE_S16LE, SPS_FORMAT_S16_LE, 2},
|
||||
{PA_SAMPLE_S16BE, SPS_FORMAT_S16_BE, 2},
|
||||
{PA_SAMPLE_S32LE, SPS_FORMAT_S32_LE, 4},
|
||||
{PA_SAMPLE_S32BE, SPS_FORMAT_S32_BE, 4}};
|
||||
|
||||
#define CHANNEL_MAP_SIZE 1024
|
||||
static char channel_map[CHANNEL_MAP_SIZE + 1];
|
||||
|
||||
static pthread_mutex_t buffer_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
@@ -49,9 +57,24 @@ pa_threaded_mainloop *mainloop;
|
||||
pa_mainloop_api *mainloop_api;
|
||||
pa_context *context;
|
||||
pa_stream *stream;
|
||||
|
||||
static int32_t current_encoded_output_format = 0;
|
||||
const char *default_channel_layouts = NULL;
|
||||
static char *audio_lmb, *audio_umb, *audio_toq, *audio_eoq;
|
||||
static size_t audio_size = buffer_allocation;
|
||||
static size_t audio_occupancy;
|
||||
static size_t audio_size, audio_occupancy;
|
||||
|
||||
// use an SPS_FORMAT_... to find an entry in the format_lookup table or return NULL
|
||||
static pa_sps_t *sps_format_lookup(sps_format_t to_find) {
|
||||
pa_sps_t *response = NULL;
|
||||
unsigned int i = 0;
|
||||
while ((response == NULL) && (i < sizeof(format_lookup) / sizeof(pa_sps_t))) {
|
||||
if (format_lookup[i].sps_format == to_find)
|
||||
response = &format_lookup[i];
|
||||
else
|
||||
i++;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
void context_state_cb(pa_context *context, void *mainloop);
|
||||
void stream_state_cb(pa_stream *s, void *mainloop);
|
||||
@@ -90,66 +113,367 @@ static void check_pa_stream_status(pa_stream *p, const char *message) {
|
||||
}
|
||||
}
|
||||
|
||||
static void connect_stream() {
|
||||
// debug(1, "connect_stream");
|
||||
uint32_t buffer_size_in_bytes = (uint32_t)2 * 2 * RATE * 0.1; // hard wired in here
|
||||
// debug(1, "pa_buffer size is %u bytes.", buffer_size_in_bytes);
|
||||
static int check_settings(sps_format_t sample_format, unsigned int sample_rate,
|
||||
unsigned int channel_count) {
|
||||
|
||||
pa_threaded_mainloop_lock(mainloop);
|
||||
// Create a playback stream
|
||||
pa_sample_spec sample_specifications;
|
||||
sample_specifications.format = FORMAT;
|
||||
sample_specifications.rate = RATE;
|
||||
sample_specifications.channels = 2;
|
||||
// debug(1, "pa check_settings: configuration: %u/%s/%u.", sample_rate,
|
||||
// sps_format_description_string(sample_format), channel_count);
|
||||
|
||||
pa_channel_map map;
|
||||
pa_channel_map_init_stereo(&map);
|
||||
int response = EINVAL;
|
||||
|
||||
stream = pa_stream_new(context, "Playback", &sample_specifications, &map);
|
||||
pa_stream_set_state_callback(stream, stream_state_cb, mainloop);
|
||||
pa_stream_set_write_callback(stream, stream_write_cb, mainloop);
|
||||
// pa_stream_set_latency_update_callback(stream, stream_latency_cb, mainloop);
|
||||
pa_sps_t *format_info = sps_format_lookup(sample_format);
|
||||
if (format_info != NULL) {
|
||||
// here, try to create a stream of the given format.
|
||||
|
||||
// recommended settings, i.e. server uses sensible values
|
||||
pa_buffer_attr buffer_attr;
|
||||
buffer_attr.maxlength = (uint32_t)-1;
|
||||
buffer_attr.tlength = buffer_size_in_bytes;
|
||||
buffer_attr.prebuf = (uint32_t)0;
|
||||
buffer_attr.minreq = (uint32_t)-1;
|
||||
pa_threaded_mainloop_lock(mainloop);
|
||||
// Create a playback stream
|
||||
pa_sample_spec sample_specifications;
|
||||
sample_specifications.format = format_info->pa_format;
|
||||
sample_specifications.rate = sample_rate;
|
||||
sample_specifications.channels = channel_count;
|
||||
|
||||
pa_stream_flags_t stream_flags;
|
||||
stream_flags = PA_STREAM_START_CORKED | PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_NOT_MONOTONIC |
|
||||
PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_ADJUST_LATENCY;
|
||||
pa_channel_map map;
|
||||
pa_channel_map_init_auto(&map, sample_specifications.channels, PA_CHANNEL_MAP_DEFAULT);
|
||||
|
||||
int connect_result;
|
||||
pa_stream *check_settings_stream =
|
||||
pa_stream_new(context, "Playback", &sample_specifications, &map);
|
||||
|
||||
if (config.pa_sink) {
|
||||
// Connect stream to the sink specified in the config
|
||||
connect_result =
|
||||
pa_stream_connect_playback(stream, config.pa_sink, &buffer_attr, stream_flags, NULL, NULL);
|
||||
} else {
|
||||
// Connect stream to the default audio output sink
|
||||
connect_result =
|
||||
pa_stream_connect_playback(stream, NULL, &buffer_attr, stream_flags, NULL, NULL);
|
||||
if (check_settings_stream != NULL) {
|
||||
response = 0; // success
|
||||
pa_stream_unref(check_settings_stream);
|
||||
}
|
||||
pa_threaded_mainloop_unlock(mainloop);
|
||||
|
||||
// response = 0;
|
||||
}
|
||||
|
||||
if (connect_result != 0)
|
||||
die("could not connect to the pulseaudio playback stream -- the error message is \"%s\".",
|
||||
pa_strerror(pa_context_errno(context)));
|
||||
// debug(1, "pa check_settings: configuration: %u/%s/%u %s.", sample_rate,
|
||||
// sps_format_description_string(sample_format), channel_count,
|
||||
// response == 0 ? "is okay" : "can not be configured");
|
||||
return response;
|
||||
}
|
||||
|
||||
// Wait for the stream to be ready
|
||||
for (;;) {
|
||||
pa_stream_state_t stream_state = pa_stream_get_state(stream);
|
||||
if (!PA_STREAM_IS_GOOD(stream_state))
|
||||
die("stream state is no longer good while waiting for stream to become ready -- the error "
|
||||
"message is \"%s\".",
|
||||
static int check_configuration(unsigned int channels, unsigned int rate, unsigned int format) {
|
||||
return check_settings(format, rate, channels);
|
||||
}
|
||||
|
||||
static int32_t get_configuration(unsigned int channels, unsigned int rate, unsigned int format) {
|
||||
uint64_t start_time = get_absolute_time_in_ns();
|
||||
int32_t response =
|
||||
search_for_suitable_configuration(channels, rate, format, &check_configuration);
|
||||
int64_t elapsed_time = get_absolute_time_in_ns() - start_time;
|
||||
debug(3, "pa: get_configuration took %0.3f mS.", elapsed_time * 0.000001);
|
||||
return response;
|
||||
}
|
||||
|
||||
static int configure(int32_t requested_encoded_format, char **resulting_channel_map) {
|
||||
debug(3, "pa: configure %s.", short_format_description(requested_encoded_format));
|
||||
int response = 0;
|
||||
if (current_encoded_output_format != requested_encoded_format) {
|
||||
uint64_t start_time = get_absolute_time_in_ns();
|
||||
if (current_encoded_output_format == 0)
|
||||
debug(3, "pa: setting output configuration to %s.",
|
||||
short_format_description(requested_encoded_format));
|
||||
else
|
||||
// note -- can't use short_format_description twice in one call because it returns the same
|
||||
// string buffer each time
|
||||
debug(3, "pa: changing output configuration to %s.",
|
||||
short_format_description(requested_encoded_format));
|
||||
current_encoded_output_format = requested_encoded_format;
|
||||
pa_sps_t *format_info =
|
||||
sps_format_lookup(FORMAT_FROM_ENCODED_FORMAT(current_encoded_output_format));
|
||||
|
||||
if (format_info == NULL)
|
||||
die("pa: can't find format information!");
|
||||
|
||||
if (audio_lmb != NULL) {
|
||||
free(audio_lmb); // release previous buffer
|
||||
}
|
||||
|
||||
if (stream != NULL) {
|
||||
// debug(1, "pa: stopping and releasing the current stream...");
|
||||
pa_threaded_mainloop_lock(mainloop);
|
||||
if (pa_stream_is_corked(stream) == 0) {
|
||||
// debug(1,"Flush and cork for flush.");
|
||||
pa_stream_flush(stream, stream_success_cb, NULL);
|
||||
pa_stream_cork(stream, 1, stream_success_cb, mainloop);
|
||||
}
|
||||
pa_stream_disconnect(stream);
|
||||
pa_stream_unref(stream);
|
||||
pa_threaded_mainloop_unlock(mainloop);
|
||||
stream = NULL;
|
||||
}
|
||||
|
||||
audio_size = RATE_FROM_ENCODED_FORMAT(current_encoded_output_format) *
|
||||
format_info->bytes_per_sample *
|
||||
CHANNELS_FROM_ENCODED_FORMAT(current_encoded_output_format) * 1; // one seconds
|
||||
audio_lmb = malloc(audio_size);
|
||||
if (audio_lmb == NULL)
|
||||
die("Can't allocate %d bytes for pulseaudio buffer.", audio_size);
|
||||
audio_toq = audio_eoq = audio_lmb;
|
||||
audio_umb = audio_lmb + audio_size;
|
||||
audio_occupancy = 0;
|
||||
|
||||
pa_threaded_mainloop_lock(mainloop);
|
||||
// Create a playback stream
|
||||
pa_sample_spec sample_specifications;
|
||||
sample_specifications.format = format_info->pa_format;
|
||||
sample_specifications.rate = RATE_FROM_ENCODED_FORMAT(current_encoded_output_format);
|
||||
sample_specifications.channels = CHANNELS_FROM_ENCODED_FORMAT(current_encoded_output_format);
|
||||
|
||||
uint32_t buffer_size_in_bytes =
|
||||
(uint32_t)CHANNELS_FROM_ENCODED_FORMAT(current_encoded_output_format) *
|
||||
format_info->bytes_per_sample * RATE_FROM_ENCODED_FORMAT(current_encoded_output_format) /
|
||||
10;
|
||||
|
||||
pa_channel_map map;
|
||||
|
||||
pa_channel_map *pacm = NULL;
|
||||
|
||||
// if we've not asked specifically for native formats, ask for the alsa format...
|
||||
if ((default_channel_layouts == NULL) || (strcasecmp(default_channel_layouts, "alsa") == 0)) {
|
||||
pacm = pa_channel_map_init_auto(&map, sample_specifications.channels, PA_CHANNEL_MAP_ALSA);
|
||||
}
|
||||
|
||||
// ask for the native format...
|
||||
if (pacm == NULL) {
|
||||
pacm = pa_channel_map_init_auto(&map, sample_specifications.channels, PA_CHANNEL_MAP_DEFAULT);
|
||||
}
|
||||
|
||||
// ask for some format...
|
||||
if (pacm == NULL) {
|
||||
pacm =
|
||||
pa_channel_map_init_extend(&map, sample_specifications.channels, PA_CHANNEL_MAP_DEFAULT);
|
||||
}
|
||||
|
||||
if (resulting_channel_map != NULL) { // if needed...
|
||||
// PA_CHANNEL_MAP_ALSA gives default channel maps that correspond to the FFmpeg defaults.
|
||||
// make up a channel map
|
||||
channel_map[0] = '\0';
|
||||
int c;
|
||||
for (c = 0; c < map.channels; c++) {
|
||||
switch (map.map[c]) {
|
||||
|
||||
case PA_CHANNEL_POSITION_MONO:
|
||||
strncat(channel_map, "FC", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_FRONT_LEFT:
|
||||
strncat(channel_map, "FL", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_FRONT_RIGHT:
|
||||
strncat(channel_map, "FR", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_FRONT_CENTER:
|
||||
strncat(channel_map, "FC", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_REAR_CENTER:
|
||||
strncat(channel_map, "BC", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_REAR_LEFT:
|
||||
strncat(channel_map, "BL", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_REAR_RIGHT:
|
||||
strncat(channel_map, "BR", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_LFE:
|
||||
strncat(channel_map, "LFE", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER:
|
||||
strncat(channel_map, "FLC", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER:
|
||||
strncat(channel_map, "FRC", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_SIDE_LEFT:
|
||||
strncat(channel_map, "SL", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_SIDE_RIGHT:
|
||||
strncat(channel_map, "SR", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX0:
|
||||
strncat(channel_map, "AUX0", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX1:
|
||||
strncat(channel_map, "AUX1", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX2:
|
||||
strncat(channel_map, "AUX2", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX3:
|
||||
strncat(channel_map, "AUX3", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX4:
|
||||
strncat(channel_map, "AUX4", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX5:
|
||||
strncat(channel_map, "AUX5", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX6:
|
||||
strncat(channel_map, "AUX6", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX7:
|
||||
strncat(channel_map, "AUX7", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX8:
|
||||
strncat(channel_map, "AUX8", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX9:
|
||||
strncat(channel_map, "AUX9", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX10:
|
||||
strncat(channel_map, "AUX10", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX11:
|
||||
strncat(channel_map, "AUX11", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX12:
|
||||
strncat(channel_map, "AUX12", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX13:
|
||||
strncat(channel_map, "AUX13", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX14:
|
||||
strncat(channel_map, "AUX14", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX15:
|
||||
strncat(channel_map, "AUX15", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX16:
|
||||
strncat(channel_map, "AUX16", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX17:
|
||||
strncat(channel_map, "AUX17", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX18:
|
||||
strncat(channel_map, "AUX18", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX19:
|
||||
strncat(channel_map, "AUX19", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX20:
|
||||
strncat(channel_map, "AUX20", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX21:
|
||||
strncat(channel_map, "AUX21", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX22:
|
||||
strncat(channel_map, "AUX22", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX23:
|
||||
strncat(channel_map, "AUX23", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX24:
|
||||
strncat(channel_map, "AUX24", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX25:
|
||||
strncat(channel_map, "AUX25", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX26:
|
||||
strncat(channel_map, "AUX26", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX27:
|
||||
strncat(channel_map, "AUX27", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX28:
|
||||
strncat(channel_map, "AUX28", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX29:
|
||||
strncat(channel_map, "AUX29", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX30:
|
||||
strncat(channel_map, "AUX30", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_AUX31:
|
||||
strncat(channel_map, "AUX31", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_TOP_CENTER:
|
||||
strncat(channel_map, "TC", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_TOP_FRONT_LEFT:
|
||||
strncat(channel_map, "TFL", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_TOP_FRONT_RIGHT:
|
||||
strncat(channel_map, "TFR", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_TOP_FRONT_CENTER:
|
||||
strncat(channel_map, "TFC", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_TOP_REAR_LEFT:
|
||||
strncat(channel_map, "TBL", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_TOP_REAR_RIGHT:
|
||||
strncat(channel_map, "TBR", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
case PA_CHANNEL_POSITION_TOP_REAR_CENTER:
|
||||
strncat(channel_map, "TBC", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (c != (map.channels - 1))
|
||||
strncat(channel_map, " ", sizeof(channel_map) - 1 - strlen(channel_map));
|
||||
}
|
||||
|
||||
debug(1, "audio_pa: channel map for %d channels is \"%s\".", sample_specifications.channels,
|
||||
channel_map);
|
||||
}
|
||||
|
||||
stream = pa_stream_new(context, "Playback", &sample_specifications, &map);
|
||||
pa_stream_set_state_callback(stream, stream_state_cb, mainloop);
|
||||
pa_stream_set_write_callback(stream, stream_write_cb, mainloop);
|
||||
|
||||
// recommended settings, i.e. server uses sensible values
|
||||
pa_buffer_attr buffer_attr;
|
||||
buffer_attr.maxlength = (uint32_t)-1;
|
||||
buffer_attr.tlength = buffer_size_in_bytes;
|
||||
buffer_attr.prebuf = (uint32_t)0;
|
||||
buffer_attr.minreq = (uint32_t)-1;
|
||||
|
||||
pa_stream_flags_t stream_flags;
|
||||
stream_flags = PA_STREAM_START_CORKED | PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_NOT_MONOTONIC |
|
||||
PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_ADJUST_LATENCY;
|
||||
|
||||
int connect_result;
|
||||
|
||||
if (config.pa_sink) {
|
||||
// Connect stream to the sink specified in the config
|
||||
connect_result = pa_stream_connect_playback(stream, config.pa_sink, &buffer_attr,
|
||||
stream_flags, NULL, NULL);
|
||||
} else {
|
||||
// Connect stream to the default audio output sink
|
||||
connect_result =
|
||||
pa_stream_connect_playback(stream, NULL, &buffer_attr, stream_flags, NULL, NULL);
|
||||
}
|
||||
|
||||
if (connect_result != 0)
|
||||
die("could not connect to the pulseaudio playback stream -- the error message is \"%s\".",
|
||||
pa_strerror(pa_context_errno(context)));
|
||||
if (stream_state == PA_STREAM_READY)
|
||||
break;
|
||||
pa_threaded_mainloop_wait(mainloop);
|
||||
}
|
||||
|
||||
pa_threaded_mainloop_unlock(mainloop);
|
||||
// Wait for the stream to be ready
|
||||
for (;;) {
|
||||
pa_stream_state_t stream_state = pa_stream_get_state(stream);
|
||||
if (!PA_STREAM_IS_GOOD(stream_state))
|
||||
die("stream state is no longer good while waiting for stream to become ready -- the error "
|
||||
"message is \"%s\".",
|
||||
pa_strerror(pa_context_errno(context)));
|
||||
if (stream_state == PA_STREAM_READY)
|
||||
break;
|
||||
pa_threaded_mainloop_wait(mainloop);
|
||||
}
|
||||
|
||||
pa_threaded_mainloop_unlock(mainloop);
|
||||
|
||||
// to here
|
||||
|
||||
int64_t elapsed_time = get_absolute_time_in_ns() - start_time;
|
||||
debug(3, "pa: configuration took %0.3f mS.", elapsed_time * 0.000001);
|
||||
} else {
|
||||
debug(3, "pa: setting output configuration -- configuration unchanged, so nothing done.");
|
||||
}
|
||||
if ((response == 0) && (resulting_channel_map != NULL)) {
|
||||
*resulting_channel_map = channel_map;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
static int init(__attribute__((unused)) int argc, __attribute__((unused)) char **argv) {
|
||||
@@ -162,42 +486,55 @@ static int init(__attribute__((unused)) int argc, __attribute__((unused)) char *
|
||||
|
||||
config.audio_backend_latency_offset = 0;
|
||||
|
||||
// get settings from settings file
|
||||
|
||||
// do the "general" audio options. Note, these options are in the "general" stanza!
|
||||
parse_general_audio_options();
|
||||
// get settings from settings file, passing in defaults for format_set, rate_set and channel_set
|
||||
// Note, these options may be in the "general" stanza or the named stanza
|
||||
#ifdef CONFIG_FFMPEG
|
||||
parse_audio_options("pulseaudio", SPS_FORMAT_SET, SPS_RATE_SET, SPS_CHANNEL_SET);
|
||||
#else
|
||||
parse_audio_options("pulseaudio", SPS_FORMAT_NON_FFMPEG_SET, SPS_RATE_NON_FFMPEG_SET,
|
||||
SPS_CHANNNEL_NON_FFMPEG_SET);
|
||||
#endif
|
||||
|
||||
// now the specific options
|
||||
if (config.cfg != NULL) {
|
||||
const char *str;
|
||||
|
||||
/* Get the PulseAudio server name. */
|
||||
if (config_lookup_string(config.cfg, "pa.server", &str)) {
|
||||
if (config_lookup_string(config.cfg, "pulseaudio.server", &str)) {
|
||||
config.pa_server = (char *)str;
|
||||
}
|
||||
|
||||
// get the default channel mapping setting basis -- native ("no") or pa ("yes").
|
||||
|
||||
if (config_lookup_string(config.cfg, "pulseaudio", &default_channel_layouts)) {
|
||||
if ((strcasecmp(default_channel_layouts, "alsa") == 0) ||
|
||||
(strcasecmp(default_channel_layouts, "pulseaudio") == 0)) {
|
||||
debug(1, "pulseaudio default_channel_layouts setting: \"%s\".", default_channel_layouts);
|
||||
} else {
|
||||
debug(1, "Invalid pulseaudio default_channel_layouts setting. Must be \"alsa\" or "
|
||||
"\"pulseaudio\".");
|
||||
default_channel_layouts = NULL;
|
||||
}
|
||||
};
|
||||
|
||||
/* Get the Application Name. */
|
||||
if (config_lookup_string(config.cfg, "pa.application_name", &str)) {
|
||||
if (config_lookup_string(config.cfg, "pulseaudio.application_name", &str)) {
|
||||
config.pa_application_name = (char *)str;
|
||||
}
|
||||
|
||||
/* Get the PulseAudio sink name. */
|
||||
if (config_lookup_string(config.cfg, "pa.sink", &str)) {
|
||||
if (config_lookup_string(config.cfg, "pulseaudio.sink", &str)) {
|
||||
config.pa_sink = (char *)str;
|
||||
}
|
||||
}
|
||||
|
||||
// finish collecting settings
|
||||
|
||||
// allocate space for the audio buffer
|
||||
audio_lmb = malloc(audio_size);
|
||||
if (audio_lmb == NULL)
|
||||
die("Can't allocate %d bytes for pulseaudio buffer.", audio_size);
|
||||
audio_toq = audio_eoq = audio_lmb;
|
||||
audio_umb = audio_lmb + audio_size;
|
||||
audio_occupancy = 0;
|
||||
stream = NULL; // no stream
|
||||
audio_lmb = NULL; // no buffer
|
||||
|
||||
// Get a mainloop and its context
|
||||
|
||||
mainloop = pa_threaded_mainloop_new();
|
||||
if (mainloop == NULL)
|
||||
die("could not create a pa_threaded_mainloop.");
|
||||
@@ -232,25 +569,27 @@ static int init(__attribute__((unused)) int argc, __attribute__((unused)) char *
|
||||
break;
|
||||
pa_threaded_mainloop_wait(mainloop);
|
||||
}
|
||||
|
||||
pa_threaded_mainloop_unlock(mainloop);
|
||||
connect_stream();
|
||||
check_pa_stream_status(stream, "audio_pa initialisation.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void deinit(void) {
|
||||
check_pa_stream_status(stream, "audio_pa deinitialisation.");
|
||||
pa_stream_disconnect(stream);
|
||||
pa_threaded_mainloop_stop(mainloop);
|
||||
pa_threaded_mainloop_free(mainloop);
|
||||
// debug(1, "pa deinit done");
|
||||
// debug(1, "pa deinit");
|
||||
if (stream != NULL) {
|
||||
check_pa_stream_status(stream, "audio_pa deinitialisation.");
|
||||
pa_stream_disconnect(stream);
|
||||
pa_threaded_mainloop_stop(mainloop);
|
||||
pa_threaded_mainloop_free(mainloop);
|
||||
debug(1, "pa deinit done");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
static void start(__attribute__((unused)) int sample_rate,
|
||||
__attribute__((unused)) int sample_format) {
|
||||
check_pa_stream_status(stream, "audio_pa start.");
|
||||
}
|
||||
*/
|
||||
|
||||
static int play(void *buf, int samples, __attribute__((unused)) int sample_type,
|
||||
__attribute__((unused)) uint32_t timestamp,
|
||||
@@ -258,13 +597,17 @@ static int play(void *buf, int samples, __attribute__((unused)) int sample_type,
|
||||
// debug(1,"pa_play of %d samples.",samples);
|
||||
// copy the samples into the queue
|
||||
check_pa_stream_status(stream, "audio_pa play.");
|
||||
size_t bytes_to_transfer = samples * 2 * 2;
|
||||
|
||||
pa_sps_t *format_info =
|
||||
sps_format_lookup(FORMAT_FROM_ENCODED_FORMAT(current_encoded_output_format));
|
||||
size_t bytes_to_transfer = samples * format_info->bytes_per_sample *
|
||||
CHANNELS_FROM_ENCODED_FORMAT(current_encoded_output_format);
|
||||
|
||||
pthread_mutex_lock(&buffer_mutex);
|
||||
size_t bytes_available = audio_size - audio_occupancy;
|
||||
if (bytes_available < bytes_to_transfer)
|
||||
bytes_to_transfer = bytes_available;
|
||||
if (bytes_to_transfer > 0) {
|
||||
if ((bytes_to_transfer > 0) && (audio_lmb != NULL)) {
|
||||
size_t space_to_end_of_buffer = audio_umb - audio_eoq;
|
||||
if (space_to_end_of_buffer >= bytes_to_transfer) {
|
||||
memcpy(audio_eoq, buf, bytes_to_transfer);
|
||||
@@ -278,7 +621,11 @@ static int play(void *buf, int samples, __attribute__((unused)) int sample_type,
|
||||
audio_occupancy += bytes_to_transfer;
|
||||
}
|
||||
|
||||
if ((audio_occupancy >= 11025 * 2 * 2) && (pa_stream_is_corked(stream))) {
|
||||
if ((audio_occupancy >=
|
||||
(RATE_FROM_ENCODED_FORMAT(current_encoded_output_format) * format_info->bytes_per_sample *
|
||||
CHANNELS_FROM_ENCODED_FORMAT(current_encoded_output_format)) /
|
||||
4) &&
|
||||
(pa_stream_is_corked(stream))) {
|
||||
// debug(1,"Uncorked");
|
||||
pthread_mutex_unlock(&buffer_mutex);
|
||||
pa_threaded_mainloop_lock(mainloop);
|
||||
@@ -291,6 +638,7 @@ static int play(void *buf, int samples, __attribute__((unused)) int sample_type,
|
||||
}
|
||||
|
||||
int pa_delay(long *the_delay) {
|
||||
// debug(1, "pa delay");
|
||||
check_pa_stream_status(stream, "audio_pa delay.");
|
||||
// debug(1,"pa_delay");
|
||||
long result = 0;
|
||||
@@ -301,13 +649,16 @@ int pa_delay(long *the_delay) {
|
||||
int gl = pa_stream_get_latency(stream, &latency, &negative);
|
||||
pa_threaded_mainloop_unlock(mainloop);
|
||||
if (gl == PA_ERR_NODATA) {
|
||||
// debug(1, "No latency data yet.");
|
||||
reply = -ENODEV;
|
||||
} else if (gl != 0) {
|
||||
// debug(1,"Error %d getting latency.",gl);
|
||||
reply = -EIO;
|
||||
} else {
|
||||
result = (audio_occupancy / (2 * 2)) + (latency * 44100) / 1000000;
|
||||
pa_sps_t *format_info =
|
||||
sps_format_lookup(FORMAT_FROM_ENCODED_FORMAT(current_encoded_output_format));
|
||||
// convert audio_occupancy bytes to frames and latency microseconds into frames
|
||||
result = (audio_occupancy / (format_info->bytes_per_sample *
|
||||
CHANNELS_FROM_ENCODED_FORMAT(current_encoded_output_format))) +
|
||||
(latency * RATE_FROM_ENCODED_FORMAT(current_encoded_output_format)) / 1000000;
|
||||
reply = 0;
|
||||
}
|
||||
*the_delay = result;
|
||||
@@ -315,14 +666,17 @@ int pa_delay(long *the_delay) {
|
||||
}
|
||||
|
||||
static void flush(void) {
|
||||
check_pa_stream_status(stream, "audio_pa flush.");
|
||||
pa_threaded_mainloop_lock(mainloop);
|
||||
if (pa_stream_is_corked(stream) == 0) {
|
||||
// debug(1,"Flush and cork for flush.");
|
||||
pa_stream_flush(stream, stream_success_cb, NULL);
|
||||
pa_stream_cork(stream, 1, stream_success_cb, mainloop);
|
||||
// debug(1, "pa flush");
|
||||
if (stream != NULL) {
|
||||
check_pa_stream_status(stream, "audio_pa flush.");
|
||||
pa_threaded_mainloop_lock(mainloop);
|
||||
if (pa_stream_is_corked(stream) == 0) {
|
||||
// debug(1,"Flush and cork for flush.");
|
||||
pa_stream_flush(stream, stream_success_cb, NULL);
|
||||
pa_stream_cork(stream, 1, stream_success_cb, mainloop);
|
||||
}
|
||||
pa_threaded_mainloop_unlock(mainloop);
|
||||
}
|
||||
pa_threaded_mainloop_unlock(mainloop);
|
||||
pthread_mutex_lock(&buffer_mutex);
|
||||
audio_toq = audio_eoq = audio_lmb;
|
||||
audio_umb = audio_lmb + audio_size;
|
||||
@@ -331,15 +685,18 @@ static void flush(void) {
|
||||
}
|
||||
|
||||
static void stop(void) {
|
||||
check_pa_stream_status(stream, "audio_pa stop.");
|
||||
// Cork the stream so it will stop playing
|
||||
pa_threaded_mainloop_lock(mainloop);
|
||||
if (pa_stream_is_corked(stream) == 0) {
|
||||
// debug(1,"Flush and cork for stop.");
|
||||
pa_stream_flush(stream, stream_success_cb, NULL);
|
||||
pa_stream_cork(stream, 1, stream_success_cb, mainloop);
|
||||
// debug(1, "pa stop");
|
||||
if (stream != NULL) {
|
||||
check_pa_stream_status(stream, "audio_pa stop.");
|
||||
// Cork the stream so it will stop playing
|
||||
pa_threaded_mainloop_lock(mainloop);
|
||||
if (pa_stream_is_corked(stream) == 0) {
|
||||
// debug(1,"Flush and cork for stop.");
|
||||
pa_stream_flush(stream, stream_success_cb, NULL);
|
||||
pa_stream_cork(stream, 1, stream_success_cb, mainloop);
|
||||
}
|
||||
pa_threaded_mainloop_unlock(mainloop);
|
||||
}
|
||||
pa_threaded_mainloop_unlock(mainloop);
|
||||
pthread_mutex_lock(&buffer_mutex);
|
||||
audio_toq = audio_eoq = audio_lmb;
|
||||
audio_umb = audio_lmb + audio_size;
|
||||
@@ -347,12 +704,86 @@ static void stop(void) {
|
||||
pthread_mutex_unlock(&buffer_mutex);
|
||||
}
|
||||
|
||||
audio_output audio_pa = {.name = "pa",
|
||||
void context_state_cb(__attribute__((unused)) pa_context *local_context, void *local_mainloop) {
|
||||
// debug(1,"context_state_cb called.");
|
||||
pa_threaded_mainloop_signal(local_mainloop, 0);
|
||||
}
|
||||
|
||||
void stream_state_cb(__attribute__((unused)) pa_stream *s, void *local_mainloop) {
|
||||
// debug(1,"stream_state_cb called.");
|
||||
pa_threaded_mainloop_signal(local_mainloop, 0);
|
||||
}
|
||||
|
||||
void stream_write_cb(pa_stream *local_stream, size_t requested_bytes,
|
||||
__attribute__((unused)) void *userdata) {
|
||||
// debug(1, "pa stream_write_cb");
|
||||
check_pa_stream_status(local_stream, "audio_pa stream_write_cb.");
|
||||
int bytes_to_transfer = requested_bytes;
|
||||
int bytes_transferred = 0;
|
||||
uint8_t *buffer = NULL;
|
||||
int ret = 0;
|
||||
pthread_mutex_lock(&buffer_mutex);
|
||||
pthread_cleanup_push(mutex_unlock, (void *)&buffer_mutex);
|
||||
while ((bytes_to_transfer > 0) && (audio_occupancy > 0) && (ret == 0)) {
|
||||
if (pa_stream_is_suspended(local_stream))
|
||||
debug(1, "local_stream is suspended");
|
||||
size_t bytes_we_can_transfer = bytes_to_transfer;
|
||||
if (audio_occupancy == 0) {
|
||||
pa_stream_cork(local_stream, 1, stream_success_cb, mainloop);
|
||||
debug(1, "stream_write_cb: corked");
|
||||
}
|
||||
if (audio_occupancy < bytes_we_can_transfer) {
|
||||
// debug(1, "Underflow? We have %d bytes but we are asked for %d bytes", audio_occupancy,
|
||||
// bytes_we_can_transfer);
|
||||
bytes_we_can_transfer = audio_occupancy;
|
||||
}
|
||||
|
||||
// bytes we can transfer will never be greater than the bytes available
|
||||
|
||||
ret = pa_stream_begin_write(local_stream, (void **)&buffer, &bytes_we_can_transfer);
|
||||
if ((ret == 0) && (buffer != NULL) && (audio_lmb != NULL)) {
|
||||
if (bytes_we_can_transfer <= (size_t)(audio_umb - audio_toq)) {
|
||||
// the bytes are all in a row in the audo buffer
|
||||
memcpy(buffer, audio_toq, bytes_we_can_transfer);
|
||||
audio_toq += bytes_we_can_transfer;
|
||||
ret = pa_stream_write(local_stream, buffer, bytes_we_can_transfer, NULL, 0LL,
|
||||
PA_SEEK_RELATIVE);
|
||||
} else {
|
||||
// the bytes are in two places in the audio buffer
|
||||
size_t first_portion_to_write = audio_umb - audio_toq;
|
||||
if (first_portion_to_write != 0)
|
||||
memcpy(buffer, audio_toq, first_portion_to_write);
|
||||
uint8_t *new_buffer = buffer + first_portion_to_write;
|
||||
memcpy(new_buffer, audio_lmb, bytes_we_can_transfer - first_portion_to_write);
|
||||
ret = pa_stream_write(local_stream, buffer, bytes_we_can_transfer, NULL, 0LL,
|
||||
PA_SEEK_RELATIVE);
|
||||
audio_toq = audio_lmb + bytes_we_can_transfer - first_portion_to_write;
|
||||
}
|
||||
bytes_transferred += bytes_we_can_transfer;
|
||||
audio_occupancy -= bytes_we_can_transfer;
|
||||
bytes_to_transfer -= bytes_we_can_transfer;
|
||||
}
|
||||
}
|
||||
pthread_cleanup_pop(1); // release the mutex
|
||||
if (ret != 0)
|
||||
debug(1, "error writing to pa buffer");
|
||||
// debug(1,"<<<Frames requested %d, written to pa: %d, corked status:
|
||||
// %d.",requested_bytes/4,bytes_transferred/4,pa_stream_is_corked(local_stream));
|
||||
}
|
||||
|
||||
void stream_success_cb(__attribute__((unused)) pa_stream *local_stream,
|
||||
__attribute__((unused)) int success,
|
||||
__attribute__((unused)) void *userdata) {
|
||||
return;
|
||||
}
|
||||
|
||||
audio_output audio_pa = {.name = "pulseaudio",
|
||||
.help = NULL,
|
||||
.init = &init,
|
||||
.deinit = &deinit,
|
||||
.prepare = NULL,
|
||||
.start = &start,
|
||||
.start = NULL,
|
||||
.configure = &configure,
|
||||
.get_configuration = &get_configuration,
|
||||
.stop = &stop,
|
||||
.is_running = NULL,
|
||||
.flush = &flush,
|
||||
@@ -362,71 +793,3 @@ audio_output audio_pa = {.name = "pa",
|
||||
.volume = NULL,
|
||||
.parameters = NULL,
|
||||
.mute = NULL};
|
||||
|
||||
void context_state_cb(__attribute__((unused)) pa_context *context, void *mainloop) {
|
||||
// debug(1,"context_state_cb called.");
|
||||
pa_threaded_mainloop_signal(mainloop, 0);
|
||||
}
|
||||
|
||||
void stream_state_cb(__attribute__((unused)) pa_stream *s, void *mainloop) {
|
||||
// debug(1,"stream_state_cb called.");
|
||||
pa_threaded_mainloop_signal(mainloop, 0);
|
||||
}
|
||||
|
||||
void stream_write_cb(pa_stream *stream, size_t requested_bytes,
|
||||
__attribute__((unused)) void *userdata) {
|
||||
check_pa_stream_status(stream, "audio_pa stream_write_cb.");
|
||||
int bytes_to_transfer = requested_bytes;
|
||||
// int bytes_transferred = 0;
|
||||
uint8_t *buffer = NULL;
|
||||
int ret = 0;
|
||||
pthread_mutex_lock(&buffer_mutex);
|
||||
pthread_cleanup_push(mutex_unlock, (void *)&buffer_mutex);
|
||||
while ((bytes_to_transfer > 0) && (audio_occupancy > 0) && (ret == 0)) {
|
||||
if (pa_stream_is_suspended(stream))
|
||||
debug(1, "stream is suspended");
|
||||
size_t bytes_we_can_transfer = bytes_to_transfer;
|
||||
if (audio_occupancy < bytes_we_can_transfer) {
|
||||
// debug(1, "Underflow? We have %d bytes but we are asked for %d bytes", audio_occupancy,
|
||||
// bytes_we_can_transfer);
|
||||
pa_stream_cork(stream, 1, stream_success_cb, mainloop);
|
||||
// debug(1, "Corked");
|
||||
bytes_we_can_transfer = audio_occupancy;
|
||||
}
|
||||
|
||||
// bytes we can transfer will never be greater than the bytes available
|
||||
|
||||
ret = pa_stream_begin_write(stream, (void **)&buffer, &bytes_we_can_transfer);
|
||||
if ((ret == 0) && (buffer != NULL)) {
|
||||
if (bytes_we_can_transfer <= (size_t)(audio_umb - audio_toq)) {
|
||||
// the bytes are all in a row in the audio buffer
|
||||
memcpy(buffer, audio_toq, bytes_we_can_transfer);
|
||||
audio_toq += bytes_we_can_transfer;
|
||||
ret = pa_stream_write(stream, buffer, bytes_we_can_transfer, NULL, 0LL, PA_SEEK_RELATIVE);
|
||||
} else {
|
||||
// the bytes are in two places in the audio buffer
|
||||
size_t first_portion_to_write = audio_umb - audio_toq;
|
||||
if (first_portion_to_write != 0)
|
||||
memcpy(buffer, audio_toq, first_portion_to_write);
|
||||
uint8_t *new_buffer = buffer + first_portion_to_write;
|
||||
memcpy(new_buffer, audio_lmb, bytes_we_can_transfer - first_portion_to_write);
|
||||
ret = pa_stream_write(stream, buffer, bytes_we_can_transfer, NULL, 0LL, PA_SEEK_RELATIVE);
|
||||
audio_toq = audio_lmb + bytes_we_can_transfer - first_portion_to_write;
|
||||
}
|
||||
// bytes_transferred += bytes_we_can_transfer;
|
||||
audio_occupancy -= bytes_we_can_transfer;
|
||||
bytes_to_transfer -= bytes_we_can_transfer;
|
||||
}
|
||||
}
|
||||
pthread_cleanup_pop(1); // release the mutex
|
||||
if (ret != 0)
|
||||
debug(1, "error writing to pa buffer");
|
||||
// debug(1,"<<<Frames requested %d, written to pa: %d, corked status:
|
||||
// %d.",requested_bytes/4,bytes_transferred/4,pa_stream_is_corked(stream));
|
||||
}
|
||||
|
||||
void stream_success_cb(__attribute__((unused)) pa_stream *stream,
|
||||
__attribute__((unused)) int success,
|
||||
__attribute__((unused)) void *userdata) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user