/* * Copyright 2015 Canonical Ltd. * Copyright 2021 Robert Tari * * 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; version 3. * * 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 . * * Authors: * Charles Kerr * Robert Tari */ using PulseAudio; public abstract class VolumeWarning : Object { // true if headphones are in use public bool headphones_active { get; set; default = false; } // true if the warning dialog is being shown public bool active { get; protected set; default = false; } // true if we're playing unapproved loud multimedia over headphones public bool high_volume { get; protected set; default = false; } public enum Key { VOLUME_UP, VOLUME_DOWN } public void user_keypress (Key key) { if ((key == Key.VOLUME_DOWN) && active) { _notification.close (); on_user_response (IndicatorSound.WarnNotification.Response.CANCEL); } } protected VolumeWarning (IndicatorSound.Options options) { _options = options; init_high_volume (); init_approved (); _notification.user_responded.connect ((n, r) => on_user_response (r)); } ~VolumeWarning () { clear_timer (ref _approved_timer); } /*** **** ***/ // true if the user has approved high volumes recently protected bool approved { get; set; default = false; } // true if multimedia is currently playing protected bool multimedia_active { get; set; default = false; } /* Cached value of the multimedia volume reported by pulse. Setting this only updates the cache -- to change the volume, use sound_system_set_multimedia_volume. NB: This PulseAudio.Volume is typed as uint to unconfuse valac. */ protected uint multimedia_volume { get; set; default = PulseAudio.Volume.INVALID; } protected abstract void sound_system_set_multimedia_volume (PulseAudio.Volume volume); protected void clear_timer (ref uint timer) { if (timer != 0) { Source.remove (timer); timer = 0; } } private IndicatorSound.Options _options; /** *** HIGH VOLUME PROPERTY **/ private void init_high_volume () { const string self_keys[] = { "multimedia-volume", "multimedia-active", "headphones-active", "high-volume-approved" }; foreach (var key in self_keys) this.notify[key].connect (() => update_high_volume ()); const string options_keys[] = { "loud-volume", "loud-warning-enabled" }; foreach (var key in options_keys) _options.notify[key].connect (() => update_high_volume ()); update_high_volume (); } private void update_high_volume () { var newval = _options.loud_warning_enabled && headphones_active && multimedia_active && !approved && (multimedia_volume != PulseAudio.Volume.INVALID) // from the sound specs: // "Whenever you increase volume,..., such that acoustic output would be MORE than 85 dB && (multimedia_volume > _options.loud_volume); if (high_volume != newval) { debug ("changing high_volume from %d to %d", (int)high_volume, (int)newval); if (newval && !active) activate (); high_volume = newval; } } /** *** HIGH VOLUME APPROVED PROPERTY **/ private Settings _settings = new Settings ("org.ayatana.indicator.sound"); private const string TTL_KEY = "warning-volume-confirmation-ttl"; private uint _approved_timer = 0; private int64 _approved_at = 0; private int64 _approved_ttl_usec = 0; private void approve_high_volume () { _approved_at = GLib.get_monotonic_time (); update_approved (); update_approved_timer (); } private void init_approved () { _settings.changed[TTL_KEY].connect (() => update_approved_cache ()); update_approved_cache (); } private void update_approved_cache () { _approved_ttl_usec = _settings.get_int (TTL_KEY); _approved_ttl_usec *= 1000000; update_approved (); update_approved_timer (); } private void update_approved_timer () { clear_timer (ref _approved_timer); if (_approved_at == 0) return; int64 expires_at = _approved_at + _approved_ttl_usec; int64 now = GLib.get_monotonic_time (); if (expires_at > now) { var seconds_left = 1 + ((expires_at - now) / 1000000); _approved_timer = Timeout.add_seconds ((uint)seconds_left, () => { _approved_timer = 0; update_approved (); return Source.REMOVE; }); } } private void update_approved () { var new_approved = calculate_approved (); if (approved != new_approved) { debug ("changing approved from %d to %d", (int)approved, (int)new_approved); approved = new_approved; } } private bool calculate_approved () { int64 now = GLib.get_monotonic_time (); return (_approved_at != 0) && (_approved_at + _approved_ttl_usec >= now); } // NOTIFICATION private IndicatorSound.WarnNotification _notification = new IndicatorSound.WarnNotification (); private PulseAudio.Volume _ok_volume = PulseAudio.Volume.INVALID; protected virtual void preshow () {} private void activate () { preshow (); _ok_volume = multimedia_volume; _notification.show (); this.active = true; // lower the volume to just the warning level // from the sound specs: // "Whenever you increase volume,..., such that acoustic output would be MORE than 85 dB sound_system_set_multimedia_volume (_options.loud_volume); } private void on_user_response (IndicatorSound.WarnNotification.Response response) { this.active = false; if (response == IndicatorSound.WarnNotification.Response.OK) { approve_high_volume (); sound_system_set_multimedia_volume (_ok_volume); } else { this.cancel_pressed (VolumeWarning.volume_to_double(_options.loud_volume)); } _ok_volume = PulseAudio.Volume.INVALID; } private static double volume_to_double (PulseAudio.Volume vol) { double tmp = (double)(vol - PulseAudio.Volume.MUTED); return tmp / (double)(PulseAudio.Volume.NORM - PulseAudio.Volume.MUTED); } public signal void cancel_pressed (double cancel_volume); }