text: /*  RetroArch - A frontend for libretro.
   *  Copyright (C) 2018 The RetroArch team
   *
   *  RetroArch 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 Found-
   *  ation, either version 3 of the License, or (at your option) any later version.
   *
   *  RetroArch 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.  See the GNU General Public License for more details.
   *
   *  You should have received a copy of the GNU General Public License along with RetroArch.
   *  If not, see <http://www.gnu.org/licenses/>.
   */
  
  #include <windows.h>
  
  #include <libretro.h>
  #include <lists/string_list.h>
  #include <string/stdstring.h>
  
  #include "../../verbosity.h"
  #include "../midi_driver.h"
  
  #define WINMM_MIDI_BUF_CNT 3
  #define WINMM_MIDI_BUF_LEN 1024
  
  typedef struct
  {
     MIDIHDR header;
     DWORD data[WINMM_MIDI_BUF_LEN];
  } winmm_midi_buffer_t;
  
  typedef struct
  {
     uint8_t data[WINMM_MIDI_BUF_LEN * 4];
     midi_event_t events[WINMM_MIDI_BUF_LEN];
     int rd_idx;
     int wr_idx;
  } winmm_midi_queue_t;
  
  typedef struct
  {
     HMIDIIN in_dev;
     HMIDISTRM out_dev;
     winmm_midi_queue_t in_queue;
     winmm_midi_buffer_t out_bufs[WINMM_MIDI_BUF_CNT];
     int out_buf_idx;
     double tick_dur;
  } winmm_midi_t;
  
  static void winmm_midi_free(void *p);
  
  static bool winmm_midi_queue_read(winmm_midi_queue_t *q, midi_event_t *ev)
  {
     unsigned i;
     midi_event_t *src_ev = NULL;
  
     if (q->rd_idx == q->wr_idx)
        return false;
  
     if (ev->data_size < q->events[q->rd_idx].data_size)
     {
  #ifdef DEBUG
        RARCH_ERR("[MIDI]: Input queue read failed (event data too small).\n");
  #endif
        return false;
     }
  
     src_ev = &q->events[q->rd_idx];
  
     for (i = 0; i < src_ev->data_size; ++i)
        ev->data[i] = src_ev->data[i];
  
     ev->data_size = src_ev->data_size;
     ev->delta_time = src_ev->delta_time;
  
     if (q->rd_idx + 1 == WINMM_MIDI_BUF_LEN)
        q->rd_idx = 0;
     else
        ++q->rd_idx;
  
     return true;
  }
  
  static bool winmm_midi_queue_write(winmm_midi_queue_t *q, const midi_event_t *ev)
  {
     int wr_avail;
     unsigned i;
     int rd_idx            = q->rd_idx;
     midi_event_t *dest_ev = NULL;
  
     if (q->wr_idx >= rd_idx)
        wr_avail = WINMM_MIDI_BUF_LEN - q->wr_idx + rd_idx;
     else
        wr_avail = rd_idx - q->wr_idx - 1;
  
     if (wr_avail < 1)
     {
  #ifdef DEBUG
        RARCH_ERR("[MIDI]: Input queue overflow.\n");
  #endif
        return false;
     }
  
     dest_ev = &q->events[q->wr_idx];
     if (ev->data_size > 4)
     {
  #ifdef DEBUG
        RARCH_ERR("[MIDI]: Input queue write failed (event too big).\n");
  #endif
        return false;
     }
  
     for (i = 0; i < ev->data_size; ++i)
        dest_ev->data[i] = ev->data[i];
  
     dest_ev->data_size = ev->data_size;
     dest_ev->delta_time = ev->delta_time;
  
     if (q->wr_idx + 1 == WINMM_MIDI_BUF_LEN)
        q->wr_idx = 0;
     else
        ++q->wr_idx;
  
     return true;
  }
  
  static void winmm_midi_queue_init(winmm_midi_queue_t *q)
  {
     unsigned i, j;
  
     for (i = j = 0; i < WINMM_MIDI_BUF_LEN; ++i, j += 4)
     {
        q->events[i].data       = &q->data[j];
        q->events[i].delta_time = 0;
     }
  
     q->rd_idx = 0;
     q->wr_idx = 0;
  }
  
  static void CALLBACK winmm_midi_input_callback(HMIDIIN dev, UINT msg,
        DWORD_PTR q, DWORD_PTR par1, DWORD_PTR par2)
  {
     uint8_t data[3];
     midi_event_t event;
     winmm_midi_queue_t *queue = (winmm_midi_queue_t*)q;
  
     (void)dev;
  
     if (msg == MIM_OPEN)
        winmm_midi_queue_init(queue);
     else if (msg == MIM_DATA)
     {
        data[0] = (uint8_t)(par1 & 0xFF);
        data[1] = (uint8_t)((par1 >> 8) & 0xFF);
        data[2] = (uint8_t)((par1 >> 16) & 0xFF);
  
        event.data       = data;
        event.data_size  = midi_driver_get_event_size(data[0]);
        event.delta_time = 0;
  
        if (!winmm_midi_queue_write(queue, &event))
        {
  #ifdef DEBUG
           RARCH_ERR("[MIDI]: Input event dropped.\n");
  #endif
        }
     }
     else if (msg == MIM_LONGDATA)
     {
  #ifdef DEBUG
        RARCH_WARN("[MIDI]: SysEx input not implemented, event dropped.\n");
  #endif
     }
  }
  
  static HMIDIIN winmm_midi_open_input_device(const char *dev_name,
        winmm_midi_queue_t *queue)
  {
     unsigned i;
     UINT dev_count = midiInGetNumDevs();
     HMIDIIN dev    = NULL;
  
     for (i = 0; i < dev_count; ++i)
     {
        MIDIINCAPSA caps;
        MMRESULT mmr = midiInGetDevCapsA((UINT)i, &caps, sizeof(caps));
        if (mmr == MMSYSERR_NOERROR)
        {
           if (string_is_equal(caps.szPname, dev_name))
           {
              mmr = midiInOpen(&dev, (UINT)i, (DWORD_PTR)winmm_midi_input_callback,
                    (DWORD_PTR)queue, CALLBACK_FUNCTION);
              if (mmr != MMSYSERR_NOERROR)
                 RARCH_ERR("[MIDI]: midiInOpen failed with error %d.\n", mmr);
              break;
           }
        }
        else
           RARCH_WARN("[MIDI]: midiInGetDevCapsA failed with error %d.\n", mmr);
     }
  
     return dev;
  }
  
  static HMIDISTRM winmm_midi_open_output_device(const char *dev_name)
  {
     unsigned i;
     UINT dev_count = midiOutGetNumDevs();
     HMIDISTRM dev  = NULL;
  
     for (i = 0; i < dev_count; ++i)
     {
        MIDIOUTCAPSA caps;
        MMRESULT mmr = midiOutGetDevCapsA(i, &caps, sizeof(caps));
        if (mmr == MMSYSERR_NOERROR)
        {
           if (string_is_equal(caps.szPname, dev_name))
           {
              mmr = midiStreamOpen(&dev, &i, 1, 0, 0, CALLBACK_NULL);
              if (mmr != MMSYSERR_NOERROR)
                 RARCH_ERR("[MIDI]: midiStreamOpen failed with error %d.\n", mmr);
              break;
           }
        }
        else
           RARCH_WARN("[MIDI]: midiOutGetDevCapsA failed with error %d.\n", mmr);
     }
  
     return dev;
  }
  
  static bool winmm_midi_init_clock(HMIDISTRM out_dev, double *tick_dur)
  {
     MIDIPROPTIMEDIV division;
     MIDIPROPTEMPO tempo;
     MMRESULT mmr;
  
     tempo.cbStruct = sizeof(tempo);
     mmr = midiStreamProperty(out_dev, (LPBYTE)&tempo,
           MIDIPROP_GET | MIDIPROP_TEMPO);
     if (mmr != MMSYSERR_NOERROR)
     {
        RARCH_ERR("[MIDI]: Current tempo unavailable (error %d).\n", mmr);
        return false;
     }
  
     division.dwTimeDiv = 3;
     while (tempo.dwTempo / division.dwTimeDiv > 320)
        division.dwTimeDiv *= 2;
  
     division.cbStruct = sizeof(division);
     mmr = midiStreamProperty(out_dev, (LPBYTE)&division,
           MIDIPROP_SET | MIDIPROP_TIMEDIV);
     if (mmr != MMSYSERR_NOERROR)
     {
        RARCH_ERR("[MIDI]: Time division change failed (error %d).\n", mmr);
        return false;
     }
  
     *tick_dur = (double)tempo.dwTempo / (double)division.dwTimeDiv;
  #ifdef DEBUG
     RARCH_LOG("[MIDI]: Tick duration %f us.\n", *tick_dur);
  #endif
  
     return true;
  }
  
  static bool winmm_midi_init_output_buffers(HMIDISTRM dev,
        winmm_midi_buffer_t *bufs)
  {
     unsigned i;
  
     for (i = 0; i < WINMM_MIDI_BUF_CNT; ++i)
     {
        MMRESULT mmr;
  
        bufs[i].header.dwBufferLength  = sizeof(DWORD) * WINMM_MIDI_BUF_LEN;
        bufs[i].header.dwBytesRecorded = 0;
        bufs[i].header.dwFlags         = 0;
        bufs[i].header.lpData          = (LPSTR)bufs[i].data;
  
        mmr = midiOutPrepareHeader((HMIDIOUT)dev, &bufs[i].header, sizeof(MIDIHDR));
        if (mmr != MMSYSERR_NOERROR)
        {
           RARCH_ERR("[MIDI]: midiOutPrepareHeader failed with error %d.\n", mmr);
  
           while (--i <= 0)
              midiOutUnprepareHeader((HMIDIOUT)dev, &bufs[i].header, sizeof(MIDIHDR));
  
           return false;
        }
     }
  
     return true;
  }
  
  static void winmm_midi_free_output_buffers(HMIDISTRM dev, winmm_midi_buffer_t *bufs)
  {
     unsigned i;
  
     for (i = 0; i < WINMM_MIDI_BUF_CNT; ++i)
     {
        MMRESULT mmr = midiOutUnprepareHeader(
              (HMIDIOUT)dev, &bufs[i].header, sizeof(MIDIHDR));
        if (mmr != MMSYSERR_NOERROR)
           RARCH_ERR("[MIDI]: midiOutUnprepareHeader failed with error %d.\n", mmr);
     }
  }
  
  static bool winmm_midi_write_short_event(winmm_midi_buffer_t *buf,
        const uint8_t *data, size_t data_size, DWORD delta_time)
  {
     DWORD i = buf->header.dwBytesRecorded / sizeof(DWORD);
  
     if (buf->header.dwBytesRecorded + sizeof(DWORD) * 3 >
           sizeof(DWORD) * WINMM_MIDI_BUF_LEN)
        return false;
  
     buf->data[i++] = delta_time;
     buf->data[i++] = 0;
     buf->data[i] = MEVT_F_SHORT << 24;
     if (data_size == 0)
        buf->data[i] |= MEVT_NOP;
     else
     {
        buf->data[i] |= MEVT_SHORTMSG << 24 | data[0];
        if (data_size > 1)
           buf->data[i] |= data[1] << 8;
        if (data_size > 2)
           buf->data[i] |= data[2] << 16;
     }
  
     buf->header.dwBytesRecorded += sizeof(DWORD) * 3;
  
     return true;
  }
  
  static bool winmm_midi_write_long_event(winmm_midi_buffer_t *buf,
        const uint8_t *data, size_t data_size, DWORD delta_time)
  {
     DWORD i = buf->header.dwBytesRecorded / sizeof(DWORD);
  
     if (buf->header.dwBytesRecorded + sizeof(DWORD) * 3 + data_size >
           sizeof(DWORD) * WINMM_MIDI_BUF_LEN)
        return false;
  
     buf->data[i++] = delta_time;
     buf->data[i++] = 0;
     buf->data[i++] = MEVT_F_LONG << 24 | MEVT_LONGMSG << 24 | data_size;
  
     memcpy(&buf->data[i], data, data_size);
     buf->header.dwBytesRecorded += sizeof(DWORD) * 3 + data_size;
  
     return true;
  }
  
  static bool winmm_midi_get_avail_inputs(struct string_list *inputs)
  {
     unsigned i;
     union string_list_elem_attr attr = {0};
     UINT dev_count                   = midiInGetNumDevs();
  
     for (i = 0; i < dev_count; ++i)
     {
        MIDIINCAPSA caps;
        MMRESULT mmr = midiInGetDevCapsA((UINT)i, &caps, sizeof(caps));
        if (mmr != MMSYSERR_NOERROR)
        {
           RARCH_ERR("[MIDI]: midiInGetDevCapsA failed with error %d.\n", mmr);
           return false;
        }
  
        if (!string_list_append(inputs, caps.szPname, attr))
        {
           RARCH_ERR("[MIDI]: string_list_append failed.\n");
           return false;
        }
     }
  
     return true;
  }
  
  static bool winmm_midi_get_avail_outputs(struct string_list *outputs)
  {
     unsigned i;
     union string_list_elem_attr attr = {0};
     UINT dev_count                   = midiOutGetNumDevs();
  
     for (i = 0; i < dev_count; ++i)
     {
        MIDIOUTCAPSA caps;
        MMRESULT mmr = midiOutGetDevCapsA((UINT)i, &caps, sizeof(caps));
        if (mmr != MMSYSERR_NOERROR)
        {
           RARCH_ERR("[MIDI]: midiOutGetDevCapsA failed with error %d.\n", mmr);
           return false;
        }
  
        if (!string_list_append(outputs, caps.szPname, attr))
        {
           RARCH_ERR("[MIDI]: string_list_append failed.\n");
           return false;
        }
     }
  
     return true;
  }
  
  static void *winmm_midi_init(const char *input, const char *output)
  {
     MMRESULT mmr;
     bool err        = false;
     winmm_midi_t *d = (winmm_midi_t*)calloc(sizeof(winmm_midi_t), 1);
  
     if (!d)
     {
        RARCH_ERR("[MIDI]: Out of memory.\n");
        return NULL;
     }
  
     if (input)
     {
        d->in_dev = winmm_midi_open_input_device(input, &d->in_queue);
        if (!d->in_dev)
           err = true;
        else
        {
           mmr = midiInStart(d->in_dev);
           if (mmr != MMSYSERR_NOERROR)
           {
              RARCH_ERR("[MIDI]: midiInStart failed with error %d.\n", mmr);
              err = true;
           }
        }
     }
  
     if (output)
     {
        d->out_dev = winmm_midi_open_output_device(output);
        if (!d->out_dev)
           err = true;
        else if (!winmm_midi_init_clock(d->out_dev, &d->tick_dur))
           err = true;
        else if (!winmm_midi_init_output_buffers(d->out_dev, d->out_bufs))
           err = true;
        else
        {
           mmr = midiStreamRestart(d->out_dev);
           if (mmr != MMSYSERR_NOERROR)
           {
              RARCH_ERR("[MIDI]: midiStreamRestart failed with error %d.\n", mmr);
              err = true;
           }
        }
     }
  
     if (err)
     {
        winmm_midi_free(d);
        return NULL;
     }
  
     return d;
  }
  
  static void winmm_midi_free(void *p)
  {
     winmm_midi_t *d = (winmm_midi_t*)p;
  
     if (!d)
        return;
  
     if (d->in_dev)
     {
        midiInStop(d->in_dev);
        midiInClose(d->in_dev);
     }
  
     if (d->out_dev)
     {
        winmm_midi_free_output_buffers(d->out_dev, d->out_bufs);
        midiStreamClose(d->out_dev);
     }
  
     free(d);
  }
  
  static bool winmm_midi_set_input(void *p, const char *input)
  {
     winmm_midi_t *d = (winmm_midi_t*)p;
     HMIDIIN new_dev = NULL;
  
     if (input)
     {
        new_dev = winmm_midi_open_input_device(input, &d->in_queue);
        if (!new_dev)
           return false;
     }
  
     if (d->in_dev)
     {
        midiInStop(d->in_dev);
        midiInClose(d->in_dev);
     }
  
     d->in_dev = new_dev;
     if (d->in_dev)
     {
        MMRESULT mmr = midiInStart(d->in_dev);
        if (mmr != MMSYSERR_NOERROR)
        {
           RARCH_ERR("[MIDI]: midiInStart failed with error %d.\n", mmr);
           return false;
        }
     }
  
     return true;
  }
  
  static bool winmm_midi_set_output(void *p, const char *output)
  {
     winmm_midi_t *d   = (winmm_midi_t*)p;
     HMIDISTRM new_dev = NULL;
  
     if (output)
     {
        new_dev = winmm_midi_open_output_device(output);
        if (!new_dev)
           return false;
     }
  
     if (d->out_dev)
     {
        midiStreamStop(d->out_dev);
        winmm_midi_free_output_buffers(d->out_dev, d->out_bufs);
        midiStreamClose(d->out_dev);
     }
  
     d->out_dev = new_dev;
     if (d->out_dev)
     {
        MMRESULT mmr;
        if (!winmm_midi_init_output_buffers(d->out_dev, d->out_bufs))
           return false;
  
        d->out_buf_idx = 0;
  
        mmr = midiStreamRestart(d->out_dev);
        if (mmr != MMSYSERR_NOERROR)
        {
           RARCH_ERR("[MIDI]: midiStreamRestart failed with error %d.\n", mmr);
           return false;
        }
     }
  
     return true;
  }
  
  static bool winmm_midi_read(void *p, midi_event_t *event)
  {
     winmm_midi_t *d = (winmm_midi_t*)p;
  
     return winmm_midi_queue_read(&d->in_queue, event);
  }
  
  static bool winmm_midi_write(void *p, const midi_event_t *event)
  {
     winmm_midi_t *d          = (winmm_midi_t*)p;
     winmm_midi_buffer_t *buf = &d->out_bufs[d->out_buf_idx];
     DWORD delta_time;
  
     if (buf->header.dwFlags & MHDR_INQUEUE)
        return false;
  
     if (buf->header.dwFlags & MHDR_DONE)
     {
        buf->header.dwBytesRecorded = 0;
        buf->header.dwFlags ^= MHDR_DONE;
     }
  
     delta_time = (DWORD)((double)event->delta_time / d->tick_dur);
     if (event->data_size < 4)
        return winmm_midi_write_short_event(buf, event->data,
              event->data_size, delta_time);
  
     return winmm_midi_write_long_event(buf, event->data,
           event->data_size, delta_time);
  }
  
  static bool winmm_midi_flush(void *p)
  {
     winmm_midi_t *d          = (winmm_midi_t*)p;
     winmm_midi_buffer_t *buf = &d->out_bufs[d->out_buf_idx];
  
     if (buf->header.dwBytesRecorded)
     {
        MMRESULT mmr = midiStreamOut(
              d->out_dev, &buf->header, sizeof(buf->header));
  
        if (mmr != MMSYSERR_NOERROR)
        {
  #ifdef DEBUG
           RARCH_ERR("[MIDI]: midiStreamOut failed with error %d.\n", mmr);
  #endif
           /* Core sent MIDI message not understood by the MIDI driver
            * Make this buffer available to be used again */
           buf->header.dwFlags |= MHDR_DONE;
           buf->header.dwFlags &= ~MHDR_INQUEUE;
           return false;
        }
  
        if (++d->out_buf_idx == WINMM_MIDI_BUF_CNT)
           d->out_buf_idx = 0;
     }
  
     return true;
  }
  
  midi_driver_t midi_winmm = {
     "winmm",
     winmm_midi_get_avail_inputs,
     winmm_midi_get_avail_outputs,
     winmm_midi_init,
     winmm_midi_free,
     winmm_midi_set_input,
     winmm_midi_set_output,
     winmm_midi_read,
     winmm_midi_write,
     winmm_midi_flush
  };
  