In that situation currenctly, when you play something, it will come out at the last volume set on the device, which will be that very high volume.
This could happen, for example, the morning after a loud party :).
The new code works as follows: If nothing has been played for a period, say a few hours, and the volume is higher than a threshold, new connections that ask
for the current volume setting will be offered the default (low) volume rather than the current (very high) one.
Most players, including the HomePod mini, iOS and iPadOS use this to set their initial volume.
The Mac Music app retains its own setting, and the Mac System Sounds preference sometimes seems to take its setting from the Music app,
but at least you can see the settings they will apply.
Existing connections are unaffected, so this facility should be unobtrusive.
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;
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"
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;
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
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);
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) {
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");
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;
}
// 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);
}
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
// 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
* 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
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++) {
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
} 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
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.");
}
#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;
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);
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);