OpenTTD Source  20240919-master-gdf0233f4c2
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 <http://www.gnu.org/licenses/>.
6  */
7 
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>
22 
23 #include "../safeguards.h"
24 
25 struct PlaybackSegment {
26  uint32_t start, end;
27  size_t start_block;
28  bool loop;
29 };
30 
31 static struct {
32  UINT time_period;
33  HMIDIOUT midi_out;
34  UINT timer_id;
35  std::mutex lock;
36 
37  bool playing;
38  int do_start;
39  bool do_stop;
40  uint8_t current_volume;
41  uint8_t new_volume;
42 
46  size_t current_block;
49 
50  uint8_t channel_volumes[16];
51 } _midi;
52 
53 static FMusicDriver_Win32 iFMusicDriver_Win32;
54 
55 
56 static uint8_t ScaleVolume(uint8_t original, uint8_t scale)
57 {
58  return original * scale / 127;
59 }
60 
61 
62 void 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  free(hdr);
68  }
69 }
70 
71 static 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 
76 static 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  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);
93  }
94 
95  /* update position in buffer */
96  remaining -= msg_end - msg_start;
97  msg_start = msg_end;
98 }
99 
100 static void TransmitStandardSysex(MidiSysexMessage msg)
101 {
102  size_t length = 0;
103  const uint8_t *data = MidiGetStandardSysexMessage(msg, length);
104  TransmitSysex(data, length);
105 }
106 
111 void CALLBACK TimerCallback(UINT uTimerID, UINT, DWORD_PTR, DWORD_PTR, DWORD_PTR)
112 {
113  static int volume_throttle = 0;
114 
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;
118 
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;
126  }
127 
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;
133  }
134  Debug(driver, 2, "Win32-MIDI: timer: do_start step {}", _midi.do_start);
135 
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;
141 
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;
148 
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;
155 
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;
166 
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;
171  }
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;
178  }
179 
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);
189  }
190  } else {
191  volume_throttle--;
192  }
193  }
194 
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.
214  */
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;
218  }
219  }
220  }
221  }
222 
223 
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];
229 
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;
237  }
238  break;
239  }
240  /* check that block is not in the future */
241  if (block.realtime / 1000 > playback_time) {
242  break;
243  }
244 
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;
258  }
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]);
286  }
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;
307  }
308  break;
309  }
310  }
311 
312  _midi.current_block++;
313  }
314 
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;
322  }
323  }
324 }
325 
327 {
328  Debug(driver, 2, "Win32-MIDI: PlaySong: entry");
329 
330  MidiFile new_song;
331  if (!new_song.LoadSong(song)) return;
332  Debug(driver, 2, "Win32-MIDI: PlaySong: Loaded song");
333 
334  std::lock_guard<std::mutex> mutex_lock(_midi.lock);
335 
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;
340 
341  Debug(driver, 2, "Win32-MIDI: PlaySong: setting flag");
342  _midi.do_stop = _midi.playing;
343  _midi.do_start = 1;
344 
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);
348  }
349 }
350 
352 {
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;
357 }
358 
360 {
361  return _midi.playing || (_midi.do_start != 0);
362 }
363 
365 {
366  std::lock_guard<std::mutex> mutex_lock(_midi.lock);
367  _midi.new_volume = vol;
368 }
369 
370 std::optional<std::string_view> MusicDriver_Win32::Start(const StringList &parm)
371 {
372  Debug(driver, 2, "Win32-MIDI: Start: initializing");
373 
374  int resolution = GetDriverParamInt(parm, "resolution", 5);
375  uint port = (uint)GetDriverParamInt(parm, "port", UINT_MAX);
376  const char *portname = GetDriverParam(parm, "portname");
377 
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);
387 
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;
391 
392  Debug(driver, 1, "MIDI port {:2d}: {}{}", tryport, tryportname, (tryport == port) ? " [selected]" : "");
393  }
394  }
395  }
396 
397  UINT devid;
398  if (port == UINT_MAX) {
399  devid = MIDI_MAPPER;
400  } else {
401  devid = (UINT)port;
402  }
403 
404  resolution = Clamp(resolution, 1, 20);
405 
406  if (midiOutOpen(&_midi.midi_out, devid, (DWORD_PTR)&MidiOutProc, (DWORD_PTR)this, CALLBACK_FUNCTION) != MMSYSERR_NOERROR) {
407  return "could not open midi device";
408  }
409 
410  midiOutReset(_midi.midi_out);
411 
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;
420  }
421  }
422  midiOutClose(_midi.midi_out);
423  return "could not set timer resolution";
424 }
425 
427 {
428  std::lock_guard<std::mutex> mutex_lock(_midi.lock);
429 
430  if (_midi.timer_id) {
431  timeKillEvent(_midi.timer_id);
432  _midi.timer_id = 0;
433  }
434 
435  timeEndPeriod(_midi.time_period);
436  midiOutReset(_midi.midi_out);
437  midiOutClose(_midi.midi_out);
438 }
MidiFile
Definition: midifile.hpp:19
MusicDriver_Win32::Start
std::optional< std::string_view > Start(const StringList &param) override
Start this driver.
Definition: win32_m.cpp:370
do_start
int do_start
flag for starting playback of next_file at next opportunity
Definition: win32_m.cpp:38
PlaybackSegment
Definition: dmusic.cpp:118
lock
std::mutex lock
synchronization for playback status fields
Definition: win32_m.cpp:35
new_volume
uint8_t new_volume
volume setting to change to
Definition: win32_m.cpp:41
next_file
MidiFile next_file
upcoming file to play
Definition: win32_m.cpp:47
MusicSongInfo::override_start
int override_start
MIDI ticks to skip over in beginning.
Definition: base_media_base.h:332
MusicSongInfo::loop
bool loop
song should play in a tight loop if possible, never ending
Definition: base_media_base.h:331
current_volume
uint8_t current_volume
current effective volume setting
Definition: win32_m.cpp:40
TimerCallback
void CALLBACK TimerCallback(UINT uTimerID, UINT, DWORD_PTR, DWORD_PTR, DWORD_PTR)
Realtime MIDI playback service routine.
Definition: win32_m.cpp:111
GetDriverParam
const char * GetDriverParam(const StringList &parm, const char *name)
Get a string parameter the list of parameters.
Definition: driver.cpp:44
MusicDriver_Win32::PlaySong
void PlaySong(const MusicSongInfo &song) override
Play a particular song.
Definition: win32_m.cpp:326
MusicSongInfo::override_end
int override_end
MIDI tick to end the song at (0 if no override)
Definition: base_media_base.h:333
Debug
#define Debug(category, level, format_string,...)
Ouptut a line of debugging information.
Definition: debug.h:37
_midi
static struct @11 _midi
Metadata about the midi we're playing.
MusicDriver_Win32::SetVolume
void SetVolume(uint8_t vol) override
Set the volume, if possible.
Definition: win32_m.cpp:364
FMusicDriver_Win32
Factory for Windows' music player.
Definition: win32_m.h:33
free
void free(const void *ptr)
Version of the standard free that accepts const pointers.
Definition: stdafx.h:334
convert_from_fs
char * 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:372
current_segment
PlaybackSegment current_segment
segment info for current playback
Definition: win32_m.cpp:44
MusicSongInfo
Metadata about a music track.
Definition: base_media_base.h:325
StringList
std::vector< std::string > StringList
Type for a list of strings.
Definition: string_type.h:60
lengthof
#define lengthof(array)
Return the length of an fixed size array.
Definition: stdafx.h:280
midi_out
HMIDIOUT midi_out
handle to open midiOut
Definition: win32_m.cpp:33
do_stop
bool do_stop
flag for stopping playback at next opportunity
Definition: win32_m.cpp:39
MusicDriver_Win32::StopSong
void StopSong() override
Stop playing the current song.
Definition: win32_m.cpp:351
MidiFile::DataBlock::data
std::vector< uint8_t > data
raw midi data contained in block
Definition: midifile.hpp:23
GetDriverParamInt
int GetDriverParamInt(const StringList &parm, const char *name, int def)
Get an integer parameter the list of parameters.
Definition: driver.cpp:76
MusicDriver_Win32::Stop
void Stop() override
Stop this driver.
Definition: win32_m.cpp:426
current_file
MidiFile current_file
file currently being played from
Definition: win32_m.cpp:43
MusicDriver_Win32::IsSongPlaying
bool IsSongPlaying() override
Are we currently playing a song?
Definition: win32_m.cpp:359
win32_m.h
playing
bool playing
flag indicating that playback is active
Definition: win32_m.cpp:37
time_period
UINT time_period
obtained timer precision value
Definition: win32_m.cpp:32
current_block
size_t current_block
next block index to send
Definition: win32_m.cpp:46
Clamp
constexpr T Clamp(const T a, const T min, const T max)
Clamp a value between an interval.
Definition: math_func.hpp:79
MidiFile::DataBlock::realtime
uint32_t realtime
real-time (microseconds) since start of file this block should be triggered at
Definition: midifile.hpp:22
timer_id
UINT timer_id
ID of active multimedia timer.
Definition: win32_m.cpp:34
next_segment
PlaybackSegment next_segment
segment info for upcoming file
Definition: win32_m.cpp:48
channel_volumes
uint8_t channel_volumes[16]
last seen volume controller values in raw data
Definition: win32_m.cpp:50
MidiFile::DataBlock
Definition: midifile.hpp:20
MidiFile::DataBlock::ticktime
uint32_t ticktime
tick number since start of file this block should be triggered at
Definition: midifile.hpp:21
playback_start_time
DWORD playback_start_time
timestamp current file began playback
Definition: win32_m.cpp:45