OpenTTD Source 20260311-master-g511d3794ce
win32_m.cpp
Go to the documentation of this file.
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 <https://www.gnu.org/licenses/old-licenses/gpl-2.0>.
6 */
7
9
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 "../base_media_music.h"
21#include <mutex>
22
23#include "../safeguards.h"
24
25struct PlaybackSegment {
26 uint32_t start, end;
27 size_t start_block;
28 bool loop;
29};
30
31static struct {
33 HMIDIOUT midi_out;
34 UINT timer_id;
35 std::mutex lock;
36
37 bool playing;
39 bool do_stop;
41 uint8_t new_volume;
42
49
50 std::array<uint8_t, 16> channel_volumes;
51} _midi;
52
53static FMusicDriver_Win32 iFMusicDriver_Win32;
54
55
56static uint8_t ScaleVolume(uint8_t original, uint8_t scale)
57{
58 return original * scale / 127;
59}
60
61
62void CALLBACK MidiOutProc(HMIDIOUT hmo, UINT wMsg, DWORD_PTR, DWORD_PTR dwParam1, DWORD_PTR)
63{
64 if (wMsg == MOM_DONE) {
65 MIDIHDR *hdr = (LPMIDIHDR)dwParam1;
66 midiOutUnprepareHeader(hmo, hdr, sizeof(*hdr));
67 delete hdr;
68 }
69}
70
71static void TransmitChannelMsg(uint8_t status, uint8_t p1, uint8_t p2 = 0)
72{
73 midiOutShortMsg(_midi.midi_out, status | (p1 << 8) | (p2 << 16));
74}
75
76static void TransmitSysex(const uint8_t *&msg_start, size_t &remaining)
77{
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 */
82
83 /* prepare header */
84 auto hdr = std::make_unique<MIDIHDR>();
85 hdr->lpData = reinterpret_cast<LPSTR>(const_cast<uint8_t *>(msg_start));
86 hdr->dwBufferLength = static_cast<DWORD>(msg_end - msg_start);
87 if (midiOutPrepareHeader(_midi.midi_out, hdr.get(), sizeof(MIDIHDR)) == MMSYSERR_NOERROR) {
88 /* transmit - just point directly into the data buffer */
89 hdr->dwBytesRecorded = hdr->dwBufferLength;
90 midiOutLongMsg(_midi.midi_out, hdr.release(), sizeof(MIDIHDR));
91 }
92
93 /* update position in buffer */
94 remaining -= msg_end - msg_start;
95 msg_start = msg_end;
96}
97
98static void TransmitStandardSysex(MidiSysexMessage msg)
99{
100 size_t length = 0;
101 const uint8_t *data = MidiGetStandardSysexMessage(msg, length);
102 TransmitSysex(data, length);
103}
104
110void CALLBACK TimerCallback(UINT uTimerID, UINT, DWORD_PTR, DWORD_PTR, DWORD_PTR)
111{
112 static int volume_throttle = 0;
113
114 /* Ensure only one timer callback is running at once, and prevent races on status flags */
115 std::unique_lock<std::mutex> mutex_lock(_midi.lock, std::defer_lock);
116 if (!mutex_lock.try_lock()) return;
117
118 /* check for stop */
119 if (_midi.do_stop) {
120 Debug(driver, 2, "Win32-MIDI: timer: do_stop is set");
121 midiOutReset(_midi.midi_out);
122 _midi.playing = false;
123 _midi.do_stop = false;
124 return;
125 }
126
127 /* check for start/restart/change song */
128 if (_midi.do_start != 0) {
129 /* Have a delay between playback start steps, prevents jumbled-together notes at the start of song */
130 if (timeGetTime() - _midi.playback_start_time < 50) {
131 return;
132 }
133 Debug(driver, 2, "Win32-MIDI: timer: do_start step {}", _midi.do_start);
134
135 if (_midi.do_start == 1) {
136 /* Send "all notes off" */
137 midiOutReset(_midi.midi_out);
138 _midi.playback_start_time = timeGetTime();
139 _midi.do_start = 2;
140
141 return;
142 } else if (_midi.do_start == 2) {
143 /* Reset the device to General MIDI defaults */
144 TransmitStandardSysex(MidiSysexMessage::ResetGM);
145 _midi.playback_start_time = timeGetTime();
146 _midi.do_start = 3;
147
148 return;
149 } else if (_midi.do_start == 3) {
150 /* Set up device-specific effects */
151 TransmitStandardSysex(MidiSysexMessage::RolandSetReverb);
152 _midi.playback_start_time = timeGetTime();
153 _midi.do_start = 4;
154
155 return;
156 } else if (_midi.do_start == 4) {
157 /* Load the new file */
158 _midi.current_file.MoveFrom(_midi.next_file);
159 std::swap(_midi.next_segment, _midi.current_segment);
160 _midi.current_segment.start_block = 0;
161 _midi.playback_start_time = timeGetTime();
162 _midi.playing = true;
163 _midi.do_start = 0;
164 _midi.current_block = 0;
165
166 _midi.channel_volumes.fill(127);
167 /* Invalidate current volume. */
168 _midi.current_volume = UINT8_MAX;
169 volume_throttle = 0;
170 }
171 } else if (!_midi.playing) {
172 /* not playing, stop the timer */
173 Debug(driver, 2, "Win32-MIDI: timer: not playing, stopping timer");
174 timeKillEvent(uTimerID);
175 _midi.timer_id = 0;
176 return;
177 }
178
179 /* check for volume change */
180 if (_midi.current_volume != _midi.new_volume) {
181 if (volume_throttle == 0) {
182 Debug(driver, 2, "Win32-MIDI: timer: volume change");
183 _midi.current_volume = _midi.new_volume;
184 volume_throttle = 20 / _midi.time_period;
185 for (int ch = 0; ch < 16; ch++) {
186 uint8_t vol = ScaleVolume(_midi.channel_volumes[ch], _midi.current_volume);
187 TransmitChannelMsg(MIDIST_CONTROLLER | ch, MIDICT_CHANVOLUME, vol);
188 }
189 } else {
190 volume_throttle--;
191 }
192 }
193
194 /* skip beginning of file? */
195 if (_midi.current_segment.start > 0 && _midi.current_block == 0 && _midi.current_segment.start_block == 0) {
196 /* find first block after start time and pretend playback started earlier
197 * this is to allow all blocks prior to the actual start to still affect playback,
198 * as they may contain important controller and program changes */
199 size_t preload_bytes = 0;
200 for (size_t bl = 0; bl < _midi.current_file.blocks.size(); bl++) {
201 MidiFile::DataBlock &block = _midi.current_file.blocks[bl];
202 preload_bytes += block.data.size();
203 if (block.ticktime >= _midi.current_segment.start) {
204 if (_midi.current_segment.loop) {
205 Debug(driver, 2, "Win32-MIDI: timer: loop from block {} (ticktime {}, realtime {:.3f}, bytes {})", bl, block.ticktime, block.realtime / 1000.0, preload_bytes);
206 _midi.current_segment.start_block = bl;
207 break;
208 } else {
209 /* Calculate offset start time for playback.
210 * The preload_bytes are used to compensate for delay in transmission over traditional serial MIDI interfaces,
211 * which have a bitrate of 31,250 bits/sec, and transmit 1+8+1 start/data/stop bits per byte.
212 * The delay compensation is needed to avoid time-compression of following messages.
213 */
214 Debug(driver, 2, "Win32-MIDI: timer: start from block {} (ticktime {}, realtime {:.3f}, bytes {})", bl, block.ticktime, block.realtime / 1000.0, preload_bytes);
215 _midi.playback_start_time -= block.realtime / 1000 - (DWORD)(preload_bytes * 1000 / 3125);
216 break;
217 }
218 }
219 }
220 }
221
222
223 /* play pending blocks */
224 DWORD current_time = timeGetTime();
225 DWORD playback_time = current_time - _midi.playback_start_time;
226 while (_midi.current_block < _midi.current_file.blocks.size()) {
227 MidiFile::DataBlock &block = _midi.current_file.blocks[_midi.current_block];
228
229 /* check that block isn't at end-of-song override */
230 if (_midi.current_segment.end > 0 && block.ticktime >= _midi.current_segment.end) {
231 if (_midi.current_segment.loop) {
232 _midi.current_block = _midi.current_segment.start_block;
233 _midi.playback_start_time = timeGetTime() - _midi.current_file.blocks[_midi.current_block].realtime / 1000;
234 } else {
235 _midi.do_stop = true;
236 }
237 break;
238 }
239 /* check that block is not in the future */
240 if (block.realtime / 1000 > playback_time) {
241 break;
242 }
243
244 const uint8_t *data = block.data.data();
245 size_t remaining = block.data.size();
246 uint8_t last_status = 0;
247 while (remaining > 0) {
248 /* MidiFile ought to have converted everything out of running status,
249 * but handle it anyway just to be safe */
250 uint8_t status = data[0];
251 if (status & 0x80) {
252 last_status = status;
253 data++;
254 remaining--;
255 } else {
256 status = last_status;
257 }
258 switch (status & 0xF0) {
259 case MIDIST_PROGCHG:
260 case MIDIST_CHANPRESS:
261 /* 2 byte channel messages */
262 TransmitChannelMsg(status, data[0]);
263 data++;
264 remaining--;
265 break;
266 case MIDIST_NOTEOFF:
267 case MIDIST_NOTEON:
268 case MIDIST_POLYPRESS:
269 case MIDIST_PITCHBEND:
270 /* 3 byte channel messages */
271 TransmitChannelMsg(status, data[0], data[1]);
272 data += 2;
273 remaining -= 2;
274 break;
275 case MIDIST_CONTROLLER:
276 /* controller change */
277 if (data[0] == MIDICT_CHANVOLUME) {
278 /* volume controller, adjust for user volume */
279 _midi.channel_volumes[status & 0x0F] = data[1];
280 int vol = ScaleVolume(data[1], _midi.current_volume);
281 TransmitChannelMsg(status, data[0], vol);
282 } else {
283 /* handle other controllers normally */
284 TransmitChannelMsg(status, data[0], data[1]);
285 }
286 data += 2;
287 remaining -= 2;
288 break;
289 case 0xF0:
290 /* system messages */
291 switch (status) {
292 case MIDIST_SYSEX: /* system exclusive */
293 TransmitSysex(data, remaining);
294 break;
295 case MIDIST_TC_QFRAME: /* time code quarter frame */
296 case MIDIST_SONGSEL: /* song select */
297 data++;
298 remaining--;
299 break;
300 case MIDIST_SONGPOSPTR: /* song position pointer */
301 data += 2;
302 remaining -= 2;
303 break;
304 default: /* remaining have no data bytes */
305 break;
306 }
307 break;
308 }
309 }
310
311 _midi.current_block++;
312 }
313
314 /* end? */
315 if (_midi.current_block == _midi.current_file.blocks.size()) {
316 if (_midi.current_segment.loop) {
317 _midi.current_block = _midi.current_segment.start_block;
318 _midi.playback_start_time = timeGetTime() - _midi.current_file.blocks[_midi.current_block].realtime / 1000;
319 } else {
320 _midi.do_stop = true;
321 }
322 }
323}
324
326{
327 Debug(driver, 2, "Win32-MIDI: PlaySong: entry");
328
329 MidiFile new_song;
330 if (!new_song.LoadSong(song)) return;
331 Debug(driver, 2, "Win32-MIDI: PlaySong: Loaded song");
332
333 std::lock_guard<std::mutex> mutex_lock(_midi.lock);
334
335 _midi.next_file.MoveFrom(new_song);
336 _midi.next_segment.start = song.override_start;
337 _midi.next_segment.end = song.override_end;
338 _midi.next_segment.loop = song.loop;
339
340 Debug(driver, 2, "Win32-MIDI: PlaySong: setting flag");
341 _midi.do_stop = _midi.playing;
342 _midi.do_start = 1;
343
344 if (_midi.timer_id == 0) {
345 Debug(driver, 2, "Win32-MIDI: PlaySong: starting timer");
346 _midi.timer_id = timeSetEvent(_midi.time_period, _midi.time_period, TimerCallback, (DWORD_PTR)this, TIME_PERIODIC | TIME_CALLBACK_FUNCTION);
347 }
348}
349
351{
352 Debug(driver, 2, "Win32-MIDI: StopSong: entry");
353 std::lock_guard<std::mutex> mutex_lock(_midi.lock);
354 Debug(driver, 2, "Win32-MIDI: StopSong: setting flag");
355 _midi.do_stop = true;
356}
357
359{
360 return _midi.playing || (_midi.do_start != 0);
361}
362
364{
365 std::lock_guard<std::mutex> mutex_lock(_midi.lock);
366 _midi.new_volume = vol;
367}
368
369std::optional<std::string_view> MusicDriver_Win32::Start(const StringList &parm)
370{
371 Debug(driver, 2, "Win32-MIDI: Start: initializing");
372
373 int resolution = GetDriverParamInt(parm, "resolution", 5);
374 uint port = (uint)GetDriverParamInt(parm, "port", UINT_MAX);
375 auto portname = GetDriverParam(parm, "portname");
376
377 /* Enumerate ports either for selecting port by name, or for debug output */
378 if (portname.has_value() || _debug_driver_level > 0) {
379 uint numports = midiOutGetNumDevs();
380 Debug(driver, 1, "Win32-MIDI: Found {} output devices:", numports);
381 for (uint tryport = 0; tryport < numports; tryport++) {
382 MIDIOUTCAPS moc{};
383 if (midiOutGetDevCaps(tryport, &moc, sizeof(moc)) == MMSYSERR_NOERROR) {
384 char tryportname[128];
385 convert_from_fs(moc.szPname, tryportname);
386
387 /* Compare requested and detected port name.
388 * If multiple ports have the same name, this will select the last matching port, and the debug output will be confusing. */
389 if (portname.has_value() && *portname == tryportname) port = tryport;
390
391 Debug(driver, 1, "MIDI port {:2d}: {}{}", tryport, tryportname, (tryport == port) ? " [selected]" : "");
392 }
393 }
394 }
395
396 UINT devid;
397 if (port == UINT_MAX) {
398 devid = MIDI_MAPPER;
399 } else {
400 devid = (UINT)port;
401 }
402
403 resolution = Clamp(resolution, 1, 20);
404
405 if (midiOutOpen(&_midi.midi_out, devid, (DWORD_PTR)&MidiOutProc, (DWORD_PTR)this, CALLBACK_FUNCTION) != MMSYSERR_NOERROR) {
406 return "could not open midi device";
407 }
408
409 midiOutReset(_midi.midi_out);
410
411 /* prepare multimedia timer */
412 TIMECAPS timecaps;
413 if (timeGetDevCaps(&timecaps, sizeof(timecaps)) == MMSYSERR_NOERROR) {
414 _midi.time_period = std::min(std::max((UINT)resolution, timecaps.wPeriodMin), timecaps.wPeriodMax);
415 if (timeBeginPeriod(_midi.time_period) == MMSYSERR_NOERROR) {
416 /* success */
417 Debug(driver, 2, "Win32-MIDI: Start: timer resolution is {}", _midi.time_period);
418 return std::nullopt;
419 }
420 }
421 midiOutClose(_midi.midi_out);
422 return "could not set timer resolution";
423}
424
426{
427 std::lock_guard<std::mutex> mutex_lock(_midi.lock);
428
429 if (_midi.timer_id) {
430 timeKillEvent(_midi.timer_id);
431 _midi.timer_id = 0;
432 }
433
434 timeEndPeriod(_midi.time_period);
435 midiOutReset(_midi.midi_out);
436 midiOutClose(_midi.midi_out);
437}
Generic functions for replacing base data (graphics, sounds).
Generic functions for replacing base music data.
Factory for Windows' music player.
Definition win32_m.h:33
void PlaySong(const MusicSongInfo &song) override
Play a particular song.
Definition win32_m.cpp:325
void StopSong() override
Stop playing the current song.
Definition win32_m.cpp:350
bool IsSongPlaying() override
Are we currently playing a song?
Definition win32_m.cpp:358
void SetVolume(uint8_t vol) override
Set the volume, if possible.
Definition win32_m.cpp:363
void Stop() override
Stop this driver.
Definition win32_m.cpp:425
std::optional< std::string_view > Start(const StringList &param) override
Start this driver.
Definition win32_m.cpp:369
Functions related to debugging.
#define Debug(category, level, format_string,...)
Output a line of debugging information.
Definition debug.h:37
bool playing
flag indicating that playback is active
Definition dmusic.cpp:170
uint8_t new_volume
volume setting to change to
Definition dmusic.cpp:175
bool do_stop
flag for stopping playback at next opportunity
Definition dmusic.cpp:172
MidiFile next_file
upcoming file to play
Definition dmusic.cpp:177
bool do_start
flag for starting playback of next_file at next opportunity
Definition dmusic.cpp:171
PlaybackSegment next_segment
segment info for upcoming file
Definition dmusic.cpp:178
int GetDriverParamInt(const StringList &parm, std::string_view name, int def)
Get an integer parameter the list of parameters.
Definition driver.cpp:79
std::optional< std::string_view > GetDriverParam(const StringList &parm, std::string_view name)
Get a string parameter the list of parameters.
Definition driver.cpp:47
static struct @363362336073004235014321267016377305167030167165 _midi
Metadata about the midi we're playing.
constexpr T Clamp(const T a, const T min, const T max)
Clamp a value between an interval.
Definition math_func.hpp:79
Declarations for MIDI data.
const uint8_t * MidiGetStandardSysexMessage(MidiSysexMessage msg, size_t &length)
Retrieve a well-known MIDI system exclusive message.
Definition midifile.cpp:39
MidiSysexMessage
Well-known MIDI system exclusive message values for use with the MidiGetStandardSysexMessage function...
Definition midi.h:142
@ ResetGM
Reset device to General MIDI defaults.
Definition midi.h:144
@ RolandSetReverb
Set up Roland SoundCanvas reverb room as TTD does.
Definition midi.h:150
@ MIDIST_ENDSYSEX
only occurs in realtime data
Definition midi.h:39
Parser for standard MIDI files.
A number of safeguards to prevent using unsafe methods.
Definition of base types and functions in a cross-platform compatible way.
Functions related to low-level strings.
std::vector< std::string > StringList
Type for a list of strings.
Definition string_type.h:60
std::vector< uint8_t > data
raw midi data contained in block
Definition midifile.hpp:22
uint32_t ticktime
tick number since start of file this block should be triggered at
Definition midifile.hpp:20
int64_t realtime
real-time (microseconds) since start of file this block should be triggered at
Definition midifile.hpp:21
Metadata about a music track.
int override_start
MIDI ticks to skip over in beginning.
bool loop
song should play in a tight loop if possible, never ending
int override_end
MIDI tick to end the song at (0 if no override).
std::string_view convert_from_fs(const std::wstring_view src, std::span< char > dst_buf)
Convert to OpenTTD's encoding from that of the environment in UNICODE.
Definition win32.cpp:386
Declarations of functions for MS windows systems.
uint8_t current_volume
current effective volume setting
Definition win32_m.cpp:40
MidiFile current_file
file currently being played from
Definition win32_m.cpp:43
PlaybackSegment current_segment
segment info for current playback
Definition win32_m.cpp:44
size_t current_block
next block index to send
Definition win32_m.cpp:46
std::array< uint8_t, 16 > channel_volumes
last seen volume controller values in raw data
Definition win32_m.cpp:50
UINT timer_id
ID of active multimedia timer.
Definition win32_m.cpp:34
std::mutex lock
synchronization for playback status fields
Definition win32_m.cpp:35
HMIDIOUT midi_out
handle to open midiOut
Definition win32_m.cpp:33
DWORD playback_start_time
timestamp current file began playback
Definition win32_m.cpp:45
UINT time_period
obtained timer precision value
Definition win32_m.cpp:32
void TimerCallback(UINT uTimerID, UINT, DWORD_PTR, DWORD_PTR, DWORD_PTR)
Realtime MIDI playback service routine.
Definition win32_m.cpp:110
Base for Windows music playback.