From: Richard Kettlewell Date: Sun, 1 Mar 2009 19:01:36 +0000 (+0000) Subject: Uniform audio command back end now rate limited. X-Git-Tag: 5.0~168^2~7 X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~mdw/git/disorder/commitdiff_plain/ec57f6c97b41d54ade912f7e3b9f727b40e38e16 Uniform audio command back end now rate limited. The algorithm is the same as for the RTP backend, and is therefore split out into a separate file, uaudio-schedule.c. --- diff --git a/lib/Makefile.am b/lib/Makefile.am index 6a6596f..7ef6039 100644 --- a/lib/Makefile.am +++ b/lib/Makefile.am @@ -85,7 +85,7 @@ libdisorder_a_SOURCES=charset.c charset.h \ uaudio.c uaudio-thread.c uaudio.h \ uaudio-oss.c uaudio-alsa.c \ uaudio-coreaudio.c \ - uaudio-rtp.c uaudio-command.c \ + uaudio-rtp.c uaudio-command.c uaudio-schedule.c \ url.h url.c \ user.h user.c \ unicode.h unicode.c \ diff --git a/lib/uaudio-command.c b/lib/uaudio-command.c index 6b7ef66..b9a1abb 100644 --- a/lib/uaudio-command.c +++ b/lib/uaudio-command.c @@ -17,7 +17,13 @@ * along with this program. If not, see . */ /** @file lib/uaudio-command.c - * @brief Support for commmand backend */ + * @brief Support for commmand backend + * + * We use the code in @ref lib/uaudio-schedule.c to ensure that we write at + * approximately the 'real' rate. For disorder-playrtp this isn't very useful + * (thought it might reduce the size of various buffers downstream of us) but + * when run from the speaker it means that pausing stands a chance of working. + */ #include "common.h" #include @@ -85,6 +91,7 @@ static void command_open(void) { /** @brief Send audio data to subprocess */ static size_t command_play(void *buffer, size_t nsamples) { + uaudio_schedule_synchronize(); const size_t bytes = nsamples * uaudio_sample_size; int written = write(command_fd, buffer, bytes); if(written < 0) { @@ -100,12 +107,15 @@ static size_t command_play(void *buffer, size_t nsamples) { fatal(errno, "error writing to audio command subprocess"); } } - return written / uaudio_sample_size; + const size_t written_samples = written / uaudio_sample_size; + uaudio_schedule_update(written_samples); + return written_samples; } static void command_start(uaudio_callback *callback, void *userdata) { command_open(); + uaudio_schedule_init(); uaudio_thread_start(callback, userdata, command_play, @@ -119,6 +129,7 @@ static void command_stop(void) { } static void command_activate(void) { + uaudio_schedule_reactivated = 1; uaudio_thread_activate(); } diff --git a/lib/uaudio-rtp.c b/lib/uaudio-rtp.c index 9980ba0..ea1d1ca 100644 --- a/lib/uaudio-rtp.c +++ b/lib/uaudio-rtp.c @@ -59,28 +59,6 @@ static uint32_t rtp_id; /** @brief RTP sequence number */ static uint16_t rtp_sequence; -/** @brief RTP timestamp - * - * This is the timestamp that will be used on the next outbound packet. - * - * The timestamp in the packet header is only 32 bits wide. With 44100Hz - * stereo, that only gives about half a day before wrapping, which is not - * particularly convenient for certain debugging purposes. Therefore the - * timestamp is maintained as a 64-bit integer, giving around six million years - * before wrapping, and truncated to 32 bits when transmitting. - */ -static uint64_t rtp_timestamp; - -/** @brief Actual time corresponding to @ref rtp_timestamp - * - * This is the time, on this machine, at which the sample at @ref rtp_timestamp - * ought to be sent, interpreted as the time the last packet was sent plus the - * time length of the packet. */ -static struct timeval rtp_timeval; - -/** @brief Set when we (re-)activate, to provoke timestamp resync */ -static int rtp_reactivated; - /** @brief Network error count * * If too many errors occur in too short a time, we give up. @@ -100,21 +78,20 @@ static const char *const rtp_options[] = { "rtp-source-port", "multicast-ttl", "multicast-loop", - "rtp-delay-threshold", + "delay-threshold", NULL }; static size_t rtp_play(void *buffer, size_t nsamples) { struct rtp_header header; struct iovec vec[2]; - struct timeval now; /* We do as much work as possible before checking what time it is */ /* Fill out header */ header.vpxcc = 2 << 6; /* V=2, P=0, X=0, CC=0 */ header.seq = htons(rtp_sequence++); header.ssrc = rtp_id; - header.mpt = (rtp_reactivated ? 0x80 : 0x00) | rtp_payload; + header.mpt = (uaudio_schedule_reactivated ? 0x80 : 0x00) | rtp_payload; #if !WORDS_BIGENDIAN /* Convert samples to network byte order */ uint16_t *u = buffer, *const limit = u + nsamples; @@ -127,61 +104,8 @@ static size_t rtp_play(void *buffer, size_t nsamples) { vec[0].iov_len = sizeof header; vec[1].iov_base = buffer; vec[1].iov_len = nsamples * uaudio_sample_size; -retry: - xgettimeofday(&now, NULL); - if(rtp_reactivated) { - /* We've been deactivated for some unknown interval. We need to advance - * rtp_timestamp to account for the dead air. */ - /* On the first run through we'll set the start time. */ - if(!rtp_timeval.tv_sec) - rtp_timeval = now; - /* See how much time we missed. - * - * This will be 0 on the first run through, in which case we'll not modify - * anything. - * - * It'll be negative in the (rare) situation where the deactivation - * interval is shorter than the last packet we sent. In this case we wait - * for that much time and then return having sent no samples, which will - * cause uaudio_play_thread_fn() to retry. - * - * In the normal case it will be positive. - */ - const int64_t delay = tvsub_us(now, rtp_timeval); /* microseconds */ - if(delay < 0) { - usleep(-delay); - goto retry; - } - /* Advance the RTP timestamp to the present. With 44.1KHz stereo this will - * overflow the intermediate value with a delay of a bit over 6 years. - * This seems acceptable. */ - uint64_t update = (delay * uaudio_rate * uaudio_channels) / 1000000; - /* Don't throw off channel synchronization */ - update -= update % uaudio_channels; - /* We log nontrivial changes */ - if(update) - info("advancing rtp_time by %"PRIu64" samples", update); - rtp_timestamp += update; - rtp_timeval = now; - rtp_reactivated = 0; - } else { - /* Chances are we've been called right on the heels of the previous packet. - * If we just sent packets as fast as we got audio data we'd get way ahead - * of the player and some buffer somewhere would fill (or at least become - * unreasonably large). - * - * First find out how far ahead of the target time we are. - */ - const int64_t ahead = tvsub_us(now, rtp_timeval); /* microseconds */ - /* Only delay at all if we are nontrivially ahead. */ - if(ahead > rtp_delay_threshold) { - /* Don't delay by the full amount */ - usleep(ahead - rtp_delay_threshold / 2); - /* Refetch time (so we don't get out of step with reality) */ - xgettimeofday(&now, NULL); - } - } - header.timestamp = htonl((uint32_t)rtp_timestamp); + uaudio_schedule_synchronize(); + header.timestamp = htonl((uint32_t)uaudio_schedule_timestamp); int written_bytes; do { written_bytes = writev(rtp_fd, vec, 2); @@ -195,17 +119,8 @@ retry: } else rtp_errors /= 2; /* gradual decay */ written_bytes -= sizeof (struct rtp_header); - size_t written_samples = written_bytes / uaudio_sample_size; - /* rtp_timestamp and rtp_timestamp are supposed to refer to the first sample - * of the next packet */ - rtp_timestamp += written_samples; - const unsigned usec = (rtp_timeval.tv_usec - + 1000000 * written_samples / (uaudio_rate - * uaudio_channels)); - /* ...will only overflow 32 bits if one packet is more than about half an - * hour long, which is not plausible. */ - rtp_timeval.tv_sec += usec / 1000000; - rtp_timeval.tv_usec = usec % 1000000; + const size_t written_samples = written_bytes / uaudio_sample_size; + uaudio_schedule_update(written_samples); return written_samples; } @@ -340,14 +255,6 @@ static void rtp_open(void) { fatal(errno, "error binding broadcast socket to %s", ssockname); if(connect(rtp_fd, res->ai_addr, res->ai_addrlen) < 0) fatal(errno, "error connecting broadcast socket to %s", sockname); - /* Various fields are required to have random initial values by RFC3550. The - * packet contents are highly public so there's no point asking for very - * strong randomness. */ - gcry_create_nonce(&rtp_id, sizeof rtp_id); - gcry_create_nonce(&rtp_sequence, sizeof rtp_sequence); - gcry_create_nonce(&rtp_timestamp, sizeof rtp_timestamp); - /* rtp_play() will spot this and choose an initial value */ - rtp_timeval.tv_sec = 0; } static void rtp_start(uaudio_callback *callback, @@ -364,7 +271,13 @@ static void rtp_start(uaudio_callback *callback, else fatal(0, "asked for %d/%d/%d 16/44100/1 and 16/44100/2", uaudio_bits, uaudio_rate, uaudio_channels); + /* Various fields are required to have random initial values by RFC3550. The + * packet contents are highly public so there's no point asking for very + * strong randomness. */ + gcry_create_nonce(&rtp_id, sizeof rtp_id); + gcry_create_nonce(&rtp_sequence, sizeof rtp_sequence); rtp_open(); + uaudio_schedule_init(); uaudio_thread_start(callback, userdata, rtp_play, @@ -380,7 +293,7 @@ static void rtp_stop(void) { } static void rtp_activate(void) { - rtp_reactivated = 1; + uaudio_schedule_reactivated = 1; uaudio_thread_activate(); } diff --git a/lib/uaudio-schedule.c b/lib/uaudio-schedule.c new file mode 100644 index 0000000..95fa0c0 --- /dev/null +++ b/lib/uaudio-schedule.c @@ -0,0 +1,180 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2009 Richard Kettlewell + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +/** @file lib/uaudio-schedule.c + * @brief Scheduler for RTP and command backends + * + * These functions ensure that audio is only written at approximately the rate + * it should play at, allowing pause to function properly. + * + * OSS and ALSA we expect to be essentially synchronous (though we could use + * this code if they don't play nicely). Core Audio sorts out its own timing + * issues itself. + * + * The sequence numbers are intended for RTP's use but it's more convenient to + * maintain them here. + */ + +#include "common.h" + +#include +#include + +#include "uaudio.h" +#include "mem.h" +#include "log.h" +#include "syscalls.h" +#include "timeval.h" + +/** @brief Sample timestamp + * + * This is the timestamp that will be used on the next outbound packet. + * + * The timestamp in an RTP packet header is only 32 bits wide. With 44100Hz + * stereo, that only gives about half a day before wrapping, which is not + * particularly convenient for certain debugging purposes. Therefore the + * timestamp is maintained as a 64-bit integer, giving around six million years + * before wrapping, and truncated to 32 bits when transmitting. + */ +uint64_t uaudio_schedule_timestamp; + +/** @brief Actual time corresponding to @ref uaudio_schedule_timestamp + * + * This is the time, on this machine, at which the sample at @ref + * uaudio_schedule_timestamp ought to be sent, interpreted as the time the last + * packet was sent plus the time length of the packet. */ +static struct timeval uaudio_schedule_timeval; + +/** @brief Set when we (re-)activate, to provoke timestamp resync */ +int uaudio_schedule_reactivated; + +/** @brief Delay threshold in microseconds + * + * uaudio_schedule_play() never attempts to introduce a delay shorter than this. + */ +static int64_t uaudio_schedule_delay_threshold; + +/** @brief Time for current packet */ +static struct timeval uaudio_schedule_now; + +/** @brief Synchronize playback operations against real time + * + * This function sleeps as necessary to rate-limit playback operations to match + * the actual playback rate. It also maintains @ref uaudio_schedule_timestamp + * as an arbitrarily-based sample counter, for use by RTP. + * + * You should call this in your API's @ref uaudio_playcallback before writing + * and call uaudio_schedule_update() afterwards. + */ +void uaudio_schedule_synchronize(void) { +retry: + xgettimeofday(&uaudio_schedule_now, NULL); + if(uaudio_schedule_reactivated) { + /* We've been deactivated for some unknown interval. We need to advance + * rtp_timestamp to account for the dead air. */ + /* On the first run through we'll set the start time. */ + if(!uaudio_schedule_timeval.tv_sec) + uaudio_schedule_timeval = uaudio_schedule_now; + /* See how much time we missed. + * + * This will be 0 on the first run through, in which case we'll not modify + * anything. + * + * It'll be negative in the (rare) situation where the deactivation + * interval is shorter than the last packet we sent. In this case we wait + * for that much time and then return having sent no samples, which will + * cause uaudio_play_thread_fn() to retry. + * + * In the normal case it will be positive. + */ + const int64_t delay = tvsub_us(uaudio_schedule_now, + uaudio_schedule_timeval); /* microseconds */ + if(delay < 0) { + usleep(-delay); + goto retry; + } + /* Advance the RTP timestamp to the present. With 44.1KHz stereo this will + * overflow the intermediate value with a delay of a bit over 6 years. + * This seems acceptable. */ + uint64_t update = (delay * uaudio_rate * uaudio_channels) / 1000000; + /* Don't throw off channel synchronization */ + update -= update % uaudio_channels; + /* We log nontrivial changes */ + if(update) + info("advancing uaudio_schedule_timeval by %"PRIu64" samples", update); + uaudio_schedule_timestamp += update; + uaudio_schedule_timeval = uaudio_schedule_now; + uaudio_schedule_reactivated = 0; + } else { + /* Chances are we've been called right on the heels of the previous packet. + * If we just sent packets as fast as we got audio data we'd get way ahead + * of the player and some buffer somewhere would fill (or at least become + * unreasonably large). + * + * First find out how far ahead of the target time we are. + */ + const int64_t ahead = tvsub_us(uaudio_schedule_timeval, + uaudio_schedule_now); /* microseconds */ + /* Only delay at all if we are nontrivially ahead. */ + if(ahead > uaudio_schedule_delay_threshold) { + /* Don't delay by the full amount */ + usleep(ahead - uaudio_schedule_delay_threshold / 2); + /* Refetch time (so we don't get out of step with reality) */ + xgettimeofday(&uaudio_schedule_now, NULL); + } + } +} + +/** @brief Update schedule after writing + * + * Called by your API's @ref uaudio_playcallback after sending audio data (to a + * subprocess or network or whatever). A separate function so that the caller + * doesn't have to know how many samples they're going to write until they've + * done so. + */ +void uaudio_schedule_update(size_t written_samples) { + /* uaudio_schedule_timestamp and uaudio_schedule_timestamp are supposed to + * refer to the first sample of the next packet */ + uaudio_schedule_timestamp += written_samples; + const unsigned usec = (uaudio_schedule_timeval.tv_usec + + 1000000 * written_samples / (uaudio_rate + * uaudio_channels)); + /* ...will only overflow 32 bits if one packet is more than about half an + * hour long, which is not plausible. */ + uaudio_schedule_timeval.tv_sec += usec / 1000000; + uaudio_schedule_timeval.tv_usec = usec % 1000000; +} + +/** @brief Initialize audio scheduling + * + * Should be called from your API's @c start callback. + */ +void uaudio_schedule_init(void) { + gcry_create_nonce(&uaudio_schedule_timestamp, + sizeof uaudio_schedule_timestamp); + /* uaudio_schedule_play() will spot this and choose an initial value */ + uaudio_schedule_timeval.tv_sec = 0; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ diff --git a/lib/uaudio.h b/lib/uaudio.h index 3e62aad..be396df 100644 --- a/lib/uaudio.h +++ b/lib/uaudio.h @@ -103,6 +103,12 @@ void uaudio_thread_start(uaudio_callback *callback, void uaudio_thread_stop(void); void uaudio_thread_activate(void); void uaudio_thread_deactivate(void); +void uaudio_schedule_synchronize(void); +void uaudio_schedule_update(size_t written_samples); +void uaudio_schedule_init(void); + +extern uint64_t uaudio_schedule_timestamp; +extern int uaudio_schedule_reactivated; #if HAVE_COREAUDIO_AUDIOHARDWARE_H extern const struct uaudio uaudio_coreaudio;