diff --git a/common.c b/common.c index 63ea1cd4..d10a2fad 100644 --- a/common.c +++ b/common.c @@ -240,21 +240,6 @@ void log_to_syslog() { shairport_cfg config; -// accessors for multi-thread-access fields in the conn structure - -double get_config_airplay_volume() { - config_lock; - double v = config.airplay_volume; - config_unlock; - return v; -} - -void set_config_airplay_volume(double v) { - config_lock; - config.airplay_volume = v; - config_unlock; -} - volatile int debuglev = 0; sigset_t pselect_sigset; diff --git a/common.h b/common.h index 298bdbd0..e152db68 100644 --- a/common.h +++ b/common.h @@ -122,7 +122,14 @@ typedef struct { config_t *cfg; int endianness; double airplay_volume; // stored here for reloading when necessary - char *appName; // normally the app is called shairport-syn, but it may be symlinked + double default_airplay_volume; + double high_threshold_airplay_volume; + uint64_t last_access_to_volume_info_time; + int limit_to_high_volume_threshold_time_in_minutes; // revert to the high threshold volume level + // if the existing volume level exceeds this + // and hasn't been used for this amount of + // time (0 means never revert) + char *appName; // normally the app is called shairport-syn, but it may be symlinked char *password; char *service_name; // the name for the shairport service, e.g. "Shairport Sync Version %v running // on host %h" @@ -169,10 +176,10 @@ typedef struct { int ignore_volume_control; int volume_max_db_set; // set to 1 if a maximum volume db has been set int volume_max_db; - int no_sync; // disable synchronisation, even if it's available - int no_mmap; // disable use of mmap-based output, even if it's available - double resync_threshold; // if it gets out of whack by more than this number of seconds, do a - // resync. if zero, never do a resync. + int no_sync; // disable synchronisation, even if it's available + int no_mmap; // disable use of mmap-based output, even if it's available + double resync_threshold; // if it gets out of whack by more than this number of seconds, do a + // resync. if zero, never do a resync. double resync_recovery_time; // if sync is late, drop the delay but also drop the following frames // up to the resync_recovery_time int allow_session_interruption; @@ -318,10 +325,6 @@ typedef struct { int unfixable_error_reported; // only report once. } shairport_cfg; -// accessors to config for multi-thread access -double get_config_airplay_volume(); -void set_config_airplay_volume(double v); - uint32_t nctohl(const uint8_t *p); // read 4 characters from *p and do ntohl on them uint16_t nctohs(const uint8_t *p); // read 2 characters from *p and do ntohs on them uint64_t nctoh64(const uint8_t *p); // read 8 characters from *p to a uint64_t diff --git a/dbus-service.c b/dbus-service.c index 70792248..6a3b544f 100644 --- a/dbus-service.c +++ b/dbus-service.c @@ -636,11 +636,14 @@ gboolean notify_volume_callback(ShairportSync *skeleton, debug(2, ">> setting volume to %7.4f.", iv); lock_player(); - config.airplay_volume = iv; - if (playing_conn != NULL) + if (playing_conn != NULL) { player_volume(iv, playing_conn); + playing_conn->own_airplay_volume = iv; + playing_conn->own_airplay_volume_set = 1; + } unlock_player(); - + config.airplay_volume = iv; + config.last_access_to_volume_info_time = get_absolute_time_in_ns(); } else { debug(1, ">> invalid volume: %f. Ignored.", iv); shairport_sync_set_volume(skeleton, config.airplay_volume); diff --git a/player.c b/player.c index 21107ecb..db7b8ae0 100644 --- a/player.c +++ b/player.c @@ -1702,6 +1702,41 @@ void statistics_item(const char *heading, const char *format, ...) { statistics_column++; } +double suggested_volume(rtsp_conn_info *conn) { + double response = config.airplay_volume; + if (conn) { + if (conn->own_airplay_volume_set != 0) { + response = conn->own_airplay_volume; + } else if (config.airplay_volume > config.high_threshold_airplay_volume) { + int64_t volume_validity_time = config.limit_to_high_volume_threshold_time_in_minutes; + // zero means never check the volume + if (volume_validity_time != 0) { + // If the volume is higher than the high volume threshold + // and enough time has gone past, suggest the default volume. + uint64_t time_now = get_absolute_time_in_ns(); + int64_t time_since_last_access_to_volume_info = + time_now - config.last_access_to_volume_info_time; + + volume_validity_time = volume_validity_time * 60; // to seconds + volume_validity_time = volume_validity_time * 1000000000; // to nanoseconds + + if ((config.airplay_volume > config.high_threshold_airplay_volume) && + ((config.last_access_to_volume_info_time == 0) || + (time_since_last_access_to_volume_info > volume_validity_time))) { + + debug(2, + "the current volume %.6f is higher than the high volume threshold %.6f, so the " + "default volume %.6f is suggested.", + config.airplay_volume, config.high_threshold_airplay_volume, + config.default_airplay_volume); + response = config.default_airplay_volume; + } + } + } + } + return response; +} + void player_thread_cleanup_handler(void *arg) { rtsp_conn_info *conn = (rtsp_conn_info *)arg; if (pthread_mutex_trylock(&playing_conn_lock) == 0) { @@ -2192,9 +2227,11 @@ void *player_thread_func(void *arg) { pthread_setcancelstate(oldState, NULL); - double initial_volume = config.airplay_volume; // default - // set the default volume to whatever it was before, as stored in the config airplay_volume - debug(2, "Set initial volume to %f.", initial_volume); + // if not already set, set the volume to the pending_airplay_volume, if any, or otherwise to the + // suggested volume. + + double initial_volume = suggested_volume(conn); + debug(2, "Set initial volume to %.6f.", initial_volume); player_volume(initial_volume, conn); // will contain a cancellation point if asked to wait debug(2, "Play begin"); @@ -2216,6 +2253,9 @@ void *player_thread_func(void *arg) { abuf_t *inframe = buffer_get_frame(conn); // this has cancellation point(s), but it's not // guaranteed that they'll always be executed uint64_t local_time_now = get_absolute_time_in_ns(); // types okay + config.last_access_to_volume_info_time = + local_time_now; // ensure volume info remains seen as valid + if (inframe) { inbuf = inframe->data; inbuflength = inframe->length; @@ -3462,6 +3502,7 @@ void player_volume_without_notification(double airplay_volume, rtsp_conn_info *c } // here, store the volume for possible use in the future config.airplay_volume = airplay_volume; + conn->own_airplay_volume = airplay_volume; debug_mutex_unlock(&conn->volume_control_mutex, 3); } diff --git a/player.h b/player.h index 14e352ff..a35b54e6 100644 --- a/player.h +++ b/player.h @@ -224,8 +224,8 @@ typedef struct { pthread_mutex_t ab_mutex, flush_mutex, volume_control_mutex; int fix_volume; - double initial_airplay_volume; - int initial_airplay_volume_set; + double own_airplay_volume; + int own_airplay_volume_set; uint32_t timestamp_epoch, last_timestamp, maximum_timestamp_interval; // timestamp_epoch of zero means not initialised, could start at 2 @@ -448,4 +448,6 @@ int64_t monotonic_timestamp(uint32_t timestamp, // assumes, without checking, that successive timestamps in a series always span an interval of less // than one minute. +double suggested_volume(rtsp_conn_info *conn); // volume suggested for the connection + #endif //_PLAYER_H diff --git a/rtsp.c b/rtsp.c index aa95e2d5..83374deb 100644 --- a/rtsp.c +++ b/rtsp.c @@ -3,7 +3,7 @@ * Copyright (c) James Laird 2013 * Modifications associated with audio synchronization, multithreading and - * metadata handling copyright (c) Mike Brady 2014-2022 + * metadata handling copyright (c) Mike Brady 2014-2023 * All rights reserved. * * Permission is hereby granted, free of charge, to any person @@ -739,10 +739,13 @@ void cancel_all_RTSP_threads(airplay_stream_c stream_category, int except_this_o debug_mutex_unlock(&conns_lock, 3); } +int old_connection_count = -1; + void cleanup_threads(void) { void *retval; int i; + int connection_count = 0; // debug(2, "culling threads."); debug_mutex_lock(&conns_lock, 1000000, 3); for (i = 0; i < nconns; i++) { @@ -754,8 +757,19 @@ void cleanup_threads(void) { free(conns[i]); conns[i] = NULL; } + if (conns[i] != NULL) + connection_count++; } debug_mutex_unlock(&conns_lock, 3); + if (old_connection_count != connection_count) { + if (connection_count == 0) + debug(2, "No active connections."); + else if (connection_count == 1) + debug(2, "One active connection."); + else + debug(2, "%d active connections.", connection_count); + old_connection_count = connection_count; + } } // park a null at the line ending, and return the next line pointer @@ -3572,9 +3586,15 @@ void handle_set_parameter_parameter(rtsp_conn_info *conn, rtsp_message *req, } else { #endif lock_player(); - if (playing_conn == conn) + if (playing_conn == conn) { player_volume(volume, conn); + } + if (conn != NULL) { + conn->own_airplay_volume = volume; + conn->own_airplay_volume_set = 1; + } unlock_player(); + config.last_access_to_volume_info_time = get_absolute_time_in_ns(); #ifdef CONFIG_DBUS_INTERFACE } #endif @@ -4327,12 +4347,13 @@ static void handle_get_parameter(__attribute__((unused)) rtsp_conn_info *conn, r if ((req->content) && (req->contentlength == strlen("volume\r\n")) && strstr(req->content, "volume") == req->content) { - debug(1, "Connection %d: Current volume (%.6f) requested", conn->connection_number, - config.airplay_volume); + debug(2, "Connection %d: current volume (%.6f) requested", conn->connection_number, + suggested_volume(conn)); + char *p = malloc(128); // will be automatically deallocated with the response is deleted if (p) { resp->content = p; - resp->contentlength = snprintf(p, 128, "\r\nvolume: %.6f\r\n", config.airplay_volume); + resp->contentlength = snprintf(p, 128, "\r\nvolume: %.6f\r\n", suggested_volume(conn)); } else { debug(1, "Couldn't allocate space for a response."); } diff --git a/shairport.c b/shairport.c index 4b2129cc..dc7753ef 100644 --- a/shairport.c +++ b/shairport.c @@ -465,9 +465,14 @@ int parse_options(int argc, char **argv) { #endif config.audio_backend_silent_lead_in_time_auto = - 1; // start outputting silence as soon as packets start arriving - config.airplay_volume = -24.0; // if no volume is ever set, default to initial default value if - // nothing else comes in first. + 1; // start outputting silence as soon as packets start arriving + config.default_airplay_volume = -24.0; + config.high_threshold_airplay_volume = + -20.0; // if the volume exceeds this, reset to this if idle for the + // limit_to_high_volume_threshold_time_in_minutes time + config.limit_to_high_volume_threshold_time_in_minutes = + 1; // after this time, if the volume is higher, use the high_threshold_airplay_volume volume + // for new play sessions. config.fixedLatencyOffset = 11025; // this sounds like it works properly. config.diagnostic_drop_packet_fraction = 0.0; config.active_state_timeout = 10.0; @@ -1440,6 +1445,10 @@ int parse_options(int argc, char **argv) { if (tdebuglev != 0) debuglev = tdebuglev; + // now set the initial volume to the default volume + config.airplay_volume = + config.default_airplay_volume; // if no volume is ever set or requested, default to initial + // default value if nothing else comes in first. // now, do the substitutions in the service name char hostname[100]; gethostname(hostname, 100); @@ -2373,8 +2382,9 @@ int main(int argc, char **argv) { debug(1, "mdns backend \"%s\".", strnull(config.mdns_name)); debug(2, "userSuppliedLatency is %d.", config.userSuppliedLatency); debug(1, "interpolation setting is \"%s\".", - config.packet_stuffing == ST_basic ? "basic" - : config.packet_stuffing == ST_soxr ? "soxr" : "auto"); + config.packet_stuffing == ST_basic ? "basic" + : config.packet_stuffing == ST_soxr ? "soxr" + : "auto"); debug(1, "interpolation soxr_delay_threshold is %d.", config.soxr_delay_threshold); debug(1, "resync time is %f seconds.", config.resync_threshold); debug(1, "resync recovery time is %f seconds.", config.resync_recovery_time);