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