]> git.ipfire.org Git - thirdparty/shairport-sync.git/commitdiff
Add code to try to save your ears by handling a situation where the player volume...
authorMike Brady <4265913+mikebrady@users.noreply.github.com>
Wed, 18 Jan 2023 10:41:50 +0000 (10:41 +0000)
committerMike Brady <4265913+mikebrady@users.noreply.github.com>
Wed, 18 Jan 2023 10:41:50 +0000 (10:41 +0000)
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.

common.c
common.h
dbus-service.c
player.c
player.h
rtsp.c
shairport.c

index 63ea1cd412c47b64bdb807df4eb7a458500bb507..d10a2fad3a7387aa8d3b3165113df2675a3f885c 100644 (file)
--- 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;
index 298bdbd0064bba206cf774543b9daf2184feba7f..e152db687e03aa8793627a67d03bc531c3ecf532 100644 (file)
--- 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
index 7079224815df72a05c67090232dadf16fa356b10..6a3b544f8b8b92b97c87dfa9972093f3a50af111 100644 (file)
@@ -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);
index 21107ecb106af47ee79d0f9b3219612f470d704f..db7b8ae00129d4caca0fd8fc7a2cf0cd6b190df5 100644 (file)
--- 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);
 }
 
index 14e352ff578fd1fcf946858d9dc8203124e68d60..a35b54e6104cbd87aa3c8f2b28ffe0c515b24d8d 100644 (file)
--- 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 aa95e2d517655490ff011690594ff0cc3821b028..83374debe4fd5417e99fcd68dfa24841419e52ff 100644 (file)
--- 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.");
     }
index 4b2129cc726bbceefa10b8401ceca029cd78294b..dc7753ef82c8ae72f7e889a227c797f10bd49fa8 100644 (file)
@@ -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);