- Run the Alsa MIDI thread every 40ms or so, use non-blocking sequencer

This commit is contained in:
Petr Mrázek 2020-01-04 10:46:40 +01:00 committed by drfrag
parent 37db863bab
commit 848438839b
6 changed files with 253 additions and 152 deletions

View file

@ -34,13 +34,9 @@
#if defined __linux__ && defined HAVE_SYSTEM_MIDI #if defined __linux__ && defined HAVE_SYSTEM_MIDI
#include <algorithm>
#include <memory>
#include <assert.h>
#include <thread> #include <thread>
#include <pthread.h> #include <mutex>
#include <atomic> #include <condition_variable>
#include <cstring>
#include "mididevice.h" #include "mididevice.h"
#include "zmusic/m_swap.h" #include "zmusic/m_swap.h"
@ -51,10 +47,28 @@
namespace { namespace {
enum class EventType {
Null,
Delay,
Action
};
struct EventState {
int ticks = 0;
snd_seq_event_t data;
int size_of = 0;
void Clear() {
ticks = 0;
snd_seq_ev_clear(&data);
size_of = 0;
}
};
class AlsaMIDIDevice : public MIDIDevice class AlsaMIDIDevice : public MIDIDevice
{ {
public: public:
AlsaMIDIDevice(int dev_id); AlsaMIDIDevice(int dev_id, int (*printfunc_)(const char *, ...));
~AlsaMIDIDevice(); ~AlsaMIDIDevice();
int Open() override; int Open() override;
void Close() override; void Close() override;
@ -83,15 +97,16 @@ public:
return true; return true;
} }
void HandleTempoChange(int tick, int tempo);
void HandleEvent(int tick, int status, int parm1, int parm2);
void HandleLongEvent(int tick, const uint8_t *data, int len);
void SendStopEvents(); void SendStopEvents();
void SetExit(bool exit);
bool WaitForExit(std::chrono::microseconds usec, snd_seq_queue_status_t * status);
EventType PullEvent(EventState & state);
void PumpEvents(); void PumpEvents();
protected: protected:
AlsaSequencer &sequencer; AlsaSequencer &sequencer;
int (*printfunc)(const char*, ...);
MidiHeader *Events = nullptr; MidiHeader *Events = nullptr;
bool Started = false; bool Started = false;
@ -104,22 +119,26 @@ protected:
int DestinationClientId; int DestinationClientId;
int DestinationPortId; int DestinationPortId;
int Technology;
int Tempo = 480000; int Tempo = 480000;
int TimeDiv = 480; int TimeDiv = 480;
std::thread PlayerThread; std::thread PlayerThread;
std::atomic<bool> Exit; bool Exit = false;
std::mutex ExitLock;
std::condition_variable ExitCond;
}; };
} }
AlsaMIDIDevice::AlsaMIDIDevice(int dev_id) : sequencer(AlsaSequencer::Get()) AlsaMIDIDevice::AlsaMIDIDevice(int dev_id, int (*printfunc_)(const char*, ...) = nullptr) : sequencer(AlsaSequencer::Get()), printfunc(printfunc_)
{ {
auto & internalDevices = sequencer.GetInternalDevices(); auto & internalDevices = sequencer.GetInternalDevices();
auto & device = internalDevices.at(dev_id); auto & device = internalDevices.at(dev_id);
DestinationClientId = device.ClientID; DestinationClientId = device.ClientID;
DestinationPortId = device.PortNumber; DestinationPortId = device.PortNumber;
Technology = device.GetDeviceClass();
} }
AlsaMIDIDevice::~AlsaMIDIDevice() AlsaMIDIDevice::~AlsaMIDIDevice()
@ -185,8 +204,7 @@ bool AlsaMIDIDevice::IsOpen() const
int AlsaMIDIDevice::GetTechnology() const int AlsaMIDIDevice::GetTechnology() const
{ {
// TODO: implement properly, for now assume everything is an external MIDI device return Technology;
return MIDIDEV_MIDIPORT;
} }
int AlsaMIDIDevice::SetTempo(int tempo) int AlsaMIDIDevice::SetTempo(int tempo)
@ -201,192 +219,248 @@ int AlsaMIDIDevice::SetTimeDiv(int timediv)
return 0; return 0;
} }
void AlsaMIDIDevice::HandleEvent(int tick, int status, int parm1, int parm2) EventType AlsaMIDIDevice::PullEvent(EventState & state) {
{ state.Clear();
int command = status & 0xF0;
int channel = status & 0x0F;
snd_seq_event_t ev; if(!Events) {
snd_seq_ev_clear(&ev); Callback(CallbackData);
snd_seq_ev_set_source(&ev, PortId); if(!Events) {
snd_seq_ev_set_subs(&ev); return EventType::Null;
snd_seq_ev_schedule_tick(&ev, QueueId, false, tick); }
}
switch (command) if (Position >= Events->dwBytesRecorded)
{ {
case MIDI_NOTEOFF: Events = Events->lpNext;
snd_seq_ev_set_noteoff(&ev, channel, parm1, parm2); Position = 0;
break;
case MIDI_NOTEON: if (Callback != NULL)
snd_seq_ev_set_noteon(&ev, channel, parm1, parm2); {
break; Callback(CallbackData);
}
case MIDI_POLYPRESS: if(!Events) {
// FIXME: Seems to be missing in the Alsa sequencer implementation return EventType::Null;
return; }
case MIDI_CTRLCHANGE:
snd_seq_ev_set_controller(&ev, channel, parm1, parm2);
break;
case MIDI_PRGMCHANGE:
snd_seq_ev_set_pgmchange(&ev, channel, parm1);
break;
case MIDI_CHANPRESS:
snd_seq_ev_set_chanpress(&ev, channel, parm1);
break;
case MIDI_PITCHBEND: {
long bend = ((long)parm1 + (long)(parm2 << 7)) - 0x2000;
snd_seq_ev_set_pitchbend(&ev, channel, bend);
break;
} }
default: uint32_t *event = (uint32_t *)(Events->lpData + Position);
return; state.ticks = event[0];
// Advance to next event.
if (event[2] < 0x80000000)
{ // Short message
state.size_of = 12;
} }
snd_seq_event_output(sequencer.handle, &ev); else
{ // Long message
state.size_of = 12 + ((MEVENT_EVENTPARM(event[2]) + 3) & ~3);
}
if (MEVENT_EVENTTYPE(event[2]) == MEVENT_TEMPO) {
int tempo = MEVENT_EVENTPARM(event[2]);
if(Tempo != tempo) {
Tempo = tempo;
snd_seq_change_queue_tempo(sequencer.handle, QueueId, Tempo, &state.data);
return EventType::Action;
}
}
else if (MEVENT_EVENTTYPE(event[2]) == MEVENT_LONGMSG) {
// SysEx messages...
uint8_t * data = (uint8_t *)&event[3];
int len = MEVENT_EVENTPARM(event[2]);
if (len > 1 && (data[0] == 0xF0 || data[0] == 0xF7))
{
snd_seq_ev_set_sysex(&state.data, len, (void *)data);
return EventType::Action;
}
}
else if (MEVENT_EVENTTYPE(event[2]) == 0) {
// Short MIDI event
int command = event[2] & 0xF0;
int channel = event[2] & 0x0F;
int parm1 = (event[2] >> 8) & 0x7f;
int parm2 = (event[2] >> 16) & 0x7f;
switch (command)
{
case MIDI_NOTEOFF:
snd_seq_ev_set_noteoff(&state.data, channel, parm1, parm2);
return EventType::Action;
case MIDI_NOTEON:
snd_seq_ev_set_noteon(&state.data, channel, parm1, parm2);
return EventType::Action;
case MIDI_POLYPRESS:
// FIXME: Seems to be missing in the Alsa sequencer implementation
break;
case MIDI_CTRLCHANGE:
snd_seq_ev_set_controller(&state.data, channel, parm1, parm2);
return EventType::Action;
case MIDI_PRGMCHANGE:
snd_seq_ev_set_pgmchange(&state.data, channel, parm1);
return EventType::Action;
case MIDI_CHANPRESS:
snd_seq_ev_set_chanpress(&state.data, channel, parm1);
return EventType::Action;
case MIDI_PITCHBEND: {
long bend = ((long)parm1 + (long)(parm2 << 7)) - 0x2000;
snd_seq_ev_set_pitchbend(&state.data, channel, bend);
return EventType::Action;
}
default:
break;
}
}
// We didn't really recognize the event, treat it as a delay
return EventType::Delay;
} }
void AlsaMIDIDevice::HandleLongEvent(int tick, const uint8_t *data, int len) void AlsaMIDIDevice::SetExit(bool exit) {
{ std::unique_lock<std::mutex> lock(ExitLock);
// SysEx messages... if(exit != Exit) {
if (len > 1 && (data[0] == 0xF0 || data[0] == 0xF7)) Exit = exit;
{ ExitCond.notify_all();
snd_seq_event_t ev;
snd_seq_ev_clear(&ev);
snd_seq_ev_set_source(&ev, PortId);
snd_seq_ev_set_subs(&ev);
snd_seq_ev_schedule_tick(&ev, QueueId, false, tick);
snd_seq_ev_set_sysex(&ev, len, (void *)data);
snd_seq_event_output(sequencer.handle, &ev);
} }
} }
void AlsaMIDIDevice::HandleTempoChange(int tick, int tempo) { bool AlsaMIDIDevice::WaitForExit(std::chrono::microseconds usec, snd_seq_queue_status_t * status) {
if(Tempo != tempo) { std::unique_lock<std::mutex> lock(ExitLock);
Tempo = tempo; if(Exit) {
snd_seq_event_t ev; return true;
snd_seq_ev_clear(&ev);
snd_seq_ev_set_source(&ev, PortId);
snd_seq_ev_set_subs(&ev);
snd_seq_ev_schedule_tick(&ev, QueueId, false, tick);
snd_seq_change_queue_tempo(sequencer.handle, QueueId, Tempo, &ev);
snd_seq_event_output(sequencer.handle, &ev);
} }
ExitCond.wait_for(lock, usec);
if(Exit) {
return true;
}
snd_seq_get_queue_status(sequencer.handle, QueueId, status);
return false;
} }
/*
* Pumps events from the input to the output in a worker thread.
* It tries to keep the amount of events (time-wise) in the ALSA sequencer queue to be between 40 and 80ms by sleeping where necessary.
* This means Alsa can play them safely without running out of things to do, and we have good control over the events themselves (volume, pause, etc.).
*/
void AlsaMIDIDevice::PumpEvents() { void AlsaMIDIDevice::PumpEvents() {
int error = 0; const std::chrono::microseconds pump_step(40000);
// TODO: fill in error handling throughout this.
snd_seq_queue_tempo_t *tempo; snd_seq_queue_tempo_t *tempo;
snd_seq_queue_tempo_alloca(&tempo); snd_seq_queue_tempo_alloca(&tempo);
snd_seq_queue_tempo_set_tempo(tempo, Tempo); snd_seq_queue_tempo_set_tempo(tempo, Tempo);
snd_seq_queue_tempo_set_ppq(tempo, TimeDiv); snd_seq_queue_tempo_set_ppq(tempo, TimeDiv);
error = snd_seq_set_queue_tempo(sequencer.handle, QueueId, tempo); snd_seq_set_queue_tempo(sequencer.handle, QueueId, tempo);
snd_seq_start_queue(sequencer.handle, QueueId, NULL); snd_seq_start_queue(sequencer.handle, QueueId, NULL);
error = snd_seq_drain_output(sequencer.handle); snd_seq_drain_output(sequencer.handle);
int running_time = 0; int buffer_ticks = 0;
while (!Exit) { EventState event;
if(!Events) {
// NOTE: in practice, this is never reached. however, if it were, it would prevent crashes below. snd_seq_queue_status_t *status;
snd_seq_queue_status_malloc(&status);
while (true) {
auto type = PullEvent(event);
// if we reach the end of events, await our doom at a steady rate while looking for more events
if(type == EventType::Null) {
if(WaitForExit(pump_step, status)) {
break;
}
continue; continue;
} }
uint32_t *event = (uint32_t *)(Events->lpData + Position); // chomp delays as they come...
int ticks = event[0]; if(type == EventType::Delay) {
running_time += ticks; buffer_ticks += event.ticks;
if (MEVENT_EVENTTYPE(event[2]) == MEVENT_TEMPO) { Position += event.size_of;
HandleTempoChange(running_time, MEVENT_EVENTPARM(event[2])); continue;
}
else if (MEVENT_EVENTTYPE(event[2]) == MEVENT_LONGMSG) {
HandleLongEvent(running_time, (uint8_t *)&event[3], MEVENT_EVENTPARM(event[2]));
}
else if (MEVENT_EVENTTYPE(event[2]) == 0) {
// Short MIDI event
int status = event[2] & 0xff;
int parm1 = (event[2] >> 8) & 0x7f;
int parm2 = (event[2] >> 16) & 0x7f;
HandleEvent(running_time, status, parm1, parm2);
} }
// Advance to next event. // Figure out if we should sleep (the event is too far in the future for us to care), and for how long
if (event[2] < 0x80000000) int next_event_tick = buffer_ticks + event.ticks;
{ // Short message int queue_tick = snd_seq_queue_status_get_tick_time(status);
Position += 12; int tick_delta = next_event_tick - queue_tick;
} auto usecs = std::chrono::microseconds(tick_delta * Tempo / TimeDiv);
else auto schedule_time = std::max(std::chrono::microseconds(0), usecs - pump_step);
{ // Long message if(schedule_time >= pump_step) {
Position += 12 + ((MEVENT_EVENTPARM(event[2]) + 3) & ~3); if(WaitForExit(schedule_time, status)) {
} break;
// Did we use up this buffer?
if (Position >= Events->dwBytesRecorded)
{
Events = Events->lpNext;
Position = 0;
if (Callback != NULL)
{
Callback(CallbackData);
} }
snd_seq_drain_output(sequencer.handle); continue;
snd_seq_sync_output_queue(sequencer.handle);
} }
if (tick_delta < 0) {
if(printfunc) {
printfunc("Alsa sequencer underrun: %d ticks!\n", tick_delta);
}
}
// We found an event worthy of sending to the sequencer
snd_seq_ev_set_source(&event.data, PortId);
snd_seq_ev_set_subs(&event.data);
snd_seq_ev_schedule_tick(&event.data, QueueId, false, buffer_ticks + event.ticks);
int result = snd_seq_event_output(sequencer.handle, &event.data);
if(result < 0) {
if(printfunc) {
printfunc("Alsa sequencer did not accept event: error %d!\n", result);
}
if(WaitForExit(pump_step, status)) {
break;
}
continue;
}
buffer_ticks += event.ticks;
Position += event.size_of;
snd_seq_drain_output(sequencer.handle);
} }
// Send stop events, just to be sure we don't end up with stuck notes
snd_seq_queue_status_free(status);
snd_seq_drop_output(sequencer.handle);
// FIXME: the event source should give use these, but it doesn't.
{ {
snd_seq_drop_output(sequencer.handle); for (int channel = 0; channel < 16; ++channel)
SendStopEvents(); {
// FIXME: attach to a timestamped event and make it go through the queue? snd_seq_event_t ev;
snd_seq_stop_queue(sequencer.handle, QueueId, NULL); snd_seq_ev_clear(&ev);
snd_seq_ev_set_source(&ev, PortId);
snd_seq_ev_set_subs(&ev);
snd_seq_ev_schedule_tick(&ev, QueueId, true, 0);
snd_seq_ev_set_controller(&ev, channel, MIDI_CTL_ALL_NOTES_OFF, 0);
snd_seq_event_output(sequencer.handle, &ev);
snd_seq_ev_set_controller(&ev, channel, MIDI_CTL_RESET_CONTROLLERS, 0);
snd_seq_event_output(sequencer.handle, &ev);
}
snd_seq_drain_output(sequencer.handle); snd_seq_drain_output(sequencer.handle);
snd_seq_sync_output_queue(sequencer.handle); snd_seq_sync_output_queue(sequencer.handle);
} }
snd_seq_sync_output_queue(sequencer.handle);
snd_seq_stop_queue(sequencer.handle, QueueId, NULL);
snd_seq_drain_output(sequencer.handle);
} }
void AlsaMIDIDevice::SendStopEvents() {
// NOTE: for some reason, the midi streamer doesn't send us these.
for (int channel = 0; channel < 16; ++channel)
{
snd_seq_event_t ev;
snd_seq_ev_clear(&ev);
snd_seq_ev_set_source(&ev, PortId);
snd_seq_ev_set_subs(&ev);
snd_seq_ev_schedule_tick(&ev, QueueId, true, 0);
snd_seq_ev_set_controller(&ev, channel, MIDI_CTL_ALL_NOTES_OFF, 0);
snd_seq_event_output(sequencer.handle, &ev);
snd_seq_ev_set_controller(&ev, channel, MIDI_CTL_RESET_CONTROLLERS, 0);
snd_seq_event_output(sequencer.handle, &ev);
}
snd_seq_drain_output(sequencer.handle);
snd_seq_sync_output_queue(sequencer.handle);
}
int AlsaMIDIDevice::Resume() int AlsaMIDIDevice::Resume()
{ {
if(!Connected) { if(!Connected) {
return 1; return 1;
} }
Exit = false; SetExit(false);
PlayerThread = std::thread(&AlsaMIDIDevice::PumpEvents, this); PlayerThread = std::thread(&AlsaMIDIDevice::PumpEvents, this);
return 0; return 0;
} }
void AlsaMIDIDevice::InitPlayback() void AlsaMIDIDevice::InitPlayback()
{ {
Exit = false; SetExit(false);
} }
void AlsaMIDIDevice::Stop() void AlsaMIDIDevice::Stop()
{ {
/* SetExit(true);
* NOTE: this is slow. Maybe we should just leave the thread be and let it asynchronously drain in the background.
*/
Exit = true;
PlayerThread.join(); PlayerThread.join();
} }
@ -429,6 +503,6 @@ bool AlsaMIDIDevice::Update()
MIDIDevice *CreateAlsaMIDIDevice(int mididevice) MIDIDevice *CreateAlsaMIDIDevice(int mididevice)
{ {
return new AlsaMIDIDevice(mididevice); return new AlsaMIDIDevice(mididevice, musicCallbacks.Alsa_MessageFunc);
} }
#endif #endif

View file

@ -37,6 +37,18 @@
#include <alsa/asoundlib.h> #include <alsa/asoundlib.h>
#include <sstream> #include <sstream>
EMidiDeviceClass MidiOutDeviceInternal::GetDeviceClass() const
{
if (type & SND_SEQ_PORT_TYPE_SYNTH)
return MIDIDEV_FMSYNTH;
if (type & (SND_SEQ_PORT_TYPE_DIRECT_SAMPLE|SND_SEQ_PORT_TYPE_SAMPLE))
return MIDIDEV_SYNTH;
if (type & (SND_SEQ_PORT_TYPE_MIDI_GENERIC|SND_SEQ_PORT_TYPE_APPLICATION))
return MIDIDEV_MIDIPORT;
// assume FM synth otherwise
return MIDIDEV_FMSYNTH;
}
AlsaSequencer & AlsaSequencer::Get() { AlsaSequencer & AlsaSequencer::Get() {
static AlsaSequencer sequencer; static AlsaSequencer sequencer;
return sequencer; return sequencer;
@ -51,7 +63,7 @@ AlsaSequencer::~AlsaSequencer() {
} }
bool AlsaSequencer::Open() { bool AlsaSequencer::Open() {
error = snd_seq_open(&handle, "default", SND_SEQ_OPEN_DUPLEX, 0); error = snd_seq_open(&handle, "default", SND_SEQ_OPEN_OUTPUT, SND_SEQ_NONBLOCK);
if(error) { if(error) {
return false; return false;
} }
@ -95,7 +107,8 @@ bool filter(snd_seq_port_info_t *pinfo)
if((capability & writable) != writable) { if((capability & writable) != writable) {
return false; return false;
} }
int type = snd_seq_port_info_get_type(pinfo); // TODO: filter based on type here? maybe?
// int type = snd_seq_port_info_get_type(pinfo);
return true; return true;
} }
} }

View file

@ -48,6 +48,7 @@ struct MidiOutDeviceInternal {
int ClientID = -1; int ClientID = -1;
int PortNumber = -1; int PortNumber = -1;
unsigned int type = 0; unsigned int type = 0;
EMidiDeviceClass GetDeviceClass() const;
}; };
// NOTE: the sequencer state is shared between actually playing MIDI music and device enumeration, therefore we keep it around. // NOTE: the sequencer state is shared between actually playing MIDI music and device enumeration, therefore we keep it around.

View file

@ -187,7 +187,7 @@ struct MidiDeviceList
auto& dev = sequencer.GetInternalDevices(); auto& dev = sequencer.GetInternalDevices();
for (auto& d : dev) for (auto& d : dev)
{ {
MidiOutDevice mdev = { strdup(d.Name.c_str()), d.ID, MIDIDEV_MAPPER }; // fixme: Correctly determine the type of the device. MidiOutDevice mdev = { strdup(d.Name.c_str()), d.ID, d.GetDeviceClass() }; // fixme: Correctly determine the type of the device.
devices.push_back(mdev); devices.push_back(mdev);
} }
#elif _WIN32 #elif _WIN32

View file

@ -206,6 +206,7 @@ struct Callbacks
void (*GUS_MessageFunc)(int type, int verbosity_level, const char* fmt, ...); void (*GUS_MessageFunc)(int type, int verbosity_level, const char* fmt, ...);
void (*Timidity_Messagefunc)(int type, int verbosity_level, const char* fmt, ...); void (*Timidity_Messagefunc)(int type, int verbosity_level, const char* fmt, ...);
int (*Fluid_MessageFunc)(const char *fmt, ...); int (*Fluid_MessageFunc)(const char *fmt, ...);
int (*Alsa_MessageFunc)(const char *fmt, ...);
// Retrieves the path to a soundfont identified by an identifier. Only needed if the client virtualizes the sound font names // Retrieves the path to a soundfont identified by an identifier. Only needed if the client virtualizes the sound font names
const char *(*PathForSoundfont)(const char *name, int type); const char *(*PathForSoundfont)(const char *name, int type);

View file

@ -169,6 +169,17 @@ static void tim_printfunc(int type, int verbosity_level, const char* fmt, ...)
} }
} }
static int alsa_printfunc(const char* fmt, ...)
{
va_list args;
va_start(args, fmt);
FString msg;
msg.VFormat(fmt, args);
va_end(args);
return Printf(TEXTCOLOR_RED "%s\n", msg.GetChars());
}
static void wm_printfunc(const char* wmfmt, va_list args) static void wm_printfunc(const char* wmfmt, va_list args)
{ {
Printf(TEXTCOLOR_RED); Printf(TEXTCOLOR_RED);
@ -277,6 +288,7 @@ void I_InitMusic (void)
Callbacks callbacks{}; Callbacks callbacks{};
callbacks.Fluid_MessageFunc = Printf; callbacks.Fluid_MessageFunc = Printf;
callbacks.Alsa_MessageFunc = alsa_printfunc;
callbacks.GUS_MessageFunc = callbacks.Timidity_Messagefunc = tim_printfunc; callbacks.GUS_MessageFunc = callbacks.Timidity_Messagefunc = tim_printfunc;
callbacks.WildMidi_MessageFunc = wm_printfunc; callbacks.WildMidi_MessageFunc = wm_printfunc;
callbacks.NicePath = mus_NicePath; callbacks.NicePath = mus_NicePath;