diff --git a/libraries/zmusic/CMakeLists.txt b/libraries/zmusic/CMakeLists.txt index dfe74c90e..1de9061d5 100644 --- a/libraries/zmusic/CMakeLists.txt +++ b/libraries/zmusic/CMakeLists.txt @@ -10,6 +10,11 @@ endif() include( CheckFunctionExists ) +find_package( ALSA ) +if (WIN32 OR ALSA_FOUND) + add_definitions( -DHAVE_SYSTEM_MIDI ) +endif() + if( DYN_SNDFILE) add_definitions( -DHAVE_SNDFILE -DDYN_SNDFILE ) else() @@ -44,13 +49,21 @@ endif() include_directories( "${CMAKE_CURRENT_SOURCE_DIR}/../libraries/dumb/include" "${ZLIB_INCLUDE_DIR}" "${ADL_INCLUDE_DIR}" "${OPN_INCLUDE_DIR}" "${TIMIDITYPP_INCLUDE_DIR}" "${TIMIDITY_INCLUDE_DIR}" "${WILDMIDI_INCLUDE_DIR}" "${OPLSYNTH_INCLUDE_DIR}" "${GME_INCLUDE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}" ) if (WIN32) -set( PLAT_SOURCES - mididevices/music_win_mididevice.cpp - musicformats/win32/i_cd.cpp - musicformats/win32/helperthread.cpp - ) + set( PLAT_SOURCES + mididevices/music_win_mididevice.cpp + musicformats/win32/i_cd.cpp + musicformats/win32/helperthread.cpp + ) +elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + if (ALSA_FOUND) + set( PLAT_SOURCES + mididevices/music_alsa_mididevice.cpp + mididevices/music_alsa_state.cpp + ) + endif() endif() + file( GLOB HEADER_FILES zmusic/*.h mididevices/*.h @@ -111,6 +124,11 @@ if( NOT DYN_FLUIDSYNTH AND FLUIDSYNTH_FOUND ) target_link_libraries( zmusic ${FLUIDSYNTH_LIBRARIES} ) endif() +if(ALSA_FOUND) + include_directories( "${ALSA_INCLUDE_DIR}" ) + target_link_libraries( zmusic ${ALSA_LIBRARIES} ) +endif() + source_group("MIDI Devices" REGULAR_EXPRESSION "^${CMAKE_CURRENT_SOURCE_DIR}/mididevices/.+") source_group("MIDI Sources" REGULAR_EXPRESSION "^${CMAKE_CURRENT_SOURCE_DIR}/midisources/.+") source_group("Music Formats" REGULAR_EXPRESSION "^${CMAKE_CURRENT_SOURCE_DIR}/musicformats/.+") diff --git a/libraries/zmusic/mididevices/mididevice.h b/libraries/zmusic/mididevices/mididevice.h index 0021249b2..2cdd167ef 100644 --- a/libraries/zmusic/mididevices/mididevice.h +++ b/libraries/zmusic/mididevices/mididevice.h @@ -154,3 +154,6 @@ MIDIDevice *CreateWildMIDIDevice(const char *Args, int samplerate); MIDIDevice* CreateWinMIDIDevice(int mididevice); #endif +#ifdef __linux__ +MIDIDevice* CreateAlsaMIDIDevice(int mididevice); +#endif diff --git a/libraries/zmusic/mididevices/music_alsa_mididevice.cpp b/libraries/zmusic/mididevices/music_alsa_mididevice.cpp new file mode 100644 index 000000000..e088b8cc0 --- /dev/null +++ b/libraries/zmusic/mididevices/music_alsa_mididevice.cpp @@ -0,0 +1,434 @@ +/* +** Provides an ALSA implementation of a MIDI output device. +** +**--------------------------------------------------------------------------- +** Copyright 2008-2010 Randy Heit +** Copyright 2020 Petr Mrazek +** All rights reserved. +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions +** are met: +** +** 1. Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** 2. Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in the +** documentation and/or other materials provided with the distribution. +** 3. The name of the author may not be used to endorse or promote products +** derived from this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**--------------------------------------------------------------------------- +** +*/ + +#if defined __linux__ && defined HAVE_SYSTEM_MIDI + +#include +#include +#include +#include +#include +#include +#include + +#include "mididevice.h" +#include "zmusic/m_swap.h" +#include "zmusic/mus2midi.h" + +#include "music_alsa_state.h" +#include + +namespace { + +class AlsaMIDIDevice : public MIDIDevice +{ +public: + AlsaMIDIDevice(int dev_id); + ~AlsaMIDIDevice(); + int Open() override; + void Close() override; + bool IsOpen() const override; + int GetTechnology() const override; + int SetTempo(int tempo) override; + int SetTimeDiv(int timediv) override; + int StreamOut(MidiHeader *data) override; + int StreamOutSync(MidiHeader *data) override; + int Resume() override; + void Stop() override; + + bool FakeVolume() override { + // Not sure if we even can control the volume this way with Alsa, so make it fake. + return true; + }; + + bool Pause(bool paused) override; + void InitPlayback() override; + bool Update() override; + void PrecacheInstruments(const uint16_t *instruments, int count) override {} + + bool CanHandleSysex() const override + { + // Assume we can, let Alsa sort it out. We do not truly have full control. + 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 PumpEvents(); + + +protected: + AlsaSequencer &sequencer; + + MidiHeader *Events = nullptr; + bool Started = false; + uint32_t Position = 0; + + const static int IntendedPortId = 0; + bool Connected = false; + int PortId = -1; + int QueueId = -1; + + int DestinationClientId; + int DestinationPortId; + + int Tempo = 480000; + int TimeDiv = 480; + + std::thread PlayerThread; + std::atomic Exit; +}; + +} + +AlsaMIDIDevice::AlsaMIDIDevice(int dev_id) : sequencer(AlsaSequencer::Get()) +{ + auto & internalDevices = sequencer.GetInternalDevices(); + auto & device = internalDevices.at(dev_id); + DestinationClientId = device.ClientID; + DestinationPortId = device.PortNumber; +} + +AlsaMIDIDevice::~AlsaMIDIDevice() +{ + Close(); +} + +int AlsaMIDIDevice::Open() +{ + if (!sequencer.IsOpen()) { + return 1; + } + + if(PortId < 0) + { + snd_seq_port_info_t *pinfo; + snd_seq_port_info_alloca(&pinfo); + + snd_seq_port_info_set_port(pinfo, IntendedPortId); + snd_seq_port_info_set_port_specified(pinfo, 1); + + snd_seq_port_info_set_name(pinfo, "GZDoom Music"); + + snd_seq_port_info_set_capability(pinfo, 0); + snd_seq_port_info_set_type(pinfo, SND_SEQ_PORT_TYPE_MIDI_GENERIC | SND_SEQ_PORT_TYPE_APPLICATION); + + int err = 0; + err = snd_seq_create_port(sequencer.handle, pinfo); + PortId = IntendedPortId; + } + + if (QueueId < 0) + { + QueueId = snd_seq_alloc_named_queue(sequencer.handle, "GZDoom Queue"); + } + + if (!Connected) { + Connected = (snd_seq_connect_to(sequencer.handle, PortId, DestinationClientId, DestinationPortId) == 0); + } + return 0; +} + +void AlsaMIDIDevice::Close() +{ + if(Connected) { + snd_seq_disconnect_to(sequencer.handle, PortId, DestinationClientId, DestinationPortId); + Connected = false; + } + if(QueueId >= 0) { + snd_seq_free_queue(sequencer.handle, QueueId); + QueueId = -1; + } + if(PortId >= 0) { + snd_seq_delete_port(sequencer.handle, PortId); + PortId = -1; + } +} + +bool AlsaMIDIDevice::IsOpen() const +{ + return Connected; +} + +int AlsaMIDIDevice::GetTechnology() const +{ + // TODO: implement properly, for now assume everything is an external MIDI device + return MIDIDEV_MIDIPORT; +} + +int AlsaMIDIDevice::SetTempo(int tempo) +{ + Tempo = tempo; + return 0; +} + +int AlsaMIDIDevice::SetTimeDiv(int timediv) +{ + TimeDiv = timediv; + return 0; +} + +void AlsaMIDIDevice::HandleEvent(int tick, int status, int parm1, int parm2) +{ + int command = status & 0xF0; + int channel = status & 0x0F; + + 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); + + switch (command) + { + case MIDI_NOTEOFF: + snd_seq_ev_set_noteoff(&ev, channel, parm1, parm2); + break; + + case MIDI_NOTEON: + snd_seq_ev_set_noteon(&ev, channel, parm1, parm2); + break; + + case MIDI_POLYPRESS: + // FIXME: Seems to be missing in the Alsa sequencer implementation + 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: + return; + } + snd_seq_event_output(sequencer.handle, &ev); +} + +void AlsaMIDIDevice::HandleLongEvent(int tick, const uint8_t *data, int len) +{ + // SysEx messages... + if (len > 1 && (data[0] == 0xF0 || data[0] == 0xF7)) + { + 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) { + if(Tempo != tempo) { + Tempo = tempo; + 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_change_queue_tempo(sequencer.handle, QueueId, Tempo, &ev); + snd_seq_event_output(sequencer.handle, &ev); + } +} + +void AlsaMIDIDevice::PumpEvents() { + int error = 0; + snd_seq_queue_tempo_t *tempo; + snd_seq_queue_tempo_alloca(&tempo); + snd_seq_queue_tempo_set_tempo(tempo, Tempo); + snd_seq_queue_tempo_set_ppq(tempo, TimeDiv); + error = snd_seq_set_queue_tempo(sequencer.handle, QueueId, tempo); + + snd_seq_start_queue(sequencer.handle, QueueId, NULL); + error = snd_seq_drain_output(sequencer.handle); + + int running_time = 0; + while (!Exit) { + if(!Events) { + // NOTE: in practice, this is never reached. however, if it were, it would prevent crashes below. + continue; + } + + uint32_t *event = (uint32_t *)(Events->lpData + Position); + int ticks = event[0]; + running_time += ticks; + if (MEVENT_EVENTTYPE(event[2]) == MEVENT_TEMPO) { + HandleTempoChange(running_time, MEVENT_EVENTPARM(event[2])); + } + 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. + if (event[2] < 0x80000000) + { // Short message + Position += 12; + } + else + { // Long message + Position += 12 + ((MEVENT_EVENTPARM(event[2]) + 3) & ~3); + } + + // 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); + snd_seq_sync_output_queue(sequencer.handle); + } + } + // Send stop events, just to be sure we don't end up with stuck notes + { + snd_seq_drop_output(sequencer.handle); + SendStopEvents(); + // FIXME: attach to a timestamped event and make it go through the queue? + snd_seq_stop_queue(sequencer.handle, QueueId, NULL); + snd_seq_drain_output(sequencer.handle); + snd_seq_sync_output_queue(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() +{ + if(!Connected) { + return 1; + } + Exit = false; + PlayerThread = std::thread(&AlsaMIDIDevice::PumpEvents, this); + return 0; +} + +void AlsaMIDIDevice::InitPlayback() +{ + Exit = false; +} + +void AlsaMIDIDevice::Stop() +{ + /* + * NOTE: this is slow. Maybe we should just leave the thread be and let it asynchronously drain in the background. + */ + Exit = true; + PlayerThread.join(); +} + +bool AlsaMIDIDevice::Pause(bool paused) +{ + // TODO: implement + return false; +} + + +int AlsaMIDIDevice::StreamOut(MidiHeader *header) +{ + header->lpNext = NULL; + if (Events == NULL) + { + Events = header; + Position = 0; + } + else + { + MidiHeader **p; + + for (p = &Events; *p != NULL; p = &(*p)->lpNext) + { } + *p = header; + } + return 0; +} + + +int AlsaMIDIDevice::StreamOutSync(MidiHeader *header) +{ + return StreamOut(header); +} + +bool AlsaMIDIDevice::Update() +{ + return true; +} + +MIDIDevice *CreateAlsaMIDIDevice(int mididevice) +{ + return new AlsaMIDIDevice(mididevice); +} +#endif diff --git a/libraries/zmusic/mididevices/music_alsa_state.cpp b/libraries/zmusic/mididevices/music_alsa_state.cpp new file mode 100644 index 000000000..064b38038 --- /dev/null +++ b/libraries/zmusic/mididevices/music_alsa_state.cpp @@ -0,0 +1,166 @@ +/* +** Provides an implementation of an ALSA sequencer wrapper +** +**--------------------------------------------------------------------------- +** Copyright 2020 Petr Mrazek +** All rights reserved. +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions +** are met: +** +** 1. Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** 2. Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in the +** documentation and/or other materials provided with the distribution. +** 3. The name of the author may not be used to endorse or promote products +** derived from this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**--------------------------------------------------------------------------- +** +*/ +#include "music_alsa_state.h" + +#if defined __linux__ && defined HAVE_SYSTEM_MIDI + +#include +#include + +AlsaSequencer & AlsaSequencer::Get() { + static AlsaSequencer sequencer; + return sequencer; +} + +AlsaSequencer::AlsaSequencer() { + Open(); +} + +AlsaSequencer::~AlsaSequencer() { + Close(); +} + +bool AlsaSequencer::Open() { + error = snd_seq_open(&handle, "default", SND_SEQ_OPEN_DUPLEX, 0); + if(error) { + return false; + } + error = snd_seq_set_client_name(handle, "GZDoom"); + if(error) { + snd_seq_close(handle); + handle = nullptr; + return false; + } + OurId = snd_seq_client_id(handle); + if (OurId < 0) { + error = OurId; + OurId = -1; + + snd_seq_close(handle); + handle = nullptr; + return false; + } + + return true; +} + + +void AlsaSequencer::Close() { + if(!handle) { + return; + } + snd_seq_close(handle); + handle = nullptr; +} + +namespace { + +bool filter(snd_seq_port_info_t *pinfo) +{ + int capability = snd_seq_port_info_get_capability(pinfo); + if(capability & SND_SEQ_PORT_CAP_NO_EXPORT) { + return false; + } + const int writable = (SND_SEQ_PORT_CAP_WRITE|SND_SEQ_PORT_CAP_SUBS_WRITE); + if((capability & writable) != writable) { + return false; + } + int type = snd_seq_port_info_get_type(pinfo); + return true; +} +} + +int AlsaSequencer::EnumerateDevices() { + if(!handle) { + return 0; + } + + snd_seq_client_info_t *cinfo; + snd_seq_port_info_t *pinfo; + + snd_seq_client_info_alloca(&cinfo); + snd_seq_port_info_alloca(&pinfo); + + int index = 0; + + // enumerate clients + snd_seq_client_info_set_client(cinfo, -1); + while (snd_seq_query_next_client(handle, cinfo) >= 0) { + snd_seq_port_info_set_client(pinfo, snd_seq_client_info_get_client(cinfo)); + + // Ignore 'ALSA oddities' that we don't want to use + int clientID = snd_seq_client_info_get_client(cinfo); + if(clientID < 16) { + continue; + } + + snd_seq_port_info_set_port(pinfo, -1); + // enumerate ports + while (snd_seq_query_next_port(handle, pinfo) >= 0) { + if (!filter(pinfo)) { + continue; + } + externalDevices.emplace_back(); + internalDevices.emplace_back(); + + auto & item = externalDevices.back(); + auto & itemInternal = internalDevices.back(); + itemInternal.ID = item.ID = index++; + const char *name = snd_seq_port_info_get_name(pinfo); + int portNumber = snd_seq_port_info_get_port(pinfo); + if(!name) { + std::ostringstream out; + out << "MIDI Port " << clientID << ":" << portNumber; + itemInternal.Name = item.Name = out.str(); + } + else { + itemInternal.Name = item.Name = name; + } + itemInternal.ClientID = clientID; + itemInternal.PortNumber = portNumber; + itemInternal.type = snd_seq_port_info_get_type(pinfo); + } + } + return index; +} + +const std::vector & AlsaSequencer::GetInternalDevices() +{ + return internalDevices; +} + +const std::vector & AlsaSequencer::GetDevices() { + return externalDevices; +} + +#endif diff --git a/libraries/zmusic/mididevices/music_alsa_state.h b/libraries/zmusic/mididevices/music_alsa_state.h new file mode 100644 index 000000000..3955bdc2c --- /dev/null +++ b/libraries/zmusic/mididevices/music_alsa_state.h @@ -0,0 +1,81 @@ +/* +** Provides an implementation of an ALSA sequencer wrapper +** +**--------------------------------------------------------------------------- +** Copyright 2008-2010 Randy Heit +** Copyright 2020 Petr Mrazek +** All rights reserved. +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions +** are met: +** +** 1. Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** 2. Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in the +** documentation and/or other materials provided with the distribution. +** 3. The name of the author may not be used to endorse or promote products +** derived from this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**--------------------------------------------------------------------------- +** +*/ + +#pragma once + +#if defined __linux__ && defined HAVE_SYSTEM_MIDI + +#include "zmusic/zmusic.h" +#include +#include +typedef struct _snd_seq snd_seq_t; + +// FIXME: make not visible from outside +struct MidiOutDeviceInternal { + std::string Name; + int ID = -1; + int ClientID = -1; + int PortNumber = -1; + unsigned int type = 0; +}; + +// NOTE: the sequencer state is shared between actually playing MIDI music and device enumeration, therefore we keep it around. +class AlsaSequencer { +private: + AlsaSequencer(); + ~AlsaSequencer(); + +public: + static AlsaSequencer &Get(); + bool Open(); + void Close(); + bool IsOpen() const { + return nullptr != handle; + } + + int EnumerateDevices(); + const std::vector &GetDevices(); + const std::vector &GetInternalDevices(); + + snd_seq_t *handle = nullptr; + + int OurId = -1; + int error = -1; + +private: + std::vector internalDevices; + std::vector externalDevices; +}; + +#endif diff --git a/libraries/zmusic/musicformats/music_midi.cpp b/libraries/zmusic/musicformats/music_midi.cpp index 614081e89..f4f7eba18 100644 --- a/libraries/zmusic/musicformats/music_midi.cpp +++ b/libraries/zmusic/musicformats/music_midi.cpp @@ -41,6 +41,12 @@ #include "mididevices/mididevice.h" #include "midisources/midisource.h" +#ifdef HAVE_SYSTEM_MIDI +#ifdef __linux__ +#include "mididevices/music_alsa_state.h" +#endif +#endif + // MACROS ------------------------------------------------------------------ enum @@ -226,8 +232,8 @@ EMidiDevice MIDIStreamer::SelectMIDIDevice(EMidiDevice device) case -7: return MDEV_ADL; case -8: return MDEV_OPN; default: - #ifdef _WIN32 - return MDEV_MMAPI; + #ifdef HAVE_SYSTEM_MIDI + return MDEV_STANDARD; #else return MDEV_SNDSYS; #endif @@ -268,13 +274,17 @@ MIDIDevice *MIDIStreamer::CreateMIDIDevice(EMidiDevice devtype, int samplerate) dev = CreateOPNMIDIDevice(Args.c_str()); break; - case MDEV_MMAPI: + case MDEV_STANDARD: +#ifdef HAVE_SYSTEM_MIDI #ifdef _WIN32 dev = CreateWinMIDIDevice(std::max(0, miscConfig.snd_mididevice)); +#elif __linux__ + dev = CreateAlsaMIDIDevice(std::max(0, miscConfig.snd_mididevice)); +#endif break; #endif - // Intentional fall-through for non-Windows systems. + // Intentional fall-through for systems without standard midi support #ifdef HAVE_FLUIDSYNTH case MDEV_FLUIDSYNTH: @@ -308,8 +318,8 @@ MIDIDevice *MIDIStreamer::CreateMIDIDevice(EMidiDevice devtype, int samplerate) else if (!checked[MDEV_TIMIDITY]) devtype = MDEV_TIMIDITY; else if (!checked[MDEV_WILDMIDI]) devtype = MDEV_WILDMIDI; else if (!checked[MDEV_GUS]) devtype = MDEV_GUS; -#ifdef _WIN32 - else if (!checked[MDEV_MMAPI]) devtype = MDEV_MMAPI; +#ifdef HAVE_SYSTEM_MIDI + else if (!checked[MDEV_STANDARD]) devtype = MDEV_STANDARD; #endif else if (!checked[MDEV_OPL]) devtype = MDEV_OPL; @@ -374,9 +384,9 @@ bool MIDIStreamer::DumpWave(const char *filename, int subsong, int samplerate) assert(MIDI == NULL); auto devtype = SelectMIDIDevice(DeviceType); - if (devtype == MDEV_MMAPI) + if (devtype == MDEV_STANDARD) { - throw std::runtime_error("MMAPI device is not supported"); + throw std::runtime_error("System MIDI device is not supported"); } auto iMIDI = CreateMIDIDevice(devtype, samplerate); auto writer = new MIDIWaveWriter(filename, static_cast(iMIDI)); diff --git a/libraries/zmusic/zmusic/configuration.cpp b/libraries/zmusic/zmusic/configuration.cpp index 9a73e2880..a4ffdd1b2 100644 --- a/libraries/zmusic/zmusic/configuration.cpp +++ b/libraries/zmusic/zmusic/configuration.cpp @@ -40,6 +40,7 @@ #include "zmusic.h" #include "musinfo.h" #include "midiconfig.h" +#include "mididevices/music_alsa_state.h" struct Dummy { @@ -78,6 +79,38 @@ void ZMusic_SetDmxGus(const void* data, unsigned len) memcpy(gusConfig.dmxgus.data(), data, len); } +int ZMusic_EnumerateMidiDevices() +{ +#ifdef HAVE_SYSTEM_MIDI + #ifdef __linux__ + auto & sequencer = AlsaSequencer::Get(); + return sequencer.EnumerateDevices(); + #elif _WIN32 + // TODO: move the weird stuff from music_midi_base.cpp here, or at least to this lib and call it here + return {}; + #endif +#else + return {}; +#endif +} + + +const std::vector &ZMusic_GetMidiDevices() +{ +#ifdef HAVE_SYSTEM_MIDI + #ifdef __linux__ + auto & sequencer = AlsaSequencer::Get(); + return sequencer.GetDevices(); + #elif _WIN32 + // TODO: move the weird stuff from music_midi_base.cpp here, or at least to this lib and call it here + return {}; + #endif +#else + return {}; +#endif +} + + template void ChangeAndReturn(valtype &variable, valtype value, valtype *realv) { diff --git a/libraries/zmusic/zmusic/mididefs.h b/libraries/zmusic/zmusic/mididefs.h index 29cfcb5a5..7f573544f 100644 --- a/libraries/zmusic/zmusic/mididefs.h +++ b/libraries/zmusic/zmusic/mididefs.h @@ -43,7 +43,7 @@ enum EMidiEvent : uint8_t enum EMidiDevice { MDEV_DEFAULT = -1, - MDEV_MMAPI = 0, + MDEV_STANDARD = 0, MDEV_OPL = 1, MDEV_SNDSYS = 2, MDEV_TIMIDITY = 3, diff --git a/libraries/zmusic/zmusic/zmusic.cpp b/libraries/zmusic/zmusic/zmusic.cpp index 7d6b18f8a..fd76505c2 100644 --- a/libraries/zmusic/zmusic/zmusic.cpp +++ b/libraries/zmusic/zmusic/zmusic.cpp @@ -210,10 +210,10 @@ MusInfo *ZMusic_OpenSong (MusicIO::FileInterface *reader, EMidiDevice device, co delete source; return nullptr; } - -#ifndef _WIN32 - // non-Windows platforms don't support MDEV_MMAPI so map to MDEV_SNDSYS - if (device == MDEV_MMAPI) + +#ifndef HAVE_SYSTEM_MIDI + // some platforms don't support MDEV_STANDARD so map to MDEV_SNDSYS + if (device == MDEV_STANDARD) device = MDEV_SNDSYS; #endif diff --git a/libraries/zmusic/zmusic/zmusic.h b/libraries/zmusic/zmusic/zmusic.h index abb1f7d4c..6422512f6 100644 --- a/libraries/zmusic/zmusic/zmusic.h +++ b/libraries/zmusic/zmusic/zmusic.h @@ -2,6 +2,8 @@ #include "mididefs.h" #include "../../music_common/fileio.h" +#include +#include namespace ZMusic // Namespaced because these conflict with the same-named CVARs { @@ -149,6 +151,13 @@ void ZMusic_SetWgOpn(const void* data, unsigned len); // Set DMXGUS data for running the GUS synth in actual GUS mode. void ZMusic_SetDmxGus(const void* data, unsigned len); +struct MidiOutDevice { + std::string Name; + int ID = -1; +}; +int ZMusic_EnumerateMidiDevices(); +const std::vector &ZMusic_GetMidiDevices(); + // These exports are needed by the MIDI dumpers which need to remain on the client side. class MIDISource; // abstract for the client class MusInfo; diff --git a/src/sound/music/i_music.cpp b/src/sound/music/i_music.cpp index bef7fe307..1d3c0f715 100644 --- a/src/sound/music/i_music.cpp +++ b/src/sound/music/i_music.cpp @@ -270,9 +270,12 @@ void I_InitMusic (void) nomusic = !!Args->CheckParm("-nomusic") || !!Args->CheckParm("-nosound"); +// TODO: remove, move functionality to ZMusic_EnumerateMidiDevices #ifdef _WIN32 I_InitMusicWin32 (); #endif // _WIN32 + + ZMusic_EnumerateMidiDevices(); snd_mididevice.Callback(); Callbacks callbacks; diff --git a/src/sound/music/music_midi_base.cpp b/src/sound/music/music_midi_base.cpp index 4b5f014c9..9af4b7ebc 100644 --- a/src/sound/music/music_midi_base.cpp +++ b/src/sound/music/music_midi_base.cpp @@ -167,12 +167,20 @@ CCMD (snd_listmididevices) } } } - -#else +#else // _WIN32 void I_BuildMIDIMenuList (FOptionValues *opt) { AddDefaultMidiDevices(opt); + + auto devices = ZMusic_GetMidiDevices(); + + for (auto & device: devices) + { + FOptionValues::Pair *pair = &opt->mValues[opt->mValues.Reserve(1)]; + pair->Text = device.Name.c_str(); + pair->Value = (float)device.ID; + } } CCMD (snd_listmididevices) @@ -184,13 +192,21 @@ CCMD (snd_listmididevices) Printf("%s-4. Gravis Ultrasound Emulation\n", -4 == snd_mididevice ? TEXTCOLOR_BOLD : ""); Printf("%s-3. Emulated OPL FM Synth\n", -3 == snd_mididevice ? TEXTCOLOR_BOLD : ""); Printf("%s-2. TiMidity++\n", -2 == snd_mididevice ? TEXTCOLOR_BOLD : ""); + + auto devices = ZMusic_GetMidiDevices(); + + for (auto & device: devices) + { + Printf("%s%d. %s\n", -2 == snd_mididevice ? TEXTCOLOR_BOLD : "", device.ID, device.Name.c_str()); + } } #endif CUSTOM_CVAR (Int, snd_mididevice, DEF_MIDIDEV, CVAR_ARCHIVE|CVAR_GLOBALCONFIG|CVAR_NOINITCALL) { - if ((self >= (signed)nummididevices) || (self < -8)) + auto devices = ZMusic_GetMidiDevices(); + if ((self >= (signed)devices.size()) || (self < -8)) { // Don't do repeated message spam if there is no valid device. if (self != 0) diff --git a/src/sound/s_advsound.cpp b/src/sound/s_advsound.cpp index 1ae59a886..874ff70be 100644 --- a/src/sound/s_advsound.cpp +++ b/src/sound/s_advsound.cpp @@ -1230,7 +1230,7 @@ static void S_AddSNDINFO (int lump) MidiDeviceSetting devset; if (sc.Compare("timidity")) devset.device = MDEV_TIMIDITY; else if (sc.Compare("fmod") || sc.Compare("sndsys")) devset.device = MDEV_SNDSYS; - else if (sc.Compare("standard")) devset.device = MDEV_MMAPI; + else if (sc.Compare("standard")) devset.device = MDEV_STANDARD; else if (sc.Compare("opl")) devset.device = MDEV_OPL; else if (sc.Compare("default")) devset.device = MDEV_DEFAULT; else if (sc.Compare("fluidsynth")) devset.device = MDEV_FLUIDSYNTH;