Aegisub/src/audio_player_alsa.cpp
arch1t3cht 819d90e9b0 Stereo playback on Linux audio players
After the audio provider rework, adjust the audio players to not
use the int16 mono downmixed audio unless necessary. Furthermore,
the pulseaudio-based player now controls the volume directly through
pulseaudio instead of by modifying the buffer. This also reduces latency
when changing the volume.

The entire set of GetAudio functions is quite messy now. After wangqr's
audio rework, it was split into GetAudio and GetInt16MonoAudio
functions, but now volume scaling is also necessary. Really, this should
go back to a type constructor based system with audio players being
allowed to choose what properties out of mono / 16 bytes / int samples /
volume they need.
2022-08-18 22:48:40 +02:00

384 lines
10 KiB
C++

// Copyright (c) 2011, Niels Martin Hansen
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
// * Neither the name of the Aegisub Group nor the names of its contributors
// may be used to endorse or promote products derived from this software
// without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//
// Aegisub Project http://www.aegisub.org/
/// @file audio_player_alsa.cpp
/// @brief ALSA-based audio output
/// @ingroup audio_output
///
#ifdef WITH_ALSA
#include "include/aegisub/audio_player.h"
#include "audio_controller.h"
#include "compat.h"
#include "frame_main.h"
#include "options.h"
#include <libaegisub/audio/provider.h>
#include <libaegisub/log.h>
#include <libaegisub/make_unique.h>
#include <atomic>
#include <algorithm>
#include <boost/scope_exit.hpp>
#include <chrono>
#include <condition_variable>
#include <alsa/asoundlib.h>
#include <memory>
#include <mutex>
#include <thread>
// X11 is the bestest
#undef None
namespace {
enum class Message {
None,
Start,
Stop,
Close
};
using clock = std::chrono::steady_clock;
class AlsaPlayer final : public AudioPlayer {
std::mutex mutex;
std::condition_variable cond;
std::string device_name = OPT_GET("Player/Audio/ALSA/Device")->GetString();
Message message = Message::None;
std::atomic<bool> playing{false};
std::atomic<double> volume{1.0};
int64_t start_position = 0;
std::atomic<int64_t> end_position{0};
bool fallback_mono16 = false; // whether to convert to 16 bit mono. FIXME: more flexible conversion
std::mutex position_mutex;
int64_t last_position = 0;
clock::time_point last_position_time;
std::vector<char> decode_buffer;
std::thread thread;
snd_pcm_format_t GetPCMFormat(const agi::AudioProvider *provider);
void PlaybackThread();
void UpdatePlaybackPosition(snd_pcm_t *pcm, int64_t position)
{
snd_pcm_sframes_t delay;
if (snd_pcm_delay(pcm, &delay) == 0)
{
std::unique_lock<std::mutex> playback_lock;
last_position = position - delay;
last_position_time = clock::now();
}
}
public:
AlsaPlayer(agi::AudioProvider *provider);
~AlsaPlayer();
void Play(int64_t start, int64_t count) override;
void Stop() override;
bool IsPlaying() override { return playing; }
void SetVolume(double vol) override { volume = vol; }
int64_t GetEndPosition() override { return end_position; }
int64_t GetCurrentPosition() override;
void SetEndPosition(int64_t pos) override;
};
snd_pcm_format_t AlsaPlayer::GetPCMFormat(const agi::AudioProvider *provider) {
if (provider->AreSamplesFloat()) {
switch (provider->GetBytesPerSample()) {
case 4:
return SND_PCM_FORMAT_FLOAT_LE;
case 8:
return SND_PCM_FORMAT_FLOAT64_LE;
default:
fallback_mono16 = true;
return SND_PCM_FORMAT_S16_LE;
}
} else {
switch (provider->GetBytesPerSample()) {
case 1:
return SND_PCM_FORMAT_U8;
case 2:
return SND_PCM_FORMAT_S16_LE;
case 3:
return SND_PCM_FORMAT_S24_LE;
case 4:
return SND_PCM_FORMAT_S32_LE;
case 8:
return SND_PCM_FORMAT_S32_LE;
default:
fallback_mono16 = true;
return SND_PCM_FORMAT_S16_LE;
}
}
}
void AlsaPlayer::PlaybackThread()
{
std::unique_lock<std::mutex> lock(mutex);
snd_pcm_t *pcm = nullptr;
if (snd_pcm_open(&pcm, device_name.c_str(), SND_PCM_STREAM_PLAYBACK, 0) != 0)
return;
LOG_D("audio/player/alsa") << "opened pcm";
BOOST_SCOPE_EXIT_ALL(&) { snd_pcm_close(pcm); };
do_setup:
snd_pcm_format_t pcm_format = GetPCMFormat(provider);
if (snd_pcm_set_params(pcm,
pcm_format,
SND_PCM_ACCESS_RW_INTERLEAVED,
fallback_mono16 ? 1 : provider->GetChannels(),
provider->GetSampleRate(),
1, // allow resample
100*1000 // 100 milliseconds latency
) != 0)
return;
LOG_D("audio/player/alsa") << "set pcm params";
size_t framesize = fallback_mono16 ? sizeof(int16_t) : provider->GetChannels() * provider->GetBytesPerSample();
while (true)
{
// Wait for condition to trigger
while (message != Message::Start)
{
cond.wait(lock, [&] { return message != Message::None; });
if (message == Message::Close)
return;
if (message == Message::Start && end_position > start_position)
break;
// Not playing, so don't need to stop...
message = Message::None;
}
message = Message::None;
LOG_D("audio/player/alsa") << "starting playback";
int64_t position = start_position;
// Initial buffer-fill
{
auto avail = std::min(snd_pcm_avail(pcm), (snd_pcm_sframes_t)(end_position-position));
decode_buffer.resize(avail * framesize);
if (fallback_mono16) {
provider->GetInt16MonoAudioWithVolume(reinterpret_cast<int16_t*>(decode_buffer.data()), position, avail, volume);
} else {
provider->GetAudioWithVolume(decode_buffer.data(), position, avail, volume);
}
snd_pcm_sframes_t written = 0;
while (written <= 0)
{
written = snd_pcm_writei(pcm, decode_buffer.data(), avail);
if (written == -ESTRPIPE)
snd_pcm_recover(pcm, written, 0);
else if (written <= 0)
{
LOG_D("audio/player/alsa") << "error filling buffer";
return;
}
}
position += written;
}
// Start playback
LOG_D("audio/player/alsa") << "initial buffer filled, hitting start";
snd_pcm_start(pcm);
UpdatePlaybackPosition(pcm, position);
playing = true;
BOOST_SCOPE_EXIT_ALL(&) { playing = false; };
while (true)
{
// Sleep a bit, or until an event
cond.wait_for(lock, std::chrono::milliseconds{25});
if (message == Message::Close)
{
snd_pcm_drop(pcm);
return;
}
// Check for stop signal
if (message == Message::Stop || message == Message::Start)
{
LOG_D("audio/player/alsa") << "playback loop, stop signal";
snd_pcm_drop(pcm);
break;
}
// Fill buffer
snd_pcm_sframes_t tmp_pcm_avail = snd_pcm_avail(pcm);
if (tmp_pcm_avail == -EPIPE)
{
if (snd_pcm_recover(pcm, -EPIPE, 1) < 0)
{
LOG_D("audio/player/alsa") << "failed to recover from underrun";
return;
}
tmp_pcm_avail = snd_pcm_avail(pcm);
}
auto avail = std::min(tmp_pcm_avail, (snd_pcm_sframes_t)(end_position-position));
if (avail < 0)
continue;
{
decode_buffer.resize(avail * framesize);
if (fallback_mono16) {
provider->GetInt16MonoAudioWithVolume(reinterpret_cast<int16_t*>(decode_buffer.data()), position, avail, volume);
} else {
provider->GetAudioWithVolume(decode_buffer.data(), position, avail, volume);
}
snd_pcm_sframes_t written = 0;
while (written <= 0)
{
written = snd_pcm_writei(pcm, decode_buffer.data(), avail);
if (written == -ESTRPIPE || written == -EPIPE)
snd_pcm_recover(pcm, written, 0);
else if (written == 0)
break;
else if (written < 0)
{
LOG_D("audio/player/alsa") << "error filling buffer, written=" << written;
return;
}
}
position += written;
}
UpdatePlaybackPosition(pcm, position);
// Check for end of playback
if (position >= end_position)
{
LOG_D("audio/player/alsa") << "playback loop, past end, draining";
snd_pcm_drain(pcm);
break;
}
}
playing = false;
LOG_D("audio/player/alsa") << "out of playback loop";
switch (snd_pcm_state(pcm))
{
case SND_PCM_STATE_OPEN:
// no clue what could have happened here, but start over
goto do_setup;
case SND_PCM_STATE_SETUP:
// we lost the preparedness?
snd_pcm_prepare(pcm);
break;
case SND_PCM_STATE_DISCONNECTED:
// lost device, close the handle and return error
return;
default:
// everything else should either be fine or impossible (here)
break;
}
}
}
AlsaPlayer::AlsaPlayer(agi::AudioProvider *provider) try
: AudioPlayer(provider)
, thread(&AlsaPlayer::PlaybackThread, this)
{
}
catch (std::system_error const&) {
throw AudioPlayerOpenError("AlsaPlayer: Creating the playback thread failed");
}
AlsaPlayer::~AlsaPlayer()
{
{
std::unique_lock<std::mutex> lock(mutex);
message = Message::Close;
cond.notify_all();
}
thread.join();
}
void AlsaPlayer::Play(int64_t start, int64_t count)
{
std::unique_lock<std::mutex> lock(mutex);
message = Message::Start;
start_position = start;
end_position = start + count;
cond.notify_all();
}
void AlsaPlayer::Stop()
{
std::unique_lock<std::mutex> lock(mutex);
message = Message::Stop;
cond.notify_all();
}
void AlsaPlayer::SetEndPosition(int64_t pos)
{
std::unique_lock<std::mutex> lock(mutex);
end_position = pos;
}
int64_t AlsaPlayer::GetCurrentPosition()
{
int64_t lastpos;
clock::time_point lasttime;
{
std::unique_lock<std::mutex> playback_lock;
lastpos = last_position;
lasttime = last_position_time;
}
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(clock::now() - lasttime).count();
return lastpos + ms * provider->GetSampleRate() / 1000;
}
}
std::unique_ptr<AudioPlayer> CreateAlsaPlayer(agi::AudioProvider *provider, wxWindow *)
{
return agi::make_unique<AlsaPlayer>(provider);
}
#endif // WITH_ALSA