mirror of
https://github.com/mpv-player/mpv
synced 2024-12-25 08:12:17 +00:00
ao_portaudio: add new PortAudio audio output driver
This AO has potential to be useful on platforms other than Linux. On Windows in particular, PortAudio can make use of newer/better audio APIs like WASAPI, instead of DirectSound. As an implementation choice, the PortAudio callback API was used. The blocking API might be a better match for mplayer's requirements, but caused severe problems on Linux/ALSA (possibly PortAudio bugs).
This commit is contained in:
parent
bb90802717
commit
cd21ce3779
1
Makefile
1
Makefile
@ -468,6 +468,7 @@ SRCS_MPLAYER-$(OPENAL) += libao2/ao_openal.c
|
||||
SRCS_MPLAYER-$(OSS) += libao2/ao_oss.c
|
||||
SRCS_MPLAYER-$(PNM) += libvo/vo_pnm.c
|
||||
SRCS_MPLAYER-$(PULSE) += libao2/ao_pulse.c
|
||||
SRCS_MPLAYER-$(PORTAUDIO) += libao2/ao_portaudio.c
|
||||
SRCS_MPLAYER-$(RSOUND) += libao2/ao_rsound.c
|
||||
SRCS_MPLAYER-$(S3FB) += libvo/vo_s3fb.c
|
||||
SRCS_MPLAYER-$(SDL) += libao2/ao_sdl.c libvo/vo_sdl.c libvo/sdl_common.c
|
||||
|
28
configure
vendored
28
configure
vendored
@ -443,6 +443,7 @@ Audio output:
|
||||
--disable-esd disable esd audio output [autodetect]
|
||||
--disable-rsound disable RSound audio output [autodetect]
|
||||
--disable-pulse disable Pulseaudio audio output [autodetect]
|
||||
--disable-portaudio disable PortAudio audio output [autodetect]
|
||||
--disable-jack disable JACK audio output [autodetect]
|
||||
--enable-openal enable OpenAL audio output [disable]
|
||||
--disable-nas disable NAS audio output [autodetect]
|
||||
@ -602,6 +603,7 @@ _arts=auto
|
||||
_esd=auto
|
||||
_rsound=auto
|
||||
_pulse=auto
|
||||
_portaudio=auto
|
||||
_jack=auto
|
||||
_openal=no
|
||||
_libcdio=auto
|
||||
@ -924,6 +926,8 @@ for ac_option do
|
||||
--disable-rsound) _rsound=no ;;
|
||||
--enable-pulse) _pulse=yes ;;
|
||||
--disable-pulse) _pulse=no ;;
|
||||
--enable-portaudio) _portaudio=yes ;;
|
||||
--disable-portaudio) _portaudio=no ;;
|
||||
--enable-jack) _jack=yes ;;
|
||||
--disable-jack) _jack=no ;;
|
||||
--enable-openal) _openal=yes ;;
|
||||
@ -4898,6 +4902,28 @@ else
|
||||
fi
|
||||
|
||||
|
||||
echocheck "PortAudio"
|
||||
if test "$_portaudio" = auto && test "$_pthreads" != yes ; then
|
||||
_portaudio=no
|
||||
res_comment="pthreads not enabled"
|
||||
fi
|
||||
if test "$_portaudio" = auto ; then
|
||||
_portaudio=no
|
||||
if pkg_config_add 'portaudio-2.0 >= 19' ; then
|
||||
_portaudio=yes
|
||||
fi
|
||||
fi
|
||||
echores "$_portaudio"
|
||||
|
||||
if test "$_portaudio" = yes ; then
|
||||
def_portaudio='#define CONFIG_PORTAUDIO 1'
|
||||
aomodules="portaudio $aomodules"
|
||||
else
|
||||
def_portaudio='#undef CONFIG_PORTAUDIO'
|
||||
noaomodules="portaudio $noaomodules"
|
||||
fi
|
||||
|
||||
|
||||
echocheck "JACK"
|
||||
if test "$_jack" = auto ; then
|
||||
_jack=no
|
||||
@ -6433,6 +6459,7 @@ PNG = $_png
|
||||
PNM = $_pnm
|
||||
PRIORITY = $_priority
|
||||
PULSE = $_pulse
|
||||
PORTAUDIO = $_portaudio
|
||||
PVR = $_pvr
|
||||
QTX_CODECS = $_qtx
|
||||
QTX_CODECS_WIN32 = $_qtx_codecs_win32
|
||||
@ -6710,6 +6737,7 @@ $def_ossaudio
|
||||
$def_ossaudio_devdsp
|
||||
$def_ossaudio_devmixer
|
||||
$def_pulse
|
||||
$def_portaudio
|
||||
$def_rsound
|
||||
$def_sgiaudio
|
||||
$def_sunaudio
|
||||
|
431
libao2/ao_portaudio.c
Normal file
431
libao2/ao_portaudio.c
Normal file
@ -0,0 +1,431 @@
|
||||
/*
|
||||
* This file is part of mplayer2.
|
||||
*
|
||||
* mplayer2 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 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* mplayer2 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 mplayer2. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <assert.h>
|
||||
#include <pthread.h>
|
||||
|
||||
#include <libavutil/avutil.h>
|
||||
#include <portaudio.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "subopt-helper.h"
|
||||
#include "libaf/af_format.h"
|
||||
#include "mp_msg.h"
|
||||
#include "audio_out.h"
|
||||
|
||||
struct priv {
|
||||
PaStream *stream;
|
||||
int framelen;
|
||||
|
||||
pthread_mutex_t ring_mutex;
|
||||
|
||||
// protected by ring_mutex
|
||||
unsigned char *ring;
|
||||
int ring_size; // max size of the ring
|
||||
int read_pos; // points to first byte that can be read
|
||||
int read_len; // number of bytes that can be read
|
||||
double play_time; // time when last packet returned to PA is on speaker
|
||||
// 0 is N/A (0 is not a valid PA time value)
|
||||
int play_silence; // play this many bytes of silence, before real data
|
||||
bool play_remaining;// play what's left in the buffer, then stop stream
|
||||
};
|
||||
|
||||
struct format_map {
|
||||
int mp_format;
|
||||
PaSampleFormat pa_format;
|
||||
};
|
||||
|
||||
static const struct format_map format_maps[] = {
|
||||
// first entry is the default format
|
||||
{AF_FORMAT_S16_NE, paInt16},
|
||||
{AF_FORMAT_S24_NE, paInt24},
|
||||
{AF_FORMAT_S32_NE, paInt32},
|
||||
{AF_FORMAT_S8, paInt8},
|
||||
{AF_FORMAT_U8, paUInt8},
|
||||
{AF_FORMAT_FLOAT_NE, paFloat32},
|
||||
{AF_FORMAT_UNKNOWN, 0}
|
||||
};
|
||||
|
||||
static void print_help(void)
|
||||
{
|
||||
mp_msg(MSGT_AO, MSGL_FATAL,
|
||||
"\n-ao portaudio commandline help:\n"
|
||||
"Example: mplayer -ao portaudio:device=subdevice\n"
|
||||
"\nOptions:\n"
|
||||
" device=subdevice\n"
|
||||
" Audio device PortAudio should use. Devices can be listed\n"
|
||||
" with -ao portaudio:device=help\n"
|
||||
" The subdevice can be passed as index, or as complete name.\n");
|
||||
}
|
||||
|
||||
static bool check_pa_ret(int ret)
|
||||
{
|
||||
if (ret < 0) {
|
||||
mp_msg(MSGT_AO, MSGL_ERR, "[portaudio] %s\n",
|
||||
Pa_GetErrorText(ret));
|
||||
if (ret == paUnanticipatedHostError) {
|
||||
const PaHostErrorInfo* hosterr = Pa_GetLastHostErrorInfo();
|
||||
mp_msg(MSGT_AO, MSGL_ERR, "[portaudio] Host error: %s\n",
|
||||
hosterr->errorText);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Amount of bytes that contain audio of the given duration, aligned to frames.
|
||||
static int seconds_to_bytes(struct ao *ao, double duration_seconds)
|
||||
{
|
||||
struct priv *priv = ao->priv;
|
||||
|
||||
int bytes = duration_seconds * ao->bps;
|
||||
if (bytes % priv->framelen)
|
||||
bytes += priv->framelen - (bytes % priv->framelen);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
static int to_int(const char *s, int return_on_error)
|
||||
{
|
||||
char *endptr;
|
||||
int res = strtol(s, &endptr, 10);
|
||||
return (s[0] && !endptr[0]) ? res : return_on_error;
|
||||
}
|
||||
|
||||
static int find_device(struct ao *ao, const char *name)
|
||||
{
|
||||
int help = strcmp(name, "help") == 0;
|
||||
int count = Pa_GetDeviceCount();
|
||||
check_pa_ret(count);
|
||||
int found = paNoDevice;
|
||||
int index = to_int(name, -1);
|
||||
if (help)
|
||||
mp_msg(MSGT_AO, MSGL_INFO, "PortAudio devices:\n");
|
||||
for (int n = 0; n < count; n++) {
|
||||
const PaDeviceInfo* info = Pa_GetDeviceInfo(n);
|
||||
if (help) {
|
||||
if (info->maxOutputChannels < 1)
|
||||
continue;
|
||||
mp_msg(MSGT_AO, MSGL_INFO, " %d '%s', %d channels, latency: %.2f "
|
||||
"ms, sample rate: %.0f\n", n, info->name,
|
||||
info->maxOutputChannels,
|
||||
info->defaultHighOutputLatency * 1000,
|
||||
info->defaultSampleRate);
|
||||
}
|
||||
if (strcmp(name, info->name) == 0 || n == index) {
|
||||
found = n;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found == paNoDevice && !help)
|
||||
mp_msg(MSGT_AO, MSGL_FATAL, "[portaudio] Device '%s' not found!\n",
|
||||
name);
|
||||
return found;
|
||||
}
|
||||
|
||||
static int ring_write(struct ao *ao, unsigned char *data, int len)
|
||||
{
|
||||
struct priv *priv = ao->priv;
|
||||
|
||||
int free = priv->ring_size - priv->read_len;
|
||||
int write_pos = (priv->read_pos + priv->read_len) % priv->ring_size;
|
||||
int write_len = FFMIN(len, free);
|
||||
int len1 = FFMIN(priv->ring_size - write_pos, write_len);
|
||||
int len2 = write_len - len1;
|
||||
|
||||
memcpy(priv->ring + write_pos, data, len1);
|
||||
memcpy(priv->ring, data + len1, len2);
|
||||
|
||||
priv->read_len += write_len;
|
||||
|
||||
return write_len;
|
||||
}
|
||||
|
||||
static int ring_read(struct ao *ao, unsigned char *data, int len)
|
||||
{
|
||||
struct priv *priv = ao->priv;
|
||||
|
||||
int read_len = FFMIN(len, priv->read_len);
|
||||
int len1 = FFMIN(priv->ring_size - priv->read_pos, read_len);
|
||||
int len2 = read_len - len1;
|
||||
|
||||
memcpy(data, priv->ring + priv->read_pos, len1);
|
||||
memcpy(data + len1, priv->ring, len2);
|
||||
|
||||
priv->read_len -= read_len;
|
||||
priv->read_pos = (priv->read_pos + read_len) % priv->ring_size;
|
||||
|
||||
return read_len;
|
||||
}
|
||||
|
||||
static void fill_silence(unsigned char *ptr, int len)
|
||||
{
|
||||
memset(ptr, 0, len);
|
||||
}
|
||||
|
||||
static int stream_callback(const void *input,
|
||||
void *output_v,
|
||||
unsigned long frameCount,
|
||||
const PaStreamCallbackTimeInfo *timeInfo,
|
||||
PaStreamCallbackFlags statusFlags,
|
||||
void *userData)
|
||||
{
|
||||
struct ao *ao = userData;
|
||||
struct priv *priv = ao->priv;
|
||||
int res = paContinue;
|
||||
unsigned char *output = output_v;
|
||||
int len_bytes = frameCount * priv->framelen;
|
||||
|
||||
pthread_mutex_lock(&priv->ring_mutex);
|
||||
|
||||
// NOTE: PA + ALSA in dmix mode seems to pretend that there is no latency
|
||||
// (outputBufferDacTime == currentTime)
|
||||
priv->play_time = timeInfo->outputBufferDacTime
|
||||
+ len_bytes / (float)ao->bps;
|
||||
|
||||
if (priv->play_silence > 0) {
|
||||
int bytes = FFMIN(priv->play_silence, len_bytes);
|
||||
fill_silence(output, bytes);
|
||||
priv->play_silence -= bytes;
|
||||
len_bytes -= bytes;
|
||||
output += bytes;
|
||||
}
|
||||
int read = ring_read(ao, output, len_bytes);
|
||||
len_bytes -= read;
|
||||
output += read;
|
||||
|
||||
if (len_bytes > 0) {
|
||||
if (priv->play_remaining) {
|
||||
res = paComplete;
|
||||
priv->play_remaining = false;
|
||||
} else {
|
||||
mp_msg(MSGT_AO, MSGL_ERR, "[portaudio] Buffer underflow!\n");
|
||||
}
|
||||
fill_silence(output, len_bytes);
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&priv->ring_mutex);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static void uninit(struct ao *ao, bool cut_audio)
|
||||
{
|
||||
struct priv *priv = ao->priv;
|
||||
|
||||
if (priv->stream) {
|
||||
if (!cut_audio && Pa_IsStreamActive(priv->stream) == 1) {
|
||||
pthread_mutex_lock(&priv->ring_mutex);
|
||||
|
||||
priv->play_remaining = true;
|
||||
|
||||
pthread_mutex_unlock(&priv->ring_mutex);
|
||||
|
||||
check_pa_ret(Pa_StopStream(priv->stream));
|
||||
}
|
||||
check_pa_ret(Pa_CloseStream(priv->stream));
|
||||
}
|
||||
|
||||
pthread_mutex_destroy(&priv->ring_mutex);
|
||||
Pa_Terminate();
|
||||
}
|
||||
|
||||
static int init(struct ao *ao, char *params)
|
||||
{
|
||||
struct priv *priv = talloc_zero(ao, struct priv);
|
||||
ao->priv = priv;
|
||||
|
||||
if (!check_pa_ret(Pa_Initialize()))
|
||||
return -1;
|
||||
|
||||
pthread_mutex_init(&priv->ring_mutex, NULL);
|
||||
|
||||
char *device = NULL;
|
||||
const opt_t subopts[] = {
|
||||
{"device", OPT_ARG_MSTRZ, &device, NULL},
|
||||
{NULL}
|
||||
};
|
||||
if (subopt_parse(params, subopts) != 0) {
|
||||
print_help();
|
||||
goto error_exit;
|
||||
}
|
||||
|
||||
int pa_device = Pa_GetDefaultOutputDevice();
|
||||
if (device)
|
||||
pa_device = find_device(ao, device);
|
||||
if (pa_device == paNoDevice)
|
||||
goto error_exit;
|
||||
|
||||
PaStreamParameters sp = {
|
||||
.device = pa_device,
|
||||
.channelCount = ao->channels,
|
||||
.suggestedLatency
|
||||
= Pa_GetDeviceInfo(pa_device)->defaultHighOutputLatency,
|
||||
};
|
||||
|
||||
const struct format_map *fmt = format_maps;
|
||||
while (fmt->pa_format) {
|
||||
if (fmt->mp_format == ao->format) {
|
||||
PaStreamParameters test = sp;
|
||||
test.sampleFormat = fmt->pa_format;
|
||||
if (Pa_IsFormatSupported(NULL, &test, ao->samplerate) == paNoError)
|
||||
break;
|
||||
}
|
||||
fmt++;
|
||||
}
|
||||
if (!fmt->pa_format) {
|
||||
mp_msg(MSGT_AO, MSGL_V,
|
||||
"[portaudio] Unsupported format, using default.\n");
|
||||
fmt = format_maps;
|
||||
}
|
||||
|
||||
ao->format = fmt->mp_format;
|
||||
sp.sampleFormat = fmt->pa_format;
|
||||
priv->framelen = ao->channels * (af_fmt2bits(ao->format) / 8);
|
||||
ao->bps = ao->samplerate * priv->framelen;
|
||||
|
||||
if (!check_pa_ret(Pa_IsFormatSupported(NULL, &sp, ao->samplerate)))
|
||||
goto error_exit;
|
||||
if (!check_pa_ret(Pa_OpenStream(&priv->stream, NULL, &sp, ao->samplerate,
|
||||
paFramesPerBufferUnspecified, paNoFlag,
|
||||
stream_callback, ao)))
|
||||
goto error_exit;
|
||||
|
||||
priv->ring_size = seconds_to_bytes(ao, 0.5);
|
||||
priv->ring = talloc_zero_size(priv, priv->ring_size);
|
||||
|
||||
free(device);
|
||||
return 0;
|
||||
|
||||
error_exit:
|
||||
uninit(ao, true);
|
||||
free(device);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int play(struct ao *ao, void *data, int len, int flags)
|
||||
{
|
||||
struct priv *priv = ao->priv;
|
||||
|
||||
pthread_mutex_lock(&priv->ring_mutex);
|
||||
|
||||
int write_len = ring_write(ao, data, len);
|
||||
if (flags & AOPLAY_FINAL_CHUNK)
|
||||
priv->play_remaining = true;
|
||||
|
||||
pthread_mutex_unlock(&priv->ring_mutex);
|
||||
|
||||
if (Pa_IsStreamStopped(priv->stream) == 1)
|
||||
check_pa_ret(Pa_StartStream(priv->stream));
|
||||
|
||||
return write_len;
|
||||
}
|
||||
|
||||
static int get_space(struct ao *ao)
|
||||
{
|
||||
struct priv *priv = ao->priv;
|
||||
|
||||
pthread_mutex_lock(&priv->ring_mutex);
|
||||
|
||||
int free = priv->ring_size - priv->read_len;
|
||||
|
||||
pthread_mutex_unlock(&priv->ring_mutex);
|
||||
|
||||
return free;
|
||||
}
|
||||
|
||||
static float get_delay(struct ao *ao)
|
||||
{
|
||||
struct priv *priv = ao->priv;
|
||||
|
||||
double stream_time = Pa_GetStreamTime(priv->stream);
|
||||
|
||||
pthread_mutex_lock(&priv->ring_mutex);
|
||||
|
||||
float frame_time = priv->play_time ? priv->play_time - stream_time : 0;
|
||||
float buffer_latency = (priv->read_len + priv->play_silence)
|
||||
/ (float)ao->bps;
|
||||
|
||||
pthread_mutex_unlock(&priv->ring_mutex);
|
||||
|
||||
return buffer_latency + frame_time;
|
||||
}
|
||||
|
||||
static void reset(struct ao *ao)
|
||||
{
|
||||
struct priv *priv = ao->priv;
|
||||
|
||||
if (Pa_IsStreamStopped(priv->stream) != 1)
|
||||
check_pa_ret(Pa_AbortStream(priv->stream));
|
||||
|
||||
pthread_mutex_lock(&priv->ring_mutex);
|
||||
|
||||
priv->read_len = 0;
|
||||
priv->read_pos = 0;
|
||||
priv->play_remaining = false;
|
||||
priv->play_time = 0;
|
||||
priv->play_silence = 0;
|
||||
|
||||
pthread_mutex_unlock(&priv->ring_mutex);
|
||||
}
|
||||
|
||||
static void pause(struct ao *ao)
|
||||
{
|
||||
struct priv *priv = ao->priv;
|
||||
|
||||
check_pa_ret(Pa_AbortStream(priv->stream));
|
||||
|
||||
double stream_time = Pa_GetStreamTime(priv->stream);
|
||||
|
||||
pthread_mutex_lock(&priv->ring_mutex);
|
||||
|
||||
// When playback resumes, replace the lost audio (due to dropping the
|
||||
// portaudio/driver/hardware internal buffers) with silence.
|
||||
float frame_time = priv->play_time ? priv->play_time - stream_time : 0;
|
||||
priv->play_silence += seconds_to_bytes(ao, FFMAX(frame_time, 0));
|
||||
priv->play_time = 0;
|
||||
|
||||
pthread_mutex_unlock(&priv->ring_mutex);
|
||||
}
|
||||
|
||||
static void resume(struct ao *ao)
|
||||
{
|
||||
struct priv *priv = ao->priv;
|
||||
|
||||
check_pa_ret(Pa_StartStream(priv->stream));
|
||||
}
|
||||
|
||||
const struct ao_driver audio_out_portaudio = {
|
||||
.is_new = true,
|
||||
.info = &(const struct ao_info) {
|
||||
"PortAudio",
|
||||
"portaudio",
|
||||
"wm4",
|
||||
"",
|
||||
},
|
||||
.init = init,
|
||||
.uninit = uninit,
|
||||
.reset = reset,
|
||||
.get_space = get_space,
|
||||
.play = play,
|
||||
.get_delay = get_delay,
|
||||
.pause = pause,
|
||||
.resume = resume,
|
||||
};
|
@ -55,6 +55,7 @@ extern const struct ao_driver audio_out_v4l2;
|
||||
extern const struct ao_driver audio_out_mpegpes;
|
||||
extern const struct ao_driver audio_out_pcm;
|
||||
extern const struct ao_driver audio_out_pss;
|
||||
extern const struct ao_driver audio_out_portaudio;
|
||||
|
||||
static const struct ao_driver * const audio_out_drivers[] = {
|
||||
// native:
|
||||
@ -82,6 +83,9 @@ static const struct ao_driver * const audio_out_drivers[] = {
|
||||
#ifdef CONFIG_OSS_AUDIO
|
||||
&audio_out_oss,
|
||||
#endif
|
||||
#ifdef CONFIG_PORTAUDIO
|
||||
&audio_out_portaudio,
|
||||
#endif
|
||||
#ifdef CONFIG_SGI_AUDIO
|
||||
&audio_out_sgi,
|
||||
#endif
|
||||
|
Loading…
Reference in New Issue
Block a user