Change #6685: Replace Win32 music driver with one not depending on MCI
[openttd-joker.git] / src / music / win32_m.cpp
blob57bbd1f937a04cdf8d4cc23824bbee306ce23005
1 /* $Id$ */
3 /*
4 * This file is part of OpenTTD.
5 * OpenTTD 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, version 2.
6 * OpenTTD 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.
7 * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
8 */
10 /** @file win32_m.cpp Music playback for Windows. */
12 #include "../stdafx.h"
13 #include "../string_func.h"
14 #include "win32_m.h"
15 #include <windows.h>
16 #include <mmsystem.h>
17 #include "../os/windows/win32.h"
18 #include "../debug.h"
19 #include "midifile.hpp"
20 #include "midi.h"
22 #include "../safeguards.h"
24 struct PlaybackSegment {
25 uint32 start, end;
26 size_t start_block;
27 bool loop;
30 static struct {
31 UINT time_period; ///< obtained timer precision value
32 HMIDIOUT midi_out; ///< handle to open midiOut
33 UINT timer_id; ///< ID of active multimedia timer
34 CRITICAL_SECTION lock; ///< synchronization for playback status fields
36 bool playing; ///< flag indicating that playback is active
37 bool do_start; ///< flag for starting playback of next_file at next opportunity
38 bool do_stop; ///< flag for stopping playback at next opportunity
39 byte current_volume; ///< current effective volume setting
40 byte new_volume; ///< volume setting to change to
42 MidiFile current_file; ///< file currently being played from
43 PlaybackSegment current_segment; ///< segment info for current playback
44 DWORD playback_start_time; ///< timestamp current file began playback
45 size_t current_block; ///< next block index to send
46 MidiFile next_file; ///< upcoming file to play
47 PlaybackSegment next_segment; ///< segment info for upcoming file
49 byte channel_volumes[16]; ///< last seen volume controller values in raw data
50 } _midi;
52 static FMusicDriver_Win32 iFMusicDriver_Win32;
55 static byte ScaleVolume(byte original, byte scale)
57 return original * scale / 127;
61 void CALLBACK MidiOutProc(HMIDIOUT hmo, UINT wMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2)
63 if (wMsg == MOM_DONE) {
64 MIDIHDR *hdr = (LPMIDIHDR)dwParam1;
65 midiOutUnprepareHeader(hmo, hdr, sizeof(*hdr));
66 free(hdr);
70 static void TransmitChannelMsg(byte status, byte p1, byte p2 = 0)
72 midiOutShortMsg(_midi.midi_out, status | (p1 << 8) | (p2 << 16));
75 static void TransmitSysex(byte *&msg_start, size_t &remaining)
77 /* find end of message */
78 byte *msg_end = msg_start;
79 while (*msg_end != MIDIST_ENDSYSEX) msg_end++;
80 msg_end++; /* also include sysex end byte */
82 /* prepare header */
83 MIDIHDR *hdr = CallocT<MIDIHDR>(1);
84 hdr->lpData = (LPSTR)msg_start;
85 hdr->dwBufferLength = msg_end - msg_start;
86 if (midiOutPrepareHeader(_midi.midi_out, hdr, sizeof(*hdr)) == MMSYSERR_NOERROR) {
87 /* transmit - just point directly into the data buffer */
88 hdr->dwBytesRecorded = hdr->dwBufferLength;
89 midiOutLongMsg(_midi.midi_out, hdr, sizeof(*hdr));
90 } else {
91 free(hdr);
94 /* update position in buffer */
95 remaining -= msg_end - msg_start;
96 msg_start = msg_end;
99 static void TransmitSysexConst(byte *msg_start, size_t length)
101 TransmitSysex(msg_start, length);
105 * Realtime MIDI playback service routine.
106 * This is called by the multimedia timer.
108 void CALLBACK TimerCallback(UINT uTimerID, UINT, DWORD_PTR dwUser, DWORD_PTR, DWORD_PTR)
110 /* Try to check playback status changes.
111 * If _midi is already locked, skip checking for this cycle and try again
112 * next cycle, instead of waiting for locks in the realtime callback. */
113 if (TryEnterCriticalSection(&_midi.lock)) {
114 /* check for stop */
115 if (_midi.do_stop) {
116 DEBUG(driver, 2, "Win32-MIDI: timer: do_stop is set");
117 midiOutReset(_midi.midi_out);
118 _midi.playing = false;
119 _midi.do_stop = false;
120 LeaveCriticalSection(&_midi.lock);
121 return;
124 /* check for start/restart/change song */
125 if (_midi.do_start) {
126 DEBUG(driver, 2, "Win32-MIDI: timer: do_start is set");
127 if (_midi.playing) {
128 midiOutReset(_midi.midi_out);
130 _midi.current_file.MoveFrom(_midi.next_file);
131 std::swap(_midi.next_segment, _midi.current_segment);
132 _midi.current_segment.start_block = 0;
133 _midi.playback_start_time = timeGetTime();
134 _midi.playing = true;
135 _midi.do_start = false;
136 _midi.current_block = 0;
138 MemSetT<byte>(_midi.channel_volumes, 127, lengthof(_midi.channel_volumes));
139 } else if (!_midi.playing) {
140 /* not playing, stop the timer */
141 DEBUG(driver, 2, "Win32-MIDI: timer: not playing, stopping timer");
142 timeKillEvent(uTimerID);
143 _midi.timer_id = 0;
144 LeaveCriticalSection(&_midi.lock);
145 return;
148 /* check for volume change */
149 static int volume_throttle = 0;
150 if (_midi.current_volume != _midi.new_volume) {
151 if (volume_throttle == 0) {
152 DEBUG(driver, 2, "Win32-MIDI: timer: volume change");
153 _midi.current_volume = _midi.new_volume;
154 volume_throttle = 20 / _midi.time_period;
155 for (int ch = 0; ch < 16; ch++) {
156 int vol = ScaleVolume(_midi.channel_volumes[ch], _midi.current_volume);
157 TransmitChannelMsg(MIDIST_CONTROLLER | ch, MIDICT_CHANVOLUME, vol);
160 else {
161 volume_throttle--;
165 LeaveCriticalSection(&_midi.lock);
168 /* skip beginning of file? */
169 if (_midi.current_segment.start > 0 && _midi.current_block == 0 && _midi.current_segment.start_block == 0) {
170 /* find first block after start time and pretend playback started earlier
171 * this is to allow all blocks prior to the actual start to still affect playback,
172 * as they may contain important controller and program changes */
173 size_t preload_bytes = 0;
174 for (size_t bl = 0; bl < _midi.current_file.blocks.size(); bl++) {
175 MidiFile::DataBlock &block = _midi.current_file.blocks[bl];
176 preload_bytes += block.data.Length();
177 if (block.ticktime >= _midi.current_segment.start) {
178 if (_midi.current_segment.loop) {
179 DEBUG(driver, 2, "Win32-MIDI: timer: loop from block %d (ticktime %d, realtime %.3f, bytes %d)", (int)bl, (int)block.ticktime, ((int)block.realtime)/1000.0, (int)preload_bytes);
180 _midi.current_segment.start_block = bl;
181 break;
182 } else {
183 DEBUG(driver, 2, "Win32-MIDI: timer: start from block %d (ticktime %d, realtime %.3f, bytes %d)", (int)bl, (int)block.ticktime, ((int)block.realtime) / 1000.0, (int)preload_bytes);
184 _midi.playback_start_time -= block.realtime / 1000;
185 break;
192 /* play pending blocks */
193 DWORD current_time = timeGetTime();
194 DWORD playback_time = current_time - _midi.playback_start_time;
195 while (_midi.current_block < _midi.current_file.blocks.size()) {
196 MidiFile::DataBlock &block = _midi.current_file.blocks[_midi.current_block];
198 /* check that block is not in the future */
199 if (block.realtime / 1000 > playback_time) {
200 break;
202 /* check that block isn't at end-of-song override */
203 if (_midi.current_segment.end > 0 && block.ticktime >= _midi.current_segment.end) {
204 if (_midi.current_segment.loop) {
205 _midi.current_block = _midi.current_segment.start_block;
206 _midi.playback_start_time = timeGetTime() - _midi.current_file.blocks[_midi.current_block].realtime / 1000;
207 } else {
208 _midi.do_stop = true;
210 break;
213 byte *data = block.data.Begin();
214 size_t remaining = block.data.Length();
215 byte last_status = 0;
216 while (remaining > 0) {
217 /* MidiFile ought to have converted everything out of running status,
218 * but handle it anyway just to be safe */
219 byte status = data[0];
220 if (status & 0x80) {
221 last_status = status;
222 data++;
223 remaining--;
224 } else {
225 status = last_status;
227 switch (status & 0xF0) {
228 case MIDIST_PROGCHG:
229 case MIDIST_CHANPRESS:
230 /* 2 byte channel messages */
231 TransmitChannelMsg(status, data[0]);
232 data++;
233 remaining--;
234 break;
235 case MIDIST_NOTEOFF:
236 case MIDIST_NOTEON:
237 case MIDIST_POLYPRESS:
238 case MIDIST_PITCHBEND:
239 /* 3 byte channel messages */
240 TransmitChannelMsg(status, data[0], data[1]);
241 data += 2;
242 remaining -= 2;
243 break;
244 case MIDIST_CONTROLLER:
245 /* controller change */
246 if (data[0] == MIDICT_CHANVOLUME) {
247 /* volume controller, adjust for user volume */
248 _midi.channel_volumes[status & 0x0F] = data[1];
249 int vol = ScaleVolume(data[1], _midi.current_volume);
250 TransmitChannelMsg(status, data[0], vol);
251 } else {
252 /* handle other controllers normally */
253 TransmitChannelMsg(status, data[0], data[1]);
255 data += 2;
256 remaining -= 2;
257 break;
258 case 0xF0:
259 /* system messages */
260 switch (status) {
261 case MIDIST_SYSEX: /* system exclusive */
262 TransmitSysex(data, remaining);
263 break;
264 case MIDIST_TC_QFRAME: /* time code quarter frame */
265 case MIDIST_SONGSEL: /* song select */
266 data++;
267 remaining--;
268 break;
269 case MIDIST_SONGPOSPTR: /* song position pointer */
270 data += 2;
271 remaining -= 2;
272 break;
273 default: /* remaining have no data bytes */
274 break;
276 break;
280 _midi.current_block++;
283 /* end? */
284 if (_midi.current_block == _midi.current_file.blocks.size()) {
285 if (_midi.current_segment.loop) {
286 _midi.current_block = 0;
287 _midi.playback_start_time = timeGetTime();
288 } else {
289 _midi.do_stop = true;
294 void MusicDriver_Win32::PlaySong(const char *filename)
296 DEBUG(driver, 2, "Win32-MIDI: PlaySong: entry");
297 EnterCriticalSection(&_midi.lock);
299 _midi.next_file.LoadFile(filename);
300 _midi.next_segment.start = 0;
301 _midi.next_segment.end = 0;
302 _midi.next_segment.loop = false;
304 DEBUG(driver, 2, "Win32-MIDI: PlaySong: setting flag");
305 _midi.do_stop = _midi.playing;
306 _midi.do_start = true;
308 if (_midi.timer_id == 0) {
309 DEBUG(driver, 2, "Win32-MIDI: PlaySong: starting timer");
310 _midi.timer_id = timeSetEvent(_midi.time_period, _midi.time_period, TimerCallback, (DWORD_PTR)this, TIME_PERIODIC | TIME_CALLBACK_FUNCTION);
313 LeaveCriticalSection(&_midi.lock);
316 void MusicDriver_Win32::StopSong()
318 DEBUG(driver, 2, "Win32-MIDI: StopSong: entry");
319 EnterCriticalSection(&_midi.lock);
320 DEBUG(driver, 2, "Win32-MIDI: StopSong: setting flag");
321 _midi.do_stop = true;
322 LeaveCriticalSection(&_midi.lock);
325 bool MusicDriver_Win32::IsSongPlaying()
327 return _midi.playing || _midi.do_start;
330 void MusicDriver_Win32::SetVolume(byte vol)
332 EnterCriticalSection(&_midi.lock);
333 _midi.new_volume = vol;
334 LeaveCriticalSection(&_midi.lock);
337 const char *MusicDriver_Win32::Start(const char * const *parm)
339 DEBUG(driver, 2, "Win32-MIDI: Start: initializing");
341 InitializeCriticalSection(&_midi.lock);
343 int resolution = GetDriverParamInt(parm, "resolution", 5);
344 int port = GetDriverParamInt(parm, "port", -1);
346 UINT devid;
347 if (port < 0) {
348 devid = MIDI_MAPPER;
349 } else {
350 devid = (UINT)port;
353 resolution = Clamp(resolution, 1, 20);
355 if (midiOutOpen(&_midi.midi_out, devid, (DWORD_PTR)&MidiOutProc, (DWORD_PTR)this, CALLBACK_FUNCTION) != MMSYSERR_NOERROR) {
356 return "could not open midi device";
359 midiOutReset(_midi.midi_out);
361 /* Standard "Enable General MIDI" message */
362 static byte gm_enable_sysex[] = { 0xF0, 0x7E, 0x00, 0x09, 0x01, 0xF7 };
363 TransmitSysexConst(&gm_enable_sysex[0], sizeof(gm_enable_sysex));
365 /* prepare multimedia timer */
366 TIMECAPS timecaps;
367 if (timeGetDevCaps(&timecaps, sizeof(timecaps)) == MMSYSERR_NOERROR) {
368 _midi.time_period = min(max((UINT)resolution, timecaps.wPeriodMin), timecaps.wPeriodMax);
369 if (timeBeginPeriod(_midi.time_period) == MMSYSERR_NOERROR) {
370 /* success */
371 DEBUG(driver, 2, "Win32-MIDI: Start: timer resolution is %d", (int)_midi.time_period);
372 return NULL;
375 midiOutClose(_midi.midi_out);
376 return "could not set timer resolution";
379 void MusicDriver_Win32::Stop()
381 EnterCriticalSection(&_midi.lock);
383 if (_midi.timer_id) {
384 timeKillEvent(_midi.timer_id);
385 _midi.timer_id = 0;
388 timeEndPeriod(_midi.time_period);
389 midiOutReset(_midi.midi_out);
390 midiOutClose(_midi.midi_out);
392 LeaveCriticalSection(&_midi.lock);
393 DeleteCriticalSection(&_midi.lock);