Implemented volume control for jukebox
This commit is contained in:
@@ -238,6 +238,15 @@ namespace lms::audio::alsa
|
|||||||
return ::snd_pcm_state(_pcm.get()) == SND_PCM_STATE_PAUSED;
|
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
|
std::chrono::microseconds AudioOutputStream::getPlaybackTime() const
|
||||||
{
|
{
|
||||||
::snd_pcm_sframes_t delayFrames{};
|
::snd_pcm_sframes_t delayFrames{};
|
||||||
|
|||||||
@@ -52,15 +52,18 @@ namespace lms::audio::alsa
|
|||||||
void asyncWrite(std::span<const std::byte> buffer, WriteCompletionCallback cb) override;
|
void asyncWrite(std::span<const std::byte> buffer, WriteCompletionCallback cb) override;
|
||||||
void asyncDrain(DrainCompletionCallback 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 getPlaybackTime() const override;
|
||||||
std::chrono::microseconds getLatency() const override;
|
std::chrono::microseconds getLatency() const override;
|
||||||
|
|
||||||
void flush() 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 stop();
|
||||||
void setupAllDescriptors();
|
void setupAllDescriptors();
|
||||||
void releaseAllDescriptors();
|
void releaseAllDescriptors();
|
||||||
|
|||||||
@@ -19,10 +19,13 @@
|
|||||||
|
|
||||||
#include "AudioOutputStream.hpp"
|
#include "AudioOutputStream.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#include <boost/asio/post.hpp>
|
#include <boost/asio/post.hpp>
|
||||||
#include <pulse/context.h>
|
#include <pulse/context.h>
|
||||||
#include <pulse/def.h>
|
#include <pulse/def.h>
|
||||||
#include <pulse/error.h>
|
#include <pulse/error.h>
|
||||||
|
#include <pulse/introspect.h>
|
||||||
#include <pulse/operation.h>
|
#include <pulse/operation.h>
|
||||||
#include <pulse/proplist.h>
|
#include <pulse/proplist.h>
|
||||||
#include <pulse/stream.h>
|
#include <pulse/stream.h>
|
||||||
@@ -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
|
std::chrono::microseconds AudioOutputStream::getPlaybackTime() const
|
||||||
{
|
{
|
||||||
pa_usec_t duration{};
|
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()
|
void AudioOutputStream::connect()
|
||||||
{
|
{
|
||||||
constexpr pa_stream_flags_t flags{ static_cast<pa_stream_flags_t>(
|
constexpr pa_stream_flags_t flags{ static_cast<pa_stream_flags_t>(
|
||||||
|
|||||||
@@ -62,15 +62,18 @@ namespace lms::audio::pulseaudio
|
|||||||
void asyncWrite(std::span<const std::byte> buffer, WriteCompletionCallback cb) override;
|
void asyncWrite(std::span<const std::byte> buffer, WriteCompletionCallback cb) override;
|
||||||
void asyncDrain(DrainCompletionCallback 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 getPlaybackTime() const override;
|
||||||
std::chrono::microseconds getLatency() const override;
|
std::chrono::microseconds getLatency() const override;
|
||||||
|
|
||||||
void flush() 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 connect();
|
||||||
void onStateChanged();
|
void onStateChanged();
|
||||||
void onWriteRequested(std::size_t writableSize);
|
void onWriteRequested(std::size_t writableSize);
|
||||||
@@ -83,6 +86,7 @@ namespace lms::audio::pulseaudio
|
|||||||
pa_threaded_mainloop* _mainLoop;
|
pa_threaded_mainloop* _mainLoop;
|
||||||
const PcmParameters _outputParameters;
|
const PcmParameters _outputParameters;
|
||||||
PaStreamPtr _stream;
|
PaStreamPtr _stream;
|
||||||
|
float _currentVolume{ 1.F };
|
||||||
|
|
||||||
WaitReadyCallback _waitReadyCallback;
|
WaitReadyCallback _waitReadyCallback;
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ namespace lms::audio
|
|||||||
virtual void pause() = 0;
|
virtual void pause() = 0;
|
||||||
virtual void resume() = 0;
|
virtual void resume() = 0;
|
||||||
virtual bool isPaused() const = 0;
|
virtual bool isPaused() const = 0;
|
||||||
|
|
||||||
|
virtual void setVolume(float volume) = 0;
|
||||||
|
virtual float getVolume() const = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class IAudioOutputContext
|
class IAudioOutputContext
|
||||||
|
|||||||
@@ -118,6 +118,20 @@ namespace lms::jukebox
|
|||||||
return _outputStream->isPaused();
|
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<std::size_t> JukeboxService::getCurrentTrackIndex() const
|
std::optional<std::size_t> JukeboxService::getCurrentTrackIndex() const
|
||||||
{
|
{
|
||||||
std::shared_lock lock{ _mutex };
|
std::shared_lock lock{ _mutex };
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ namespace lms::jukebox
|
|||||||
void resume() override;
|
void resume() override;
|
||||||
bool isPaused() const override;
|
bool isPaused() const override;
|
||||||
|
|
||||||
|
void setVolume(float volume) override;
|
||||||
|
float getVolume() const override;
|
||||||
|
|
||||||
std::optional<std::size_t> getCurrentTrackIndex() const override;
|
std::optional<std::size_t> getCurrentTrackIndex() const override;
|
||||||
std::chrono::microseconds getPlaybackTrackTime() const override;
|
std::chrono::microseconds getPlaybackTrackTime() const override;
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ namespace lms::jukebox
|
|||||||
virtual void resume() = 0;
|
virtual void resume() = 0;
|
||||||
virtual bool isPaused() const = 0;
|
virtual bool isPaused() const = 0;
|
||||||
|
|
||||||
|
virtual void setVolume(float volume) = 0; // from 0 to 1
|
||||||
|
virtual float getVolume() const = 0;
|
||||||
|
|
||||||
virtual std::optional<std::size_t> getCurrentTrackIndex() const = 0; // may be unset if queue is cleared while playing
|
virtual std::optional<std::size_t> getCurrentTrackIndex() const = 0; // may be unset if queue is cleared while playing
|
||||||
virtual std::chrono::microseconds getPlaybackTrackTime() const = 0;
|
virtual std::chrono::microseconds getPlaybackTrackTime() const = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ namespace lms::api::subsonic
|
|||||||
statusNode.setAttribute("currentIndex", jukeboxService.getCurrentTrackIndex() ? *jukeboxService.getCurrentTrackIndex() : -1); // required
|
statusNode.setAttribute("currentIndex", jukeboxService.getCurrentTrackIndex() ? *jukeboxService.getCurrentTrackIndex() : -1); // required
|
||||||
statusNode.setAttribute("playing", !jukeboxService.isPaused()); // required
|
statusNode.setAttribute("playing", !jukeboxService.isPaused()); // required
|
||||||
statusNode.setAttribute("position", std::chrono::duration_cast<std::chrono::seconds>(jukeboxService.getPlaybackTrackTime()).count());
|
statusNode.setAttribute("position", std::chrono::duration_cast<std::chrono::seconds>(jukeboxService.getPlaybackTrackTime()).count());
|
||||||
statusNode.setAttribute("gain", 1.f);
|
statusNode.setAttribute("gain", jukeboxService.getVolume());
|
||||||
|
|
||||||
return statusNode;
|
return statusNode;
|
||||||
}
|
}
|
||||||
@@ -158,6 +158,19 @@ namespace lms::api::subsonic
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Response handleJukeboxSetGain(RequestContext& context, jukebox::IJukeboxService& jukeboxService)
|
||||||
|
{
|
||||||
|
const auto gain{ getMandatoryParameterAs<float>(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<Response(RequestContext& context, jukebox::IJukeboxService& jukeboxService)>;
|
using Actionhandler = std::function<Response(RequestContext& context, jukebox::IJukeboxService& jukeboxService)>;
|
||||||
static const std::unordered_map<std::string, Actionhandler> actionHandlers{
|
static const std::unordered_map<std::string, Actionhandler> actionHandlers{
|
||||||
{ "get", detail::handleJukeboxGet },
|
{ "get", detail::handleJukeboxGet },
|
||||||
@@ -170,7 +183,7 @@ namespace lms::api::subsonic
|
|||||||
{ "clear", detail::handleJukeboxClear },
|
{ "clear", detail::handleJukeboxClear },
|
||||||
{ "remove", detail::handleJukeboxRemove },
|
{ "remove", detail::handleJukeboxRemove },
|
||||||
{ "shuffle", detail::handleJukeboxShuffle },
|
{ "shuffle", detail::handleJukeboxShuffle },
|
||||||
{ "setGain", detail::handleJukeboxStatus }, // not implemented
|
{ "setGain", detail::handleJukeboxSetGain },
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace detail
|
} // namespace detail
|
||||||
|
|||||||
Reference in New Issue
Block a user