From 7a30bb98f6d767dcb426392ae01988fd64dd1e23 Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 18 Feb 2026 00:12:32 +0100 Subject: [PATCH] Implemented volume control for jukebox --- .../audio/impl/alsa/AudioOutputStream.cpp | 9 ++ .../audio/impl/alsa/AudioOutputStream.hpp | 11 +- .../impl/pulseaudio/AudioOutputStream.cpp | 116 +++++++++++------- .../impl/pulseaudio/AudioOutputStream.hpp | 12 +- src/libs/audio/include/audio/IAudioOutput.hpp | 3 + .../services/jukebox/impl/JukeboxService.cpp | 14 +++ .../services/jukebox/impl/JukeboxService.hpp | 3 + .../services/jukebox/IJukeboxService.hpp | 3 + src/libs/subsonic/impl/endpoints/Jukebox.cpp | 17 ++- 9 files changed, 137 insertions(+), 51 deletions(-) diff --git a/src/libs/audio/impl/alsa/AudioOutputStream.cpp b/src/libs/audio/impl/alsa/AudioOutputStream.cpp index 3f4fa9dd..db688eea 100644 --- a/src/libs/audio/impl/alsa/AudioOutputStream.cpp +++ b/src/libs/audio/impl/alsa/AudioOutputStream.cpp @@ -238,6 +238,15 @@ namespace lms::audio::alsa return ::snd_pcm_state(_pcm.get()) == SND_PCM_STATE_PAUSED; } + void AudioOutputStream::setVolume(float) + { + } + + float AudioOutputStream::getVolume() const + { + return 1.F; + } + std::chrono::microseconds AudioOutputStream::getPlaybackTime() const { ::snd_pcm_sframes_t delayFrames{}; diff --git a/src/libs/audio/impl/alsa/AudioOutputStream.hpp b/src/libs/audio/impl/alsa/AudioOutputStream.hpp index 7b9d8779..090e8a6a 100644 --- a/src/libs/audio/impl/alsa/AudioOutputStream.hpp +++ b/src/libs/audio/impl/alsa/AudioOutputStream.hpp @@ -52,15 +52,18 @@ namespace lms::audio::alsa void asyncWrite(std::span buffer, WriteCompletionCallback cb) override; void asyncDrain(DrainCompletionCallback cb) override; - void pause() override; - void resume() override; - bool isPaused() const override; - std::chrono::microseconds getPlaybackTime() const override; std::chrono::microseconds getLatency() const override; void flush() override; + void pause() override; + void resume() override; + bool isPaused() const override; + + void setVolume(float volume) override; + float getVolume() const override; + void stop(); void setupAllDescriptors(); void releaseAllDescriptors(); diff --git a/src/libs/audio/impl/pulseaudio/AudioOutputStream.cpp b/src/libs/audio/impl/pulseaudio/AudioOutputStream.cpp index 4a74bb2a..27832fbb 100644 --- a/src/libs/audio/impl/pulseaudio/AudioOutputStream.cpp +++ b/src/libs/audio/impl/pulseaudio/AudioOutputStream.cpp @@ -19,10 +19,13 @@ #include "AudioOutputStream.hpp" +#include + #include #include #include #include +#include #include #include #include @@ -195,47 +198,6 @@ namespace lms::audio::pulseaudio } } - void AudioOutputStream::pause() - { - LMS_LOG(AUDIO_OUTPUT_STREAM, DEBUG, "Pausing stream"); - - MainLoopScopedLock lock{ _mainLoop }; - - pa_operation* op{ ::pa_stream_cork(_stream.get(), 1, nullptr, nullptr) }; - if (!op) - throw PaException("pa_stream_cork (pause) failed", pa_context_errno(_context)); - - ::pa_operation_unref(op); - } - - void AudioOutputStream::resume() - { - LMS_LOG(AUDIO_OUTPUT_STREAM, DEBUG, "Resuming stream"); - - MainLoopScopedLock lock{ _mainLoop }; - - { - pa_operation* op{ ::pa_stream_cork(_stream.get(), 0, nullptr, nullptr) }; - if (!op) - throw PaException("pa_stream_cork (resume) failed", pa_context_errno(_context)); - ::pa_operation_unref(op); - } - - { - pa_operation* op{ ::pa_stream_trigger(_stream.get(), NULL, NULL) }; - if (!op) - throw PaException("pa_stream_trigger failed", pa_context_errno(_context)); - ::pa_operation_unref(op); - } - } - - bool AudioOutputStream::isPaused() const - { - MainLoopScopedLock lock{ _mainLoop }; - - return pa_stream_is_corked(_stream.get()); - } - std::chrono::microseconds AudioOutputStream::getPlaybackTime() const { pa_usec_t duration{}; @@ -279,6 +241,78 @@ namespace lms::audio::pulseaudio } } + void AudioOutputStream::pause() + { + LMS_LOG(AUDIO_OUTPUT_STREAM, DEBUG, "Pausing stream"); + + MainLoopScopedLock lock{ _mainLoop }; + + pa_operation* op{ ::pa_stream_cork(_stream.get(), 1, nullptr, nullptr) }; + if (!op) + throw PaException("pa_stream_cork (pause) failed", pa_context_errno(_context)); + + ::pa_operation_unref(op); + } + + void AudioOutputStream::resume() + { + LMS_LOG(AUDIO_OUTPUT_STREAM, DEBUG, "Resuming stream"); + + MainLoopScopedLock lock{ _mainLoop }; + + { + pa_operation* op{ ::pa_stream_cork(_stream.get(), 0, nullptr, nullptr) }; + if (!op) + throw PaException("pa_stream_cork (resume) failed", pa_context_errno(_context)); + ::pa_operation_unref(op); + } + + { + pa_operation* op{ ::pa_stream_trigger(_stream.get(), NULL, NULL) }; + if (!op) + throw PaException("pa_stream_trigger failed", pa_context_errno(_context)); + ::pa_operation_unref(op); + } + } + + bool AudioOutputStream::isPaused() const + { + MainLoopScopedLock lock{ _mainLoop }; + + return pa_stream_is_corked(_stream.get()); + } + + void AudioOutputStream::setVolume(float volume) + { + MainLoopScopedLock lock{ _mainLoop }; + + if (volume < 0.F || volume > 1.F) + throw Exception{ "Volume must be in range 0-1" }; + + const pa_volume_t paVol{ pa_sw_volume_from_linear(volume) }; + + pa_cvolume paChanVol; + pa_cvolume_set(&paChanVol, _outputParameters.channelCount, paVol); + + pa_operation* op{ ::pa_context_set_sink_input_volume( + _context, + pa_stream_get_index(_stream.get()), + &paChanVol, + nullptr, + nullptr) }; + if (!op) + throw PaException("pa_context_set_sink_input_volume failed", pa_context_errno(_context)); + + ::pa_operation_unref(op); + + _currentVolume = volume; + } + + float AudioOutputStream::getVolume() const + { + return _currentVolume; + } + void AudioOutputStream::connect() { constexpr pa_stream_flags_t flags{ static_cast( diff --git a/src/libs/audio/impl/pulseaudio/AudioOutputStream.hpp b/src/libs/audio/impl/pulseaudio/AudioOutputStream.hpp index 78107462..c04e8490 100644 --- a/src/libs/audio/impl/pulseaudio/AudioOutputStream.hpp +++ b/src/libs/audio/impl/pulseaudio/AudioOutputStream.hpp @@ -62,15 +62,18 @@ namespace lms::audio::pulseaudio void asyncWrite(std::span buffer, WriteCompletionCallback cb) override; void asyncDrain(DrainCompletionCallback cb) override; - void pause() override; - void resume() override; - bool isPaused() const override; - std::chrono::microseconds getPlaybackTime() const override; std::chrono::microseconds getLatency() const override; void flush() override; + void pause() override; + void resume() override; + bool isPaused() const override; + + void setVolume(float volume) override; + float getVolume() const override; + void connect(); void onStateChanged(); void onWriteRequested(std::size_t writableSize); @@ -83,6 +86,7 @@ namespace lms::audio::pulseaudio pa_threaded_mainloop* _mainLoop; const PcmParameters _outputParameters; PaStreamPtr _stream; + float _currentVolume{ 1.F }; WaitReadyCallback _waitReadyCallback; diff --git a/src/libs/audio/include/audio/IAudioOutput.hpp b/src/libs/audio/include/audio/IAudioOutput.hpp index a7fe62f3..cef4cccd 100644 --- a/src/libs/audio/include/audio/IAudioOutput.hpp +++ b/src/libs/audio/include/audio/IAudioOutput.hpp @@ -60,6 +60,9 @@ namespace lms::audio virtual void pause() = 0; virtual void resume() = 0; virtual bool isPaused() const = 0; + + virtual void setVolume(float volume) = 0; + virtual float getVolume() const = 0; }; class IAudioOutputContext diff --git a/src/libs/services/jukebox/impl/JukeboxService.cpp b/src/libs/services/jukebox/impl/JukeboxService.cpp index c5512983..ac053b44 100644 --- a/src/libs/services/jukebox/impl/JukeboxService.cpp +++ b/src/libs/services/jukebox/impl/JukeboxService.cpp @@ -118,6 +118,20 @@ namespace lms::jukebox return _outputStream->isPaused(); } + void JukeboxService::setVolume(float volume) + { + std::unique_lock lock{ _mutex }; + + _outputStream->setVolume(volume); + } + + float JukeboxService::getVolume() const + { + std::shared_lock lock{ _mutex }; + + return _outputStream->getVolume(); + } + std::optional JukeboxService::getCurrentTrackIndex() const { std::shared_lock lock{ _mutex }; diff --git a/src/libs/services/jukebox/impl/JukeboxService.hpp b/src/libs/services/jukebox/impl/JukeboxService.hpp index cd5deed9..ca7db66c 100644 --- a/src/libs/services/jukebox/impl/JukeboxService.hpp +++ b/src/libs/services/jukebox/impl/JukeboxService.hpp @@ -51,6 +51,9 @@ namespace lms::jukebox void resume() override; bool isPaused() const override; + void setVolume(float volume) override; + float getVolume() const override; + std::optional getCurrentTrackIndex() const override; std::chrono::microseconds getPlaybackTrackTime() const override; diff --git a/src/libs/services/jukebox/include/services/jukebox/IJukeboxService.hpp b/src/libs/services/jukebox/include/services/jukebox/IJukeboxService.hpp index 960e0e7b..a24badee 100644 --- a/src/libs/services/jukebox/include/services/jukebox/IJukeboxService.hpp +++ b/src/libs/services/jukebox/include/services/jukebox/IJukeboxService.hpp @@ -48,6 +48,9 @@ namespace lms::jukebox virtual void resume() = 0; virtual bool isPaused() const = 0; + virtual void setVolume(float volume) = 0; // from 0 to 1 + virtual float getVolume() const = 0; + virtual std::optional getCurrentTrackIndex() const = 0; // may be unset if queue is cleared while playing virtual std::chrono::microseconds getPlaybackTrackTime() const = 0; diff --git a/src/libs/subsonic/impl/endpoints/Jukebox.cpp b/src/libs/subsonic/impl/endpoints/Jukebox.cpp index 5e4ed96a..6f641273 100644 --- a/src/libs/subsonic/impl/endpoints/Jukebox.cpp +++ b/src/libs/subsonic/impl/endpoints/Jukebox.cpp @@ -44,7 +44,7 @@ namespace lms::api::subsonic statusNode.setAttribute("currentIndex", jukeboxService.getCurrentTrackIndex() ? *jukeboxService.getCurrentTrackIndex() : -1); // required statusNode.setAttribute("playing", !jukeboxService.isPaused()); // required statusNode.setAttribute("position", std::chrono::duration_cast(jukeboxService.getPlaybackTrackTime()).count()); - statusNode.setAttribute("gain", 1.f); + statusNode.setAttribute("gain", jukeboxService.getVolume()); return statusNode; } @@ -158,6 +158,19 @@ namespace lms::api::subsonic return response; } + Response handleJukeboxSetGain(RequestContext& context, jukebox::IJukeboxService& jukeboxService) + { + const auto gain{ getMandatoryParameterAs(context.getParameters(), "gain") }; + if (gain < 0 || gain > 1) + throw BadParameterGenericError{ "gain", "gain must be between 0.0 and 1.0" }; + + jukeboxService.setVolume(gain); // consider gain is linear + + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + response.addNode("jukeboxStatus", createJukeboxStatusNode(jukeboxService)); + return response; + } + using Actionhandler = std::function; static const std::unordered_map actionHandlers{ { "get", detail::handleJukeboxGet }, @@ -170,7 +183,7 @@ namespace lms::api::subsonic { "clear", detail::handleJukeboxClear }, { "remove", detail::handleJukeboxRemove }, { "shuffle", detail::handleJukeboxShuffle }, - { "setGain", detail::handleJukeboxStatus }, // not implemented + { "setGain", detail::handleJukeboxSetGain }, }; } // namespace detail