output/alsa: move functions into the struct
This commit is contained in:
parent
f532964fde
commit
153f5854e2
|
@ -20,6 +20,7 @@
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "AlsaOutputPlugin.hxx"
|
#include "AlsaOutputPlugin.hxx"
|
||||||
#include "../OutputAPI.hxx"
|
#include "../OutputAPI.hxx"
|
||||||
|
#include "../Wrapper.hxx"
|
||||||
#include "mixer/MixerList.hxx"
|
#include "mixer/MixerList.hxx"
|
||||||
#include "pcm/PcmExport.hxx"
|
#include "pcm/PcmExport.hxx"
|
||||||
#include "config/ConfigError.hxx"
|
#include "config/ConfigError.hxx"
|
||||||
|
@ -131,17 +132,48 @@ struct AlsaOutput {
|
||||||
mode(0), writei(snd_pcm_writei) {
|
mode(0), writei(snd_pcm_writei) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
~AlsaOutput() {
|
||||||
|
/* free libasound's config cache */
|
||||||
|
snd_config_update_free_global();
|
||||||
|
}
|
||||||
|
|
||||||
|
gcc_pure
|
||||||
|
const char *GetDevice() {
|
||||||
|
return device.empty() ? default_device : device.c_str();
|
||||||
|
}
|
||||||
|
|
||||||
bool Configure(const config_param ¶m, Error &error);
|
bool Configure(const config_param ¶m, Error &error);
|
||||||
|
static AlsaOutput *Create(const config_param ¶m, Error &error);
|
||||||
|
|
||||||
|
bool Enable(Error &error);
|
||||||
|
void Disable();
|
||||||
|
|
||||||
|
bool Open(AudioFormat &audio_format, Error &error);
|
||||||
|
void Close();
|
||||||
|
|
||||||
|
size_t Play(const void *chunk, size_t size, Error &error);
|
||||||
|
void Drain();
|
||||||
|
void Cancel();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool SetupDop(AudioFormat audio_format,
|
||||||
|
bool *shift8_r, bool *packed_r, bool *reverse_endian_r,
|
||||||
|
Error &error);
|
||||||
|
bool SetupOrDop(AudioFormat &audio_format, Error &error);
|
||||||
|
|
||||||
|
int Recover(int err);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write silence to the ALSA device.
|
||||||
|
*/
|
||||||
|
void WriteSilence(snd_pcm_uframes_t nframes) {
|
||||||
|
writei(pcm, silence, nframes);
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static constexpr Domain alsa_output_domain("alsa_output");
|
static constexpr Domain alsa_output_domain("alsa_output");
|
||||||
|
|
||||||
static const char *
|
|
||||||
alsa_device(const AlsaOutput *ad)
|
|
||||||
{
|
|
||||||
return ad->device.empty() ? default_device : ad->device.c_str();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool
|
inline bool
|
||||||
AlsaOutput::Configure(const config_param ¶m, Error &error)
|
AlsaOutput::Configure(const config_param ¶m, Error &error)
|
||||||
{
|
{
|
||||||
|
@ -178,8 +210,8 @@ AlsaOutput::Configure(const config_param ¶m, Error &error)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static AudioOutput *
|
inline AlsaOutput *
|
||||||
alsa_init(const config_param ¶m, Error &error)
|
AlsaOutput::Create(const config_param ¶m, Error &error)
|
||||||
{
|
{
|
||||||
AlsaOutput *ad = new AlsaOutput();
|
AlsaOutput *ad = new AlsaOutput();
|
||||||
|
|
||||||
|
@ -188,35 +220,20 @@ alsa_init(const config_param ¶m, Error &error)
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ad->base;
|
return ad;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
inline bool
|
||||||
alsa_finish(AudioOutput *ao)
|
AlsaOutput::Enable(gcc_unused Error &error)
|
||||||
{
|
{
|
||||||
AlsaOutput *ad = (AlsaOutput *)ao;
|
pcm_export.Construct();
|
||||||
|
|
||||||
delete ad;
|
|
||||||
|
|
||||||
/* free libasound's config cache */
|
|
||||||
snd_config_update_free_global();
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
alsa_output_enable(AudioOutput *ao, gcc_unused Error &error)
|
|
||||||
{
|
|
||||||
AlsaOutput *ad = (AlsaOutput *)ao;
|
|
||||||
|
|
||||||
ad->pcm_export.Construct();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
inline void
|
||||||
alsa_output_disable(AudioOutput *ao)
|
AlsaOutput::Disable()
|
||||||
{
|
{
|
||||||
AlsaOutput *ad = (AlsaOutput *)ao;
|
pcm_export.Destruct();
|
||||||
|
|
||||||
ad->pcm_export.Destruct();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
|
@ -450,7 +467,7 @@ configure_hw:
|
||||||
if (err < 0) {
|
if (err < 0) {
|
||||||
FormatWarning(alsa_output_domain,
|
FormatWarning(alsa_output_domain,
|
||||||
"Cannot set mmap'ed mode on ALSA device \"%s\": %s",
|
"Cannot set mmap'ed mode on ALSA device \"%s\": %s",
|
||||||
alsa_device(ad), snd_strerror(-err));
|
ad->GetDevice(), snd_strerror(-err));
|
||||||
LogWarning(alsa_output_domain,
|
LogWarning(alsa_output_domain,
|
||||||
"Falling back to direct write mode");
|
"Falling back to direct write mode");
|
||||||
ad->use_mmap = false;
|
ad->use_mmap = false;
|
||||||
|
@ -472,7 +489,7 @@ configure_hw:
|
||||||
if (err < 0) {
|
if (err < 0) {
|
||||||
error.Format(alsa_output_domain, err,
|
error.Format(alsa_output_domain, err,
|
||||||
"ALSA device \"%s\" does not support format %s: %s",
|
"ALSA device \"%s\" does not support format %s: %s",
|
||||||
alsa_device(ad),
|
ad->GetDevice(),
|
||||||
sample_format_to_string(audio_format.format),
|
sample_format_to_string(audio_format.format),
|
||||||
snd_strerror(-err));
|
snd_strerror(-err));
|
||||||
return false;
|
return false;
|
||||||
|
@ -489,7 +506,7 @@ configure_hw:
|
||||||
if (err < 0) {
|
if (err < 0) {
|
||||||
error.Format(alsa_output_domain, err,
|
error.Format(alsa_output_domain, err,
|
||||||
"ALSA device \"%s\" does not support %i channels: %s",
|
"ALSA device \"%s\" does not support %i channels: %s",
|
||||||
alsa_device(ad), (int)audio_format.channels,
|
ad->GetDevice(), (int)audio_format.channels,
|
||||||
snd_strerror(-err));
|
snd_strerror(-err));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -500,7 +517,7 @@ configure_hw:
|
||||||
if (err < 0 || sample_rate == 0) {
|
if (err < 0 || sample_rate == 0) {
|
||||||
error.Format(alsa_output_domain, err,
|
error.Format(alsa_output_domain, err,
|
||||||
"ALSA device \"%s\" does not support %u Hz audio",
|
"ALSA device \"%s\" does not support %u Hz audio",
|
||||||
alsa_device(ad), audio_format.sample_rate);
|
ad->GetDevice(), audio_format.sample_rate);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
audio_format.sample_rate = sample_rate;
|
audio_format.sample_rate = sample_rate;
|
||||||
|
@ -631,16 +648,16 @@ configure_hw:
|
||||||
error:
|
error:
|
||||||
error.Format(alsa_output_domain, err,
|
error.Format(alsa_output_domain, err,
|
||||||
"Error opening ALSA device \"%s\" (%s): %s",
|
"Error opening ALSA device \"%s\" (%s): %s",
|
||||||
alsa_device(ad), cmd, snd_strerror(-err));
|
ad->GetDevice(), cmd, snd_strerror(-err));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
inline bool
|
||||||
alsa_setup_dop(AlsaOutput *ad, const AudioFormat audio_format,
|
AlsaOutput::SetupDop(const AudioFormat audio_format,
|
||||||
bool *shift8_r, bool *packed_r, bool *reverse_endian_r,
|
bool *shift8_r, bool *packed_r, bool *reverse_endian_r,
|
||||||
Error &error)
|
Error &error)
|
||||||
{
|
{
|
||||||
assert(ad->dop);
|
assert(dop);
|
||||||
assert(audio_format.format == SampleFormat::DSD);
|
assert(audio_format.format == SampleFormat::DSD);
|
||||||
|
|
||||||
/* pass 24 bit to alsa_setup() */
|
/* pass 24 bit to alsa_setup() */
|
||||||
|
@ -651,7 +668,7 @@ alsa_setup_dop(AlsaOutput *ad, const AudioFormat audio_format,
|
||||||
|
|
||||||
const AudioFormat check = dop_format;
|
const AudioFormat check = dop_format;
|
||||||
|
|
||||||
if (!alsa_setup(ad, dop_format, packed_r, reverse_endian_r, error))
|
if (!alsa_setup(this, dop_format, packed_r, reverse_endian_r, error))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
/* if the device allows only 32 bit, shift all DoP
|
/* if the device allows only 32 bit, shift all DoP
|
||||||
|
@ -668,102 +685,91 @@ alsa_setup_dop(AlsaOutput *ad, const AudioFormat audio_format,
|
||||||
for DSD over USB */
|
for DSD over USB */
|
||||||
error.Format(alsa_output_domain,
|
error.Format(alsa_output_domain,
|
||||||
"Failed to configure DSD-over-PCM on ALSA device \"%s\"",
|
"Failed to configure DSD-over-PCM on ALSA device \"%s\"",
|
||||||
alsa_device(ad));
|
GetDevice());
|
||||||
delete[] ad->silence;
|
delete[] silence;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
inline bool
|
||||||
alsa_setup_or_dop(AlsaOutput *ad, AudioFormat &audio_format,
|
AlsaOutput::SetupOrDop(AudioFormat &audio_format, Error &error)
|
||||||
Error &error)
|
|
||||||
{
|
{
|
||||||
bool shift8 = false, packed, reverse_endian;
|
bool shift8 = false, packed, reverse_endian;
|
||||||
|
|
||||||
const bool dop = ad->dop &&
|
const bool dop2 = dop &&
|
||||||
audio_format.format == SampleFormat::DSD;
|
audio_format.format == SampleFormat::DSD;
|
||||||
const bool success = dop
|
const bool success = dop2
|
||||||
? alsa_setup_dop(ad, audio_format,
|
? SetupDop(audio_format,
|
||||||
&shift8, &packed, &reverse_endian,
|
&shift8, &packed, &reverse_endian,
|
||||||
error)
|
error)
|
||||||
: alsa_setup(ad, audio_format, &packed, &reverse_endian,
|
: alsa_setup(this, audio_format, &packed, &reverse_endian,
|
||||||
error);
|
error);
|
||||||
if (!success)
|
if (!success)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
ad->pcm_export->Open(audio_format.format,
|
pcm_export->Open(audio_format.format,
|
||||||
audio_format.channels,
|
audio_format.channels,
|
||||||
dop, shift8, packed, reverse_endian);
|
dop2, shift8, packed, reverse_endian);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
inline bool
|
||||||
alsa_open(AudioOutput *ao, AudioFormat &audio_format, Error &error)
|
AlsaOutput::Open(AudioFormat &audio_format, Error &error)
|
||||||
{
|
{
|
||||||
AlsaOutput *ad = (AlsaOutput *)ao;
|
int err = snd_pcm_open(&pcm, GetDevice(),
|
||||||
|
SND_PCM_STREAM_PLAYBACK, mode);
|
||||||
int err = snd_pcm_open(&ad->pcm, alsa_device(ad),
|
|
||||||
SND_PCM_STREAM_PLAYBACK, ad->mode);
|
|
||||||
if (err < 0) {
|
if (err < 0) {
|
||||||
error.Format(alsa_output_domain, err,
|
error.Format(alsa_output_domain, err,
|
||||||
"Failed to open ALSA device \"%s\": %s",
|
"Failed to open ALSA device \"%s\": %s",
|
||||||
alsa_device(ad), snd_strerror(err));
|
GetDevice(), snd_strerror(err));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
FormatDebug(alsa_output_domain, "opened %s type=%s",
|
FormatDebug(alsa_output_domain, "opened %s type=%s",
|
||||||
snd_pcm_name(ad->pcm),
|
snd_pcm_name(pcm),
|
||||||
snd_pcm_type_name(snd_pcm_type(ad->pcm)));
|
snd_pcm_type_name(snd_pcm_type(pcm)));
|
||||||
|
|
||||||
if (!alsa_setup_or_dop(ad, audio_format, error)) {
|
if (!SetupOrDop(audio_format, error)) {
|
||||||
snd_pcm_close(ad->pcm);
|
snd_pcm_close(pcm);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ad->in_frame_size = audio_format.GetFrameSize();
|
in_frame_size = audio_format.GetFrameSize();
|
||||||
ad->out_frame_size = ad->pcm_export->GetFrameSize(audio_format);
|
out_frame_size = pcm_export->GetFrameSize(audio_format);
|
||||||
|
|
||||||
ad->must_prepare = false;
|
must_prepare = false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
inline int
|
||||||
* Write silence to the ALSA device.
|
AlsaOutput::Recover(int err)
|
||||||
*/
|
|
||||||
static void
|
|
||||||
alsa_write_silence(AlsaOutput *ad, snd_pcm_uframes_t nframes)
|
|
||||||
{
|
|
||||||
ad->writei(ad->pcm, ad->silence, nframes);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
alsa_recover(AlsaOutput *ad, int err)
|
|
||||||
{
|
{
|
||||||
if (err == -EPIPE) {
|
if (err == -EPIPE) {
|
||||||
FormatDebug(alsa_output_domain,
|
FormatDebug(alsa_output_domain,
|
||||||
"Underrun on ALSA device \"%s\"", alsa_device(ad));
|
"Underrun on ALSA device \"%s\"",
|
||||||
|
GetDevice());
|
||||||
} else if (err == -ESTRPIPE) {
|
} else if (err == -ESTRPIPE) {
|
||||||
FormatDebug(alsa_output_domain,
|
FormatDebug(alsa_output_domain,
|
||||||
"ALSA device \"%s\" was suspended",
|
"ALSA device \"%s\" was suspended",
|
||||||
alsa_device(ad));
|
GetDevice());
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (snd_pcm_state(ad->pcm)) {
|
switch (snd_pcm_state(pcm)) {
|
||||||
case SND_PCM_STATE_PAUSED:
|
case SND_PCM_STATE_PAUSED:
|
||||||
err = snd_pcm_pause(ad->pcm, /* disable */ 0);
|
err = snd_pcm_pause(pcm, /* disable */ 0);
|
||||||
break;
|
break;
|
||||||
case SND_PCM_STATE_SUSPENDED:
|
case SND_PCM_STATE_SUSPENDED:
|
||||||
err = snd_pcm_resume(ad->pcm);
|
err = snd_pcm_resume(pcm);
|
||||||
if (err == -EAGAIN)
|
if (err == -EAGAIN)
|
||||||
return 0;
|
return 0;
|
||||||
/* fall-through to snd_pcm_prepare: */
|
/* fall-through to snd_pcm_prepare: */
|
||||||
case SND_PCM_STATE_SETUP:
|
case SND_PCM_STATE_SETUP:
|
||||||
case SND_PCM_STATE_XRUN:
|
case SND_PCM_STATE_XRUN:
|
||||||
ad->period_position = 0;
|
period_position = 0;
|
||||||
err = snd_pcm_prepare(ad->pcm);
|
err = snd_pcm_prepare(pcm);
|
||||||
break;
|
break;
|
||||||
case SND_PCM_STATE_DISCONNECTED:
|
case SND_PCM_STATE_DISCONNECTED:
|
||||||
break;
|
break;
|
||||||
|
@ -779,67 +785,58 @@ alsa_recover(AlsaOutput *ad, int err)
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
inline void
|
||||||
alsa_drain(AudioOutput *ao)
|
AlsaOutput::Drain()
|
||||||
{
|
{
|
||||||
AlsaOutput *ad = (AlsaOutput *)ao;
|
if (snd_pcm_state(pcm) != SND_PCM_STATE_RUNNING)
|
||||||
|
|
||||||
if (snd_pcm_state(ad->pcm) != SND_PCM_STATE_RUNNING)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (ad->period_position > 0) {
|
if (period_position > 0) {
|
||||||
/* generate some silence to finish the partial
|
/* generate some silence to finish the partial
|
||||||
period */
|
period */
|
||||||
snd_pcm_uframes_t nframes =
|
snd_pcm_uframes_t nframes =
|
||||||
ad->period_frames - ad->period_position;
|
period_frames - period_position;
|
||||||
alsa_write_silence(ad, nframes);
|
WriteSilence(nframes);
|
||||||
}
|
}
|
||||||
|
|
||||||
snd_pcm_drain(ad->pcm);
|
snd_pcm_drain(pcm);
|
||||||
|
|
||||||
ad->period_position = 0;
|
period_position = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
inline void
|
||||||
alsa_cancel(AudioOutput *ao)
|
AlsaOutput::Cancel()
|
||||||
{
|
{
|
||||||
AlsaOutput *ad = (AlsaOutput *)ao;
|
period_position = 0;
|
||||||
|
must_prepare = true;
|
||||||
|
|
||||||
ad->period_position = 0;
|
snd_pcm_drop(pcm);
|
||||||
ad->must_prepare = true;
|
|
||||||
|
|
||||||
snd_pcm_drop(ad->pcm);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
inline void
|
||||||
alsa_close(AudioOutput *ao)
|
AlsaOutput::Close()
|
||||||
{
|
{
|
||||||
AlsaOutput *ad = (AlsaOutput *)ao;
|
snd_pcm_close(pcm);
|
||||||
|
delete[] silence;
|
||||||
snd_pcm_close(ad->pcm);
|
|
||||||
delete[] ad->silence;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static size_t
|
inline size_t
|
||||||
alsa_play(AudioOutput *ao, const void *chunk, size_t size,
|
AlsaOutput::Play(const void *chunk, size_t size, Error &error)
|
||||||
Error &error)
|
|
||||||
{
|
{
|
||||||
AlsaOutput *ad = (AlsaOutput *)ao;
|
|
||||||
|
|
||||||
assert(size > 0);
|
assert(size > 0);
|
||||||
assert(size % ad->in_frame_size == 0);
|
assert(size % in_frame_size == 0);
|
||||||
|
|
||||||
if (ad->must_prepare) {
|
if (must_prepare) {
|
||||||
ad->must_prepare = false;
|
must_prepare = false;
|
||||||
|
|
||||||
int err = snd_pcm_prepare(ad->pcm);
|
int err = snd_pcm_prepare(pcm);
|
||||||
if (err < 0) {
|
if (err < 0) {
|
||||||
error.Set(alsa_output_domain, err, snd_strerror(-err));
|
error.Set(alsa_output_domain, err, snd_strerror(-err));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto e = ad->pcm_export->Export({chunk, size});
|
const auto e = pcm_export->Export({chunk, size});
|
||||||
if (e.size == 0)
|
if (e.size == 0)
|
||||||
/* the DoP (DSD over PCM) filter converts two frames
|
/* the DoP (DSD over PCM) filter converts two frames
|
||||||
at a time and ignores the last odd frame; if there
|
at a time and ignores the last odd frame; if there
|
||||||
|
@ -852,43 +849,45 @@ alsa_play(AudioOutput *ao, const void *chunk, size_t size,
|
||||||
chunk = e.data;
|
chunk = e.data;
|
||||||
size = e.size;
|
size = e.size;
|
||||||
|
|
||||||
assert(size % ad->out_frame_size == 0);
|
assert(size % out_frame_size == 0);
|
||||||
|
|
||||||
size /= ad->out_frame_size;
|
size /= out_frame_size;
|
||||||
assert(size > 0);
|
assert(size > 0);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
snd_pcm_sframes_t ret = ad->writei(ad->pcm, chunk, size);
|
snd_pcm_sframes_t ret = writei(pcm, chunk, size);
|
||||||
if (ret > 0) {
|
if (ret > 0) {
|
||||||
ad->period_position = (ad->period_position + ret)
|
period_position = (period_position + ret)
|
||||||
% ad->period_frames;
|
% period_frames;
|
||||||
|
|
||||||
size_t bytes_written = ret * ad->out_frame_size;
|
size_t bytes_written = ret * out_frame_size;
|
||||||
return ad->pcm_export->CalcSourceSize(bytes_written);
|
return pcm_export->CalcSourceSize(bytes_written);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ret < 0 && ret != -EAGAIN && ret != -EINTR &&
|
if (ret < 0 && ret != -EAGAIN && ret != -EINTR &&
|
||||||
alsa_recover(ad, ret) < 0) {
|
Recover(ret) < 0) {
|
||||||
error.Set(alsa_output_domain, ret, snd_strerror(-ret));
|
error.Set(alsa_output_domain, ret, snd_strerror(-ret));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef AudioOutputWrapper<AlsaOutput> Wrapper;
|
||||||
|
|
||||||
const struct AudioOutputPlugin alsa_output_plugin = {
|
const struct AudioOutputPlugin alsa_output_plugin = {
|
||||||
"alsa",
|
"alsa",
|
||||||
alsa_test_default_device,
|
alsa_test_default_device,
|
||||||
alsa_init,
|
&Wrapper::Init,
|
||||||
alsa_finish,
|
&Wrapper::Finish,
|
||||||
alsa_output_enable,
|
&Wrapper::Enable,
|
||||||
alsa_output_disable,
|
&Wrapper::Disable,
|
||||||
alsa_open,
|
&Wrapper::Open,
|
||||||
alsa_close,
|
&Wrapper::Close,
|
||||||
nullptr,
|
nullptr,
|
||||||
nullptr,
|
nullptr,
|
||||||
alsa_play,
|
&Wrapper::Play,
|
||||||
alsa_drain,
|
&Wrapper::Drain,
|
||||||
alsa_cancel,
|
&Wrapper::Cancel,
|
||||||
nullptr,
|
nullptr,
|
||||||
|
|
||||||
&alsa_mixer_plugin,
|
&alsa_mixer_plugin,
|
||||||
|
|
Loading…
Reference in New Issue