]>
Commit | Line | Data |
---|---|---|
ec232363 | 1 | /* |
afaf94b7 | 2 | * Asynchronous PipeWire Backend. This file is part of Shairport Sync. |
bde2bcc8 | 3 | * Copyright (c) Mike Brady 2023 |
ec232363 LR |
4 | * All rights reserved. |
5 | * | |
6 | * Permission is hereby granted, free of charge, to any person | |
7 | * obtaining a copy of this software and associated documentation | |
8 | * files (the "Software"), to deal in the Software without | |
9 | * restriction, including without limitation the rights to use, | |
10 | * copy, modify, merge, publish, distribute, sublicense, and/or | |
11 | * sell copies of the Software, and to permit persons to whom the | |
12 | * Software is furnished to do so, subject to the following conditions: | |
13 | * | |
14 | * The above copyright notice and this permission notice shall be | |
15 | * included in all copies or substantial portions of the Software. | |
16 | * | |
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
18 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES | |
19 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |
20 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT | |
21 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
22 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
23 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR | |
24 | * OTHER DEALINGS IN THE SOFTWARE. | |
25 | */ | |
26 | ||
bde2bcc8 MB |
27 | // This uses ideas from the tone generator sample code at: |
28 | // https://github.com/PipeWire/pipewire/blob/master/src/examples/audio-src.c | |
dc197ae7 | 29 | // Thanks to Wim Taymans. |
bde2bcc8 | 30 | |
ec232363 LR |
31 | #include "audio.h" |
32 | #include "common.h" | |
afaf94b7 MB |
33 | #include <errno.h> |
34 | #include <pthread.h> | |
35 | #include <stdio.h> | |
36 | #include <string.h> | |
37 | #include <unistd.h> | |
ec232363 | 38 | |
afaf94b7 | 39 | #include <pipewire/pipewire.h> |
27e4ba09 | 40 | #include <spa/param/audio/format-utils.h> |
ec232363 | 41 | |
dc197ae7 | 42 | // note -- these are hardwired into this code. |
afaf94b7 | 43 | #define DEFAULT_FORMAT SPA_AUDIO_FORMAT_S16_LE |
3763b321 MB |
44 | #define DEFAULT_BYTES_PER_SAMPLE 2 |
45 | ||
afaf94b7 MB |
46 | #define DEFAULT_RATE 44100 |
47 | #define DEFAULT_CHANNELS 2 | |
3763b321 | 48 | #define DEFAULT_BUFFER_SIZE_IN_SECONDS 4 |
ec232363 | 49 | |
afaf94b7 | 50 | // Four seconds buffer -- should be plenty |
3763b321 | 51 | #define buffer_allocation DEFAULT_RATE * DEFAULT_BUFFER_SIZE_IN_SECONDS * DEFAULT_BYTES_PER_SAMPLE * DEFAULT_CHANNELS |
ec232363 | 52 | |
27e4ba09 | 53 | static pthread_mutex_t buffer_mutex = PTHREAD_MUTEX_INITIALIZER; |
ec232363 | 54 | |
27e4ba09 MB |
55 | static char *audio_lmb, *audio_umb, *audio_toq, *audio_eoq; |
56 | static size_t audio_size = buffer_allocation; | |
57 | static size_t audio_occupancy; | |
dc197ae7 | 58 | static int enable_fill; |
ec232363 | 59 | |
27e4ba09 | 60 | struct timing_data { |
bde2bcc8 | 61 | int pw_time_is_valid; // set when the pw_time has been set |
27e4ba09 | 62 | struct pw_time time_info; // information about the last time a process callback occurred |
bde2bcc8 | 63 | size_t frames; // the number of frames sent at that time |
27e4ba09 MB |
64 | }; |
65 | ||
66 | // to avoid using a mutex, write the same data twice and check they are the same | |
67 | // to ensure they are consistent. Make sure the first is written strictly before the second | |
68 | // using __sync_synchronize(); | |
69 | struct timing_data timing_data_1, timing_data_2; | |
70 | ||
afaf94b7 MB |
71 | struct data { |
72 | struct pw_thread_loop *loop; | |
73 | struct pw_stream *stream; | |
ec232363 LR |
74 | }; |
75 | ||
afaf94b7 | 76 | // the pipewire global data structure |
dc197ae7 | 77 | struct data data = {NULL, NULL}; |
ec232363 | 78 | |
dc197ae7 | 79 | /* |
bde2bcc8 MB |
80 | static void on_state_changed(__attribute__((unused)) void *userdata, enum pw_stream_state old, |
81 | enum pw_stream_state state, | |
82 | __attribute__((unused)) const char *error) { | |
83 | // struct pw_data *pw = userdata; | |
84 | debug(3, "pw: stream state changed %s -> %s", pw_stream_state_as_string(old), | |
85 | pw_stream_state_as_string(state)); | |
86 | } | |
dc197ae7 | 87 | */ |
bde2bcc8 MB |
88 | |
89 | static void on_process(void *userdata) { | |
90 | ||
91 | struct data *data = userdata; | |
92 | int n_frames = 0; | |
93 | ||
27e4ba09 | 94 | pthread_mutex_lock(&buffer_mutex); |
bde2bcc8 | 95 | |
dc197ae7 | 96 | if ((audio_occupancy > 0) || (enable_fill)) { |
bde2bcc8 MB |
97 | |
98 | // get a buffer to see how big it can be | |
99 | struct pw_buffer *b = pw_stream_dequeue_buffer(data->stream); | |
100 | if (b == NULL) { | |
101 | pw_log_warn("out of buffers: %m"); | |
dc197ae7 | 102 | die("PipeWire failue -- out of buffers!"); |
bde2bcc8 MB |
103 | } |
104 | struct spa_buffer *buf = b->buffer; | |
105 | uint8_t *dest = buf->datas[0].data; | |
dc197ae7 | 106 | if (dest != NULL) { |
3763b321 | 107 | int stride = DEFAULT_BYTES_PER_SAMPLE * DEFAULT_CHANNELS; |
dc197ae7 MB |
108 | |
109 | // note: the requested field is the number of frames, not bytes, requested | |
110 | int max_possible_frames = SPA_MIN(b->requested, buf->datas[0].maxsize / stride); | |
111 | ||
112 | size_t bytes_we_can_transfer = max_possible_frames * stride; | |
113 | ||
114 | if (audio_occupancy > 0) { | |
115 | // if (enable_fill == 1)) { | |
116 | // debug(1, "got audio -- disable_fill"); | |
117 | // } | |
118 | enable_fill = 0; | |
119 | ||
120 | if (bytes_we_can_transfer > audio_occupancy) | |
121 | bytes_we_can_transfer = audio_occupancy; | |
122 | ||
123 | n_frames = bytes_we_can_transfer / stride; | |
124 | ||
125 | size_t bytes_to_end_of_buffer = (size_t)(audio_umb - audio_toq); // must be zero or positive | |
126 | if (bytes_we_can_transfer <= bytes_to_end_of_buffer) { | |
127 | // the bytes are all in a row in the audio buffer | |
128 | memcpy(dest, audio_toq, bytes_we_can_transfer); | |
129 | audio_toq += bytes_we_can_transfer; | |
130 | } else { | |
131 | // the bytes are in two places in the audio buffer | |
132 | size_t first_portion_to_write = audio_umb - audio_toq; | |
133 | if (first_portion_to_write != 0) | |
134 | memcpy(dest, audio_toq, first_portion_to_write); | |
135 | uint8_t *new_dest = dest + first_portion_to_write; | |
136 | memcpy(new_dest, audio_lmb, bytes_we_can_transfer - first_portion_to_write); | |
137 | audio_toq = audio_lmb + bytes_we_can_transfer - first_portion_to_write; | |
138 | } | |
139 | audio_occupancy -= bytes_we_can_transfer; | |
140 | ||
141 | } else { | |
142 | debug(3, "send silence"); | |
143 | // this should really be dithered silence | |
144 | memset(dest, 0, bytes_we_can_transfer); | |
145 | n_frames = max_possible_frames; | |
146 | } | |
147 | buf->datas[0].chunk->offset = 0; | |
148 | buf->datas[0].chunk->stride = stride; | |
149 | buf->datas[0].chunk->size = n_frames * stride; | |
150 | pw_stream_queue_buffer(data->stream, b); | |
151 | debug(3, "Queueing %d frames for output.", n_frames); | |
152 | } // (else the first data block does not contain a data pointer) | |
bde2bcc8 MB |
153 | } |
154 | pthread_mutex_unlock(&buffer_mutex); | |
ec232363 | 155 | |
bde2bcc8 MB |
156 | timing_data_1.frames = n_frames; |
157 | if (pw_stream_get_time_n(data->stream, &timing_data_1.time_info, sizeof(struct timing_data)) == 0) | |
27e4ba09 MB |
158 | timing_data_1.pw_time_is_valid = 1; |
159 | else | |
160 | timing_data_1.pw_time_is_valid = 0; | |
161 | __sync_synchronize(); | |
162 | memcpy((char *)&timing_data_2, (char *)&timing_data_1, sizeof(struct timing_data)); | |
163 | __sync_synchronize(); | |
ec232363 LR |
164 | } |
165 | ||
dc197ae7 MB |
166 | static const struct pw_stream_events stream_events = {PW_VERSION_STREAM_EVENTS, |
167 | .process = on_process}; | |
168 | // PW_VERSION_STREAM_EVENTS, .process = on_process, .state_changed = on_state_changed}; | |
bde2bcc8 MB |
169 | |
170 | static void deinit(void) { | |
171 | pw_thread_loop_stop(data.loop); | |
172 | pw_stream_destroy(data.stream); | |
173 | pw_thread_loop_destroy(data.loop); | |
174 | pw_deinit(); | |
175 | free(audio_lmb); // deallocate that buffer | |
3763b321 MB |
176 | if (config.pw_application_name) |
177 | free(config.pw_application_name); | |
178 | if (config.pw_node_name) | |
179 | free(config.pw_node_name); | |
180 | config.pw_application_name = config.pw_node_name = NULL; | |
181 | ||
182 | ||
bde2bcc8 | 183 | } |
ec232363 | 184 | |
c0a3dacf | 185 | static int init(__attribute__((unused)) int argc, __attribute__((unused)) char **argv) { |
ec232363 | 186 | // set up default values first |
bde2bcc8 MB |
187 | memset(&timing_data_1, 0, sizeof(struct timing_data)); |
188 | memset(&timing_data_2, 0, sizeof(struct timing_data)); | |
ec232363 | 189 | config.audio_backend_buffer_desired_length = 0.35; |
afaf94b7 MB |
190 | config.audio_backend_buffer_interpolation_threshold_in_seconds = |
191 | 0.02; // below this, soxr interpolation will not occur -- it'll be basic interpolation | |
192 | // instead. | |
ec232363 | 193 | |
afaf94b7 | 194 | config.audio_backend_latency_offset = 0; |
ec232363 | 195 | |
afaf94b7 | 196 | // get settings from settings file |
afaf94b7 MB |
197 | // do the "general" audio options. Note, these options are in the "general" stanza! |
198 | parse_general_audio_options(); | |
ec232363 | 199 | |
3763b321 | 200 | |
27e4ba09 MB |
201 | // now any PipeWire-specific options |
202 | if (config.cfg != NULL) { | |
203 | const char *str; | |
3763b321 MB |
204 | |
205 | /* Get the Application Name. */ | |
206 | if (config_lookup_string(config.cfg, "pw.application_name", &str)) { | |
207 | config.pw_application_name = (char *)str; | |
208 | } | |
209 | ||
288e5eb6 | 210 | /* Get the PipeWire node name. */ |
3763b321 MB |
211 | if (config_lookup_string(config.cfg, "pa.node_name", &str)) { |
212 | config.pw_node_name = (char *)str; | |
213 | } | |
27e4ba09 | 214 | } |
3763b321 | 215 | |
afaf94b7 | 216 | // finished collecting settings |
ec232363 | 217 | |
afaf94b7 MB |
218 | // allocate space for the audio buffer |
219 | audio_lmb = malloc(audio_size); | |
220 | if (audio_lmb == NULL) | |
dc197ae7 | 221 | die("Can't allocate %d bytes for PipeWire buffer.", audio_size); |
afaf94b7 MB |
222 | audio_toq = audio_eoq = audio_lmb; |
223 | audio_umb = audio_lmb + audio_size; | |
224 | audio_occupancy = 0; | |
dc197ae7 MB |
225 | // debug(1, "init enable_fill"); |
226 | enable_fill = 1; | |
ec232363 | 227 | |
afaf94b7 MB |
228 | const struct spa_pod *params[1]; |
229 | uint8_t buffer[1024]; | |
230 | struct pw_properties *props; | |
231 | struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); | |
232 | ||
233 | int largc = 0; | |
234 | pw_init(&largc, NULL); | |
235 | ||
bde2bcc8 MB |
236 | /* make a threaded loop. */ |
237 | data.loop = pw_thread_loop_new("shairport-sync", NULL); | |
afaf94b7 MB |
238 | |
239 | pw_thread_loop_lock(data.loop); | |
27e4ba09 | 240 | |
afaf94b7 | 241 | pw_thread_loop_start(data.loop); |
3763b321 MB |
242 | |
243 | char* appname = config.pw_application_name; | |
244 | if (appname == NULL) | |
245 | appname = "Shairport Sync"; | |
246 | ||
247 | char* nodename = config.pw_node_name; | |
248 | if (nodename == NULL) | |
288e5eb6 | 249 | nodename = "Shairport Sync"; |
3763b321 MB |
250 | |
251 | ||
afaf94b7 | 252 | |
afaf94b7 | 253 | props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio", PW_KEY_MEDIA_CATEGORY, "Playback", |
3763b321 MB |
254 | PW_KEY_MEDIA_ROLE, "Music", PW_KEY_APP_NAME, appname, |
255 | PW_KEY_NODE_NAME, nodename, NULL); | |
afdf0d86 | 256 | |
3763b321 | 257 | data.stream = pw_stream_new_simple(pw_thread_loop_get_loop(data.loop), config.appName, props, |
afaf94b7 MB |
258 | &stream_events, &data); |
259 | ||
288e5eb6 MB |
260 | // Make one parameter with the supported formats. The SPA_PARAM_EnumFormat |
261 | // id means that this is a format enumeration (of 1 value). | |
afaf94b7 | 262 | params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, |
3763b321 | 263 | &SPA_AUDIO_INFO_RAW_INIT(.format = DEFAULT_FORMAT, |
afaf94b7 MB |
264 | .channels = DEFAULT_CHANNELS, |
265 | .rate = DEFAULT_RATE)); | |
266 | ||
288e5eb6 MB |
267 | // Now connect this stream. We ask that our process function is |
268 | // called in a realtime thread. | |
afaf94b7 MB |
269 | pw_stream_connect(data.stream, PW_DIRECTION_OUTPUT, PW_ID_ANY, |
270 | PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | | |
271 | PW_STREAM_FLAG_RT_PROCESS, | |
272 | params, 1); | |
273 | ||
274 | pw_thread_loop_unlock(data.loop); | |
ec232363 LR |
275 | return 0; |
276 | } | |
277 | ||
27e4ba09 | 278 | static void start(__attribute__((unused)) int sample_rate, |
dc197ae7 MB |
279 | __attribute__((unused)) int sample_format) { |
280 | } | |
27e4ba09 MB |
281 | |
282 | static int play(__attribute__((unused)) void *buf, int samples, | |
283 | __attribute__((unused)) int sample_type, __attribute__((unused)) uint32_t timestamp, | |
bde2bcc8 | 284 | __attribute__((unused)) uint64_t playtime) { |
afaf94b7 | 285 | // copy the samples into the queue |
bde2bcc8 | 286 | debug(3, "play %u samples; %u bytes already in the buffer.", samples, audio_occupancy); |
288e5eb6 | 287 | size_t bytes_to_transfer = samples * DEFAULT_CHANNELS * DEFAULT_BYTES_PER_SAMPLE; |
bde2bcc8 MB |
288 | pthread_mutex_lock(&buffer_mutex); |
289 | size_t bytes_available = audio_size - audio_occupancy; | |
290 | if (bytes_available < bytes_to_transfer) | |
291 | bytes_to_transfer = bytes_available; | |
292 | if (bytes_to_transfer > 0) { | |
293 | size_t space_to_end_of_buffer = audio_umb - audio_eoq; | |
294 | if (space_to_end_of_buffer >= bytes_to_transfer) { | |
295 | memcpy(audio_eoq, buf, bytes_to_transfer); | |
296 | audio_eoq += bytes_to_transfer; | |
297 | } else { | |
298 | memcpy(audio_eoq, buf, space_to_end_of_buffer); | |
299 | buf += space_to_end_of_buffer; | |
300 | memcpy(audio_lmb, buf, bytes_to_transfer - space_to_end_of_buffer); | |
301 | audio_eoq = audio_lmb + bytes_to_transfer - space_to_end_of_buffer; | |
302 | } | |
afaf94b7 | 303 | audio_occupancy += bytes_to_transfer; |
ec232363 | 304 | } |
bde2bcc8 | 305 | pthread_mutex_unlock(&buffer_mutex); |
afaf94b7 | 306 | return 0; |
ec232363 LR |
307 | } |
308 | ||
27e4ba09 | 309 | int delay(long *the_delay) { |
bde2bcc8 MB |
310 | long result = 0; |
311 | int reply = 0; | |
27e4ba09 MB |
312 | // find out what's already in the PipeWire system and when |
313 | struct timing_data timing_data; | |
314 | int loop_count = 1; | |
315 | do { | |
316 | memcpy(&timing_data, (char *)&timing_data_1, sizeof(struct timing_data)); | |
317 | __sync_synchronize(); | |
318 | if (memcmp(&timing_data, (char *)&timing_data_2, sizeof(struct timing_data)) != 0) { | |
bde2bcc8 MB |
319 | usleep(2); // microseconds |
320 | loop_count++; | |
321 | __sync_synchronize(); | |
27e4ba09 | 322 | } |
bde2bcc8 MB |
323 | } while ((memcmp(&timing_data, (char *)&timing_data_2, sizeof(struct timing_data)) != 0) && |
324 | (loop_count < 10)); | |
27e4ba09 MB |
325 | long total_delay_now_frames_long = 0; |
326 | if ((loop_count < 10) && (timing_data.pw_time_is_valid != 0)) { | |
bde2bcc8 | 327 | struct timespec time_now; |
27e4ba09 | 328 | clock_gettime(CLOCK_MONOTONIC, &time_now); |
bde2bcc8 MB |
329 | int64_t interval_from_process_time_to_now = |
330 | SPA_TIMESPEC_TO_NSEC(&time_now) - timing_data.time_info.now; | |
27e4ba09 MB |
331 | int64_t delay_in_ns = timing_data.time_info.delay + timing_data.time_info.buffered; |
332 | delay_in_ns = delay_in_ns * 1000000000; | |
333 | delay_in_ns = delay_in_ns * timing_data.time_info.rate.num; | |
334 | delay_in_ns = delay_in_ns / timing_data.time_info.rate.denom; | |
bde2bcc8 | 335 | |
27e4ba09 | 336 | int64_t total_delay_now_ns = delay_in_ns - interval_from_process_time_to_now; |
3763b321 | 337 | int64_t total_delay_now_frames = (total_delay_now_ns * DEFAULT_RATE) / 1000000000 + timing_data.frames; |
27e4ba09 | 338 | total_delay_now_frames_long = total_delay_now_frames; |
bde2bcc8 | 339 | debug(3, "total delay in frames: %ld.", total_delay_now_frames_long); |
27e4ba09 | 340 | |
bde2bcc8 MB |
341 | if (timing_data.time_info.queued != 0) { |
342 | debug(1, "buffers queued: %d", timing_data.time_info.queued); | |
343 | } | |
344 | /* | |
345 | debug(3, | |
346 | "interval_from_process_time_to_now: %" PRId64 " ns, " | |
347 | "delay_in_ns: %" PRId64 ", queued: %" PRId64 ", buffered: %" PRId64 ".", | |
348 | // delay_timing_data.time_info.rate.num, delay_timing_data.time_info.rate.denom, | |
349 | interval_from_process_time_to_now, delay_in_ns, | |
350 | timing_data.time_info.queued, timing_data.time_info.buffered); | |
351 | */ | |
27e4ba09 | 352 | |
afaf94b7 | 353 | } else { |
288e5eb6 MB |
354 | warn("Shairport Sync's PipeWire backend can not get timing information from the PipeWire " |
355 | "system. Is PipeWire running?"); | |
ec232363 | 356 | } |
27e4ba09 | 357 | |
27e4ba09 | 358 | pthread_mutex_lock(&buffer_mutex); |
288e5eb6 | 359 | result = total_delay_now_frames_long + audio_occupancy / (DEFAULT_BYTES_PER_SAMPLE * DEFAULT_CHANNELS); |
27e4ba09 | 360 | pthread_mutex_unlock(&buffer_mutex); |
afaf94b7 MB |
361 | *the_delay = result; |
362 | return reply; | |
ec232363 LR |
363 | } |
364 | ||
27e4ba09 | 365 | static void flush(void) { |
bde2bcc8 | 366 | pthread_mutex_lock(&buffer_mutex); |
afaf94b7 MB |
367 | audio_toq = audio_eoq = audio_lmb; |
368 | audio_umb = audio_lmb + audio_size; | |
369 | audio_occupancy = 0; | |
dc197ae7 MB |
370 | // if (enable_fill == 0) { |
371 | // debug(1, "flush enable_fill"); | |
372 | // } | |
373 | enable_fill = 1; | |
27e4ba09 | 374 | pthread_mutex_unlock(&buffer_mutex); |
ec232363 LR |
375 | } |
376 | ||
afaf94b7 | 377 | static void stop(void) { |
bde2bcc8 | 378 | pthread_mutex_lock(&buffer_mutex); |
afaf94b7 MB |
379 | audio_toq = audio_eoq = audio_lmb; |
380 | audio_umb = audio_lmb + audio_size; | |
381 | audio_occupancy = 0; | |
dc197ae7 MB |
382 | // if (enable_fill == 0) { |
383 | // debug(1, "stop enable_fill"); | |
384 | // } | |
385 | enable_fill = 1; | |
27e4ba09 | 386 | pthread_mutex_unlock(&buffer_mutex); |
ec232363 LR |
387 | } |
388 | ||
c0a3dacf MB |
389 | audio_output audio_pw = {.name = "pw", |
390 | .help = NULL, | |
391 | .init = &init, | |
392 | .deinit = &deinit, | |
393 | .prepare = NULL, | |
27e4ba09 | 394 | .start = &start, |
c0a3dacf MB |
395 | .stop = &stop, |
396 | .is_running = NULL, | |
397 | .flush = &flush, | |
27e4ba09 | 398 | .delay = &delay, |
cd9da86f | 399 | .stats = NULL, |
c0a3dacf MB |
400 | .play = &play, |
401 | .volume = NULL, | |
402 | .parameters = NULL, | |
afaf94b7 | 403 | .mute = NULL}; |