The Midi Kit: BMidi


The Midi Kit: BMidi

Derived from: public BObject

Declared in: <midi/Midi.h>


Overview

BMidi is the centerpiece of the Midi Kit. It provides base class implementations of the functions that create a MIDI performance. BMidi is abstract; all other Kit classes--and any class that you want to design to take part in a performance--derive from BMidi. When you create a BMidi-derived class, you do so mainly to re-implement the hook functions that BMidi provides. The hook functions allow instances of your class to behave in a fashion that the other objects will understand.

The functions that BMidi defines fall into four categories:


Forming Connections

A fundamental concept of the Midi Kit is that MIDI data should "stream" through your application, passing from one BMidi-derived object to another. Each object does whatever it's designed to do: Sends the data to a MIDI port, writes it to a file, modifies it and passes it on, and so on.

You form the chain of BMidi objects that propagate MIDI data by connecting them to each other. This is done through BMidi's Connect() function. The function takes a single argument: The object you want the caller to connect to. By calling Connect(), you connect the output of the calling object to the input of the argument object.

For example, let's say you want to connect a MIDI keyboard to your computer, play it, and have the performance recorded in a file. To set this up, you connect a BMidiPort object, which reads data from the MIDI port, to a BMidiStore object, which stores the data that's sent to it and can write it to a file:

   /* Connect the output of a BMidiPort to the input of a  
    * BMidiStore. 
    */
   BMidiPort *m_port = new BMidiPort();
   BMidiStore *m_store = new BMidiStore();
   
   m_port->Connect(m_store);

Simply connecting the objects isn't enough, however; you have to tell the BMidiPort to start listening to the MIDI port, by calling its Start() function. This is explained in a later section.

Once you've made the recording, you could play it back by re-connecting the objects in the opposite direction:

   /* We'll disconnect first, although this isn't strictly
    * necessary.
    */
   m_port->Disconnect(m_store);
   m_store->Connect(m_port);

In this configuration, a Start() call to m_store would cause its MIDI data to flow into the BMidiPort (and thence to a synthesizer, for example, for realization).

You can connect any number of BMidi objects to the output of another BMidi object, as depicted below:

The configuration in the illustration is created thus:

   a_object->Connect(b_object);
   a_object->Connect(c_object);
   a_object->Connect(d_object);

Every BMidi object knows which objects its output is connected to; you can get a BList of these objects through the Connections() function. For example, a_object, above, would list b_object, c_object, and d_object as its connections.

Similarly, the same BMidi object can be the argument in any number of Connect() calls, as shown below and depicted in the following illustration:

   b_object->Connect(a_object);
   c_object->Connect(a_object);
   d_object->Connect(a_object);
     

When you use a BMidi object as the argument to a Connect() method, the argument object isn't informed. In the illustration, a_object doesn't know about the objects that are connected to its input.


Generating MIDI Messages

To generate MIDI messages, you implement the Run() function in a BMidi-derived class. An implementation of Run() should include a while() loop that produces (typically) a single MIDI message on each pass, and then sprays the message to the connected objects. To predicate the loop you test the value of the KeepRunning() boolean function.

The outline of a Run() implementation looks like this:

   void MyMidi::Run()
   {
      while (KeepRunning()) {
         /* Generate a message and spray it. */
      }
   }

Although your derived class can generate more than one MIDI message each time through the loop, it's recommended that you try to stick to just one.

To tell an object to perform its Run() function, you call the object's Start() function--you never call Run() directly. Start() causes the object to spawn a thread (its "run" thread) and execute Run() within it. When you're tired of the object's performance, you call its Stop() function.

The Run() function is needed in classes that want to introduce new MIDI data into a performance. For example, in its implementation of Run(), BMidiStore sprays messages that correspond to the MIDI data that it stores. In its Run(), a BMidiPort reads data from the MIDI port and produces messages accordingly. If you're generating MIDI data algorithmically, or reading your own file format (BMidiStore can read standard MIDI files), then you'll need to implement Run(). If, on the other hand, you're creating an object that "filters" data--that accepts data at its input, modifies it, then sprays it--you won't need Run().

Another point to keep in mind is that the Run() function can run ahead of real time. It doesn't have to generate and spray data precisely at the moment that the data needs to be realized. This is further explained in the section Time .

Important: The BMidi-derived classes that you create must implement Run(), even if they don't generate MIDI data; "do-nothing" implementations are acceptable, in this case. For example, if you're creating a filter (as described in a later section), your Run() function could be, simply

   void MidiFilter::Run()
   {}


Spray Functions

The spray functions are used (primarily) within a Run() loop to send data to the running object's connections (the objects that are connected to the running object's output). There's a separate spray function for each of the MIDI message types: SprayNoteOn(), SprayNoteOff(), SprayPitchBend(), and so on. The arguments that these functions take are the data items that comprise the specific messages. The spray functions also take an additional argument that gives the message a time-stamp, as explained later (again, in the "Time" section).


Input Functions

The input functions take the names of the MIDI messages to which they respond: NoteOn() responds to a Note On message; NoteOff() responds to a Note Off; KeyPressure() to a Key Pressure change, and so on. These are all virtual functions. BMidi doesn't provide a default implementation for any of them; it's up to each BMidi-derived class to decide how to respond to MIDI messages.

Input functions are never invoked directly; they're called automatically when a running object sprays MIDI data.

Every BMidi object automatically spawns an "input" thread when it's constructed. It's in this thread that the input functions are executed. The input thread is always running--the Start() and Stop() functions don't affect it. As soon as you construct an object, it's ready to receive data.

For example, let's say, once again, that you have a BMidiPort connected to a BMidiStore:

   m_port->Connect(m_store);

Now you open the port (a BMidiPort detail that doesn't extend to other BMidi-derived classes) and tell the BMidiPort to start running:

   m_port->Open("midi1");
   m_port->Start();

As the BMidiPort is running, it sends data to its output. Since the BMidiStore is connected to the BMidiPort's output, it receives this data automatically in the form of input function invocations. In other words, when m_port calls its SprayNoteOn() function (which it does in its Run() loop), m_store's NoteOn() function is automatically called. As an instance of BMidiStore, the m_store object caches the data that it receives through the input functions.

You can derive your own BMidi classes that implement the input functions in other ways. For example the following implementation of NoteOn(), in a proposed class called NoteCounter, simply keeps track of the number of times each key (in the MIDI sense) is played:

   void NoteCounter::NoteOn(uchar channel, uchar keyNumber, 
                     uchar velocity, ulong time)
   {
      /* We'll assume the class has allocated an array that
       * holds the key counters. 
       */
      keyCounter[keyNumber]++;
   }

Note that the NoteOn() function in the example includes a time argument (the other arguments should be familiar if you understand the MIDI specification). This argument is explained in the "Time" section.

Creating a MIDI Filter

Some BMidi classes may want to create objects that act as filters: They receive data, modify it, and then pass it on. To do this, you call the appropriate spray functions from within the implementations of the input functions. Below is the implementation of the NoteOn() function for a proposed class called Transposer. It takes each Note On, transposes it up a half step, and then sprays it:

   void Transposer::NoteOn(uchar channel, uchar keyNumber, 
                     uchar velocity, ulong time)
   {
      uchar new_key = max(keyNumber + 1, 127);
      SprayNoteOn(channel, new_key, velocity, time);
   }

There's a subtle but important distinction between a filter class and a "performance" class (where the latter is a class that's designed to actually realize the MIDI data it receives). The distinction has to do with time, and is explained in the next section. An implication of the distinction that affects the current discussion is that it may not be a great idea to invest, in a single object, the ability to filter and perform MIDI data. By way of calibration, both BMidiStore and BMidiPort are performance classes--objects of these classes realize the data they receive, the former by caching it, the latter by sending it out the MIDI port. In neither of these classes do the input functions spray data.


Time

Every spray and input function takes a final time argument. This argument declares when the message that the function represents should be performed. The argument is given as an absolute measurement in ticks, or milliseconds. Tick 0 occurs when you boot your computer; the tick counter automatically starts running at that point. To get the current tick measurement, you call the global, Kernel Kit-defined system_time() function and divide by 1000.0 (system_time() returns microseconds).

A convention of the Midi Kit holds that time arguments are applied at an object's input. In other words, the implementation of a BMidi-derived input function would look at the time argument, wait until the designated time, and then do whatever it does that it does do. However, this only applies to BMidi-derived classes that are designed to perform MIDI data, as the term was defined in the previous section. Objects that filter data shouldn't apply the time argument.

To apply the time argument, you call the SnoozeUntil() function, passing the value of time. For example, a "performance" NoteOn() function would look like this:

   void MyPerformer::NoteOn(uchar channel, uchar keyNumber, 
                  uchar velocity, ulong time)
   {
      SnoozeUntil(time);
      /* Perform the data here. */
   }

If time designates a tick that has already tocked, SnoozeUntil() returns immediately; otherwise it tells the input thread to snooze until the designated tick is at hand.

An extremely important point, with regard to The SnoozeUntil() function, as used here, may cause spraying objects (objects that are spraying


Spraying Time

If you're implementing the Run() function, then you have to generate a time value yourself which you pass as the final argument to each spray functionthat you call. The value you generate depends on whether you class runs in real time, or ahead of time.

Running in Real Time

If your class conjures MIDI data that needs to be performed immediately, you should use the B_NOW macro as the value of the time arguments that you pass to your spray functions. B_NOW is simply a cover for (system_time()/1000.0) (converted to an integer). By using B_NOW as the time argument you're declaring that the data should be performed in the same tick in which it was generated. This probably won't happen; by the time the input functions are called and the data realized, a few ticks will have elapsed. In this case, the expected SnoozeUntil() calls (within the input function implementations) will see that the time value has passed, and so will return immediately, allowing the data to be realized as quickly as possible.

The lag between the time that you generate the data and the time it's realized depends on a number of factors, such as how loaded down your machine is and how much processing your BMidi objects perform. But the Midi Kit machinery itself shouldn't impose a latency that's beyond the tolerability of a sensible musical performance.

Running Ahead of Time

If you're generating data ahead of its performance time, you need to compute the time value so that it pinpoints the correct time in the future. For example, if you want to create a class that generates a note every 100 milliseconds, you need to do something like this:

   void MyTicker::Run()
   {
      ulong when = B_NOW;
      uchar key_num;
   
      while (KeepRunning()) {
         
         /* Make a new note. */
         SprayNoteOn(1, 60, 64, when);
         
         /* Turn the note off 99 ticks later. */
         when += 99;
         SprayNoteOff(1, 60, 0, when);
         
         /* Bump the when variable so the next Note On
          * will be 100 ticks after this one.
          */
         when += 1;
      }
   }

When a MyTicker object is told to start running, it generates a sequence of Note On/Note Off pairs, and sprays them to its connected objects. Somewhere down the line, a performance object will apply the time value by calling SnoozeUntil().

Tethering MyTicker

But what, you may wonder, keeps MyTicker from running wild and generating thousands or millions of notes--which aren't scheduled to be played for hours--as fast as possible?

The answer is in the mechansim that connects a spray function to an input function: The BMidi class creates a port (in the Kernel Kit sense) for every object. When you invoke a spray function, the data is encoded in a message and written to each of the connected objects' ports. The input functions (invoked on the connected objects) then read from their respective ports. The secret here is that these ports are declared to be 1 (one) message deep. So, as long as one of the input function calls SnoozeUntil(), the spraying object will never be more than one message ahead.

A useful feature of this mechanism is that if you connect a series of BMidi object that don't invoke SnoozeUntil(), you can process MIDI data faster than real-time. For example, let's say you want to spray data from one BMidiStore object, pass the data through a filter, and then store it in another BMidiStore. The BMidiStore input functions don't call SnoozeUntil(); thus, data will flow out of the first object, through the filter, and into its destination as quickly as possible, allowing you to process hours of real-time data in just a few seconds. Of course, if you add a performance object into this mix (so you can hear the data while it's being processed), the data flow will be tethered, as described above.


Hook Functions

Run() Contains a loop that generates and broadcasts MIDI messages.
Start() Starts the object's run loop. Can be overridden to provide pre-running adjustments.
Stop() Stops the object's run loop. Can be overridden to perform post-running clean-up.

The input functions (NoteOn(), NoteOff(), and so on) are also hook functions. These are listed in the section Input and Spray Functions .


Constructor and Destructor


BMidi()

      BMidi(void)

Creates and returns a new BMidi object. The object's input thread is spawned and started in this function--in other words, BMidi objects are born with the ability to accept incoming messages. The run thread, on the other hand, isn't spawned until Start() is called.


~BMidi()

      virtual ~BMidi(void)

Kills the input and run threads after they've gotten to suitable stopping points (as defined below), deletes the list that holds the connections (but doesn't delete the objects contained in the list), then destroys the BMidi object.

The input thread is stopped after all currently-waiting input messages have been read. No more messages are accepted while the input queue is being drained. The run thread is allowed to complete its current pass through the run loop and then told to stop (in the manner of the Stop() function).

While the destructor severs the connections that this BMidi object has formed, it doesn't sever the connections from other objects to this one. For example, consider the following (improper) sequence of calls:

   /* DON'T DO THIS... */
   a_midi->Connect(b_midi);
   b_midi->Connect(c_midi);
   ...
   delete b_midi;

The delete call severs the connection from b_midi to c_midi, but it doesn't disconnect a_midi and b_midi. You have to disconnect the object's "back-connections" explicitly:

   /* ...DO THIS INSTEAD */
   a_midi->Connect(b_midi);
   b_midi->Connect(c_midi);
   ...
   a_midi->Disconnect(b_midi);
   delete b_midi;

See also: Stop()


Member Functions


Connect()

      void Connect(BMidi *toObject)

Connects the BMidi object's output to toObject's input. The BMidi object can connect its output to any number of other objects. Each of these connected objects receives an input function call as the BMidi sprays messages. For example, consider the following setup:

   my_midi->Connect(your_midi);
   my_midi->Connect(his_midi);
   my_midi->Connect(her_midi);

The output of my_midi is connected to the inputs of your_midi, his_midi, and her_midi. When my_midi calls a spray function--SprayNoteOn(), for example--each of the other objects receives an input function call--in this case, NoteOn().

Any object that's been the argument in a Connect() call should ultimately be disconnected through a call to Disconnect(). In particular, care should be taken to disconnect objects when deleting a BMidi object, as described in the destructor.

See also: ~BMidi(), Connections(), IsConnected()


Connections()

      inline BList *Connections(void)

Returns a BList that contains the objects that this object has connected to itself. In other words, the objects that were arguments in previous calls to Connect(). When a BMidi object sprays, each of the objects in its connection list becomes the target of an input function invocation, as explained in the class description.

See also: Connect(), Disconnect(), IsConnected()


Disconnect()

      void Disconnect(BMidi *toObject)

Severs the BMidi's connection to the argument. The connection must have previously been formed through a call to Connect() with a like disposition of receiver and argument.

See also: Connect()


IsConnected()

      inline bool IsConnected(BMidi *toObject)

Returns TRUE if the argument is present in the receiver's list of connected objects.

See also: Connect(), Connections()


IsRunning()

      bool IsRunning(void)

Returns TRUE if the object's Run() loop is looping; in other words, if the object has received a Start() function call, but hasn't been told to Stop() (or otherwise hasn't fallen out of the loop).

See also: Start(), Stop()


KeepRunning()

protected:

      bool KeepRunning(void)

Used by the Run() function to predicate its while loop, as explained in the class description. This function should only be called from within Run().

See also: Run(), Start(), Stop()


Run()

private:

      void Run(void)

A BMidi-derived class places its data-generating machinery in the Run() function, as described in the section Message Generation and the Run() Function .

See also: Start(), Stop(), KeepRunning()


SnoozeUntil()

      void SnoozeUntil(ulong tick)

Puts the calling thread to sleep until tick milliseconds have elapsed since the computer was booted. This function is meant to be used in the implementation of the input functions, as explained in the section Time .


Start()

      virtual void Start(void)

Tells the object to begin its run loop and execute the Run() function. You can override this function in a BMidi-derived class to provide your own pre-running initialization. Make sure, however, that you call the inherited version of this function within your implementation.

See also: Stop(), Run()


Stop()

      virtual void Stop(void)

Tells the object to halt its run loop. Calling Stop() tells the KeepRunning() function to return FALSE, thus causing the run loop (in the Run() function) to terminate. You can override this function in a BMidi-derived class to predicate the stop, or to perform post-performance clean-up (as two examples). Make sure, however, that you invoke the inherited version of this function within your implementation.

See also: Start(), Run()


Input and Spray Functions

The protocols for the input and spray functions are given below, grouped by the MIDI message to which they correspond (the input function for each group is shown first, the spray function is second).

See the class overview for more information on these functions.


Channel, Pressure

      virtual void ChannelPressure(uchar channel,
         uchar pressure,
         ulong time = B_NOW)

protected:

      void SprayChannelPressure(uchar channel,
         uchar pressure,
         ulong time)


Control, Change

      virtual void ControlChange(uchar channel,
         uchar  controlNumber,
         uchar  controlValue,
         ulong time = B_NOW)

protected:

      void SprayControlChange(uchar channel,
         uchar  controlNumber,
         uchar  controlValue,
         ulong time)


Key, Pressure

      virtual void KeyPressure(uchar channel,
         uchar note,
         uchar pressure,
         ulong time = B_NOW)

protected:

      void SprayKeyPressure(uchar channel,
         uchar note,
         uchar pressure,
         ulong time)


Note, Off

      virtual void NoteOff(uchar channel,
         uchar note,
         uchar velocity,
         ulong time = B_NOW)

protected:

      void SprayNoteOff(uchar channel,
         uchar note,
         uchar velocity,
         ulong time)


Note, On

      virtual void NoteOn(uchar channel,
         uchar note,
         uchar velocity,
         ulong time = B_NOW)

protected:

      void SprayNoteOn(uchar channel,
         uchar note,
         uchar velocity,
         ulong time)


Pitch, Bend

      virtual void PitchBend(uchar channel,
         uchar lsb,
         uchar msb,
         ulong time = B_NOW)

protected:

      void SprayPitchBend(uchar channel,
         uchar lsb,
         uchar msb,
         ulong time)


Program, Change

      virtual void ProgramChange(uchar channel,
         uchar programNumber,
         ulong time = B_NOW)

protected:

      void SprayProgramChange(uchar channel,
         uchar programNumber,
         ulong time)


System, Common

      virtual void SystemCommon(uchar status,
         uchar data1,
         uchar data2,
         ulong time = B_NOW)

protected:

      void SpraySystemCommon(uchar status,
         uchar data1,
         uchar data2,
         ulong time)


System, Exclusive

      virtual void SystemExclusive(void *data,
         long dataLength,
         ulong time = B_NOW)

protected:

      void SpraySystemExclusive(void *data,
         long dataLength,
         ulong time)


SystemRealTime()

      virtual void SystemRealTime(uchar status, ulong time = B_NOW)

protected:

      void SpraySystemRealTime(uchar status, ulong time)


Tempo, Change()

      virtual void TempoChange(long beatsPerMinute, ulong time = B_NOW)

protected:

      void SprayTempoChange(long beatsPerMinute, ulong time)





The Be Book, HTML Edition, for Developer Release 8 of the Be Operating System.

Copyright © 1996 Be, Inc. All rights reserved.

Be, the Be logo, BeBox, BeOS, BeWare, and GeekPort are trademarks of Be, Inc.

Last modified September 6, 1996.