Added optional ALSA audio output
This commit is contained in:
@@ -1,6 +1,13 @@
|
|||||||
pkg_check_modules(LIBAV IMPORTED_TARGET libavcodec libavutil libavformat libswresample)
|
pkg_check_modules(LIBAV IMPORTED_TARGET libavcodec libavutil libavformat libswresample)
|
||||||
pkg_check_modules(Taglib REQUIRED IMPORTED_TARGET taglib)
|
pkg_check_modules(Taglib REQUIRED IMPORTED_TARGET taglib)
|
||||||
pkg_check_modules(PulseAudio REQUIRED IMPORTED_TARGET libpulse)
|
pkg_check_modules(PulseAudio IMPORTED_TARGET libpulse)
|
||||||
|
pkg_check_modules(ALSA IMPORTED_TARGET alsa)
|
||||||
|
|
||||||
|
if (PulseAudio_FOUND OR ALSA_FOUND)
|
||||||
|
message(STATUS "Audio output available (PulseAudio=${PulseAudio_FOUND}, ALSA=${ALSA_FOUND})")
|
||||||
|
else()
|
||||||
|
message(STATUS "No audio output backend found")
|
||||||
|
endif()
|
||||||
|
|
||||||
add_library(lmsaudio STATIC
|
add_library(lmsaudio STATIC
|
||||||
impl/ffmpeg/AudioFile.cpp
|
impl/ffmpeg/AudioFile.cpp
|
||||||
@@ -12,16 +19,13 @@ add_library(lmsaudio STATIC
|
|||||||
impl/ffmpeg/TagReader.cpp
|
impl/ffmpeg/TagReader.cpp
|
||||||
impl/ffmpeg/Transcoder.cpp
|
impl/ffmpeg/Transcoder.cpp
|
||||||
impl/ffmpeg/Utils.cpp
|
impl/ffmpeg/Utils.cpp
|
||||||
impl/pulseaudio/AudioOutput.cpp
|
|
||||||
impl/pulseaudio/AudioOutputStream.cpp
|
|
||||||
impl/pulseaudio/Exception.cpp
|
|
||||||
impl/pulseaudio/MainLoopScopedLock.cpp
|
|
||||||
impl/taglib/AudioFileInfo.cpp
|
impl/taglib/AudioFileInfo.cpp
|
||||||
impl/taglib/AudioFileInfoParser.cpp
|
impl/taglib/AudioFileInfoParser.cpp
|
||||||
impl/taglib/ImageReader.cpp
|
impl/taglib/ImageReader.cpp
|
||||||
impl/taglib/TagReader.cpp
|
impl/taglib/TagReader.cpp
|
||||||
impl/taglib/Utils.cpp
|
impl/taglib/Utils.cpp
|
||||||
impl/AudioFileInfoParser.cpp
|
impl/AudioFileInfoParser.cpp
|
||||||
|
impl/AudioOutput.cpp
|
||||||
impl/PcmTypes.cpp
|
impl/PcmTypes.cpp
|
||||||
impl/TagReader.cpp
|
impl/TagReader.cpp
|
||||||
)
|
)
|
||||||
@@ -45,5 +49,34 @@ target_link_libraries(lmsaudio PUBLIC
|
|||||||
target_link_libraries(lmsaudio PRIVATE
|
target_link_libraries(lmsaudio PRIVATE
|
||||||
PkgConfig::LIBAV
|
PkgConfig::LIBAV
|
||||||
PkgConfig::Taglib
|
PkgConfig::Taglib
|
||||||
PkgConfig::PulseAudio
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
target_compile_definitions(lmsaudio PRIVATE
|
||||||
|
$<$<BOOL:${PulseAudio_FOUND}>:LMS_HAVE_PULSEAUDIO>
|
||||||
|
$<$<BOOL:${ALSA_FOUND}>:LMS_HAVE_ALSA>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (PulseAudio_FOUND)
|
||||||
|
target_sources(lmsaudio PRIVATE
|
||||||
|
impl/pulseaudio/AudioOutput.cpp
|
||||||
|
impl/pulseaudio/AudioOutputStream.cpp
|
||||||
|
impl/pulseaudio/Exception.cpp
|
||||||
|
impl/pulseaudio/MainLoopScopedLock.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(lmsaudio PRIVATE
|
||||||
|
PkgConfig::PulseAudio
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (ALSA_FOUND)
|
||||||
|
target_sources(lmsaudio PRIVATE
|
||||||
|
impl/alsa/AudioOutput.cpp
|
||||||
|
impl/alsa/AudioOutputStream.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(lmsaudio PRIVATE
|
||||||
|
PkgConfig::ALSA
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS 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, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "audio/IAudioOutput.hpp"
|
||||||
|
#if LMS_HAVE_PULSEAUDIO
|
||||||
|
#include "pulseaudio/AudioOutput.hpp"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if LMS_HAVE_ALSA
|
||||||
|
#include "alsa/AudioOutput.hpp"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace lms::audio
|
||||||
|
{
|
||||||
|
consteval core::EnumSet<AudioOutputBackend> buildAudioOutputBackends()
|
||||||
|
{
|
||||||
|
core::EnumSet<AudioOutputBackend> res;
|
||||||
|
#if LMS_HAVE_ALSA
|
||||||
|
res.insert(AudioOutputBackend::ALSA);
|
||||||
|
#endif
|
||||||
|
#if LMS_HAVE_PULSEAUDIO
|
||||||
|
res.insert(AudioOutputBackend::PulseAudio);
|
||||||
|
#endif
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
core::EnumSet<AudioOutputBackend> getAudioOutputBackends()
|
||||||
|
{
|
||||||
|
return buildAudioOutputBackends();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<IAudioOutputContext> createAudioOutputContext(boost::asio::io_context& ioContext, std::string_view name, AudioOutputBackend backend)
|
||||||
|
{
|
||||||
|
std::unique_ptr<IAudioOutputContext> context;
|
||||||
|
|
||||||
|
switch (backend)
|
||||||
|
{
|
||||||
|
case AudioOutputBackend::ALSA:
|
||||||
|
#if LMS_HAVE_ALSA
|
||||||
|
context = std::make_unique<alsa::AudioOutputContext>(ioContext, name);
|
||||||
|
#endif
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AudioOutputBackend::PulseAudio:
|
||||||
|
#if LMS_HAVE_PULSEAUDIO
|
||||||
|
context = std::make_unique<pulseaudio::AudioOutputContext>(ioContext, name);
|
||||||
|
#endif
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
} // namespace lms::audio
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS 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, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "AudioOutput.hpp"
|
||||||
|
|
||||||
|
#include <boost/asio/post.hpp>
|
||||||
|
|
||||||
|
#include "AudioOutputStream.hpp"
|
||||||
|
|
||||||
|
namespace lms::audio::alsa
|
||||||
|
{
|
||||||
|
AudioOutputContext::AudioOutputContext(boost::asio::io_context& ioContext, std::string_view name)
|
||||||
|
: _ioContext{ ioContext }
|
||||||
|
, _name{ name }
|
||||||
|
, _device{ "default" }
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioOutputContext::~AudioOutputContext() = default;
|
||||||
|
|
||||||
|
void AudioOutputContext::asyncWaitReady(WaitReadyCallback cb)
|
||||||
|
{
|
||||||
|
// ALSA device is to be open when creating the stream, nothing to wait for here
|
||||||
|
boost::asio::post(_ioContext, std::move(cb));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<IAudioOutputStream> AudioOutputContext::createOutputStream(std::string_view name, const PcmParameters& outputParameters)
|
||||||
|
{
|
||||||
|
return std::make_unique<AudioOutputStream>(_ioContext, _device, name, outputParameters);
|
||||||
|
}
|
||||||
|
} // namespace lms::audio::alsa
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS 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, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "audio/IAudioOutput.hpp"
|
||||||
|
|
||||||
|
namespace lms::audio::alsa
|
||||||
|
{
|
||||||
|
class AudioOutputContext : public IAudioOutputContext
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioOutputContext(boost::asio::io_context& ioContext, std::string_view name);
|
||||||
|
~AudioOutputContext() override;
|
||||||
|
|
||||||
|
AudioOutputContext(const AudioOutputContext&) = delete;
|
||||||
|
AudioOutputContext& operator=(const AudioOutputContext&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void asyncWaitReady(WaitReadyCallback cb) override;
|
||||||
|
std::unique_ptr<IAudioOutputStream> createOutputStream(std::string_view name, const PcmParameters& outputParameters) override;
|
||||||
|
|
||||||
|
boost::asio::io_context& _ioContext;
|
||||||
|
const std::string _name;
|
||||||
|
const std::string _device;
|
||||||
|
};
|
||||||
|
} // namespace lms::audio::alsa
|
||||||
@@ -0,0 +1,393 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS 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, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "AudioOutputStream.hpp"
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <chrono>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include <alsa/asoundlib.h>
|
||||||
|
#include <boost/asio/bind_executor.hpp>
|
||||||
|
#include <boost/asio/error.hpp>
|
||||||
|
#include <boost/asio/post.hpp>
|
||||||
|
|
||||||
|
#include "core/ILogger.hpp"
|
||||||
|
|
||||||
|
#include "audio/Exception.hpp"
|
||||||
|
|
||||||
|
namespace lms::audio::alsa
|
||||||
|
{
|
||||||
|
namespace detail
|
||||||
|
{
|
||||||
|
::snd_pcm_format_t toSndPcmFormat(PcmSampleType sampleType, std::endian byteOrder)
|
||||||
|
{
|
||||||
|
switch (sampleType)
|
||||||
|
{
|
||||||
|
case PcmSampleType::Signed16:
|
||||||
|
return byteOrder == std::endian::little ? SND_PCM_FORMAT_S16_LE : SND_PCM_FORMAT_S16_BE;
|
||||||
|
case PcmSampleType::Signed32:
|
||||||
|
return byteOrder == std::endian::little ? SND_PCM_FORMAT_S32_LE : SND_PCM_FORMAT_S32_BE;
|
||||||
|
case PcmSampleType::Float32:
|
||||||
|
return byteOrder == std::endian::little ? SND_PCM_FORMAT_FLOAT_LE : SND_PCM_FORMAT_FLOAT_BE;
|
||||||
|
case PcmSampleType::Float64:
|
||||||
|
return byteOrder == std::endian::little ? SND_PCM_FORMAT_FLOAT64_LE : SND_PCM_FORMAT_FLOAT64_BE;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception{ "Unexpected sample type!" };
|
||||||
|
}
|
||||||
|
} // namespace detail
|
||||||
|
|
||||||
|
void SndPcmDeleter::operator()(snd_pcm_t* pcm) const noexcept
|
||||||
|
{
|
||||||
|
const int error{ ::snd_pcm_close(pcm) };
|
||||||
|
LMS_LOG_IF(AUDIO, ERROR, error != 0, "snd_pcm_close failed: " << ::snd_strerror(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
class AlsaException : public Exception
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AlsaException(std::string_view msg, int error)
|
||||||
|
: Exception{ std::string{ msg } + ": " + ::snd_strerror(error) }
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
AudioOutputStream::AudioOutputStream(boost::asio::io_context& ioContext, std::string_view device, std::string_view name, const PcmParameters& outputParameters)
|
||||||
|
: _ioContext{ ioContext }
|
||||||
|
, _name{ name }
|
||||||
|
, _outputParameters{ outputParameters }
|
||||||
|
, _strand{ _ioContext }
|
||||||
|
{
|
||||||
|
if (_outputParameters.planar)
|
||||||
|
throw Exception{ "Planar output format not supported" };
|
||||||
|
|
||||||
|
{
|
||||||
|
snd_pcm_t* pcm{};
|
||||||
|
const int error{ ::snd_pcm_open(&pcm, std::string{ device }.c_str(), SND_PCM_STREAM_PLAYBACK, 0) };
|
||||||
|
if (error != 0)
|
||||||
|
throw AlsaException{ "snd_pcm_open failed", error };
|
||||||
|
|
||||||
|
_pcm = SndPcmPtr{ pcm };
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
::snd_pcm_hw_params_t* hw_params{};
|
||||||
|
snd_pcm_hw_params_alloca(&hw_params);
|
||||||
|
::snd_pcm_hw_params_any(_pcm.get(), hw_params);
|
||||||
|
|
||||||
|
::snd_pcm_hw_params_set_access(_pcm.get(), hw_params, SND_PCM_ACCESS_RW_INTERLEAVED);
|
||||||
|
::snd_pcm_hw_params_set_format(_pcm.get(), hw_params, detail::toSndPcmFormat(_outputParameters.sampleType, outputParameters.byteOrder));
|
||||||
|
::snd_pcm_hw_params_set_channels(_pcm.get(), hw_params, _outputParameters.channelCount);
|
||||||
|
::snd_pcm_hw_params_set_rate(_pcm.get(), hw_params, _outputParameters.sampleRate, 1);
|
||||||
|
|
||||||
|
// TODO fragile!!
|
||||||
|
{
|
||||||
|
constexpr std::chrono::milliseconds wantedBufferDuration{ 500 }; // should be enough...
|
||||||
|
int dir{};
|
||||||
|
unsigned int bufferDuration{ std::chrono::duration_cast<std::chrono::microseconds>(wantedBufferDuration).count() };
|
||||||
|
const int error{ ::snd_pcm_hw_params_set_buffer_time_near(_pcm.get(), hw_params, &bufferDuration, &dir) };
|
||||||
|
if (error < 0)
|
||||||
|
throw AlsaException{ "snd_pcm_hw_params_set_buffer_size failed", error };
|
||||||
|
|
||||||
|
LMS_LOG(AUDIO, DEBUG, ::snd_pcm_name(_pcm.get()) << ", buffer time set to " << bufferDuration << " mus");
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
constexpr std::chrono::milliseconds wantedPeriodDuration{ 100 }; // should be enough...
|
||||||
|
int dir{};
|
||||||
|
unsigned int periodDuration{ std::chrono::duration_cast<std::chrono::microseconds>(wantedPeriodDuration).count() };
|
||||||
|
const int error{ ::snd_pcm_hw_params_set_period_time_near(_pcm.get(), hw_params, &periodDuration, &dir) };
|
||||||
|
if (error < 0)
|
||||||
|
throw AlsaException{ "snd_pcm_hw_params_set_period_size failed", error };
|
||||||
|
|
||||||
|
LMS_LOG(AUDIO, DEBUG, ::snd_pcm_name(_pcm.get()) << ", period time set to " << periodDuration << " mus");
|
||||||
|
}
|
||||||
|
|
||||||
|
const int error{ ::snd_pcm_hw_params(_pcm.get(), hw_params) };
|
||||||
|
if (error != 0)
|
||||||
|
throw AlsaException{ "snd_pcm_hw_params failed", error };
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
::snd_pcm_sw_params_t* sw{};
|
||||||
|
snd_pcm_sw_params_alloca(&sw);
|
||||||
|
|
||||||
|
::snd_pcm_sw_params_current(_pcm.get(), sw);
|
||||||
|
|
||||||
|
{
|
||||||
|
const int error{ ::snd_pcm_sw_params_set_tstamp_mode(_pcm.get(), sw, SND_PCM_TSTAMP_ENABLE) };
|
||||||
|
if (error != 0)
|
||||||
|
throw AlsaException{ "snd_pcm_sw_params_set_tstamp_mode failed", error };
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const int error{ ::snd_pcm_sw_params_set_tstamp_type(_pcm.get(), sw, SND_PCM_TSTAMP_TYPE_MONOTONIC) };
|
||||||
|
if (error != 0)
|
||||||
|
throw AlsaException{ "snd_pcm_sw_params_set_tstamp_type failed", error };
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const int error{ ::snd_pcm_sw_params(_pcm.get(), sw) };
|
||||||
|
if (error != 0)
|
||||||
|
throw AlsaException{ "snd_pcm_sw_params failed", error };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const int error{ ::snd_pcm_prepare(_pcm.get()) };
|
||||||
|
if (error != 0)
|
||||||
|
throw AlsaException{ "snd_pcm_prepare failed", error };
|
||||||
|
}
|
||||||
|
|
||||||
|
setupAllDescriptors();
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioOutputStream::~AudioOutputStream()
|
||||||
|
{
|
||||||
|
if (_drainThread.joinable())
|
||||||
|
_drainThread.join();
|
||||||
|
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
const PcmParameters& AudioOutputStream::getParameters() const
|
||||||
|
{
|
||||||
|
return _outputParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioOutputStream::asyncWaitReady(WaitReadyCallback cb)
|
||||||
|
{
|
||||||
|
// Always ready
|
||||||
|
boost::asio::post(_ioContext, std::move(cb));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioOutputStream::asyncWrite(std::span<const std::byte> buffer, WriteCompletionCallback cb)
|
||||||
|
{
|
||||||
|
if (buffer.size() % (getSampleSize(_outputParameters.sampleType) * _outputParameters.channelCount) != 0)
|
||||||
|
throw Exception{ "Unexpected buffer size" };
|
||||||
|
|
||||||
|
boost::asio::post(_strand, [this, buffer, cb = std::move(cb)]() mutable {
|
||||||
|
assert(_strand.running_in_this_thread());
|
||||||
|
|
||||||
|
if (_drainRequested)
|
||||||
|
throw Exception{ "asyncDrain already called!" };
|
||||||
|
|
||||||
|
WriteOperation operation;
|
||||||
|
operation.buffer = buffer;
|
||||||
|
operation.callback = std::move(cb);
|
||||||
|
|
||||||
|
_ioContext.get_executor().on_work_started();
|
||||||
|
_operations.push_back(std::move(operation));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioOutputStream::asyncDrain(DrainCompletionCallback cb)
|
||||||
|
{
|
||||||
|
boost::asio::post(_strand, [this, cb = std::move(cb)]() mutable {
|
||||||
|
if (_drainRequested)
|
||||||
|
throw Exception{ "asyncDrain already called!" };
|
||||||
|
|
||||||
|
_ioContext.get_executor().on_work_started();
|
||||||
|
|
||||||
|
_drainRequested = true;
|
||||||
|
_drainCallback = std::move(cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioOutputStream::pause()
|
||||||
|
{
|
||||||
|
const int error{ ::snd_pcm_pause(_pcm.get(), 0) };
|
||||||
|
if (error < 0)
|
||||||
|
throw AlsaException{ "snd_pcm_pause(0) failed", error };
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioOutputStream::resume()
|
||||||
|
{
|
||||||
|
// Initial case (no auto play)
|
||||||
|
if (::snd_pcm_state(_pcm.get()) == SND_PCM_STATE_PREPARED)
|
||||||
|
{
|
||||||
|
boost::asio::post(_strand, [this] { asyncWaitAllDescriptors(); });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int error{ ::snd_pcm_pause(_pcm.get(), 1) };
|
||||||
|
if (error < 0)
|
||||||
|
throw AlsaException{ "snd_pcm_pause(1) failed", error };
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputStream::isPaused() const
|
||||||
|
{
|
||||||
|
return ::snd_pcm_state(_pcm.get()) == SND_PCM_STATE_PAUSED;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::chrono::microseconds AudioOutputStream::getPlaybackTime() const
|
||||||
|
{
|
||||||
|
::snd_pcm_sframes_t delayFrames{};
|
||||||
|
|
||||||
|
const int error{ ::snd_pcm_delay(_pcm.get(), &delayFrames) };
|
||||||
|
if (error != 0)
|
||||||
|
{
|
||||||
|
LMS_LOG(AUDIO, WARNING, "snd_pcm_delay failed: " << snd_strerror(error));
|
||||||
|
delayFrames = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(static_cast<snd_pcm_sframes_t>(_totalWrittenFrameCount) >= delayFrames);
|
||||||
|
const snd_pcm_sframes_t playedFrameCount{ static_cast<snd_pcm_sframes_t>(_totalWrittenFrameCount) - delayFrames };
|
||||||
|
|
||||||
|
return std::chrono::microseconds{ playedFrameCount * std::chrono::microseconds::period::den / _outputParameters.sampleRate };
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioOutputStream::stop()
|
||||||
|
{
|
||||||
|
releaseAllDescriptors();
|
||||||
|
_pcm.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioOutputStream::setupAllDescriptors()
|
||||||
|
{
|
||||||
|
const int fdCount{ ::snd_pcm_poll_descriptors_count(_pcm.get()) };
|
||||||
|
_fileDescriptors.resize(fdCount);
|
||||||
|
|
||||||
|
const int error{ ::snd_pcm_poll_descriptors(_pcm.get(), _fileDescriptors.data(), _fileDescriptors.size()) };
|
||||||
|
if (error < 0)
|
||||||
|
throw AlsaException{ "snd_pcm_poll_descriptors failed", error };
|
||||||
|
|
||||||
|
for (const ::pollfd& fd : _fileDescriptors)
|
||||||
|
_streamDescriptors.emplace_back(_ioContext, fd.fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioOutputStream::releaseAllDescriptors()
|
||||||
|
{
|
||||||
|
for (auto& streamDescriptor : _streamDescriptors)
|
||||||
|
streamDescriptor.release();
|
||||||
|
|
||||||
|
_streamDescriptors.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioOutputStream::asyncWaitAllDescriptors()
|
||||||
|
{
|
||||||
|
assert(_strand.running_in_this_thread());
|
||||||
|
|
||||||
|
for (std::size_t i{}; i < _fileDescriptors.size(); ++i)
|
||||||
|
{
|
||||||
|
if (_fileDescriptors[i].events & POLLOUT)
|
||||||
|
asyncWaitDescriptor(_streamDescriptors[i], boost::asio::posix::stream_descriptor::wait_write);
|
||||||
|
if (_fileDescriptors[i].events & POLLIN)
|
||||||
|
asyncWaitDescriptor(_streamDescriptors[i], boost::asio::posix::stream_descriptor::wait_read);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioOutputStream::cancelAllDescriptors()
|
||||||
|
{
|
||||||
|
assert(_strand.running_in_this_thread());
|
||||||
|
|
||||||
|
for (std::size_t i{}; i < _fileDescriptors.size(); ++i)
|
||||||
|
{
|
||||||
|
if (_fileDescriptors[i].events & (POLLOUT || POLLIN))
|
||||||
|
_streamDescriptors[i].cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioOutputStream::asyncWaitDescriptor(boost::asio::posix::stream_descriptor& streamDescriptor, boost::asio::posix::stream_descriptor::wait_type waitType)
|
||||||
|
{
|
||||||
|
auto waitCallback{ [this, &streamDescriptor, waitType](const boost::system::error_code& ec) {
|
||||||
|
assert(_strand.running_in_this_thread());
|
||||||
|
|
||||||
|
if (ec == boost::asio::error::operation_aborted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (ec)
|
||||||
|
{
|
||||||
|
LMS_LOG(AUDIO, ERROR, "Poll failed: " << ec);
|
||||||
|
throw Exception{ "poll failed: " + ec.message() };
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned short revents{};
|
||||||
|
::snd_pcm_poll_descriptors_revents(_pcm.get(), _fileDescriptors.data(), _fileDescriptors.size(), &revents);
|
||||||
|
if (revents & POLLERR)
|
||||||
|
LMS_LOG(AUDIO, ERROR, "ERROR");
|
||||||
|
if (revents & POLLOUT)
|
||||||
|
writeSomeFrames();
|
||||||
|
|
||||||
|
if (_drainRequested && _operations.empty())
|
||||||
|
{
|
||||||
|
cancelAllDescriptors();
|
||||||
|
|
||||||
|
_drainThread = std::thread{ [this] {
|
||||||
|
const int error{ ::snd_pcm_drain(_pcm.get()) };
|
||||||
|
if (error != 0)
|
||||||
|
throw AlsaException{ "snd_pcm_drain failed: ", error };
|
||||||
|
|
||||||
|
boost::asio::post(_strand, [this] { onDrainComplete(); });
|
||||||
|
} };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
asyncWaitDescriptor(streamDescriptor, waitType);
|
||||||
|
} };
|
||||||
|
|
||||||
|
streamDescriptor.async_wait(waitType, boost::asio::bind_executor(_strand, std::move(waitCallback)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioOutputStream::writeSomeFrames()
|
||||||
|
{
|
||||||
|
assert(_strand.running_in_this_thread());
|
||||||
|
|
||||||
|
while (!_operations.empty())
|
||||||
|
{
|
||||||
|
WriteOperation& operation{ _operations.front() };
|
||||||
|
|
||||||
|
snd_pcm_sframes_t availableFrameCount{ ::snd_pcm_avail(_pcm.get()) };
|
||||||
|
std::size_t frameCount{ operation.buffer.size() / (getSampleSize(_outputParameters.sampleType) * _outputParameters.channelCount) };
|
||||||
|
|
||||||
|
if (frameCount > static_cast<std::size_t>(availableFrameCount))
|
||||||
|
frameCount = availableFrameCount;
|
||||||
|
|
||||||
|
const ::snd_pcm_sframes_t writtenFrameCount{ ::snd_pcm_writei(_pcm.get(), operation.buffer.data(), frameCount) };
|
||||||
|
if (writtenFrameCount < 0)
|
||||||
|
{
|
||||||
|
LMS_LOG(AUDIO, WARNING, ::snd_pcm_name(_pcm.get()) << ", recovery needed! error = " << snd_strerror(writtenFrameCount) << ", pcm state = " << ::snd_pcm_state_name(::snd_pcm_state(_pcm.get())));
|
||||||
|
const int error{ ::snd_pcm_recover(_pcm.get(), static_cast<int>(writtenFrameCount), 1) };
|
||||||
|
if (error)
|
||||||
|
throw AlsaException{ "Unrecoverable error", error };
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t writtenByteCount{ writtenFrameCount * getSampleSize(_outputParameters.sampleType) * _outputParameters.channelCount };
|
||||||
|
assert(writtenFrameCount <= static_cast<::snd_pcm_sframes_t>(frameCount));
|
||||||
|
_totalWrittenFrameCount += writtenFrameCount;
|
||||||
|
|
||||||
|
operation.buffer = std::span<const std::byte>{ operation.buffer.data() + writtenByteCount, operation.buffer.size() - writtenByteCount };
|
||||||
|
if (!operation.buffer.empty())
|
||||||
|
break;
|
||||||
|
|
||||||
|
boost::asio::post(_ioContext, std::move(operation.callback));
|
||||||
|
_ioContext.get_executor().on_work_finished();
|
||||||
|
|
||||||
|
_operations.pop_front();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioOutputStream::onDrainComplete()
|
||||||
|
{
|
||||||
|
assert(_strand.running_in_this_thread());
|
||||||
|
|
||||||
|
boost::asio::post(_ioContext, std::move(_drainCallback));
|
||||||
|
_ioContext.get_executor().on_work_finished();
|
||||||
|
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
} // namespace lms::audio::alsa
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS 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, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "audio/IAudioOutput.hpp"
|
||||||
|
|
||||||
|
#include <deque>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <alsa/asoundlib.h>
|
||||||
|
#include <boost/asio/posix/stream_descriptor.hpp>
|
||||||
|
#include <boost/asio/strand.hpp>
|
||||||
|
|
||||||
|
namespace lms::audio::alsa
|
||||||
|
{
|
||||||
|
struct SndPcmDeleter
|
||||||
|
{
|
||||||
|
void operator()(snd_pcm_t* ctx) const noexcept;
|
||||||
|
};
|
||||||
|
using SndPcmPtr = std::unique_ptr<snd_pcm_t, SndPcmDeleter>;
|
||||||
|
|
||||||
|
class AudioOutputStream : public IAudioOutputStream
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioOutputStream(boost::asio::io_context& ioContext, std::string_view device, std::string_view name, const PcmParameters& outputParameters);
|
||||||
|
~AudioOutputStream() override;
|
||||||
|
|
||||||
|
AudioOutputStream(AudioOutputStream&) = delete;
|
||||||
|
AudioOutputStream& operator=(AudioOutputStream&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const PcmParameters& getParameters() const override;
|
||||||
|
void asyncWaitReady(WaitReadyCallback cb) override;
|
||||||
|
void asyncWrite(std::span<const std::byte> 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;
|
||||||
|
|
||||||
|
void stop();
|
||||||
|
void setupAllDescriptors();
|
||||||
|
void releaseAllDescriptors();
|
||||||
|
void asyncWaitAllDescriptors();
|
||||||
|
void cancelAllDescriptors();
|
||||||
|
void asyncWaitDescriptor(boost::asio::posix::stream_descriptor& streamDescriptor, boost::asio::posix::stream_descriptor::wait_type waitType);
|
||||||
|
void handleFdEvent();
|
||||||
|
void writeSomeFrames();
|
||||||
|
void onDrainComplete();
|
||||||
|
|
||||||
|
boost::asio::io_context& _ioContext;
|
||||||
|
const std::string _name;
|
||||||
|
const PcmParameters _outputParameters;
|
||||||
|
boost::asio::io_context::strand _strand;
|
||||||
|
SndPcmPtr _pcm;
|
||||||
|
|
||||||
|
std::vector<::pollfd> _fileDescriptors;
|
||||||
|
std::vector<boost::asio::posix::stream_descriptor> _streamDescriptors;
|
||||||
|
|
||||||
|
struct WriteOperation
|
||||||
|
{
|
||||||
|
std::span<const std::byte> buffer;
|
||||||
|
WriteCompletionCallback callback;
|
||||||
|
};
|
||||||
|
std::deque<WriteOperation> _operations;
|
||||||
|
std::size_t _totalWrittenFrameCount{};
|
||||||
|
|
||||||
|
bool _drainRequested{};
|
||||||
|
DrainCompletionCallback _drainCallback;
|
||||||
|
std::thread _drainThread;
|
||||||
|
};
|
||||||
|
} // namespace lms::audio::alsa
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2025 Emeric Poupon
|
* Copyright (C) 2026 Emeric Poupon
|
||||||
*
|
*
|
||||||
* This file is part of LMS.
|
* This file is part of LMS.
|
||||||
*
|
*
|
||||||
@@ -34,14 +34,6 @@
|
|||||||
#include "Exception.hpp"
|
#include "Exception.hpp"
|
||||||
#include "MainLoopScopedLock.hpp"
|
#include "MainLoopScopedLock.hpp"
|
||||||
|
|
||||||
namespace lms::audio
|
|
||||||
{
|
|
||||||
std::unique_ptr<IAudioOutputContext> createAudioOutputContext(boost::asio::io_context& ioContext, std::string_view name)
|
|
||||||
{
|
|
||||||
return std::make_unique<pulseaudio::AudioOutputContext>(ioContext, name);
|
|
||||||
}
|
|
||||||
} // namespace lms::audio
|
|
||||||
|
|
||||||
namespace lms::audio::pulseaudio
|
namespace lms::audio::pulseaudio
|
||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2025 Emeric Poupon
|
* Copyright (C) 2026 Emeric Poupon
|
||||||
*
|
*
|
||||||
* This file is part of LMS.
|
* This file is part of LMS.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2025 Emeric Poupon
|
* Copyright (C) 2026 Emeric Poupon
|
||||||
*
|
*
|
||||||
* This file is part of LMS.
|
* This file is part of LMS.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2025 Emeric Poupon
|
* Copyright (C) 2026 Emeric Poupon
|
||||||
*
|
*
|
||||||
* This file is part of LMS.
|
* This file is part of LMS.
|
||||||
*
|
*
|
||||||
@@ -78,7 +78,7 @@ namespace lms::audio::pulseaudio
|
|||||||
boost::asio::io_context& _ioContext;
|
boost::asio::io_context& _ioContext;
|
||||||
pa_context* _context;
|
pa_context* _context;
|
||||||
pa_threaded_mainloop* _mainLoop;
|
pa_threaded_mainloop* _mainLoop;
|
||||||
const PcmParameters& _outputParameters;
|
const PcmParameters _outputParameters;
|
||||||
PaStreamPtr _stream;
|
PaStreamPtr _stream;
|
||||||
|
|
||||||
WaitReadyCallback _waitReadyCallback;
|
WaitReadyCallback _waitReadyCallback;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2025 Emeric Poupon
|
* Copyright (C) 2026 Emeric Poupon
|
||||||
*
|
*
|
||||||
* This file is part of LMS.
|
* This file is part of LMS.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2025 Emeric Poupon
|
* Copyright (C) 2026 Emeric Poupon
|
||||||
*
|
*
|
||||||
* This file is part of LMS.
|
* This file is part of LMS.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2025 Emeric Poupon
|
* Copyright (C) 2026 Emeric Poupon
|
||||||
*
|
*
|
||||||
* This file is part of LMS.
|
* This file is part of LMS.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2025 Emeric Poupon
|
* Copyright (C) 2026 Emeric Poupon
|
||||||
*
|
*
|
||||||
* This file is part of LMS.
|
* This file is part of LMS.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -26,6 +26,8 @@
|
|||||||
|
|
||||||
#include <boost/asio/io_context.hpp>
|
#include <boost/asio/io_context.hpp>
|
||||||
|
|
||||||
|
#include "core/EnumSet.hpp"
|
||||||
|
|
||||||
#include "audio/PcmTypes.hpp"
|
#include "audio/PcmTypes.hpp"
|
||||||
|
|
||||||
namespace lms::audio
|
namespace lms::audio
|
||||||
@@ -63,10 +65,16 @@ namespace lms::audio
|
|||||||
virtual void asyncWaitReady(WaitReadyCallback cb) = 0;
|
virtual void asyncWaitReady(WaitReadyCallback cb) = 0;
|
||||||
|
|
||||||
// Must be called once output context is ready
|
// Must be called once output context is ready
|
||||||
// the created stream is in pause state
|
// The created stream is in pause state; you must call resume() to start it
|
||||||
// planar format is not accepted!
|
// Planar format is not accepted!
|
||||||
[[nodiscard]] virtual std::unique_ptr<IAudioOutputStream> createOutputStream(std::string_view name, const PcmParameters& pcmParameters) = 0;
|
[[nodiscard]] virtual std::unique_ptr<IAudioOutputStream> createOutputStream(std::string_view name, const PcmParameters& pcmParameters) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::unique_ptr<IAudioOutputContext> createAudioOutputContext(boost::asio::io_context& ioContext, std::string_view name);
|
enum class AudioOutputBackend
|
||||||
|
{
|
||||||
|
ALSA,
|
||||||
|
PulseAudio,
|
||||||
|
};
|
||||||
|
core::EnumSet<AudioOutputBackend> getAudioOutputBackends();
|
||||||
|
std::unique_ptr<IAudioOutputContext> createAudioOutputContext(boost::asio::io_context& ioContext, std::string_view name, AudioOutputBackend backend);
|
||||||
} // namespace lms::audio
|
} // namespace lms::audio
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
#include <boost/program_options.hpp>
|
#include <boost/program_options.hpp>
|
||||||
|
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
|
#include "core/String.hpp"
|
||||||
|
|
||||||
#include "audio/Exception.hpp"
|
#include "audio/Exception.hpp"
|
||||||
#include "audio/IAudioOutput.hpp"
|
#include "audio/IAudioOutput.hpp"
|
||||||
@@ -37,12 +38,12 @@ namespace lms
|
|||||||
class FilePlayer
|
class FilePlayer
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
FilePlayer(boost::asio::io_context& ioContext, const std::filesystem::path& filePath, const audio::PcmParameters& params)
|
FilePlayer(boost::asio::io_context& ioContext, audio::IAudioOutputContext& context, const std::filesystem::path& filePath, const audio::PcmParameters& params)
|
||||||
: _ioContext{ ioContext }
|
: _ioContext{ ioContext }
|
||||||
|
, _context{ context }
|
||||||
, _pcmDecoder{ audio::createPcmDecoder(filePath, params) }
|
, _pcmDecoder{ audio::createPcmDecoder(filePath, params) }
|
||||||
, _context{ audio::createAudioOutputContext(_ioContext, "LMS") }
|
|
||||||
{
|
{
|
||||||
_context->asyncWaitReady([this] {
|
_context.asyncWaitReady([this] {
|
||||||
createStream();
|
createStream();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -58,7 +59,7 @@ namespace lms
|
|||||||
|
|
||||||
void createStream()
|
void createStream()
|
||||||
{
|
{
|
||||||
_outputStream = _context->createOutputStream("LMS-player", getPcmParameters());
|
_outputStream = _context.createOutputStream("LMS-player", getPcmParameters());
|
||||||
|
|
||||||
prepareBuffers();
|
prepareBuffers();
|
||||||
decodeSome();
|
decodeSome();
|
||||||
@@ -87,7 +88,7 @@ namespace lms
|
|||||||
{
|
{
|
||||||
BufferDesc& bufferDesc{ _buffers[_nextBufferIndex] };
|
BufferDesc& bufferDesc{ _buffers[_nextBufferIndex] };
|
||||||
if (bufferDesc.isInWrite)
|
if (bufferDesc.isInWrite)
|
||||||
return;
|
break;
|
||||||
|
|
||||||
const std::size_t bufferIndex{ _nextBufferIndex };
|
const std::size_t bufferIndex{ _nextBufferIndex };
|
||||||
if (++_nextBufferIndex >= _buffers.size())
|
if (++_nextBufferIndex >= _buffers.size())
|
||||||
@@ -148,8 +149,8 @@ namespace lms
|
|||||||
}
|
}
|
||||||
|
|
||||||
boost::asio::io_context& _ioContext;
|
boost::asio::io_context& _ioContext;
|
||||||
|
audio::IAudioOutputContext& _context;
|
||||||
std::unique_ptr<audio::IPcmDecoder> _pcmDecoder;
|
std::unique_ptr<audio::IPcmDecoder> _pcmDecoder;
|
||||||
std::unique_ptr<audio::IAudioOutputContext> _context;
|
|
||||||
std::unique_ptr<audio::IAudioOutputStream> _outputStream;
|
std::unique_ptr<audio::IAudioOutputStream> _outputStream;
|
||||||
|
|
||||||
struct BufferDesc
|
struct BufferDesc
|
||||||
@@ -161,7 +162,7 @@ namespace lms
|
|||||||
static constexpr std::size_t bufferCount{ 4 };
|
static constexpr std::size_t bufferCount{ 4 };
|
||||||
std::vector<BufferDesc> _buffers;
|
std::vector<BufferDesc> _buffers;
|
||||||
std::size_t _nextBufferIndex{};
|
std::size_t _nextBufferIndex{};
|
||||||
std::size_t _sampleCountPerBuffer;
|
std::size_t _sampleCountPerBuffer{};
|
||||||
bool _draining{};
|
bool _draining{};
|
||||||
};
|
};
|
||||||
} // namespace lms
|
} // namespace lms
|
||||||
@@ -177,7 +178,8 @@ int main(int argc, char* argv[])
|
|||||||
// clang-format off
|
// clang-format off
|
||||||
options.add_options()
|
options.add_options()
|
||||||
("help,h", "Display this help message")
|
("help,h", "Display this help message")
|
||||||
("input",program_options::value<std::string>()->required(), "Input audio file path");
|
("input",program_options::value<std::string>()->required(), "Input audio file path")
|
||||||
|
("backend", program_options::value<std::string>()->default_value(std::string{ "auto" }, "auto"), "Backend to be used (value can be \"alsa\", \"pulseaudio\")");
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|
||||||
program_options::variables_map vm;
|
program_options::variables_map vm;
|
||||||
@@ -196,6 +198,24 @@ int main(int argc, char* argv[])
|
|||||||
if (!std::filesystem::exists(inputPath))
|
if (!std::filesystem::exists(inputPath))
|
||||||
throw std::runtime_error{ "File '" + inputPath.string() + "' does not exist!" };
|
throw std::runtime_error{ "File '" + inputPath.string() + "' does not exist!" };
|
||||||
|
|
||||||
|
audio::AudioOutputBackend outputBackend;
|
||||||
|
if (core::stringUtils::stringCaseInsensitiveEqual(vm["backend"].as<std::string>(), "alsa"))
|
||||||
|
outputBackend = audio::AudioOutputBackend::ALSA;
|
||||||
|
else if (core::stringUtils::stringCaseInsensitiveEqual(vm["backend"].as<std::string>(), "pulseaudio"))
|
||||||
|
outputBackend = audio::AudioOutputBackend::PulseAudio;
|
||||||
|
else if (core::stringUtils::stringCaseInsensitiveEqual(vm["backend"].as<std::string>(), "auto"))
|
||||||
|
{
|
||||||
|
const auto backends{ audio::getAudioOutputBackends() };
|
||||||
|
if (backends.contains(audio::AudioOutputBackend::PulseAudio))
|
||||||
|
outputBackend = audio::AudioOutputBackend::PulseAudio;
|
||||||
|
else if (backends.contains(audio::AudioOutputBackend::ALSA))
|
||||||
|
outputBackend = audio::AudioOutputBackend::ALSA;
|
||||||
|
else
|
||||||
|
throw std::runtime_error{ "No audio output backend available!" };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
throw program_options::validation_error{ program_options::validation_error::invalid_option_value, "backend" };
|
||||||
|
|
||||||
core::Service<core::logging::ILogger> logger{ core::logging::createLogger(core::logging::Severity::INFO) };
|
core::Service<core::logging::ILogger> logger{ core::logging::createLogger(core::logging::Severity::INFO) };
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -203,12 +223,18 @@ int main(int argc, char* argv[])
|
|||||||
audio::PcmParameters decoderParams;
|
audio::PcmParameters decoderParams;
|
||||||
decoderParams.byteOrder = std::endian::little;
|
decoderParams.byteOrder = std::endian::little;
|
||||||
decoderParams.channelCount = 2;
|
decoderParams.channelCount = 2;
|
||||||
decoderParams.sampleRate = 48000;
|
decoderParams.sampleRate = 44100;
|
||||||
decoderParams.planar = false;
|
decoderParams.planar = false;
|
||||||
decoderParams.sampleType = audio::PcmSampleType::Float32;
|
decoderParams.sampleType = audio::PcmSampleType::Signed16;
|
||||||
|
|
||||||
|
const auto availableBackends{ audio::getAudioOutputBackends() };
|
||||||
|
if (availableBackends.empty())
|
||||||
|
throw std::runtime_error{ "No audio output backend available" };
|
||||||
|
|
||||||
boost::asio::io_context context;
|
boost::asio::io_context context;
|
||||||
FilePlayer filePlayer{ context, inputPath, decoderParams };
|
auto audioOutputContext{ audio::createAudioOutputContext(context, "LMS", outputBackend) };
|
||||||
|
|
||||||
|
FilePlayer filePlayer{ context, *audioOutputContext, inputPath, decoderParams };
|
||||||
|
|
||||||
context.run();
|
context.run();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user