mpv/audio/out/ao_dsound.c

682 lines
22 KiB
C

/*
* Windows DirectSound interface
*
* Copyright (c) 2004 Gabor Szecsi <deje@miki.hu>
*
* This file is part of MPlayer.
*
* MPlayer 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.
*
* MPlayer 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 MPlayer; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
/**
\todo verify/extend multichannel support
*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#define DIRECTSOUND_VERSION 0x0600
#include <dsound.h>
#include <math.h>
#include <libavutil/avutil.h>
#include <libavutil/common.h>
#include "config.h"
#include "audio/format.h"
#include "ao.h"
#include "audio/reorder_ch.h"
#include "core/mp_msg.h"
#include "osdep/timer.h"
#include "core/subopt-helper.h"
/**
\todo use the definitions from the win32 api headers when they define these
*/
#define WAVE_FORMAT_IEEE_FLOAT 0x0003
#define WAVE_FORMAT_DOLBY_AC3_SPDIF 0x0092
#define WAVE_FORMAT_EXTENSIBLE 0xFFFE
static const GUID KSDATAFORMAT_SUBTYPE_PCM = {
0x1, 0x0000, 0x0010, {0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}
};
#if 0
#define DSSPEAKER_HEADPHONE 0x00000001
#define DSSPEAKER_MONO 0x00000002
#define DSSPEAKER_QUAD 0x00000003
#define DSSPEAKER_STEREO 0x00000004
#define DSSPEAKER_SURROUND 0x00000005
#define DSSPEAKER_5POINT1 0x00000006
#endif
#ifndef _WAVEFORMATEXTENSIBLE_
typedef struct {
WAVEFORMATEX Format;
union {
WORD wValidBitsPerSample; /* bits of precision */
WORD wSamplesPerBlock; /* valid if wBitsPerSample==0 */
WORD wReserved; /* If neither applies, set to zero. */
} Samples;
DWORD dwChannelMask; /* which channels are */
/* present in stream */
GUID SubFormat;
} WAVEFORMATEXTENSIBLE, *PWAVEFORMATEXTENSIBLE;
#endif
struct priv {
HINSTANCE hdsound_dll; ///handle to the dll
LPDIRECTSOUND hds; ///direct sound object
LPDIRECTSOUNDBUFFER hdspribuf; ///primary direct sound buffer
LPDIRECTSOUNDBUFFER hdsbuf; ///secondary direct sound buffer (stream buffer)
int buffer_size; ///size in bytes of the direct sound buffer
int write_offset; ///offset of the write cursor in the direct sound buffer
int min_free_space; ///if the free space is below this value get_space() will return 0
///there will always be at least this amout of free space to prevent
///get_space() from returning wrong values when buffer is 100% full.
///will be replaced with nBlockAlign in init()
int underrun_check; ///0 or last reported free space (underrun detection)
int device_num; ///wanted device number
GUID device; ///guid of the device
int audio_volume;
int device_index;
int outburst; ///play in multiple of chunks of this size
};
static float get_delay(struct ao *ao);
/***************************************************************************************/
/**
\brief output error message
\param err error code
\return string with the error message
*/
static char * dserr2str(int err)
{
switch (err) {
case DS_OK: return "DS_OK";
case DS_NO_VIRTUALIZATION: return "DS_NO_VIRTUALIZATION";
case DSERR_ALLOCATED: return "DS_NO_VIRTUALIZATION";
case DSERR_CONTROLUNAVAIL: return "DSERR_CONTROLUNAVAIL";
case DSERR_INVALIDPARAM: return "DSERR_INVALIDPARAM";
case DSERR_INVALIDCALL: return "DSERR_INVALIDCALL";
case DSERR_GENERIC: return "DSERR_GENERIC";
case DSERR_PRIOLEVELNEEDED: return "DSERR_PRIOLEVELNEEDED";
case DSERR_OUTOFMEMORY: return "DSERR_OUTOFMEMORY";
case DSERR_BADFORMAT: return "DSERR_BADFORMAT";
case DSERR_UNSUPPORTED: return "DSERR_UNSUPPORTED";
case DSERR_NODRIVER: return "DSERR_NODRIVER";
case DSERR_ALREADYINITIALIZED: return "DSERR_ALREADYINITIALIZED";
case DSERR_NOAGGREGATION: return "DSERR_NOAGGREGATION";
case DSERR_BUFFERLOST: return "DSERR_BUFFERLOST";
case DSERR_OTHERAPPHASPRIO: return "DSERR_OTHERAPPHASPRIO";
case DSERR_UNINITIALIZED: return "DSERR_UNINITIALIZED";
case DSERR_NOINTERFACE: return "DSERR_NOINTERFACE";
case DSERR_ACCESSDENIED: return "DSERR_ACCESSDENIED";
}
return "unknown";
}
/**
\brief uninitialize direct sound
*/
static void UninitDirectSound(struct ao *ao)
{
struct priv *p = ao->priv;
// finally release the DirectSound object
if (p->hds) {
IDirectSound_Release(p->hds);
p->hds = NULL;
}
// free DSOUND.DLL
if (p->hdsound_dll) {
FreeLibrary(p->hdsound_dll);
p->hdsound_dll = NULL;
}
mp_msg(MSGT_AO, MSGL_V, "ao_dsound: DirectSound uninitialized\n");
}
/**
\brief print the commandline help
*/
static void print_help(void)
{
mp_msg(MSGT_AO, MSGL_FATAL,
"\n-ao dsound commandline help:\n"
"Example: mpv -ao dsound:device=1\n"
" sets 1st device\n"
"\nOptions:\n"
" device=<device-number>\n"
" Sets device number, use -v to get a list\n");
}
/**
\brief enumerate direct sound devices
\return TRUE to continue with the enumeration
*/
static BOOL CALLBACK DirectSoundEnum(LPGUID guid, LPCSTR desc, LPCSTR module,
LPVOID context)
{
struct ao *ao = context;
struct priv *p = ao->priv;
mp_msg(MSGT_AO, MSGL_V, "%i %s ", p->device_index, desc);
if (p->device_num == p->device_index) {
mp_msg(MSGT_AO, MSGL_V, "<--");
if (guid)
memcpy(&p->device, guid, sizeof(GUID));
}
mp_msg(MSGT_AO, MSGL_V, "\n");
p->device_index++;
return TRUE;
}
/**
\brief initilize direct sound
\return 0 if error, 1 if ok
*/
static int InitDirectSound(struct ao *ao, char *params)
{
struct priv *p = ao->priv;
DSCAPS dscaps;
// initialize directsound
HRESULT (WINAPI *OurDirectSoundCreate)(LPGUID, LPDIRECTSOUND *, LPUNKNOWN);
HRESULT (WINAPI *OurDirectSoundEnumerate)(LPDSENUMCALLBACKA, LPVOID);
p->device_index = 0;
const opt_t subopts[] = {
{"device", OPT_ARG_INT, &p->device_num, NULL},
{NULL}
};
if (subopt_parse(params, subopts) != 0) {
print_help();
return 0;
}
p->hdsound_dll = LoadLibrary("DSOUND.DLL");
if (p->hdsound_dll == NULL) {
mp_msg(MSGT_AO, MSGL_ERR, "ao_dsound: cannot load DSOUND.DLL\n");
return 0;
}
OurDirectSoundCreate = (void *)GetProcAddress(p->hdsound_dll,
"DirectSoundCreate");
OurDirectSoundEnumerate = (void *)GetProcAddress(p->hdsound_dll,
"DirectSoundEnumerateA");
if (OurDirectSoundCreate == NULL || OurDirectSoundEnumerate == NULL) {
mp_msg(MSGT_AO, MSGL_ERR, "ao_dsound: GetProcAddress FAILED\n");
FreeLibrary(p->hdsound_dll);
return 0;
}
// Enumerate all directsound p->devices
mp_msg(MSGT_AO, MSGL_V, "ao_dsound: Output Devices:\n");
OurDirectSoundEnumerate(DirectSoundEnum, ao);
// Create the direct sound object
if (FAILED(OurDirectSoundCreate((p->device_num) ? &p->device : NULL,
&p->hds, NULL)))
{
mp_msg(MSGT_AO, MSGL_ERR,
"ao_dsound: cannot create a DirectSound device\n");
FreeLibrary(p->hdsound_dll);
return 0;
}
/* Set DirectSound Cooperative level, ie what control we want over Windows
* sound device. In our case, DSSCL_EXCLUSIVE means that we can modify the
* settings of the primary buffer, but also that only the sound of our
* application will be hearable when it will have the focus.
* !!! (this is not really working as intended yet because to set the
* cooperative level you need the window handle of your application, and
* I don't know of any easy way to get it. Especially since we might play
* sound without any video, and so what window handle should we use ???
* The hack for now is to use the Desktop window handle - it seems to be
* working */
if (IDirectSound_SetCooperativeLevel(p->hds, GetDesktopWindow(),
DSSCL_EXCLUSIVE))
{
mp_msg(MSGT_AO, MSGL_ERR,
"ao_dsound: cannot set direct sound cooperative level\n");
IDirectSound_Release(p->hds);
FreeLibrary(p->hdsound_dll);
return 0;
}
mp_msg(MSGT_AO, MSGL_V, "ao_dsound: DirectSound initialized\n");
memset(&dscaps, 0, sizeof(DSCAPS));
dscaps.dwSize = sizeof(DSCAPS);
if (DS_OK == IDirectSound_GetCaps(p->hds, &dscaps)) {
if (dscaps.dwFlags & DSCAPS_EMULDRIVER)
mp_msg(MSGT_AO, MSGL_V,
"ao_dsound: DirectSound is emulated, waveOut may give better performance\n");
} else {
mp_msg(MSGT_AO, MSGL_V, "ao_dsound: cannot get device capabilities\n");
}
return 1;
}
/**
\brief destroy the direct sound buffer
*/
static void DestroyBuffer(struct ao *ao)
{
struct priv *p = ao->priv;
if (p->hdsbuf) {
IDirectSoundBuffer_Release(p->hdsbuf);
p->hdsbuf = NULL;
}
if (p->hdspribuf) {
IDirectSoundBuffer_Release(p->hdspribuf);
p->hdspribuf = NULL;
}
}
/**
\brief fill sound buffer
\param data pointer to the sound data to copy
\param len length of the data to copy in bytes
\return number of copyed bytes
*/
static int write_buffer(struct ao *ao, unsigned char *data, int len)
{
struct priv *p = ao->priv;
HRESULT res;
LPVOID lpvPtr1;
DWORD dwBytes1;
LPVOID lpvPtr2;
DWORD dwBytes2;
p->underrun_check = 0;
// Lock the buffer
res = IDirectSoundBuffer_Lock(p->hdsbuf, p->write_offset, len, &lpvPtr1,
&dwBytes1, &lpvPtr2, &dwBytes2, 0);
// If the buffer was lost, restore and retry lock.
if (DSERR_BUFFERLOST == res) {
IDirectSoundBuffer_Restore(p->hdsbuf);
res = IDirectSoundBuffer_Lock(p->hdsbuf, p->write_offset, len, &lpvPtr1,
&dwBytes1, &lpvPtr2, &dwBytes2, 0);
}
if (SUCCEEDED(res)) {
if (!AF_FORMAT_IS_AC3(ao->format)) {
memcpy(lpvPtr1, data, dwBytes1);
if (lpvPtr2 != NULL)
memcpy(lpvPtr2, (char *)data + dwBytes1, dwBytes2);
p->write_offset += dwBytes1 + dwBytes2;
if (p->write_offset >= p->buffer_size)
p->write_offset = dwBytes2;
} else {
// Write to pointers without reordering.
memcpy(lpvPtr1, data, dwBytes1);
if (NULL != lpvPtr2)
memcpy(lpvPtr2, data + dwBytes1, dwBytes2);
p->write_offset += dwBytes1 + dwBytes2;
if (p->write_offset >= p->buffer_size)
p->write_offset = dwBytes2;
}
// Release the data back to DirectSound.
res = IDirectSoundBuffer_Unlock(p->hdsbuf, lpvPtr1, dwBytes1, lpvPtr2,
dwBytes2);
if (SUCCEEDED(res)) {
// Success.
DWORD status;
IDirectSoundBuffer_GetStatus(p->hdsbuf, &status);
if (!(status & DSBSTATUS_PLAYING))
res = IDirectSoundBuffer_Play(p->hdsbuf, 0, 0, DSBPLAY_LOOPING);
return dwBytes1 + dwBytes2;
}
}
// Lock, Unlock, or Restore failed.
return 0;
}
/***************************************************************************************/
/**
\brief handle control commands
\param cmd command
\param arg argument
\return CONTROL_OK or CONTROL_UNKNOWN in case the command is not supported
*/
static int control(struct ao *ao, enum aocontrol cmd, void *arg)
{
struct priv *p = ao->priv;
DWORD volume;
switch (cmd) {
case AOCONTROL_GET_VOLUME: {
ao_control_vol_t *vol = (ao_control_vol_t *)arg;
vol->left = vol->right = p->audio_volume;
return CONTROL_OK;
}
case AOCONTROL_SET_VOLUME: {
ao_control_vol_t *vol = (ao_control_vol_t *)arg;
volume = p->audio_volume = vol->right;
if (volume < 1)
volume = 1;
volume = (DWORD)(log10(volume) * 5000.0) - 10000;
IDirectSoundBuffer_SetVolume(p->hdsbuf, volume);
return CONTROL_OK;
}
}
return CONTROL_UNKNOWN;
}
/**
\brief setup sound device
\param rate samplerate
\param channels number of channels
\param format format
\param flags unused
\return 0=success -1=fail
*/
static int init(struct ao *ao, char *params)
{
struct priv *p = talloc_zero(ao, struct priv);
int res;
ao->priv = p;
if (!InitDirectSound(ao, params))
return -1;
ao->no_persistent_volume = true;
p->audio_volume = 100;
// ok, now create the buffers
WAVEFORMATEXTENSIBLE wformat;
DSBUFFERDESC dsbpridesc;
DSBUFFERDESC dsbdesc;
int format = ao->format;
int rate = ao->samplerate;
if (AF_FORMAT_IS_AC3(format))
format = AF_FORMAT_AC3_NE;
else {
struct mp_chmap_sel sel = {0};
mp_chmap_sel_add_waveext(&sel);
if (!ao_chmap_sel_adjust(ao, &sel, &ao->channels))
return -1;
}
switch (format) {
case AF_FORMAT_AC3_NE:
case AF_FORMAT_S24_LE:
case AF_FORMAT_S16_LE:
case AF_FORMAT_U8:
break;
default:
mp_msg(MSGT_AO, MSGL_V,
"ao_dsound: format %s not supported defaulting to Signed 16-bit Little-Endian\n",
af_fmt2str_short(format));
format = AF_FORMAT_S16_LE;
}
//set our audio parameters
ao->samplerate = rate;
ao->format = format;
ao->bps = ao->channels.num * rate * (af_fmt2bits(format) >> 3);
int buffersize = ao->bps; // space for 1 sec
mp_msg(MSGT_AO, MSGL_V,
"ao_dsound: Samplerate:%iHz Channels:%i Format:%s\n", rate,
ao->channels.num, af_fmt2str_short(format));
mp_msg(MSGT_AO, MSGL_V, "ao_dsound: Buffersize:%d bytes (%d msec)\n",
buffersize, buffersize / ao->bps * 1000);
//fill waveformatex
ZeroMemory(&wformat, sizeof(WAVEFORMATEXTENSIBLE));
wformat.Format.cbSize = (ao->channels.num > 2)
? sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX) : 0;
wformat.Format.nChannels = ao->channels.num;
wformat.Format.nSamplesPerSec = rate;
if (AF_FORMAT_IS_AC3(format)) {
wformat.Format.wFormatTag = WAVE_FORMAT_DOLBY_AC3_SPDIF;
wformat.Format.wBitsPerSample = 16;
wformat.Format.nBlockAlign = 4;
} else {
wformat.Format.wFormatTag = (ao->channels.num > 2)
? WAVE_FORMAT_EXTENSIBLE : WAVE_FORMAT_PCM;
wformat.Format.wBitsPerSample = af_fmt2bits(format);
wformat.Format.nBlockAlign = wformat.Format.nChannels *
(wformat.Format.wBitsPerSample >> 3);
}
// fill in primary sound buffer descriptor
memset(&dsbpridesc, 0, sizeof(DSBUFFERDESC));
dsbpridesc.dwSize = sizeof(DSBUFFERDESC);
dsbpridesc.dwFlags = DSBCAPS_PRIMARYBUFFER;
dsbpridesc.dwBufferBytes = 0;
dsbpridesc.lpwfxFormat = NULL;
// fill in the secondary sound buffer (=stream buffer) descriptor
memset(&dsbdesc, 0, sizeof(DSBUFFERDESC));
dsbdesc.dwSize = sizeof(DSBUFFERDESC);
dsbdesc.dwFlags = DSBCAPS_GETCURRENTPOSITION2 /** Better position accuracy */
| DSBCAPS_GLOBALFOCUS /** Allows background playing */
| DSBCAPS_CTRLVOLUME; /** volume control enabled */
if (ao->channels.num > 2) {
wformat.dwChannelMask = mp_chmap_to_waveext(&ao->channels);
wformat.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
wformat.Samples.wValidBitsPerSample = wformat.Format.wBitsPerSample;
// Needed for 5.1 on emu101k - shit soundblaster
dsbdesc.dwFlags |= DSBCAPS_LOCHARDWARE;
}
wformat.Format.nAvgBytesPerSec = wformat.Format.nSamplesPerSec *
wformat.Format.nBlockAlign;
dsbdesc.dwBufferBytes = buffersize;
dsbdesc.lpwfxFormat = (WAVEFORMATEX *)&wformat;
p->buffer_size = dsbdesc.dwBufferBytes;
p->write_offset = 0;
p->min_free_space = wformat.Format.nBlockAlign;
p->outburst = wformat.Format.nBlockAlign * 512;
// create primary buffer and set its format
res = IDirectSound_CreateSoundBuffer(p->hds, &dsbpridesc, &p->hdspribuf, NULL);
if (res != DS_OK) {
UninitDirectSound(ao);
mp_msg(MSGT_AO, MSGL_ERR,
"ao_dsound: cannot create primary buffer (%s)\n",
dserr2str(res));
return -1;
}
res = IDirectSoundBuffer_SetFormat(p->hdspribuf, (WAVEFORMATEX *)&wformat);
if (res != DS_OK) {
mp_msg(MSGT_AO, MSGL_WARN,
"ao_dsound: cannot set primary buffer format (%s), using "
"standard setting (bad quality)", dserr2str(res));
}
mp_msg(MSGT_AO, MSGL_V, "ao_dsound: primary buffer created\n");
// now create the stream buffer
res = IDirectSound_CreateSoundBuffer(p->hds, &dsbdesc, &p->hdsbuf, NULL);
if (res != DS_OK) {
if (dsbdesc.dwFlags & DSBCAPS_LOCHARDWARE) {
// Try without DSBCAPS_LOCHARDWARE
dsbdesc.dwFlags &= ~DSBCAPS_LOCHARDWARE;
res = IDirectSound_CreateSoundBuffer(p->hds, &dsbdesc, &p->hdsbuf, NULL);
}
if (res != DS_OK) {
UninitDirectSound(ao);
mp_msg(MSGT_AO, MSGL_ERR,
"ao_dsound: cannot create secondary (stream)buffer (%s)\n",
dserr2str(res));
return -1;
}
}
mp_msg(MSGT_AO, MSGL_V, "ao_dsound: secondary (stream)buffer created\n");
return 0;
}
/**
\brief stop playing and empty buffers (for seeking/pause)
*/
static void reset(struct ao *ao)
{
struct priv *p = ao->priv;
IDirectSoundBuffer_Stop(p->hdsbuf);
// reset directsound buffer
IDirectSoundBuffer_SetCurrentPosition(p->hdsbuf, 0);
p->write_offset = 0;
p->underrun_check = 0;
}
/**
\brief stop playing, keep buffers (for pause)
*/
static void audio_pause(struct ao *ao)
{
struct priv *p = ao->priv;
IDirectSoundBuffer_Stop(p->hdsbuf);
}
/**
\brief resume playing, after audio_pause()
*/
static void audio_resume(struct ao *ao)
{
struct priv *p = ao->priv;
IDirectSoundBuffer_Play(p->hdsbuf, 0, 0, DSBPLAY_LOOPING);
}
/**
\brief close audio device
\param immed stop playback immediately
*/
static void uninit(struct ao *ao, bool immed)
{
if (!immed)
mp_sleep_us(get_delay(ao) * 1000000);
reset(ao);
DestroyBuffer(ao);
UninitDirectSound(ao);
}
// return exact number of free (safe to write) bytes
static int check_free_buffer_size(struct ao *ao)
{
struct priv *p = ao->priv;
int space;
DWORD play_offset;
IDirectSoundBuffer_GetCurrentPosition(p->hdsbuf, &play_offset, NULL);
space = p->buffer_size - (p->write_offset - play_offset);
// | | <-- const --> | | |
// buffer start play_cursor write_cursor p->write_offset buffer end
// play_cursor is the actual postion of the play cursor
// write_cursor is the position after which it is assumed to be save to write data
// p->write_offset is the postion where we actually write the data to
if (space > p->buffer_size)
space -= p->buffer_size; // p->write_offset < play_offset
// Check for buffer underruns. An underrun happens if DirectSound
// started to play old data beyond the current p->write_offset. Detect this
// by checking whether the free space shrinks, even though no data was
// written (i.e. no write_buffer). Doesn't always work, but the only
// reason we need this is to deal with the situation when playback ends,
// and the buffer is only half-filled.
if (space < p->underrun_check) {
// there's no useful data in the buffers
space = p->buffer_size;
reset(ao);
}
p->underrun_check = space;
return space;
}
/**
\brief find out how many bytes can be written into the audio buffer without
\return free space in bytes, has to return 0 if the buffer is almost full
*/
static int get_space(struct ao *ao)
{
struct priv *p = ao->priv;
int space = check_free_buffer_size(ao);
if (space < p->min_free_space)
return 0;
return space - p->min_free_space;
}
/**
\brief play 'len' bytes of 'data'
\param data pointer to the data to play
\param len size in bytes of the data buffer, gets rounded down to outburst*n
\param flags currently unused
\return number of played bytes
*/
static int play(struct ao *ao, void *data, int len, int flags)
{
struct priv *p = ao->priv;
int space = check_free_buffer_size(ao);
if (space < len)
len = space;
if (!(flags & AOPLAY_FINAL_CHUNK))
len = (len / p->outburst) * p->outburst;
return write_buffer(ao, data, len);
}
/**
\brief get the delay between the first and last sample in the buffer
\return delay in seconds
*/
static float get_delay(struct ao *ao)
{
struct priv *p = ao->priv;
int space = check_free_buffer_size(ao);
return (float)(p->buffer_size - space) / (float)ao->bps;
}
const struct ao_driver audio_out_dsound = {
.info = &(const struct ao_info) {
"Windows DirectSound audio output",
"dsound",
"Gabor Szecsi <deje@miki.hu>",
""
},
.init = init,
.uninit = uninit,
.control = control,
.get_space = get_space,
.play = play,
.get_delay = get_delay,
.pause = audio_pause,
.resume = audio_resume,
.reset = reset,
};