11#include "../string_func.h"
15#include "../os/windows/win32.h"
17#include "midifile.hpp"
19#include "../base_media_base.h"
20#include "../base_media_music.h"
21#include "../core/mem_func.hpp"
24#include "../safeguards.h"
57static uint8_t ScaleVolume(uint8_t original, uint8_t scale)
59 return original * scale / 127;
63void CALLBACK MidiOutProc(HMIDIOUT hmo, UINT wMsg, DWORD_PTR, DWORD_PTR dwParam1, DWORD_PTR)
65 if (wMsg == MOM_DONE) {
66 MIDIHDR *hdr = (LPMIDIHDR)dwParam1;
67 midiOutUnprepareHeader(hmo, hdr,
sizeof(*hdr));
72static void TransmitChannelMsg(uint8_t status, uint8_t p1, uint8_t p2 = 0)
74 midiOutShortMsg(_midi.midi_out, status | (p1 << 8) | (p2 << 16));
77static void TransmitSysex(
const uint8_t *&msg_start,
size_t &remaining)
80 const uint8_t *msg_end = msg_start;
81 while (*msg_end != MIDIST_ENDSYSEX) msg_end++;
85 auto hdr = std::make_unique<MIDIHDR>();
86 hdr->lpData =
reinterpret_cast<LPSTR
>(
const_cast<uint8_t *
>(msg_start));
87 hdr->dwBufferLength =
static_cast<DWORD
>(msg_end - msg_start);
88 if (midiOutPrepareHeader(_midi.midi_out, hdr.get(),
sizeof(MIDIHDR)) == MMSYSERR_NOERROR) {
90 hdr->dwBytesRecorded = hdr->dwBufferLength;
91 midiOutLongMsg(_midi.midi_out, hdr.release(),
sizeof(MIDIHDR));
95 remaining -= msg_end - msg_start;
99static void TransmitStandardSysex(MidiSysexMessage msg)
102 const uint8_t *data = MidiGetStandardSysexMessage(msg, length);
103 TransmitSysex(data, length);
110void CALLBACK
TimerCallback(UINT uTimerID, UINT, DWORD_PTR, DWORD_PTR, DWORD_PTR)
112 static int volume_throttle = 0;
115 std::unique_lock<std::mutex> mutex_lock(_midi.lock, std::defer_lock);
116 if (!mutex_lock.try_lock())
return;
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;
128 if (_midi.do_start != 0) {
130 if (timeGetTime() - _midi.playback_start_time < 50) {
133 Debug(driver, 2,
"Win32-MIDI: timer: do_start step {}", _midi.do_start);
135 if (_midi.do_start == 1) {
137 midiOutReset(_midi.midi_out);
138 _midi.playback_start_time = timeGetTime();
142 }
else if (_midi.do_start == 2) {
144 TransmitStandardSysex(MidiSysexMessage::ResetGM);
145 _midi.playback_start_time = timeGetTime();
149 }
else if (_midi.do_start == 3) {
151 TransmitStandardSysex(MidiSysexMessage::RolandSetReverb);
152 _midi.playback_start_time = timeGetTime();
156 }
else if (_midi.do_start == 4) {
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;
164 _midi.current_block = 0;
166 MemSetT<uint8_t>(_midi.channel_volumes, 127,
lengthof(_midi.channel_volumes));
168 _midi.current_volume = UINT8_MAX;
171 }
else if (!_midi.playing) {
173 Debug(driver, 2,
"Win32-MIDI: timer: not playing, stopping timer");
174 timeKillEvent(uTimerID);
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);
195 if (_midi.current_segment.start > 0 && _midi.current_block == 0 && _midi.current_segment.start_block == 0) {
199 size_t preload_bytes = 0;
200 for (
size_t bl = 0; bl < _midi.current_file.blocks.size(); 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, ((
int)block.
realtime)/1000.0, preload_bytes);
206 _midi.current_segment.start_block = bl;
214 Debug(driver, 2,
"Win32-MIDI: timer: start from block {} (ticktime {}, realtime {:.3f}, bytes {})", bl, block.
ticktime, ((
int)block.
realtime) / 1000.0, preload_bytes);
215 _midi.playback_start_time -= block.
realtime / 1000 - (DWORD)(preload_bytes * 1000 / 3125);
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()) {
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;
235 _midi.do_stop =
true;
240 if (block.
realtime / 1000 > playback_time) {
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) {
250 uint8_t status = data[0];
252 last_status = status;
256 status = last_status;
258 switch (status & 0xF0) {
260 case MIDIST_CHANPRESS:
262 TransmitChannelMsg(status, data[0]);
268 case MIDIST_POLYPRESS:
269 case MIDIST_PITCHBEND:
271 TransmitChannelMsg(status, data[0], data[1]);
275 case MIDIST_CONTROLLER:
277 if (data[0] == MIDICT_CHANVOLUME) {
279 _midi.channel_volumes[status & 0x0F] = data[1];
280 int vol = ScaleVolume(data[1], _midi.current_volume);
281 TransmitChannelMsg(status, data[0], vol);
284 TransmitChannelMsg(status, data[0], data[1]);
293 TransmitSysex(data, remaining);
295 case MIDIST_TC_QFRAME:
300 case MIDIST_SONGPOSPTR:
311 _midi.current_block++;
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;
320 _midi.do_stop =
true;
327 Debug(driver, 2,
"Win32-MIDI: PlaySong: entry");
330 if (!new_song.LoadSong(song))
return;
331 Debug(driver, 2,
"Win32-MIDI: PlaySong: Loaded song");
333 std::lock_guard<std::mutex> mutex_lock(
_midi.lock);
335 _midi.next_file.MoveFrom(new_song);
340 Debug(driver, 2,
"Win32-MIDI: PlaySong: setting flag");
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);
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;
360 return _midi.playing || (
_midi.do_start != 0);
365 std::lock_guard<std::mutex> mutex_lock(
_midi.lock);
366 _midi.new_volume = vol;
371 Debug(driver, 2,
"Win32-MIDI: Start: initializing");
378 if (portname !=
nullptr || _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++) {
383 if (midiOutGetDevCaps(tryport, &moc,
sizeof(moc)) == MMSYSERR_NOERROR) {
384 char tryportname[128];
389 if (portname !=
nullptr && strncmp(tryportname, portname,
lengthof(tryportname)) == 0) port = tryport;
391 Debug(driver, 1,
"MIDI port {:2d}: {}{}", tryport, tryportname, (tryport == port) ?
" [selected]" :
"");
397 if (port == UINT_MAX) {
403 resolution =
Clamp(resolution, 1, 20);
405 if (midiOutOpen(&
_midi.midi_out, devid, (DWORD_PTR)&MidiOutProc, (DWORD_PTR)
this, CALLBACK_FUNCTION) != MMSYSERR_NOERROR) {
406 return "could not open midi device";
409 midiOutReset(
_midi.midi_out);
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) {
417 Debug(driver, 2,
"Win32-MIDI: Start: timer resolution is {}",
_midi.time_period);
421 midiOutClose(
_midi.midi_out);
422 return "could not set timer resolution";
427 std::lock_guard<std::mutex> mutex_lock(
_midi.lock);
429 if (
_midi.timer_id) {
430 timeKillEvent(
_midi.timer_id);
434 timeEndPeriod(
_midi.time_period);
435 midiOutReset(
_midi.midi_out);
436 midiOutClose(
_midi.midi_out);
Factory for Windows' music player.
void PlaySong(const MusicSongInfo &song) override
Play a particular song.
void StopSong() override
Stop playing the current song.
bool IsSongPlaying() override
Are we currently playing a song?
void SetVolume(uint8_t vol) override
Set the volume, if possible.
void Stop() override
Stop this driver.
std::optional< std::string_view > Start(const StringList ¶m) override
Start this driver.
#define Debug(category, level, format_string,...)
Output a line of debugging information.
const char * GetDriverParam(const StringList &parm, const char *name)
Get a string parameter the list of parameters.
int GetDriverParamInt(const StringList &parm, const char *name, int def)
Get an integer parameter the list of parameters.
static struct @14 _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.
#define lengthof(array)
Return the length of an fixed size array.
std::vector< std::string > StringList
Type for a list of strings.
std::vector< uint8_t > data
raw midi data contained in block
uint32_t realtime
real-time (microseconds) since start of file this block should be triggered at
uint32_t ticktime
tick number since start of file this block should be triggered at
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)
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.
bool playing
flag indicating that playback is active
uint8_t new_volume
volume setting to change to
uint8_t current_volume
current effective volume setting
MidiFile current_file
file currently being played from
bool do_stop
flag for stopping playback at next opportunity
MidiFile next_file
upcoming file to play
void CALLBACK TimerCallback(UINT uTimerID, UINT, DWORD_PTR, DWORD_PTR, DWORD_PTR)
Realtime MIDI playback service routine.
PlaybackSegment current_segment
segment info for current playback
size_t current_block
next block index to send
UINT timer_id
ID of active multimedia timer.
std::mutex lock
synchronization for playback status fields
uint8_t channel_volumes[16]
last seen volume controller values in raw data
PlaybackSegment next_segment
segment info for upcoming file
HMIDIOUT midi_out
handle to open midiOut
DWORD playback_start_time
timestamp current file began playback
UINT time_period
obtained timer precision value
int do_start
flag for starting playback of next_file at next opportunity
Base for Windows music playback.