Condividi tramite


Alsa Driver for JDKMidi with RtMidi on Linux using C++0x Futures

One of the questions that I come across developers asking frequently is "what is the best C++ MIDI library". It often goes unanswered unequivocally.

The reason being, despite the presence of numerous C++ MIDI libraries out there, there is no single library that meets all the requirements of a music programmer. Often developers end up having to choose from among two or three incomplete MIDI libraries after a thorough search. Incomplete either in documentation, usage samples or functionality!!

RtMidi is a good library, if you are planning to use MIDI in ‘real time’. For playing midi files, you have to write your own sequencer, that is.

JDKMidi is another good library that has sequencer in-built for Windows – but does not come with a sequencer for Linux. (Even on Windows, JDKMidi has few short comings w.r.to. sequencing out of order events – which was corrected in an earlier post here)

Other libraries like PortMidi, TSE3 lack good documentation and scare the developers away right in the first glance.

Not to mention, all these libraries are low level – that is you should supply the MIDI notes, byte by byte. Good luck.

While looking for a good MIDI driver for CFugue - the first C++ high-level Music String library that lets you program MIDI in actual music notes like A B C (and not in MIDI bytes), I took the task of bridging the gap between JDKMidi and RtMIDI for Linux by creating ALSA driver for JDKMidi with RtMidi backend.

The biggest challenge in creating a sequencer for a platform independent library such as CFugue is: asynchronous constructs. How do you pump MIDI events asynchronously in a platform compatible way (without placing an additional third-party thread-library dependency).

C++0x futures and promises couldn’t have come in better time for CFugue. (Visual Studio 2010 does not yet support C++0x Futures. Gnu C++ supports them. If you would like to use CFugue or C++0x Futures in your code, you should go for Gnu C++ compiler version > 4.5 with –std=gnu++0x compiler option).

You can find lot of documentation on C++0x futures and promises on the net. Here is how CFugue launches the Alsa sequencer procedure asynchronously:

 std::future<bool> m_bgTaskResult = std::async(std::launch::async, &AlsaDriverThreadProc, this, res);

The prototype of AlsaDriverThreadProc looks as below:

 bool AlsaDriverThreadProc(MIDIDriverAlsa* pAlsaDriver, int nTimerResMS);

To wait till the asynchronous Alsa procedure completes, CFugue uses the C++0x Chrono constructs as shown below:

     void MIDIDriverAlsa::WaitTillDone()
    {
        if(m_bgTaskResult.valid() == false) return; // if not running

        auto waitStatus = m_bgTaskResult.wait_for(std::chrono::milliseconds(0));

        while(waitStatus != true)
        {
             waitStatus = m_bgTaskResult.wait_for(std::chrono::milliseconds(500));
        }
    }

When it comes to C++0x Futures just few points worth noting are:

  • You can either create threads explicitly using the std::thread constructs and use them in conjunction with std::future and std::promise or let the underlying C++0x implementation take care of them using std::async() method (as shown above)
  • std::async() can invoke the supplied procedure synchronously or asynchronously. Do not forget to use the std::launch::async parameter to let the methods launch asynchronously
  • std::async() returns a std::future that has a valid() method on it that you can check if your asynchronous procedure started successfully or not
  • std::future::wait_for() allows you to wait for the asynchronous procedure to get completed. std::future::wait_for() waits for a stipulated amount of time and returns false if the background asynchronous procedure did not complete. Returns true if the procedure got complete and the result is ready. You need to call std::future::get() to retrieve the actual result from the asynchronous procedure.
  • Note that std::future::get() is a blocking call. If you use std::future::get() before the background asynchronous thread completes processing, the caller goes into blocked state till the result is ready. If you are looking for a polled way of checking the result, std::future::wait_for() is your way.
  • You can reuse std::future objects (by assigning std::async() return values more than once). However, std::future requires get() to be called before it can be used again. std::future::valid() keeps returning true till get() is called. And std::future::get() can be called only once on the std::future object. Once get() is called, std::future::valid() becomes false and can be reused.

Here is the complete listing of C++0x related code:

 ///<Summary>MIDI Driver for Linux Alsa based machines</Summary>
class MIDIDriverAlsa : public jdkmidi::MIDIDriver
{
    std::future<bool>   m_bgTaskResult;
    //...
};

// This is thread procedure to pump MIDI events // We maintain the supplied Timer Resolution by adjusting the sleep duration
bool AlsaDriverThreadProc(MIDIDriverAlsa* pAlsaDriver, int nTimerResMS)
{
    unsigned long nBefore, nAfter;
    unsigned int nElapsed, nTimeToSleep;

    while(true)
    {
        nBefore = MidiTimer::CurrentTimeOffset();

        if(pAlsaDriver->TimeTick(nBefore) == false) break;

        nAfter = MidiTimer::CurrentTimeOffset();

        nElapsed = nAfter - nBefore;

        nTimeToSleep = (nElapsed > nTimerResMS ? 0 : nTimerResMS - nElapsed);

        std::this_thread::sleep_for(std::chrono::milliseconds(nTimeToSleep));
    }

    return true;
}

bool MIDIDriverAlsa::StartTimer ( int res )
{
    if(m_bgTaskResult.valid()) // Already running
        return false;

    m_bgTaskResult = std::async(std::launch::async, &AlsaDriverThreadProc, this, res);

    return m_bgTaskResult.valid();
}

void MIDIDriverAlsa::WaitTillDone()
{
    if(m_bgTaskResult.valid() == false) return; // if not running

    auto waitStatus = m_bgTaskResult.wait_for(std::chrono::milliseconds(0));

    while(waitStatus != true)//std::future_status::ready
    {
        waitStatus = m_bgTaskResult.wait_for(std::chrono::milliseconds(500));
    }
}

void MIDIDriverAlsa::StopTimer()
{
    // std::future requires get() to be called before it can be used again. // valid() keeps returning true till get() is called. And get() can be // called only once. Once it is called valid() becomes false again.
    if(m_bgTaskResult.valid())
        m_bgTaskResult.get();
}
        

The complete CFugue source code and the JDKMidi Alsa driver can be downloaded from: CFugue downloads

Details of CFugue Runtime and how to use C++ Music Strings can be found at: CFugue Documentation

CodeProject

Comments

  • Anonymous
    September 24, 2013
    Hello! I'm the primary author of JDKMIDI and I like this work of yours!  The jdksmidi class library is quite old; and as a fan of modern C++11, I'd love to get it refactored to be modernized and also to support ALSA and other drivers.  Perhaps you could integrate your latest Alsa specific modifications into the latest version of jdksmidi at github: github.com/.../jdksmidi ?