Complete rewrite of the DirectSound audio player, this time doing threading and synchronisation better, and following the COM apartment model strictly too. It replaces the old player by default, but the old is still available in the runtime configuration.

Originally committed to SVN as r2495.
This commit is contained in:
Niels Martin Hansen 2008-11-27 18:35:26 +00:00
parent bfb3f48cba
commit a6c169a4b2
7 changed files with 821 additions and 2 deletions

View file

@ -147,6 +147,7 @@ EXTRA_aegisub_SOURCES = \
font_file_lister.cpp \
$(FONT_LISTER) \
audio_player_dsound.cpp \
audio_player_dsound2.cpp \
audio_player_portaudio.cpp \
audio_player_pulse.cpp \
audio_provider_avs.cpp \

View file

@ -44,6 +44,7 @@
#endif
#ifdef WITH_DIRECTSOUND
#include "audio_player_dsound.h"
#include "audio_player_dsound2.h"
#endif
#ifdef WITH_OPENAL
#include "audio_player_openal.h"
@ -157,7 +158,8 @@ void AudioPlayerFactoryManager::RegisterProviders() {
RegisterFactory(new AlsaPlayerFactory(),_T("ALSA"));
#endif
#ifdef WITH_DIRECTSOUND
RegisterFactory(new DirectSoundPlayerFactory(),_T("DirectSound"));
RegisterFactory(new DirectSoundPlayerFactory(),_T("DirectSound-old"));
RegisterFactory(new DirectSoundPlayer2Factory(),_T("DirectSound"));
#endif
#ifdef WITH_OPENAL
RegisterFactory(new OpenALPlayerFactory(),_T("OpenAL"));

View file

@ -0,0 +1,723 @@
// Copyright (c) 2008, 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
//
// Website: http://www.aegisub.net/
// Contact: mailto:jiifurusu@gmail.com
//
///////////
// Headers
#ifdef WITH_DIRECTSOUND
#include <wx/wxprec.h>
#include <mmsystem.h>
#include <dsound.h>
#include <process.h>
#include "include/aegisub/audio_provider.h"
#include "utils.h"
#include "main.h"
#include "frame_main.h"
#include "audio_player_dsound2.h"
struct COMInitialization {
bool inited;
COMInitialization()
{
inited = false;
}
~COMInitialization()
{
if (inited) CoUninitialize();
}
void Init()
{
if (!inited)
{
if (FAILED(CoInitialize(NULL)))
throw std::exception();
inited = true;
}
}
};
template<class T>
struct COMObjectRetainer {
T *obj;
COMObjectRetainer()
{
obj = 0;
}
COMObjectRetainer(T *_obj)
{
obj = _obj;
}
~COMObjectRetainer()
{
if (obj) obj->Release();
}
T * operator -> ()
{
return obj;
}
};
class DirectSoundPlayer2Thread {
static unsigned int __stdcall ThreadProc(void *parameter);
void Run();
DWORD FillAndUnlockBuffers(void *buf1, DWORD buf1sz, void *buf2, DWORD buf2sz, int64_t &input_frame, IDirectSoundBuffer8 *bfr);
void CheckError();
HANDLE thread_handle;
// Used to signal state-changes to thread
HANDLE
event_start_playback,
event_stop_playback,
event_update_end_time,
event_set_volume,
event_kill_self;
// Thread communicating back
HANDLE
thread_running,
is_playing,
error_happened;
wxChar *error_message;
double volume;
int64_t start_frame;
int64_t end_frame;
DWORD last_playback_restart;
AudioProvider *provider;
public:
DirectSoundPlayer2Thread(AudioProvider *provider);
~DirectSoundPlayer2Thread();
void Play(int64_t start, int64_t count);
void Stop();
void SetEndFrame(int64_t new_end_frame);
void SetVolume(double new_volume);
bool IsPlaying();
int64_t GetStartFrame();
int64_t GetCurrentFrame();
int64_t GetEndFrame();
double GetVolume();
};
unsigned int __stdcall DirectSoundPlayer2Thread::ThreadProc(void *parameter)
{
static_cast<DirectSoundPlayer2Thread*>(parameter)->Run();
return 0;
}
#define WANTED_LATENCY 100
#define BUFFER_LENGTH 5
// The buffer will hold BUFFER_LENGTH times WANTED_LATENCY milliseconds of audio
void DirectSoundPlayer2Thread::Run()
{
#define REPORT_ERROR(msg) { error_message = _T("DirectSoundPlayer2Thread: ") _T(msg); SetEvent(error_happened); return; }
COMInitialization COM_library;
try { COM_library.Init(); }
catch (std::exception e)
REPORT_ERROR("Could not initialise COM")
// Create DirectSound object
COMObjectRetainer<IDirectSound8> ds;
if (FAILED(DirectSoundCreate8(&DSDEVID_DefaultPlayback, &ds.obj, NULL)))
REPORT_ERROR("Cound not create DirectSound object")
// Ensure we can get interesting wave formats (unless we have PRIORITY we can only use a standard 8 bit format)
ds->SetCooperativeLevel((HWND)static_cast<AegisubApp*>(wxApp::GetInstance())->frame->GetHandle(), DSSCL_PRIORITY);
// Describe the wave format
WAVEFORMATEX waveFormat;
waveFormat.wFormatTag = WAVE_FORMAT_PCM;
waveFormat.nSamplesPerSec = provider->GetSampleRate();
waveFormat.nChannels = provider->GetChannels();
waveFormat.wBitsPerSample = provider->GetBytesPerSample() * 8;
waveFormat.nBlockAlign = waveFormat.nChannels * waveFormat.wBitsPerSample / 8;
waveFormat.nAvgBytesPerSec = waveFormat.nSamplesPerSec * waveFormat.nBlockAlign;
waveFormat.cbSize = sizeof(waveFormat);
// And the buffer itself
int aim = waveFormat.nAvgBytesPerSec * (WANTED_LATENCY*BUFFER_LENGTH)/1000;
int min = DSBSIZE_MIN;
int max = DSBSIZE_MAX;
DWORD bufSize = MIN(MAX(min,aim),max); // size of entier playback buffer
DSBUFFERDESC desc;
desc.dwSize = sizeof(DSBUFFERDESC);
desc.dwFlags = DSBCAPS_GETCURRENTPOSITION2 | DSBCAPS_GLOBALFOCUS;
desc.dwBufferBytes = bufSize;
desc.dwReserved = 0;
desc.lpwfxFormat = &waveFormat;
desc.guid3DAlgorithm = GUID_NULL;
// And then create the buffer
IDirectSoundBuffer *bfr7 = 0;
if FAILED(ds->CreateSoundBuffer(&desc, &bfr7, 0))
REPORT_ERROR("Could not create buffer")
// But it's an old version interface we get, query it for the DSound8 interface
COMObjectRetainer<IDirectSoundBuffer8> bfr;
if (FAILED(bfr7->QueryInterface(IID_IDirectSoundBuffer8, (LPVOID*)&bfr.obj)))
REPORT_ERROR("Buffer doesn't support version 8 interface")
bfr7->Release();
bfr7 = 0;
//wxLogDebug(_T("DirectSoundPlayer2: Created buffer of %d bytes, supposed to be %d milliseconds or %d frames"), bufSize, WANTED_LATENCY*BUFFER_LENGTH, bufSize/provider->GetBytesPerSample());
// Now we're ready to roll!
SetEvent(thread_running);
bool running = true;
HANDLE events_to_wait[] = {
event_start_playback,
event_stop_playback,
event_update_end_time,
event_set_volume,
event_kill_self
};
int64_t next_input_frame = 0;
DWORD buffer_offset = 0;
bool playback_should_be_running = false;
while (running)
{
DWORD wait_result = WaitForMultipleObjects(sizeof(events_to_wait)/sizeof(HANDLE), events_to_wait, FALSE, WANTED_LATENCY);
switch (wait_result)
{
case WAIT_OBJECT_0+0:
{
// Start or restart playback
bfr->Stop();
ResetEvent(is_playing);
next_input_frame = start_frame;
DWORD buf_size; // size of buffer locked for filling
void *buf;
buffer_offset = 0;
HRESULT res = bfr->Lock(buffer_offset, 0, &buf, &buf_size, 0, 0, DSBLOCK_ENTIREBUFFER);
while (FAILED(res)) // yes, while, so I can break out of it without a goto!
{
if (res == DSERR_BUFFERLOST)
{
// Try to regain the buffer
if (SUCCEEDED(bfr->Restore()) &&
SUCCEEDED(bfr->Lock(buffer_offset, 0, &buf, &buf_size, 0, 0, DSBLOCK_ENTIREBUFFER)))
{
//wxLogDebug(_T("DirectSoundPlayer2: Lost and restored buffer"));
break;
}
REPORT_ERROR("Lost buffer and could not restore it.")
}
REPORT_ERROR("Could not lock buffer for playback.")
}
buffer_offset += FillAndUnlockBuffers(buf, buf_size, 0, 0, next_input_frame, bfr.obj);
if (buffer_offset >= bufSize) buffer_offset -= bufSize;
if (FAILED(bfr->SetCurrentPosition(0)))
REPORT_ERROR("Could not reset playback buffer cursor before playback.")
if (FAILED(bfr->Play(0, 0, DSBPLAY_LOOPING)))
REPORT_ERROR("Could not start looping playback.")
SetEvent(is_playing);
playback_should_be_running = true;
break;
}
case WAIT_OBJECT_0+1:
{
// Stop playing
bfr->Stop();
ResetEvent(is_playing);
playback_should_be_running = false;
break;
}
case WAIT_OBJECT_0+2:
{
// Set end frame
if (end_frame <= next_input_frame)
{
bfr->Stop();
ResetEvent(is_playing);
playback_should_be_running = false;
}
break;
}
case WAIT_OBJECT_0+3:
{
// Change volume
// We aren't thread safe right now, filling the buffers grabs volume directly
// from the field set by the controlling thread, but it shouldn't be a major
// problem if race conditions do occur, just some momentary distortion.
break;
}
case WAIT_OBJECT_0+4:
{
// Perform suicide
bfr->Stop();
ResetEvent(is_playing);
playback_should_be_running = false;
running = false;
break;
}
case WAIT_TIMEOUT:
{
// Time to fill more into buffer
if (!playback_should_be_running)
break;
DWORD status;
if (FAILED(bfr->GetStatus(&status)))
REPORT_ERROR("Could not get playback buffer status")
if (!(status & DSBSTATUS_LOOPING))
{
// Not really what we expected...
bfr->Stop();
ResetEvent(is_playing);
playback_should_be_running = false;
break;
}
DWORD play_cursor;
if (FAILED(bfr->GetCurrentPosition(&play_cursor, 0)))
REPORT_ERROR("Could not get play cursor position for filling buffer.")
int bytes_needed = (int)play_cursor - (int)buffer_offset;
if (bytes_needed < 0) bytes_needed += (int)bufSize;
DWORD buf1sz, buf2sz;
void *buf1, *buf2;
HRESULT res = bfr->Lock(buffer_offset, bytes_needed, &buf1, &buf1sz, &buf2, &buf2sz, 0);
while (FAILED(res)) // yes, while, so I can break out of it without a goto!
{
if (res == DSERR_BUFFERLOST)
{
// Try to regain the buffer
// When the buffer was lost the entire contents was lost too, so we have to start over
if (SUCCEEDED(bfr->Restore()) &&
SUCCEEDED(bfr->Lock(0, bufSize, &buf1, &buf1sz, &buf2, &buf2sz, 0)) &&
SUCCEEDED(bfr->Play(0, 0, DSBPLAY_LOOPING)))
{
wxLogDebug(_T("DirectSoundPlayer2: Lost and restored buffer"));
break;
}
REPORT_ERROR("Lost buffer and could not restore it.")
}
REPORT_ERROR("Could not lock buffer for filling.")
}
buffer_offset += FillAndUnlockBuffers(buf1, buf1sz, buf2, buf2sz, next_input_frame, bfr.obj);
if (buffer_offset >= bufSize) buffer_offset -= bufSize;
break;
}
default:
REPORT_ERROR("Something bad happened while waiting on events in playback loop, either the wait failed or an event object was abandoned.")
break;
}
}
#undef REPORT_ERROR
}
DWORD DirectSoundPlayer2Thread::FillAndUnlockBuffers(void *buf1, DWORD buf1sz, void *buf2, DWORD buf2sz, int64_t &input_frame, IDirectSoundBuffer8 *bfr)
{
// Assume buffers have been locked and are ready to be filled
DWORD bytes_per_frame = provider->GetChannels() * provider->GetBytesPerSample();
DWORD buf1szf = buf1sz / bytes_per_frame;
DWORD buf2szf = buf2sz / bytes_per_frame;
if (input_frame >= end_frame)
{
// Silence
if (buf1)
memset(buf1, 0, buf1sz);
if (buf2)
memset(buf2, 0, buf2sz);
input_frame += buf1szf + buf2szf;
bfr->Unlock(buf1, buf1sz, buf2, buf2sz); // should be checking for success
return buf1sz + buf2sz;
}
if (buf1 && buf1sz)
{
if (buf1szf + input_frame > end_frame)
{
buf1szf = end_frame - input_frame;
buf1sz = buf1szf * bytes_per_frame;
buf2szf = 0;
buf2sz = 0;
}
provider->GetAudioWithVolume(buf1, input_frame, buf1szf, volume);
input_frame += buf1szf;
}
if (buf2 && buf2sz)
{
if (buf2szf + input_frame > end_frame)
{
buf2szf = end_frame - input_frame;
buf2sz = buf2szf * bytes_per_frame;
}
provider->GetAudioWithVolume(buf2, input_frame, buf2szf, volume);
input_frame += buf2szf;
}
bfr->Unlock(buf1, buf1sz, buf2, buf2sz); // bad? should check for success
return buf1sz + buf2sz;
}
void DirectSoundPlayer2Thread::CheckError()
{
switch (WaitForSingleObject(error_happened, 0))
{
case WAIT_OBJECT_0:
throw error_message;
case WAIT_ABANDONED:
throw _T("The DirectShowPlayer2Thread error signal event was abandoned, somehow. This should not happen.");
case WAIT_FAILED:
throw _T("Failed checking state of DirectShowPlayer2Thread error signal event.");
case WAIT_TIMEOUT:
default:
return;
}
}
DirectSoundPlayer2Thread::DirectSoundPlayer2Thread(AudioProvider *provider)
{
event_start_playback = CreateEvent(0, FALSE, FALSE, 0);
event_stop_playback = CreateEvent(0, FALSE, FALSE, 0);
event_update_end_time = CreateEvent(0, FALSE, FALSE, 0);
event_set_volume = CreateEvent(0, FALSE, FALSE, 0);
event_kill_self = CreateEvent(0, FALSE, FALSE, 0);
thread_running = CreateEvent(0, TRUE, FALSE, 0);
is_playing = CreateEvent(0, TRUE, FALSE, 0);
error_happened = CreateEvent(0, TRUE, FALSE, 0);
error_message = 0;
volume = 1.0;
start_frame = 0;
end_frame = 0;
this->provider = provider;
thread_handle = (HANDLE)_beginthreadex(0, 0, ThreadProc, this, 0, 0);
if (!thread_handle)
throw _T("Failed creating playback thread in DirectSoundPlayer2. This is bad.");
CheckError();
WaitForSingleObject(thread_running, INFINITE);
}
DirectSoundPlayer2Thread::~DirectSoundPlayer2Thread()
{
SetEvent(event_kill_self);
WaitForSingleObject(thread_handle, INFINITE);
}
void DirectSoundPlayer2Thread::Play(int64_t start, int64_t count)
{
CheckError();
start_frame = start;
end_frame = start+count;
SetEvent(event_start_playback);
last_playback_restart = GetTickCount();
}
void DirectSoundPlayer2Thread::Stop()
{
CheckError();
SetEvent(event_stop_playback);
}
void DirectSoundPlayer2Thread::SetEndFrame(int64_t new_end_frame)
{
CheckError();
end_frame = new_end_frame;
SetEvent(event_update_end_time);
}
void DirectSoundPlayer2Thread::SetVolume(double new_volume)
{
CheckError();
volume = new_volume;
SetEvent(event_set_volume);
}
bool DirectSoundPlayer2Thread::IsPlaying()
{
CheckError();
switch (WaitForSingleObject(is_playing, 0))
{
case WAIT_ABANDONED:
throw _T("The DirectShowPlayer2Thread playback state event was abandoned, somehow. This should not happen.");
case WAIT_FAILED:
throw _T("Failed checking state of DirectShowPlayer2Thread playback state event.");
case WAIT_OBJECT_0:
return true;
case WAIT_TIMEOUT:
default:
return false;
}
}
int64_t DirectSoundPlayer2Thread::GetStartFrame()
{
CheckError();
return start_frame;
}
int64_t DirectSoundPlayer2Thread::GetCurrentFrame()
{
CheckError();
if (!IsPlaying()) return 0;
DWORD milliseconds_elapsed = GetTickCount() - last_playback_restart;
return start_frame + milliseconds_elapsed * provider->GetSampleRate() / 1000;
}
int64_t DirectSoundPlayer2Thread::GetEndFrame()
{
CheckError();
return end_frame;
}
double DirectSoundPlayer2Thread::GetVolume()
{
CheckError();
return volume;
}
DirectSoundPlayer2::DirectSoundPlayer2()
{
thread = 0;
}
DirectSoundPlayer2::~DirectSoundPlayer2()
{
CloseStream();
}
void DirectSoundPlayer2::OpenStream()
{
if (thread) return;
thread = new DirectSoundPlayer2Thread(GetProvider());
}
void DirectSoundPlayer2::CloseStream()
{
if (!thread) return;
delete thread;
thread = 0;
}
void DirectSoundPlayer2::SetProvider(AudioProvider *provider)
{
if (thread && provider != GetProvider())
{
delete thread;
thread = new DirectSoundPlayer2Thread(provider);
}
AudioPlayer::SetProvider(provider);
}
void DirectSoundPlayer2::Play(int64_t start,int64_t count)
{
OpenStream();
thread->Play(start, count);
if (displayTimer && !displayTimer->IsRunning()) displayTimer->Start(15);
}
void DirectSoundPlayer2::Stop(bool timerToo)
{
if (thread) thread->Stop();
if (timerToo && displayTimer) {
displayTimer->Stop();
}
}
bool DirectSoundPlayer2::IsPlaying()
{
if (!thread) return false;
return thread->IsPlaying();
}
int64_t DirectSoundPlayer2::GetStartPosition()
{
if (!thread) return 0;
return thread->GetStartFrame();
}
int64_t DirectSoundPlayer2::GetEndPosition()
{
if (!thread) return 0;
return thread->GetEndFrame();
}
int64_t DirectSoundPlayer2::GetCurrentPosition()
{
if (!thread) return 0;
return thread->GetCurrentFrame();
}
void DirectSoundPlayer2::SetEndPosition(int64_t pos)
{
if (thread) thread->SetEndFrame(pos);
}
void DirectSoundPlayer2::SetCurrentPosition(int64_t pos)
{
if (thread) thread->Play(pos, thread->GetEndFrame()-pos);
}
void DirectSoundPlayer2::SetVolume(double vol)
{
if (thread) thread->SetVolume(vol);
}
double DirectSoundPlayer2::GetVolume()
{
if (!thread) return 0;
return thread->GetVolume();
}
#endif // WITH_DIRECTSOUND

View file

@ -0,0 +1,77 @@
// Copyright (c) 2008, 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
//
// Website: http://www.aegisub.net/
// Contact: mailto:jiifurusu@gmail.com
//
#ifdef WITH_DIRECTSOUND
#include <wx/wxprec.h>
#include "include/aegisub/audio_player.h"
class DirectSoundPlayer2Thread;
class DirectSoundPlayer2 : public AudioPlayer {
DirectSoundPlayer2Thread *thread;
public:
DirectSoundPlayer2();
~DirectSoundPlayer2();
void OpenStream();
void CloseStream();
void SetProvider(AudioProvider *provider);
void Play(int64_t start,int64_t count);
void Stop(bool timerToo=true);
bool IsPlaying();
int64_t GetStartPosition();
int64_t GetEndPosition();
int64_t GetCurrentPosition();
void SetEndPosition(int64_t pos);
void SetCurrentPosition(int64_t pos);
void SetVolume(double vol);
double GetVolume();
};
class DirectSoundPlayer2Factory : public AudioPlayerFactory {
public:
AudioPlayer *CreatePlayer() { return new DirectSoundPlayer2(); }
};
#endif

View file

@ -84,7 +84,7 @@ public:
virtual wxMutex *GetMutex();
void SetProvider(AudioProvider *provider);
virtual void SetProvider(AudioProvider *provider);
AudioProvider *GetProvider();
void SetDisplayTimer(wxTimer *timer);

View file

@ -598,6 +598,14 @@
RelativePath="..\..\aegisub\audio_player_dsound.h"
>
</File>
<File
RelativePath="..\..\aegisub\audio_player_dsound2.cpp"
>
</File>
<File
RelativePath="..\..\aegisub\audio_player_dsound2.h"
>
</File>
<File
RelativePath="..\..\aegisub\audio_player_manager.h"
>

View file

@ -510,6 +510,14 @@
RelativePath="..\..\aegisub\audio_player_dsound.h"
>
</File>
<File
RelativePath="..\..\aegisub\audio_player_dsound2.cpp"
>
</File>
<File
RelativePath="..\..\aegisub\audio_player_dsound2.h"
>
</File>
<File
RelativePath="..\..\aegisub\audio_player_manager.h"
>