/* * 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 . */ #include "AudioOutputStream.hpp" #include #include #include #include #include #include #include #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_OUTPUT_STREAM, ERROR, error != 0, "snd_pcm_close failed: " << ::snd_strerror(error)); // TODO move this ::snd_config_update_free_global(); } 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(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_OUTPUT_STREAM, 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(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_OUTPUT_STREAM, 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 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_OUTPUT_STREAM, WARNING, "snd_pcm_delay failed: " << snd_strerror(error)); delayFrames = 0; } assert(static_cast(_totalWrittenFrameCount) >= delayFrames); const snd_pcm_sframes_t playedFrameCount{ static_cast(_totalWrittenFrameCount) - delayFrames }; return std::chrono::microseconds{ playedFrameCount * std::chrono::microseconds::period::den / _outputParameters.sampleRate }; } std::chrono::microseconds AudioOutputStream::getLatency() const { ::snd_pcm_sframes_t delayFrames{}; const int error{ ::snd_pcm_delay(_pcm.get(), &delayFrames) }; if (error != 0) { LMS_LOG(AUDIO_OUTPUT_STREAM, WARNING, "snd_pcm_delay failed: " << snd_strerror(error)); delayFrames = 0; } return std::chrono::microseconds{ delayFrames * std::chrono::microseconds::period::den / _outputParameters.sampleRate }; } void AudioOutputStream::flush() { boost::asio::post(_strand, [this] { assert(_strand.running_in_this_thread()); LMS_LOG(AUDIO_OUTPUT_STREAM, DEBUG, "Flushing output"); if (_drainRequested) throw Exception{ "asyncDrain already called!" }; #if 0 { const int error{ ::snd_pcm_drop(_pcm.get()) }; if (error < 0) throw AlsaException{ "snd_pcm_drop failed", error }; } { const int error{ ::snd_pcm_prepare(_pcm.get()) }; if (error < 0) throw AlsaException{ "snd_pcm_prepare failed", error }; } #endif while (!_operations.empty()) { WriteOperation& operation{ _operations.front() }; boost::asio::post(_ioContext, std::move(operation.callback)); _ioContext.get_executor().on_work_finished(); _operations.pop_front(); } }); } 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_OUTPUT_STREAM, 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_OUTPUT_STREAM, ERROR, "snd_pcm_poll_descriptors_revents raied 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(availableFrameCount)) frameCount = availableFrameCount; const ::snd_pcm_sframes_t writtenFrameCount{ ::snd_pcm_writei(_pcm.get(), operation.buffer.data(), frameCount) }; if (writtenFrameCount < 0) { LMS_LOG(AUDIO_OUTPUT_STREAM, 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(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{ 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