Fix: Don't allow right-click to close world generation progress window. (#13084)
[openttd-github.git] / src / music / win32_m.cpp
bloba9553123ed7bff273a8d2504c62e2eed48d7f593
1 /*
2 * This file is part of OpenTTD.
3 * 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.
4 * 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.
5 * 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/>.
6 */
8 /** @file win32_m.cpp Music playback for Windows. */
10 #include "../stdafx.h"
11 #include "../string_func.h"
12 #include "win32_m.h"
13 #include <windows.h>
14 #include <mmsystem.h>
15 #include "../os/windows/win32.h"
16 #include "../debug.h"
17 #include "midifile.hpp"
18 #include "midi.h"
19 #include "../base_media_base.h"
20 #include "../core/mem_func.hpp"
21 #include <mutex>
23 #include "../safeguards.h"
25 struct PlaybackSegment {
26 uint32_t start, end;
27 size_t start_block;
28 bool loop;
31 static struct {
32 UINT time_period; ///< obtained timer precision value
33 HMIDIOUT midi_out; ///< handle to open midiOut
34 UINT timer_id; ///< ID of active multimedia timer
35 std::mutex lock; ///< synchronization for playback status fields
37 bool playing; ///< flag indicating that playback is active
38 int do_start; ///< flag for starting playback of next_file at next opportunity
39 bool do_stop; ///< flag for stopping playback at next opportunity
40 uint8_t current_volume; ///< current effective volume setting
41 uint8_t new_volume; ///< volume setting to change to
43 MidiFile current_file; ///< file currently being played from
44 PlaybackSegment current_segment; ///< segment info for current playback
45 DWORD playback_start_time; ///< timestamp current file began playback
46 size_t current_block; ///< next block index to send
47 MidiFile next_file; ///< upcoming file to play
48 PlaybackSegment next_segment; ///< segment info for upcoming file
50 uint8_t channel_volumes[16]; ///< last seen volume controller values in raw data
51 } _midi;
53 static FMusicDriver_Win32 iFMusicDriver_Win32;
56 static uint8_t ScaleVolume(uint8_t original, uint8_t scale)
58 return original * scale / 127;
62 void CALLBACK MidiOutProc(HMIDIOUT hmo, UINT wMsg, DWORD_PTR, DWORD_PTR dwParam1, DWORD_PTR)
64 if (wMsg == MOM_DONE) {
65 MIDIHDR *hdr = (LPMIDIHDR)dwParam1;
66 midiOutUnprepareHeader(hmo, hdr, sizeof(*hdr));
67 free(hdr);
71 static void TransmitChannelMsg(uint8_t status, uint8_t p1, uint8_t p2 = 0)
73 midiOutShortMsg(_midi.midi_out, status | (p1 << 8) | (p2 << 16));
76 static void TransmitSysex(const uint8_t *&msg_start, size_t &remaining)
78 /* find end of message */
79 const uint8_t *msg_end = msg_start;
80 while (*msg_end != MIDIST_ENDSYSEX) msg_end++;
81 msg_end++; /* also include sysex end byte */
83 /* prepare header */
84 MIDIHDR *hdr = CallocT<MIDIHDR>(1);
85 hdr->lpData = reinterpret_cast<LPSTR>(const_cast<uint8_t *>(msg_start));
86 hdr->dwBufferLength = msg_end - msg_start;
87 if (midiOutPrepareHeader(_midi.midi_out, hdr, sizeof(*hdr)) == MMSYSERR_NOERROR) {
88 /* transmit - just point directly into the data buffer */
89 hdr->dwBytesRecorded = hdr->dwBufferLength;
90 midiOutLongMsg(_midi.midi_out, hdr, sizeof(*hdr));
91 } else {
92 free(hdr);
95 /* update position in buffer */
96 remaining -= msg_end - msg_start;
97 msg_start = msg_end;
100 static void TransmitStandardSysex(MidiSysexMessage msg)
102 size_t length = 0;
103 const uint8_t *data = MidiGetStandardSysexMessage(msg, length);
104 TransmitSysex(data, length);
108 * Realtime MIDI playback service routine.
109 * This is called by the multimedia timer.
111 void CALLBACK TimerCallback(UINT uTimerID, UINT, DWORD_PTR, DWORD_PTR, DWORD_PTR)
113 static int volume_throttle = 0;
115 /* Ensure only one timer callback is running at once, and prevent races on status flags */
116 std::unique_lock<std::mutex> mutex_lock(_midi.lock, std::defer_lock);
117 if (!mutex_lock.try_lock()) return;
119 /* check for stop */
120 if (_midi.do_stop) {
121 Debug(driver, 2, "Win32-MIDI: timer: do_stop is set");
122 midiOutReset(_midi.midi_out);
123 _midi.playing = false;
124 _midi.do_stop = false;
125 return;
128 /* check for start/restart/change song */
129 if (_midi.do_start != 0) {
130 /* Have a delay between playback start steps, prevents jumbled-together notes at the start of song */
131 if (timeGetTime() - _midi.playback_start_time < 50) {
132 return;
134 Debug(driver, 2, "Win32-MIDI: timer: do_start step {}", _midi.do_start);
136 if (_midi.do_start == 1) {
137 /* Send "all notes off" */
138 midiOutReset(_midi.midi_out);
139 _midi.playback_start_time = timeGetTime();
140 _midi.do_start = 2;
142 return;
143 } else if (_midi.do_start == 2) {
144 /* Reset the device to General MIDI defaults */
145 TransmitStandardSysex(MidiSysexMessage::ResetGM);
146 _midi.playback_start_time = timeGetTime();
147 _midi.do_start = 3;
149 return;
150 } else if (_midi.do_start == 3) {
151 /* Set up device-specific effects */
152 TransmitStandardSysex(MidiSysexMessage::RolandSetReverb);
153 _midi.playback_start_time = timeGetTime();
154 _midi.do_start = 4;
156 return;
157 } else if (_midi.do_start == 4) {
158 /* Load the new file */
159 _midi.current_file.MoveFrom(_midi.next_file);
160 std::swap(_midi.next_segment, _midi.current_segment);
161 _midi.current_segment.start_block = 0;
162 _midi.playback_start_time = timeGetTime();
163 _midi.playing = true;
164 _midi.do_start = 0;
165 _midi.current_block = 0;
167 MemSetT<uint8_t>(_midi.channel_volumes, 127, lengthof(_midi.channel_volumes));
168 /* Invalidate current volume. */
169 _midi.current_volume = UINT8_MAX;
170 volume_throttle = 0;
172 } else if (!_midi.playing) {
173 /* not playing, stop the timer */
174 Debug(driver, 2, "Win32-MIDI: timer: not playing, stopping timer");
175 timeKillEvent(uTimerID);
176 _midi.timer_id = 0;
177 return;
180 /* check for volume change */
181 if (_midi.current_volume != _midi.new_volume) {
182 if (volume_throttle == 0) {
183 Debug(driver, 2, "Win32-MIDI: timer: volume change");
184 _midi.current_volume = _midi.new_volume;
185 volume_throttle = 20 / _midi.time_period;
186 for (int ch = 0; ch < 16; ch++) {
187 uint8_t vol = ScaleVolume(_midi.channel_volumes[ch], _midi.current_volume);
188 TransmitChannelMsg(MIDIST_CONTROLLER | ch, MIDICT_CHANVOLUME, vol);
190 } else {
191 volume_throttle--;
195 /* skip beginning of file? */
196 if (_midi.current_segment.start > 0 && _midi.current_block == 0 && _midi.current_segment.start_block == 0) {
197 /* find first block after start time and pretend playback started earlier
198 * this is to allow all blocks prior to the actual start to still affect playback,
199 * as they may contain important controller and program changes */
200 size_t preload_bytes = 0;
201 for (size_t bl = 0; bl < _midi.current_file.blocks.size(); bl++) {
202 MidiFile::DataBlock &block = _midi.current_file.blocks[bl];
203 preload_bytes += block.data.size();
204 if (block.ticktime >= _midi.current_segment.start) {
205 if (_midi.current_segment.loop) {
206 Debug(driver, 2, "Win32-MIDI: timer: loop from block {} (ticktime {}, realtime {:.3f}, bytes {})", bl, block.ticktime, ((int)block.realtime)/1000.0, preload_bytes);
207 _midi.current_segment.start_block = bl;
208 break;
209 } else {
210 /* Calculate offset start time for playback.
211 * The preload_bytes are used to compensate for delay in transmission over traditional serial MIDI interfaces,
212 * which have a bitrate of 31,250 bits/sec, and transmit 1+8+1 start/data/stop bits per byte.
213 * The delay compensation is needed to avoid time-compression of following messages.
215 Debug(driver, 2, "Win32-MIDI: timer: start from block {} (ticktime {}, realtime {:.3f}, bytes {})", bl, block.ticktime, ((int)block.realtime) / 1000.0, preload_bytes);
216 _midi.playback_start_time -= block.realtime / 1000 - (DWORD)(preload_bytes * 1000 / 3125);
217 break;
224 /* play pending blocks */
225 DWORD current_time = timeGetTime();
226 DWORD playback_time = current_time - _midi.playback_start_time;
227 while (_midi.current_block < _midi.current_file.blocks.size()) {
228 MidiFile::DataBlock &block = _midi.current_file.blocks[_midi.current_block];
230 /* check that block isn't at end-of-song override */
231 if (_midi.current_segment.end > 0 && block.ticktime >= _midi.current_segment.end) {
232 if (_midi.current_segment.loop) {
233 _midi.current_block = _midi.current_segment.start_block;
234 _midi.playback_start_time = timeGetTime() - _midi.current_file.blocks[_midi.current_block].realtime / 1000;
235 } else {
236 _midi.do_stop = true;
238 break;
240 /* check that block is not in the future */
241 if (block.realtime / 1000 > playback_time) {
242 break;
245 const uint8_t *data = block.data.data();
246 size_t remaining = block.data.size();
247 uint8_t last_status = 0;
248 while (remaining > 0) {
249 /* MidiFile ought to have converted everything out of running status,
250 * but handle it anyway just to be safe */
251 uint8_t status = data[0];
252 if (status & 0x80) {
253 last_status = status;
254 data++;
255 remaining--;
256 } else {
257 status = last_status;
259 switch (status & 0xF0) {
260 case MIDIST_PROGCHG:
261 case MIDIST_CHANPRESS:
262 /* 2 byte channel messages */
263 TransmitChannelMsg(status, data[0]);
264 data++;
265 remaining--;
266 break;
267 case MIDIST_NOTEOFF:
268 case MIDIST_NOTEON:
269 case MIDIST_POLYPRESS:
270 case MIDIST_PITCHBEND:
271 /* 3 byte channel messages */
272 TransmitChannelMsg(status, data[0], data[1]);
273 data += 2;
274 remaining -= 2;
275 break;
276 case MIDIST_CONTROLLER:
277 /* controller change */
278 if (data[0] == MIDICT_CHANVOLUME) {
279 /* volume controller, adjust for user volume */
280 _midi.channel_volumes[status & 0x0F] = data[1];
281 int vol = ScaleVolume(data[1], _midi.current_volume);
282 TransmitChannelMsg(status, data[0], vol);
283 } else {
284 /* handle other controllers normally */
285 TransmitChannelMsg(status, data[0], data[1]);
287 data += 2;
288 remaining -= 2;
289 break;
290 case 0xF0:
291 /* system messages */
292 switch (status) {
293 case MIDIST_SYSEX: /* system exclusive */
294 TransmitSysex(data, remaining);
295 break;
296 case MIDIST_TC_QFRAME: /* time code quarter frame */
297 case MIDIST_SONGSEL: /* song select */
298 data++;
299 remaining--;
300 break;
301 case MIDIST_SONGPOSPTR: /* song position pointer */
302 data += 2;
303 remaining -= 2;
304 break;
305 default: /* remaining have no data bytes */
306 break;
308 break;
312 _midi.current_block++;
315 /* end? */
316 if (_midi.current_block == _midi.current_file.blocks.size()) {
317 if (_midi.current_segment.loop) {
318 _midi.current_block = _midi.current_segment.start_block;
319 _midi.playback_start_time = timeGetTime() - _midi.current_file.blocks[_midi.current_block].realtime / 1000;
320 } else {
321 _midi.do_stop = true;
326 void MusicDriver_Win32::PlaySong(const MusicSongInfo &song)
328 Debug(driver, 2, "Win32-MIDI: PlaySong: entry");
330 MidiFile new_song;
331 if (!new_song.LoadSong(song)) return;
332 Debug(driver, 2, "Win32-MIDI: PlaySong: Loaded song");
334 std::lock_guard<std::mutex> mutex_lock(_midi.lock);
336 _midi.next_file.MoveFrom(new_song);
337 _midi.next_segment.start = song.override_start;
338 _midi.next_segment.end = song.override_end;
339 _midi.next_segment.loop = song.loop;
341 Debug(driver, 2, "Win32-MIDI: PlaySong: setting flag");
342 _midi.do_stop = _midi.playing;
343 _midi.do_start = 1;
345 if (_midi.timer_id == 0) {
346 Debug(driver, 2, "Win32-MIDI: PlaySong: starting timer");
347 _midi.timer_id = timeSetEvent(_midi.time_period, _midi.time_period, TimerCallback, (DWORD_PTR)this, TIME_PERIODIC | TIME_CALLBACK_FUNCTION);
351 void MusicDriver_Win32::StopSong()
353 Debug(driver, 2, "Win32-MIDI: StopSong: entry");
354 std::lock_guard<std::mutex> mutex_lock(_midi.lock);
355 Debug(driver, 2, "Win32-MIDI: StopSong: setting flag");
356 _midi.do_stop = true;
359 bool MusicDriver_Win32::IsSongPlaying()
361 return _midi.playing || (_midi.do_start != 0);
364 void MusicDriver_Win32::SetVolume(uint8_t vol)
366 std::lock_guard<std::mutex> mutex_lock(_midi.lock);
367 _midi.new_volume = vol;
370 std::optional<std::string_view> MusicDriver_Win32::Start(const StringList &parm)
372 Debug(driver, 2, "Win32-MIDI: Start: initializing");
374 int resolution = GetDriverParamInt(parm, "resolution", 5);
375 uint port = (uint)GetDriverParamInt(parm, "port", UINT_MAX);
376 const char *portname = GetDriverParam(parm, "portname");
378 /* Enumerate ports either for selecting port by name, or for debug output */
379 if (portname != nullptr || _debug_driver_level > 0) {
380 uint numports = midiOutGetNumDevs();
381 Debug(driver, 1, "Win32-MIDI: Found {} output devices:", numports);
382 for (uint tryport = 0; tryport < numports; tryport++) {
383 MIDIOUTCAPS moc{};
384 if (midiOutGetDevCaps(tryport, &moc, sizeof(moc)) == MMSYSERR_NOERROR) {
385 char tryportname[128];
386 convert_from_fs(moc.szPname, tryportname);
388 /* Compare requested and detected port name.
389 * If multiple ports have the same name, this will select the last matching port, and the debug output will be confusing. */
390 if (portname != nullptr && strncmp(tryportname, portname, lengthof(tryportname)) == 0) port = tryport;
392 Debug(driver, 1, "MIDI port {:2d}: {}{}", tryport, tryportname, (tryport == port) ? " [selected]" : "");
397 UINT devid;
398 if (port == UINT_MAX) {
399 devid = MIDI_MAPPER;
400 } else {
401 devid = (UINT)port;
404 resolution = Clamp(resolution, 1, 20);
406 if (midiOutOpen(&_midi.midi_out, devid, (DWORD_PTR)&MidiOutProc, (DWORD_PTR)this, CALLBACK_FUNCTION) != MMSYSERR_NOERROR) {
407 return "could not open midi device";
410 midiOutReset(_midi.midi_out);
412 /* prepare multimedia timer */
413 TIMECAPS timecaps;
414 if (timeGetDevCaps(&timecaps, sizeof(timecaps)) == MMSYSERR_NOERROR) {
415 _midi.time_period = std::min(std::max((UINT)resolution, timecaps.wPeriodMin), timecaps.wPeriodMax);
416 if (timeBeginPeriod(_midi.time_period) == MMSYSERR_NOERROR) {
417 /* success */
418 Debug(driver, 2, "Win32-MIDI: Start: timer resolution is {}", _midi.time_period);
419 return std::nullopt;
422 midiOutClose(_midi.midi_out);
423 return "could not set timer resolution";
426 void MusicDriver_Win32::Stop()
428 std::lock_guard<std::mutex> mutex_lock(_midi.lock);
430 if (_midi.timer_id) {
431 timeKillEvent(_midi.timer_id);
432 _midi.timer_id = 0;
435 timeEndPeriod(_midi.time_period);
436 midiOutReset(_midi.midi_out);
437 midiOutClose(_midi.midi_out);