Details | Last modification | View Log | RSS feed
Rev | Author | Line No. | Line |
---|---|---|---|
1 | pmbaty | 1 | /* |
2 | * This file is part of the DXX-Rebirth project <https://www.dxx-rebirth.com/>. |
||
3 | * It is copyright by its individual contributors, as recorded in the |
||
4 | * project's Git history. See COPYING.txt at the top level for license |
||
5 | * terms and a link to the Git history. |
||
6 | */ |
||
7 | /* |
||
8 | * |
||
9 | * CD Audio functions for Win32 |
||
10 | * |
||
11 | * |
||
12 | */ |
||
13 | |||
14 | #include <algorithm> |
||
15 | #include <stdio.h> |
||
16 | #include <stdlib.h> |
||
17 | #include <windows.h> |
||
18 | #include <mmsystem.h> |
||
19 | #include <climits> |
||
20 | |||
21 | #include "pstypes.h" |
||
22 | #include "dxxerror.h" |
||
23 | #include "args.h" |
||
24 | #include "rbaudio.h" |
||
25 | #include "console.h" |
||
26 | #include "fwd-gr.h" |
||
27 | #include "timer.h" |
||
28 | |||
29 | #if SDL_MAJOR_VERSION == 1 |
||
30 | // we don't want this on SDL1 because there we have common/arch/sdl/rbaudio.cpp |
||
31 | // #pragma message("Skipping win32/rbaudio because of SDL1") |
||
32 | |||
33 | #elif SDL_MAJOR_VERSION == 2 |
||
34 | |||
35 | namespace dcx { |
||
36 | #define CD_FPS 75 |
||
37 | |||
38 | static UINT wCDDeviceID = 0U; |
||
39 | static bool initialised; |
||
40 | static DWORD playEnd; |
||
41 | static DWORD lastFrames; |
||
42 | static bool isPaused; |
||
43 | |||
44 | void RBAExit() |
||
45 | { |
||
46 | if (wCDDeviceID) |
||
47 | { |
||
48 | initialised = false; |
||
49 | mciSendCommand(wCDDeviceID, MCI_CLOSE, MCI_WAIT, 0); |
||
50 | wCDDeviceID = 0U; |
||
51 | } |
||
52 | } |
||
53 | |||
54 | static bool mci_HasMedia() |
||
55 | { |
||
56 | if (!wCDDeviceID) return false; |
||
57 | |||
58 | MCIERROR mciError; |
||
59 | MCI_STATUS_PARMS mciStatusParms; |
||
60 | |||
61 | mciStatusParms.dwItem = MCI_STATUS_MEDIA_PRESENT; |
||
62 | if ((mciError = mciSendCommand(wCDDeviceID, MCI_STATUS, MCI_STATUS_ITEM, reinterpret_cast<DWORD_PTR>(&mciStatusParms)))) |
||
63 | { |
||
64 | Warning("RBAudio win32/MCI: cannot determine MCI media status (%lx)", mciError); |
||
65 | RBAExit(); |
||
66 | return false; |
||
67 | } |
||
68 | return mciStatusParms.dwReturn != 0; |
||
69 | } |
||
70 | |||
71 | static unsigned mci_TotalFramesMsf(const DWORD msf) |
||
72 | { |
||
73 | return (MCI_MSF_MINUTE(msf) * 60 + MCI_MSF_SECOND(msf)) * CD_FPS + MCI_MSF_FRAME(msf); |
||
74 | } |
||
75 | |||
76 | static unsigned mci_FramesToMsf(const int frames) |
||
77 | { |
||
78 | int m = frames / (CD_FPS * 60); |
||
79 | int s = (frames / CD_FPS) % 60; |
||
80 | int f = frames % CD_FPS; |
||
81 | return MCI_MAKE_MSF(m, s, f); |
||
82 | } |
||
83 | |||
84 | static unsigned mci_GetTrackOffset(const int track) |
||
85 | { |
||
86 | MCIERROR mciError; |
||
87 | MCI_STATUS_PARMS mciStatusParms; |
||
88 | |||
89 | mciStatusParms.dwItem = MCI_STATUS_POSITION; |
||
90 | mciStatusParms.dwTrack = track; |
||
91 | if ((mciError = mciSendCommand(wCDDeviceID, MCI_STATUS, MCI_STATUS_ITEM | MCI_TRACK, reinterpret_cast<DWORD_PTR>(&mciStatusParms)))) |
||
92 | { |
||
93 | Warning("RBAudio win32/MCI: cannot determine track %i offset (%lx)", track, mciError); |
||
94 | return -1; |
||
95 | } |
||
96 | // dwReturn is a 32-bit value in MSF format, so DWORD_PTR > DWORD is not a problem |
||
97 | return mci_TotalFramesMsf(mciStatusParms.dwReturn); |
||
98 | } |
||
99 | |||
100 | static unsigned mci_GetTrackLength(const int track) |
||
101 | { |
||
102 | MCIERROR mciError; |
||
103 | MCI_STATUS_PARMS mciStatusParms; |
||
104 | |||
105 | mciStatusParms.dwItem = MCI_STATUS_LENGTH; |
||
106 | mciStatusParms.dwTrack = track; |
||
107 | if ((mciError = mciSendCommand(wCDDeviceID, MCI_STATUS, MCI_STATUS_ITEM | MCI_TRACK, reinterpret_cast<DWORD_PTR>(&mciStatusParms)))) |
||
108 | { |
||
109 | Warning("RBAudio win32/MCI: cannot determine track %i length (%lx)", track, mciError); |
||
110 | return -1; |
||
111 | } |
||
112 | // dwReturn is a 32-bit value in MSF format, so DWORD_PTR > DWORD is not a problem |
||
113 | return mci_TotalFramesMsf(mciStatusParms.dwReturn); |
||
114 | } |
||
115 | |||
116 | static unsigned mci_GetTotalLength() |
||
117 | { |
||
118 | MCIERROR mciError; |
||
119 | MCI_STATUS_PARMS mciStatusParms; |
||
120 | |||
121 | mciStatusParms.dwItem = MCI_STATUS_LENGTH; |
||
122 | if ((mciError = mciSendCommand(wCDDeviceID, MCI_STATUS, MCI_STATUS_ITEM, reinterpret_cast<DWORD_PTR>(&mciStatusParms)))) |
||
123 | { |
||
124 | Warning("RBAudio win32/MCI: cannot determine media length (%lx)", mciError); |
||
125 | return -1; |
||
126 | } |
||
127 | return mci_TotalFramesMsf(mciStatusParms.dwReturn); |
||
128 | } |
||
129 | |||
130 | void RBAInit() |
||
131 | { |
||
132 | MCIERROR mciError; |
||
133 | MCI_OPEN_PARMS mciOpenParms; |
||
134 | MCI_SET_PARMS mciSetParms; |
||
135 | |||
136 | if (initialised) return; |
||
137 | |||
138 | mciOpenParms.lpstrDeviceType = "cdaudio"; |
||
139 | if ((mciError = mciSendCommand(0, MCI_OPEN, MCI_OPEN_TYPE | MCI_OPEN_SHAREABLE, reinterpret_cast<DWORD_PTR>(&mciOpenParms)))) |
||
140 | { |
||
141 | con_puts(CON_NORMAL, "RBAudio win32/MCI: cannot find MCI cdaudio (no CD drive?)"); |
||
142 | return; |
||
143 | } |
||
144 | |||
145 | wCDDeviceID = mciOpenParms.wDeviceID; |
||
146 | |||
147 | if (!mci_HasMedia()) |
||
148 | { |
||
149 | con_puts(CON_NORMAL, "RBAudio win32/MCI: no media in CD drive."); |
||
150 | RBAExit(); |
||
151 | return; |
||
152 | } |
||
153 | |||
154 | mciSetParms.dwTimeFormat = MCI_FORMAT_MSF; |
||
155 | if ((mciError = mciSendCommand(wCDDeviceID, MCI_SET, MCI_SET_TIME_FORMAT, reinterpret_cast<DWORD_PTR>(&mciSetParms)))) |
||
156 | { |
||
157 | Warning("RBAudio win32/MCI: cannot set time format for CD to MSF (strange)"); |
||
158 | RBAExit(); |
||
159 | return; |
||
160 | } |
||
161 | |||
162 | initialised = true; |
||
163 | RBAList(); |
||
164 | } |
||
165 | |||
166 | int RBAEnabled() |
||
167 | { |
||
168 | return initialised; |
||
169 | } |
||
170 | |||
171 | static void (*redbook_finished_hook)() = nullptr; |
||
172 | |||
173 | int RBAPlayTrack(int a) |
||
174 | { |
||
175 | if (!wCDDeviceID) |
||
176 | return 0; |
||
177 | |||
178 | if (mci_HasMedia()) |
||
179 | { |
||
180 | MCIERROR mciError; |
||
181 | MCI_PLAY_PARMS mciPlayParms; |
||
182 | DWORD playStart; |
||
183 | |||
184 | con_printf(CON_VERBOSE, "RBAudio win32/MCI: Playing track %i", a); |
||
185 | |||
186 | playStart = mci_GetTrackOffset(a); |
||
187 | playEnd = playStart + mci_GetTrackLength(a); |
||
188 | |||
189 | mciPlayParms.dwFrom = mci_FramesToMsf(playStart); |
||
190 | mciPlayParms.dwTo = mci_FramesToMsf(playEnd); |
||
191 | |||
192 | if ((mciError = mciSendCommand(wCDDeviceID, MCI_PLAY, MCI_FROM | MCI_TO, reinterpret_cast<DWORD_PTR>(&mciPlayParms)))) |
||
193 | { |
||
194 | Warning("RBAudio win32/MCI: could not play track (%lx)", mciError); |
||
195 | return 0; |
||
196 | } |
||
197 | |||
198 | return 1; |
||
199 | } |
||
200 | return 0; |
||
201 | } |
||
202 | |||
203 | // plays tracks first through last, inclusive |
||
204 | int RBAPlayTracks(int first, int last, void (*hook_finished)(void)) |
||
205 | { |
||
206 | if (!wCDDeviceID) |
||
207 | return 0; |
||
208 | |||
209 | if (mci_HasMedia()) |
||
210 | { |
||
211 | MCIERROR mciError; |
||
212 | MCI_PLAY_PARMS mciPlayParms; |
||
213 | |||
214 | redbook_finished_hook = hook_finished; |
||
215 | |||
216 | con_printf(CON_VERBOSE, "RBAudio win32/MCI: Playing tracks %i to %i", first, last); |
||
217 | |||
218 | playEnd = mci_GetTrackOffset(last) + mci_GetTrackLength(last); |
||
219 | |||
220 | mciPlayParms.dwFrom = mci_FramesToMsf(mci_GetTrackOffset(first)); |
||
221 | mciPlayParms.dwTo = mci_FramesToMsf(playEnd); |
||
222 | |||
223 | if ((mciError = mciSendCommand(wCDDeviceID, MCI_PLAY, MCI_FROM | MCI_TO, reinterpret_cast<DWORD_PTR>(&mciPlayParms)))) |
||
224 | { |
||
225 | Warning("RBAudio win32/MCI: could not play tracks (%lx)", mciError); |
||
226 | return 0; |
||
227 | } |
||
228 | |||
229 | return 1; |
||
230 | } |
||
231 | return 0; |
||
232 | } |
||
233 | |||
234 | void RBAStop() |
||
235 | { |
||
236 | if (!wCDDeviceID) return; |
||
237 | |||
238 | MCIERROR mciError; |
||
239 | |||
240 | if ((mciError = mciSendCommand(wCDDeviceID, MCI_STOP, 0, 0))) |
||
241 | { |
||
242 | Warning("RBAudio win32/MCI: could not stop music (%lx)", mciError); |
||
243 | } |
||
244 | redbook_finished_hook = nullptr; |
||
245 | } |
||
246 | |||
247 | void RBAEjectDisk() |
||
248 | { |
||
249 | if (!wCDDeviceID) return; |
||
250 | |||
251 | MCIERROR mciError; |
||
252 | |||
253 | if ((mciError = mciSendCommand(wCDDeviceID, MCI_SET, MCI_SET_DOOR_OPEN | MCI_WAIT, 0))) |
||
254 | { |
||
255 | Warning("RBAudio win32/MCI: could not open CD tray (%lx)", mciError); |
||
256 | } |
||
257 | initialised = false; |
||
258 | } |
||
259 | |||
260 | void RBASetVolume(int) |
||
261 | { |
||
262 | // MCI does not support this |
||
263 | } |
||
264 | |||
265 | void RBAPause() |
||
266 | { |
||
267 | if (!wCDDeviceID) return; |
||
268 | |||
269 | MCIERROR mciError; |
||
270 | |||
271 | if ((mciError = mciSendCommand(wCDDeviceID, MCI_PAUSE, 0, 0))) |
||
272 | { |
||
273 | Warning("RBAudio win32/MCI: could not pause music (%lx)", mciError); |
||
274 | return; |
||
275 | } |
||
276 | con_puts(CON_VERBOSE, "RBAudio win32/MCI: Playback paused"); |
||
277 | isPaused = true; |
||
278 | } |
||
279 | |||
280 | int RBAResume() |
||
281 | { |
||
282 | if (!wCDDeviceID) return -1; |
||
283 | |||
284 | MCIERROR mciError; |
||
285 | |||
286 | if ((mciError = mciSendCommand(wCDDeviceID, MCI_RESUME, 0, 0))) |
||
287 | { |
||
288 | Warning("RBAudio win32/MCI: could not resume music (%lx)", mciError); |
||
289 | return -1; |
||
290 | } |
||
291 | con_puts(CON_VERBOSE, "RBAudio win32/MCI: Playback resumed"); |
||
292 | isPaused = false; |
||
293 | return 1; |
||
294 | } |
||
295 | |||
296 | int RBAPauseResume() |
||
297 | { |
||
298 | if (!wCDDeviceID) return 0; |
||
299 | |||
300 | MCIERROR mciError; |
||
301 | MCI_STATUS_PARMS mciStatusParms; |
||
302 | |||
303 | mciStatusParms.dwItem = MCI_STATUS_MODE; |
||
304 | if ((mciError = mciSendCommand(wCDDeviceID, MCI_STATUS, MCI_STATUS_ITEM, reinterpret_cast<DWORD_PTR>(&mciStatusParms)))) |
||
305 | { |
||
306 | Warning("RBAudio win32/MCI: cannot determine MCI media status (%lx)", mciError); |
||
307 | return 0; |
||
308 | } |
||
309 | |||
310 | if (mciStatusParms.dwReturn == MCI_MODE_PLAY) |
||
311 | { |
||
312 | con_puts(CON_VERBOSE, "RBAudio win32/MCI: Toggle Playback pause"); |
||
313 | RBAPause(); |
||
314 | } |
||
315 | // MCI may also use MCI_MODE_STOP for pause instead of MCI_MODE_PAUSE |
||
316 | else if (mciStatusParms.dwReturn == MCI_MODE_PAUSE || (isPaused && mciStatusParms.dwReturn == MCI_MODE_STOP)) |
||
317 | { |
||
318 | con_puts(CON_VERBOSE, "RBAudio win32/MCI: Toggle Playback resume"); |
||
319 | return RBAResume() > 0; |
||
320 | } |
||
321 | else |
||
322 | return 0; |
||
323 | |||
324 | return 1; |
||
325 | } |
||
326 | |||
327 | int RBAGetNumberOfTracks() |
||
328 | { |
||
329 | if (!wCDDeviceID) return -1; |
||
330 | |||
331 | MCIERROR mciError; |
||
332 | MCI_STATUS_PARMS mciStatusParms; |
||
333 | |||
334 | mciStatusParms.dwItem = MCI_STATUS_NUMBER_OF_TRACKS; |
||
335 | if ((mciError = mciSendCommand(wCDDeviceID, MCI_STATUS, MCI_STATUS_ITEM, reinterpret_cast<DWORD_PTR>(&mciStatusParms)))) |
||
336 | { |
||
337 | Warning("RBAudio win32/MCI: could not get track count (%lx)", mciError); |
||
338 | return -1; |
||
339 | } |
||
340 | |||
341 | return static_cast<int>(mciStatusParms.dwReturn); |
||
342 | } |
||
343 | |||
344 | // check if we need to call the 'finished' hook |
||
345 | // needs to go in all event loops |
||
346 | // MCI has a hook function via Win32 messages, but it doesn't work for CDs |
||
347 | // for whatever reason |
||
348 | void RBACheckFinishedHook() |
||
349 | { |
||
350 | static fix64 last_check_time = 0; |
||
351 | |||
352 | if (!wCDDeviceID) return; |
||
353 | |||
354 | if ((timer_query() - last_check_time) >= F2_0) |
||
355 | { |
||
356 | MCIERROR mciError; |
||
357 | MCI_STATUS_PARMS mciStatusParms; |
||
358 | |||
359 | mciStatusParms.dwItem = MCI_STATUS_POSITION; |
||
360 | if ((mciError = mciSendCommand(wCDDeviceID, MCI_STATUS, MCI_STATUS_ITEM, reinterpret_cast<DWORD_PTR>(&mciStatusParms)))) |
||
361 | { |
||
362 | Warning("RBAudio win32/MCI: cannot determine MCI position (%lx)", mciError); |
||
363 | return; |
||
364 | } |
||
365 | |||
366 | // time for a hack. for some reason, MCI sometimes stops |
||
367 | // from around a few to few dozen frames before it should, |
||
368 | // so we will check if we haven't moved from the last check |
||
369 | // and allow a bit of a leeway when checking if so. |
||
370 | |||
371 | DWORD checkValue = playEnd; |
||
372 | // dwReturn is a 32-bit value in MSF format, so DWORD_PTR > DWORD is not a problem |
||
373 | DWORD thisFrames = mci_TotalFramesMsf(mciStatusParms.dwReturn); |
||
374 | |||
375 | if (thisFrames == lastFrames) |
||
376 | checkValue = checkValue < 64 ? 0 : checkValue - 64; // prevent underflow |
||
377 | |||
378 | if (redbook_finished_hook && playEnd > 0 && thisFrames >= checkValue) |
||
379 | { |
||
380 | con_puts(CON_VERBOSE, "RBAudio win32/MCI: Playback done, calling finished-hook"); |
||
381 | redbook_finished_hook(); |
||
382 | } |
||
383 | lastFrames = thisFrames; |
||
384 | last_check_time = timer_query(); |
||
385 | } |
||
386 | } |
||
387 | |||
388 | // return the track number currently playing. Useful if RBAPlayTracks() |
||
389 | // is called. Returns 0 if no track playing, else track number |
||
390 | int RBAGetTrackNum() |
||
391 | { |
||
392 | if (!wCDDeviceID) return 0; |
||
393 | |||
394 | MCIERROR mciError; |
||
395 | MCI_STATUS_PARMS mciStatusParms; |
||
396 | |||
397 | mciStatusParms.dwItem = MCI_STATUS_MODE; |
||
398 | if ((mciError = mciSendCommand(wCDDeviceID, MCI_STATUS, MCI_STATUS_ITEM, reinterpret_cast<DWORD_PTR>(&mciStatusParms)))) |
||
399 | { |
||
400 | Warning("RBAudio win32/MCI: cannot determine MCI media status (%lx)", mciError); |
||
401 | return 0; |
||
402 | } |
||
403 | |||
404 | if (mciStatusParms.dwReturn != MCI_MODE_PLAY) |
||
405 | return 0; |
||
406 | |||
407 | mciStatusParms.dwItem = MCI_STATUS_CURRENT_TRACK; |
||
408 | if ((mciError = mciSendCommand(wCDDeviceID, MCI_STATUS, MCI_STATUS_ITEM, reinterpret_cast<DWORD_PTR>(&mciStatusParms)))) |
||
409 | { |
||
410 | Warning("RBAudio win32/MCI: cannot determine MCI track number (%lx)", mciError); |
||
411 | return 0; |
||
412 | } |
||
413 | |||
414 | return static_cast<int>(mciStatusParms.dwReturn); |
||
415 | } |
||
416 | |||
417 | int RBAPeekPlayStatus() |
||
418 | { |
||
419 | if (!wCDDeviceID) return 0; |
||
420 | |||
421 | MCIERROR mciError; |
||
422 | MCI_STATUS_PARMS mciStatusParms; |
||
423 | |||
424 | mciStatusParms.dwItem = MCI_STATUS_MODE; |
||
425 | if ((mciError = mciSendCommand(wCDDeviceID, MCI_STATUS, MCI_STATUS_ITEM, reinterpret_cast<DWORD_PTR>(&mciStatusParms)))) |
||
426 | { |
||
427 | Warning("RBAudio win32/MCI: cannot determine MCI media status (%lx)", mciError); |
||
428 | return 0; |
||
429 | } |
||
430 | |||
431 | if (mciStatusParms.dwReturn == MCI_MODE_PLAY) |
||
432 | return 1; |
||
433 | // MCI may also use MCI_MODE_STOP for pause instead of MCI_MODE_PAUSE |
||
434 | else if (mciStatusParms.dwReturn == MCI_MODE_PAUSE || (isPaused && mciStatusParms.dwReturn == MCI_MODE_STOP)) |
||
435 | return -1; // hack so it doesn't keep restarting paused music; -1 is still truthy, unlike 0 |
||
436 | else |
||
437 | return 0; |
||
438 | } |
||
439 | |||
440 | static int cddb_sum(int n) |
||
441 | { |
||
442 | int ret; |
||
443 | |||
444 | /* For backward compatibility this algorithm must not change */ |
||
445 | |||
446 | ret = 0; |
||
447 | |||
448 | while (n > 0) { |
||
449 | ret = ret + (n % 10); |
||
450 | n = n / 10; |
||
451 | } |
||
452 | |||
453 | return (ret); |
||
454 | } |
||
455 | |||
456 | unsigned long RBAGetDiscID() |
||
457 | { |
||
458 | int i, t = 0, n = 0, trackCount, totalLength; |
||
459 | |||
460 | if (!wCDDeviceID) |
||
461 | return 0; |
||
462 | |||
463 | MCIERROR mciError; |
||
464 | trackCount = RBAGetNumberOfTracks(); |
||
465 | totalLength = mci_GetTotalLength(); |
||
466 | if (totalLength < 0) |
||
467 | { |
||
468 | Warning("RBAudio win32/MCI: cannot determine total length (%lx)", mciError); |
||
469 | return 0; |
||
470 | } |
||
471 | |||
472 | /* For backward compatibility this algorithm must not change */ |
||
473 | |||
474 | i = 1; |
||
475 | |||
476 | while (i <= trackCount) |
||
477 | { |
||
478 | int offset = mci_GetTrackOffset(i); |
||
479 | if (offset < 0) |
||
480 | { |
||
481 | Warning("RBAudio win32/MCI: cannot determine track %i offset (%lx)", i, mciError); |
||
482 | return 0; |
||
483 | } |
||
484 | n += cddb_sum(offset / CD_FPS); |
||
485 | i++; |
||
486 | } |
||
487 | |||
488 | t = totalLength / CD_FPS; |
||
489 | |||
490 | return ((n % 0xff) << 24 | t << 8 | trackCount); |
||
491 | } |
||
492 | |||
493 | void RBAList(void) |
||
494 | { |
||
495 | if (!wCDDeviceID) return; |
||
496 | |||
497 | MCIERROR mciError; |
||
498 | MCI_STATUS_PARMS mciStatusParms; |
||
499 | int trackCount = RBAGetNumberOfTracks(); |
||
500 | |||
501 | for (int i = 1; i <= trackCount; ++i) { |
||
502 | int isAudioTrack, length, offset; |
||
503 | mciStatusParms.dwTrack = i; |
||
504 | |||
505 | mciStatusParms.dwItem = MCI_CDA_STATUS_TYPE_TRACK; |
||
506 | if ((mciError = mciSendCommand(wCDDeviceID, MCI_STATUS, MCI_STATUS_ITEM | MCI_TRACK, reinterpret_cast<DWORD_PTR>(&mciStatusParms)))) |
||
507 | { |
||
508 | Warning("RBAudio win32/MCI: cannot determine track %d type (%lx)", i, mciError); |
||
509 | continue; |
||
510 | } |
||
511 | isAudioTrack = mciStatusParms.dwReturn == MCI_CDA_TRACK_AUDIO; |
||
512 | |||
513 | offset = mci_GetTrackOffset(i); |
||
514 | if (offset < 0) |
||
515 | { |
||
516 | continue; |
||
517 | } |
||
518 | |||
519 | length = mci_GetTrackLength(i); |
||
520 | if (length < 0) |
||
521 | { |
||
522 | continue; |
||
523 | } |
||
524 | |||
525 | con_printf(CON_VERBOSE, "RBAudio win32/MCI: CD track %d, type %s, length %d, offset %d", i, isAudioTrack ? "audio" : "data", length, offset); |
||
526 | } |
||
527 | } |
||
528 | |||
529 | } |
||
530 | |||
531 | #endif |