diff --git a/Userland/Applets/Audio/main.cpp b/Userland/Applets/Audio/main.cpp index be6247078f..0e1e736942 100644 --- a/Userland/Applets/Audio/main.cpp +++ b/Userland/Applets/Audio/main.cpp @@ -38,8 +38,8 @@ public: update(); }; - m_audio_client->on_main_mix_volume_change = [this](int volume) { - m_audio_volume = volume; + m_audio_client->on_main_mix_volume_change = [this](double volume) { + m_audio_volume = static_cast(volume * 100); if (!m_audio_muted) update(); }; @@ -87,14 +87,14 @@ public: }; m_slider = m_root_container->add(); - m_slider->set_max(20); - int non_log_volume = sqrt(100 * m_audio_volume); - m_slider->set_value(-(non_log_volume / 5.0f) + 20); + m_slider->set_max(100); + m_slider->set_page_step(5); + m_slider->set_step(5); + m_slider->set_value(m_slider->max() - m_audio_volume); m_slider->set_knob_size_mode(GUI::Slider::KnobSizeMode::Proportional); m_slider->on_change = [&](int value) { - int volume = clamp((20 - value) * 5, 0, 100); - double volume_log = ((volume / 100.0) * (volume / 100.0)) * 100.0; - m_audio_client->set_main_mix_volume(static_cast(volume_log)); + double volume = clamp(static_cast(m_slider->max() - value) / m_slider->max(), 0.0, 1.0); + m_audio_client->set_main_mix_volume(volume); update(); }; @@ -131,8 +131,7 @@ private: { if (m_audio_muted) return; - int new_slider_value = m_slider->value() + event.wheel_delta() / 4; - m_slider->set_value(new_slider_value); + m_slider->dispatch_event(event); update(); } diff --git a/Userland/Applications/Piano/Music.h b/Userland/Applications/Piano/Music.h index a1080155e9..ce2c814735 100644 --- a/Userland/Applications/Piano/Music.h +++ b/Userland/Applications/Piano/Music.h @@ -30,7 +30,8 @@ constexpr int buffer_size = sample_count * sizeof(Sample); constexpr double sample_rate = 44100; -constexpr double volume_factor = 1800; +// Headroom for the synth +constexpr double volume_factor = 0.1; enum Switch { Off, diff --git a/Userland/Applications/Piano/Track.cpp b/Userland/Applications/Piano/Track.cpp index f077efc994..5727a945f7 100644 --- a/Userland/Applications/Piano/Track.cpp +++ b/Userland/Applications/Piano/Track.cpp @@ -95,8 +95,8 @@ void Track::fill_sample(Sample& sample) default: VERIFY_NOT_REACHED(); } - new_sample.left += note_sample.left * m_power[note] * volume_factor * (static_cast(volume()) / volume_max); - new_sample.right += note_sample.right * m_power[note] * volume_factor * (static_cast(volume()) / volume_max); + new_sample.left += note_sample.left * m_power[note] * NumericLimits::max() * volume_factor * (static_cast(volume()) / volume_max); + new_sample.right += note_sample.right * m_power[note] * NumericLimits::max() * volume_factor * (static_cast(volume()) / volume_max); } auto new_sample_dsp = LibDSP::Signal(LibDSP::Sample { new_sample.left / NumericLimits::max(), new_sample.right / NumericLimits::max() }); @@ -105,6 +105,9 @@ void Track::fill_sample(Sample& sample) new_sample.left = delayed_sample.left * NumericLimits::max(); new_sample.right = delayed_sample.right * NumericLimits::max(); + new_sample.left = clamp(new_sample.left, NumericLimits::min(), NumericLimits::max()); + new_sample.right = clamp(new_sample.right, NumericLimits::min(), NumericLimits::max()); + sample.left += new_sample.left; sample.right += new_sample.right; } diff --git a/Userland/Applications/SoundPlayer/Player.h b/Userland/Applications/SoundPlayer/Player.h index bce057c60d..23b4fdfebe 100644 --- a/Userland/Applications/SoundPlayer/Player.h +++ b/Userland/Applications/SoundPlayer/Player.h @@ -47,7 +47,7 @@ public: virtual void set_volume(double volume) { m_player_state.volume = volume; - client_connection().set_main_mix_volume((double)(volume * 100)); + client_connection().set_self_volume(volume); } virtual void set_loaded_file_samplerate(int samplerate) { m_player_state.loaded_file_samplerate = samplerate; } virtual void set_looping_file(bool loop) diff --git a/Userland/Libraries/LibAudio/Buffer.h b/Userland/Libraries/LibAudio/Buffer.h index ebf40af0bd..1a518784e7 100644 --- a/Userland/Libraries/LibAudio/Buffer.h +++ b/Userland/Libraries/LibAudio/Buffer.h @@ -8,6 +8,7 @@ #pragma once #include +#include #include #include #include @@ -16,25 +17,32 @@ #include namespace Audio { +using namespace AK::Exponentials; + +// Constants for logarithmic volume. See Frame::operator* +// Corresponds to 60dB +constexpr double DYNAMIC_RANGE = 1000; +constexpr double VOLUME_A = 1 / DYNAMIC_RANGE; +double const VOLUME_B = log(DYNAMIC_RANGE); // A single sample in an audio buffer. // Values are floating point, and should range from -1.0 to +1.0 struct Frame { - Frame() + constexpr Frame() : left(0) , right(0) { } // For mono - Frame(double left) + constexpr Frame(double left) : left(left) , right(left) { } // For stereo - Frame(double left, double right) + constexpr Frame(double left, double right) : left(left) , right(right) { @@ -53,26 +61,54 @@ struct Frame { right = -1; } - void scale(int percent) + // Logarithmic scaling, as audio should ALWAYS do. + // Reference: https://www.dr-lex.be/info-stuff/volumecontrols.html + // We use the curve `factor = a * exp(b * change)`, + // where change is the input fraction we want to change by, + // a = 1/1000, b = ln(1000) = 6.908 and factor is the multiplier used. + // The value 1000 represents the dynamic range in sound pressure, which corresponds to 60 dB(A). + // This is a good dynamic range because it can represent all loudness values from + // 30 dB(A) (barely hearable with background noise) + // to 90 dB(A) (almost too loud to hear and about the reasonable limit of actual sound equipment). + ALWAYS_INLINE Frame& log_multiply(double const change) { - double pct = (double)percent / 100.0; - left *= pct; - right *= pct; + double factor = VOLUME_A * exp(VOLUME_B * change); + left *= factor; + right *= factor; + return *this; } - // FIXME: This is temporary until we have log scaling - Frame scaled(double fraction) const + ALWAYS_INLINE Frame log_multiplied(double const volume_change) { - return Frame { left * fraction, right * fraction }; + Frame new_frame { left, right }; + new_frame.log_multiply(volume_change); + return new_frame; } - Frame& operator+=(const Frame& other) + constexpr Frame& operator*=(double const mult) + { + left *= mult; + right *= mult; + return *this; + } + + constexpr Frame operator*(double const mult) + { + return { left * mult, right * mult }; + } + + constexpr Frame& operator+=(Frame const& other) { left += other.left; right += other.right; return *this; } + constexpr Frame operator+(Frame const& other) + { + return { left + other.left, right + other.right }; + } + double left; double right; }; diff --git a/Userland/Libraries/LibAudio/ClientConnection.cpp b/Userland/Libraries/LibAudio/ClientConnection.cpp index 81e69e14df..949683e42a 100644 --- a/Userland/Libraries/LibAudio/ClientConnection.cpp +++ b/Userland/Libraries/LibAudio/ClientConnection.cpp @@ -48,7 +48,7 @@ void ClientConnection::muted_state_changed(bool muted) on_muted_state_change(muted); } -void ClientConnection::main_mix_volume_changed(i32 volume) +void ClientConnection::main_mix_volume_changed(double volume) { if (on_main_mix_volume_change) on_main_mix_volume_change(volume); diff --git a/Userland/Libraries/LibAudio/ClientConnection.h b/Userland/Libraries/LibAudio/ClientConnection.h index b69696642b..226b65215f 100644 --- a/Userland/Libraries/LibAudio/ClientConnection.h +++ b/Userland/Libraries/LibAudio/ClientConnection.h @@ -27,12 +27,12 @@ public: Function on_finish_playing_buffer; Function on_muted_state_change; - Function on_main_mix_volume_change; + Function on_main_mix_volume_change; private: virtual void finished_playing_buffer(i32) override; virtual void muted_state_changed(bool) override; - virtual void main_mix_volume_changed(i32) override; + virtual void main_mix_volume_changed(double) override; }; } diff --git a/Userland/Services/AudioServer/AudioClient.ipc b/Userland/Services/AudioServer/AudioClient.ipc index 56b4f89d1e..ee8922f944 100644 --- a/Userland/Services/AudioServer/AudioClient.ipc +++ b/Userland/Services/AudioServer/AudioClient.ipc @@ -4,5 +4,5 @@ endpoint AudioClient { finished_playing_buffer(i32 buffer_id) =| muted_state_changed(bool muted) =| - main_mix_volume_changed(i32 volume) =| + main_mix_volume_changed(double volume) =| } diff --git a/Userland/Services/AudioServer/AudioServer.ipc b/Userland/Services/AudioServer/AudioServer.ipc index d48a50c4cf..9b4973130b 100644 --- a/Userland/Services/AudioServer/AudioServer.ipc +++ b/Userland/Services/AudioServer/AudioServer.ipc @@ -5,8 +5,8 @@ endpoint AudioServer // Mixer functions set_muted(bool muted) => () get_muted() => (bool muted) - get_main_mix_volume() => (i32 volume) - set_main_mix_volume(i32 volume) => () + get_main_mix_volume() => (double volume) + set_main_mix_volume(double volume) => () // Audio device set_sample_rate(u16 sample_rate) => () diff --git a/Userland/Services/AudioServer/ClientConnection.cpp b/Userland/Services/AudioServer/ClientConnection.cpp index dc151b8ce5..a267b7126b 100644 --- a/Userland/Services/AudioServer/ClientConnection.cpp +++ b/Userland/Services/AudioServer/ClientConnection.cpp @@ -48,7 +48,7 @@ void ClientConnection::did_change_muted_state(Badge, bool muted) async_muted_state_changed(muted); } -void ClientConnection::did_change_main_mix_volume(Badge, int volume) +void ClientConnection::did_change_main_mix_volume(Badge, double volume) { async_main_mix_volume_changed(volume); } @@ -58,7 +58,7 @@ Messages::AudioServer::GetMainMixVolumeResponse ClientConnection::get_main_mix_v return m_mixer.main_volume(); } -void ClientConnection::set_main_mix_volume(i32 volume) +void ClientConnection::set_main_mix_volume(double volume) { m_mixer.set_main_volume(volume); } diff --git a/Userland/Services/AudioServer/ClientConnection.h b/Userland/Services/AudioServer/ClientConnection.h index 96b4d3fdbf..c5276e1097 100644 --- a/Userland/Services/AudioServer/ClientConnection.h +++ b/Userland/Services/AudioServer/ClientConnection.h @@ -28,7 +28,7 @@ public: void did_finish_playing_buffer(Badge, int buffer_id); void did_change_muted_state(Badge, bool muted); - void did_change_main_mix_volume(Badge, int volume); + void did_change_main_mix_volume(Badge, double volume); virtual void die() override; @@ -36,7 +36,7 @@ public: private: virtual Messages::AudioServer::GetMainMixVolumeResponse get_main_mix_volume() override; - virtual void set_main_mix_volume(i32) override; + virtual void set_main_mix_volume(double) override; virtual Messages::AudioServer::EnqueueBufferResponse enqueue_buffer(Core::AnonymousBuffer const&, i32, int) override; virtual Messages::AudioServer::GetRemainingSamplesResponse get_remaining_samples() override; virtual Messages::AudioServer::GetPlayedSamplesResponse get_played_samples() override; diff --git a/Userland/Services/AudioServer/FadingProperty.h b/Userland/Services/AudioServer/FadingProperty.h new file mode 100644 index 0000000000..5df78fe79c --- /dev/null +++ b/Userland/Services/AudioServer/FadingProperty.h @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2021, kleines Filmröllchen . + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include "Mixer.h" +#include + +namespace AudioServer { + +// This is in buffer counts. +// As each buffer is approx 1/40 of a second, this means about 1/4 of a second of fade time. +constexpr int DEFAULT_FADE_TIME = 10; + +// A property of an audio system that needs to fade briefly whenever changed. +template +class FadingProperty { +public: + FadingProperty(T const value) + : FadingProperty(value, DEFAULT_FADE_TIME) + { + } + FadingProperty(T const value, int const fade_time) + : m_old_value(value) + , m_new_value(move(value)) + , m_fade_time(fade_time) + { + } + virtual ~FadingProperty() + { + m_old_value.~T(); + m_new_value.~T(); + } + + FadingProperty& operator=(T const& new_value) + { + // The origin of the fade is wherever we're right now. + m_old_value = static_cast(*this); + m_new_value = new_value; + m_current_fade = 0; + return *this; + } + FadingProperty& operator=(FadingProperty const&) = delete; + + operator T() const + { + if (!is_fading()) + return m_new_value; + return m_old_value * (1 - m_current_fade) + m_new_value * (m_current_fade); + } + + auto operator<=>(FadingProperty const& other) const + { + return static_cast(this) <=> static_cast(other); + } + + auto operator<=>(T const& other) const + { + return static_cast(*this) <=> other; + } + + void advance_time() + { + m_current_fade += 1.0 / static_cast(m_fade_time); + m_current_fade = clamp(m_current_fade, 0.0, 1.0); + } + + bool is_fading() const + { + return m_current_fade < 1; + } + + T target() const { return m_new_value; } + +private: + T m_old_value {}; + T m_new_value {}; + double m_current_fade { 0 }; + int const m_fade_time; +}; + +} diff --git a/Userland/Services/AudioServer/Mixer.cpp b/Userland/Services/AudioServer/Mixer.cpp index 8911018748..386b79857a 100644 --- a/Userland/Services/AudioServer/Mixer.cpp +++ b/Userland/Services/AudioServer/Mixer.cpp @@ -40,7 +40,7 @@ Mixer::Mixer(NonnullRefPtr config) pthread_cond_init(&m_pending_cond, nullptr); m_muted = m_config->read_bool_entry("Master", "Mute", false); - m_main_volume = m_config->read_num_entry("Master", "Volume", 100); + m_main_volume = static_cast(m_config->read_num_entry("Master", "Volume", 100)) / 100.0; m_sound_thread->start(); } @@ -78,18 +78,23 @@ void Mixer::mix() Audio::Frame mixed_buffer[1024]; auto mixed_buffer_length = (int)(sizeof(mixed_buffer) / sizeof(Audio::Frame)); + m_main_volume.advance_time(); + + int active_queues = 0; // Mix the buffers together into the output for (auto& queue : active_mix_queues) { if (!queue->client()) { queue->clear(); continue; } + ++active_queues; for (int i = 0; i < mixed_buffer_length; ++i) { auto& mixed_sample = mixed_buffer[i]; Audio::Frame sample; if (!queue->get_next_sample(sample)) break; + sample.log_multiply(SAMPLE_HEADROOM); mixed_sample += sample; } } @@ -103,7 +108,11 @@ void Mixer::mix() for (int i = 0; i < mixed_buffer_length; ++i) { auto& mixed_sample = mixed_buffer[i]; - mixed_sample.scale(m_main_volume); + // Even though it's not realistic, the user expects no sound at 0%. + if (m_main_volume < 0.01) + mixed_sample = { 0 }; + else + mixed_sample.log_multiply(m_main_volume); mixed_sample.clip(); LittleEndian out_sample; @@ -121,20 +130,20 @@ void Mixer::mix() } } -void Mixer::set_main_volume(int volume) +void Mixer::set_main_volume(double volume) { if (volume < 0) m_main_volume = 0; - else if (volume > 200) - m_main_volume = 200; + else if (volume > 2) + m_main_volume = 2; else m_main_volume = volume; - m_config->write_num_entry("Master", "Volume", volume); + m_config->write_num_entry("Master", "Volume", static_cast(volume * 100)); request_setting_sync(); ClientConnection::for_each([&](ClientConnection& client) { - client.did_change_main_mix_volume({}, m_main_volume); + client.did_change_main_mix_volume({}, main_volume()); }); } diff --git a/Userland/Services/AudioServer/Mixer.h b/Userland/Services/AudioServer/Mixer.h index 33549fca3c..1b4c09a516 100644 --- a/Userland/Services/AudioServer/Mixer.h +++ b/Userland/Services/AudioServer/Mixer.h @@ -8,6 +8,7 @@ #pragma once #include "ClientConnection.h" +#include "FadingProperty.h" #include #include #include @@ -23,6 +24,10 @@ namespace AudioServer { +// Headroom, i.e. fixed attenuation for all audio streams. +// This is to prevent clipping when two streams with low headroom (e.g. normalized & compressed) are playing. +constexpr double SAMPLE_HEADROOM = 0.7; + class ClientConnection; class BufferQueue : public RefCounted { @@ -82,6 +87,10 @@ public: return -1; } + FadingProperty& volume() { return m_volume; } + double volume() const { return m_volume; } + void set_volume(double const volume) { m_volume = volume; } + private: RefPtr m_current; Queue> m_queue; @@ -89,7 +98,9 @@ private: int m_remaining_samples { 0 }; int m_played_samples { 0 }; bool m_paused { false }; + WeakPtr m_client; + FadingProperty m_volume { 1 }; }; class Mixer : public Core::Object { @@ -100,8 +111,9 @@ public: NonnullRefPtr create_queue(ClientConnection&); - int main_volume() const { return m_main_volume; } - void set_main_volume(int volume); + // To the outside world, we pretend that the target volume is already reached, even though it may be still fading. + double main_volume() const { return m_main_volume.target(); } + void set_main_volume(double volume); bool is_muted() const { return m_muted; } void set_muted(bool); @@ -122,7 +134,7 @@ private: NonnullRefPtr m_sound_thread; bool m_muted { false }; - int m_main_volume { 100 }; + FadingProperty m_main_volume { 1 }; NonnullRefPtr m_config; RefPtr m_config_write_timer;