1 // Copyright (c) 2006, Rodrigo Braz Monteiro
2 // All rights reserved.
3 //
4 // Redistribution and use in source and binary forms, with or without
5 // modification, are permitted provided that the following conditions are met:
6 //
7 // * Redistributions of source code must retain the above copyright notice,
8 // this list of conditions and the following disclaimer.
9 // * Redistributions in binary form must reproduce the above copyright notice,
10 // this list of conditions and the following disclaimer in the documentation
11 // and/or other materials provided with the distribution.
12 // * Neither the name of the Aegisub Group nor the names of its contributors
13 // may be used to endorse or promote products derived from this software
14 // without specific prior written permission.
15 //
16 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17 // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18 // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19 // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
20 // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21 // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22 // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23 // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24 // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25 // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26 // POSSIBILITY OF SUCH DAMAGE.
27 //
28 // Aegisub Project http://www.aegisub.org/
29
30 /// @file audio_player_dsound.cpp
31 /// @brief Old DirectSound-based audio output
32 /// @ingroup audio_output
33 ///
34
35 #ifdef WITH_DIRECTSOUND
36 #include "include/aegisub/audio_player.h"
37
38 #include "audio_controller.h"
39 #include "frame_main.h"
40 #include "utils.h"
41
42 #include <libaegisub/audio/provider.h>
43 #include <libaegisub/log.h>
44 #include <libaegisub/make_unique.h>
45
46 #include <mmsystem.h>
47 #include <dsound.h>
48
49 namespace {
50 class DirectSoundPlayer;
51
52 class DirectSoundPlayerThread final : public wxThread {
53 DirectSoundPlayer *parent;
54 HANDLE stopnotify;
55
56 public:
57 void Stop(); // Notify thread to stop audio playback. Thread safe.
58 DirectSoundPlayerThread(DirectSoundPlayer *parent);
59 ~DirectSoundPlayerThread();
60
61 wxThread::ExitCode Entry();
62 };
63
64 class DirectSoundPlayer final : public AudioPlayer {
65 friend class DirectSoundPlayerThread;
66
67 volatile bool playing = false;
68 float volume = 1.0f;
69 int offset = 0;
70
71 DWORD bufSize = 0;
72 volatile int64_t playPos = 0;
73 int64_t startPos = 0;
74 volatile int64_t endPos = 0;
75 DWORD startTime = 0;
76
77 IDirectSound8 *directSound = nullptr;
78 IDirectSoundBuffer8 *buffer = nullptr;
79
80 bool FillBuffer(bool fill);
81 DirectSoundPlayerThread *thread = nullptr;
82
83 public:
84 DirectSoundPlayer(agi::AudioProvider *provider, wxWindow *parent);
85 ~DirectSoundPlayer();
86
87 void Play(int64_t start,int64_t count);
88 void Stop();
89
IsPlaying()90 bool IsPlaying() { return playing; }
91
GetEndPosition()92 int64_t GetEndPosition() { return endPos; }
93 int64_t GetCurrentPosition();
94 void SetEndPosition(int64_t pos);
95
SetVolume(double vol)96 void SetVolume(double vol) { volume = vol; }
97 };
98
DirectSoundPlayer(agi::AudioProvider * provider,wxWindow * parent)99 DirectSoundPlayer::DirectSoundPlayer(agi::AudioProvider *provider, wxWindow *parent)
100 : AudioPlayer(provider)
101 {
102 // Initialize the DirectSound object
103 HRESULT res;
104 res = DirectSoundCreate8(&DSDEVID_DefaultPlayback,&directSound,nullptr); // TODO: support selecting audio device
105 if (FAILED(res)) throw AudioPlayerOpenError("Failed initializing DirectSound");
106
107 // Set DirectSound parameters
108 directSound->SetCooperativeLevel((HWND)parent->GetHandle(),DSSCL_PRIORITY);
109
110 // Create the wave format structure
111 WAVEFORMATEX waveFormat;
112 waveFormat.wFormatTag = WAVE_FORMAT_PCM;
113 waveFormat.nSamplesPerSec = provider->GetSampleRate();
114 waveFormat.nChannels = provider->GetChannels();
115 waveFormat.wBitsPerSample = provider->GetBytesPerSample() * 8;
116 waveFormat.nBlockAlign = waveFormat.nChannels * waveFormat.wBitsPerSample / 8;
117 waveFormat.nAvgBytesPerSec = waveFormat.nSamplesPerSec * waveFormat.nBlockAlign;
118 waveFormat.cbSize = sizeof(waveFormat);
119
120 // Create the buffer initializer
121 int aim = waveFormat.nAvgBytesPerSec * 15/100; // 150 ms buffer
122 int min = DSBSIZE_MIN;
123 int max = DSBSIZE_MAX;
124 bufSize = std::min(std::max(min,aim),max);
125 DSBUFFERDESC desc;
126 desc.dwSize = sizeof(DSBUFFERDESC);
127 desc.dwFlags = DSBCAPS_GETCURRENTPOSITION2 | DSBCAPS_GLOBALFOCUS;
128 desc.dwBufferBytes = bufSize;
129 desc.dwReserved = 0;
130 desc.lpwfxFormat = &waveFormat;
131 desc.guid3DAlgorithm = GUID_NULL;
132
133 // Create the buffer
134 IDirectSoundBuffer *buf;
135 res = directSound->CreateSoundBuffer(&desc,&buf,nullptr);
136 if (res != DS_OK) throw AudioPlayerOpenError("Failed creating DirectSound buffer");
137
138 // Copy interface to buffer
139 res = buf->QueryInterface(IID_IDirectSoundBuffer8,(LPVOID*) &buffer);
140 if (res != S_OK) throw AudioPlayerOpenError("Failed casting interface to IDirectSoundBuffer8");
141
142 // Set data
143 offset = 0;
144 }
145
~DirectSoundPlayer()146 DirectSoundPlayer::~DirectSoundPlayer() {
147 Stop();
148
149 if (buffer)
150 buffer->Release();
151
152 if (directSound)
153 directSound->Release();
154 }
155
FillBuffer(bool fill)156 bool DirectSoundPlayer::FillBuffer(bool fill) {
157 if (playPos >= endPos) return false;
158
159 // Variables
160 HRESULT res;
161 void *ptr1, *ptr2;
162 unsigned long int size1, size2;
163 int bytesps = provider->GetBytesPerSample();
164
165 // To write length
166 int toWrite = 0;
167 if (fill) {
168 toWrite = bufSize;
169 }
170 else {
171 DWORD bufplay;
172 res = buffer->GetCurrentPosition(&bufplay, nullptr);
173 if (FAILED(res)) return false;
174 toWrite = (int)bufplay - (int)offset;
175 if (toWrite < 0) toWrite += bufSize;
176 }
177 if (toWrite == 0) return true;
178
179 // Make sure we only get as many samples as are available
180 if (playPos + toWrite/bytesps > endPos) {
181 toWrite = (endPos - playPos) * bytesps;
182 }
183
184 // If we're going to fill the entire buffer (ie. at start of playback) start by zeroing it out
185 // If it's not zeroed out we might have a playback selection shorter than the buffer
186 // and then everything after the playback selection will be junk, which we don't want played.
187 if (fill) {
188 RetryClear:
189 res = buffer->Lock(0, bufSize, &ptr1, &size1, &ptr2, &size2, 0);
190 if (res == DSERR_BUFFERLOST) {
191 buffer->Restore();
192 goto RetryClear;
193 }
194 memset(ptr1, 0, size1);
195 memset(ptr2, 0, size2);
196 buffer->Unlock(ptr1, size1, ptr2, size2);
197 }
198
199 // Lock buffer
200 RetryLock:
201 if (fill) {
202 res = buffer->Lock(offset, toWrite, &ptr1, &size1, &ptr2, &size2, 0);
203 }
204 else {
205 res = buffer->Lock(offset, toWrite, &ptr1, &size1, &ptr2, &size2, 0);//DSBLOCK_FROMWRITECURSOR);
206 }
207
208 // Buffer lost?
209 if (res == DSERR_BUFFERLOST) {
210 LOG_D("audio/player/dsound1") << "lost dsound buffer";
211 buffer->Restore();
212 goto RetryLock;
213 }
214
215 if (FAILED(res)) return false;
216
217 // Convert size to number of samples
218 unsigned long int count1 = size1 / bytesps;
219 unsigned long int count2 = size2 / bytesps;
220
221 LOG_D_IF(count1, "audio/player/dsound1") << "DS fill: " << (unsigned long)playPos << " -> " << (unsigned long)playPos+count1;
222 LOG_D_IF(count2, "audio/player/dsound1") << "DS fill: " << (unsigned long)playPos+count1 << " -> " << (unsigned long)playPos+count1+count2;
223 LOG_D_IF(!count1 && !count2, "audio/player/dsound1") << "DS fill: nothing";
224
225 // Get source wave
226 if (count1) provider->GetAudioWithVolume(ptr1, playPos, count1, volume);
227 if (count2) provider->GetAudioWithVolume(ptr2, playPos+count1, count2, volume);
228 playPos += count1+count2;
229
230 buffer->Unlock(ptr1,count1*bytesps,ptr2,count2*bytesps);
231
232 offset = (offset + count1*bytesps + count2*bytesps) % bufSize;
233
234 return playPos < endPos;
235 }
236
Play(int64_t start,int64_t count)237 void DirectSoundPlayer::Play(int64_t start,int64_t count) {
238 // Make sure that it's stopped
239 Stop();
240 // The thread is now guaranteed dead
241
242 HRESULT res;
243
244 // We sure better have a buffer
245 assert(buffer);
246
247 // Set variables
248 startPos = start;
249 endPos = start+count;
250 playPos = start;
251 offset = 0;
252
253 // Fill whole buffer
254 FillBuffer(true);
255
256 DWORD play_flag = 0;
257 if (count*provider->GetBytesPerSample() > bufSize) {
258 // Start thread
259 thread = new DirectSoundPlayerThread(this);
260 thread->Create();
261 thread->Run();
262 play_flag = DSBPLAY_LOOPING;
263 }
264
265 // Play
266 buffer->SetCurrentPosition(0);
267 res = buffer->Play(0,0,play_flag);
268 if (SUCCEEDED(res)) playing = true;
269 startTime = GetTickCount();
270 }
271
Stop()272 void DirectSoundPlayer::Stop() {
273 // Stop the thread
274 if (thread) {
275 if (thread->IsAlive()) {
276 thread->Stop();
277 thread->Wait();
278 }
279 thread = nullptr;
280 }
281 // The thread is now guaranteed dead and there are no concurrency problems to worry about
282
283 if (buffer) buffer->Stop(); // the thread should have done this already
284
285 // Reset variables
286 playing = false;
287 playPos = 0;
288 startPos = 0;
289 endPos = 0;
290 offset = 0;
291 }
292
SetEndPosition(int64_t pos)293 void DirectSoundPlayer::SetEndPosition(int64_t pos) {
294 if (playing) endPos = pos;
295 }
296
GetCurrentPosition()297 int64_t DirectSoundPlayer::GetCurrentPosition() {
298 // Check if buffer is loaded
299 if (!buffer || !playing) return 0;
300
301 // FIXME: this should be based on not duration played but actual sample being heard
302 // (during vidoeo playback, cur_frame might get changed to resync)
303 DWORD curtime = GetTickCount();
304 int64_t tdiff = curtime - startTime;
305 return startPos + tdiff * provider->GetSampleRate() / 1000;
306 }
307
DirectSoundPlayerThread(DirectSoundPlayer * par)308 DirectSoundPlayerThread::DirectSoundPlayerThread(DirectSoundPlayer *par) : wxThread(wxTHREAD_JOINABLE) {
309 parent = par;
310 stopnotify = CreateEvent(nullptr, true, false, nullptr);
311 }
312
~DirectSoundPlayerThread()313 DirectSoundPlayerThread::~DirectSoundPlayerThread() {
314 CloseHandle(stopnotify);
315 }
316
Entry()317 wxThread::ExitCode DirectSoundPlayerThread::Entry() {
318 CoInitialize(0);
319
320 // Wake up thread every half second to fill buffer as needed
321 // This more or less assumes the buffer is at least one second long
322 while (WaitForSingleObject(stopnotify, 50) == WAIT_TIMEOUT) {
323 if (!parent->FillBuffer(false)) {
324 // FillBuffer returns false when end of stream is reached
325 LOG_D("audio/player/dsound1") << "DS thread hit end of stream";
326 break;
327 }
328 }
329
330 // Now fill buffer with silence
331 DWORD bytesFilled = 0;
332 while (WaitForSingleObject(stopnotify, 50) == WAIT_TIMEOUT) {
333 void *buf1, *buf2;
334 DWORD size1, size2;
335 DWORD playpos;
336 HRESULT res;
337 res = parent->buffer->GetCurrentPosition(&playpos, nullptr);
338 if (FAILED(res)) break;
339 int toWrite = playpos - parent->offset;
340 while (toWrite < 0) toWrite += parent->bufSize;
341 res = parent->buffer->Lock(parent->offset, toWrite, &buf1, &size1, &buf2, &size2, 0);
342 if (FAILED(res)) break;
343 if (size1) memset(buf1, 0, size1);
344 if (size2) memset(buf2, 0, size2);
345 LOG_D_IF(size1, "audio/player/dsound1") << "DS blnk:" << (unsigned long)parent->playPos+bytesFilled << " -> " << (unsigned long)parent->playPos+bytesFilled+size1;
346 LOG_D_IF(size2, "audio/player/dsound1") << "DS blnk:" << (unsigned long)parent->playPos+bytesFilled+size1 << " -> " << (unsigned long)parent->playPos+bytesFilled+size1+size2;
347 bytesFilled += size1 + size2;
348 parent->buffer->Unlock(buf1, size1, buf2, size2);
349 if (bytesFilled > parent->bufSize) break;
350 parent->offset = (parent->offset + size1 + size2) % parent->bufSize;
351 }
352
353 WaitForSingleObject(stopnotify, 150);
354
355 LOG_D("audio/player/dsound1") << "DS thread dead";
356
357 parent->playing = false;
358 parent->buffer->Stop();
359
360 CoUninitialize();
361 return 0;
362 }
363
Stop()364 void DirectSoundPlayerThread::Stop() {
365 // Increase the stopnotify by one, causing a wait for it to succeed
366 SetEvent(stopnotify);
367 }
368 }
369
CreateDirectSoundPlayer(agi::AudioProvider * provider,wxWindow * parent)370 std::unique_ptr<AudioPlayer> CreateDirectSoundPlayer(agi::AudioProvider *provider, wxWindow *parent) {
371 return agi::make_unique<DirectSoundPlayer>(provider, parent);
372 }
373
374 #endif // WITH_DIRECTSOUND
375