The Midi Driver Framework for LGSND on FreeBSD

Seigo Tanimura

tanimura@naklab.dnj.ynu.ac.jp

Copyright © 1999 Seigo Tanimura

$Date: 1999/06/01 13:01:17 $

$Id: midiarch.sgml,v 1.11 1999/06/01 13:01:17 tanimura Exp $

In this paper we discuss the framework for the Midi drivers on the new FreeBSD audio driver architecture. We first give the overview of the framework, followed by the role of each elements including a sequencer, a synthesizer intreface and a midi device. Finally we describe writing a driver for a new device.

Contents

1.  Framework

1.   Framework

1.1.  Introduction
1.2.  Supported devices
1.3.  The Overview of the Framework
1.4.  Configuring a Kernel
1.5.  The Roles of the Elements
1.6.  Writing a New Driver
1.7.  Conclusion

1.1.   Introduction

The current VoxWare audio driver for FreeBSD has the following problems:

To overcome the problems shown above, a new audio driver for FreeBSD should have the following design goals:

Luigi Rizzo has introduced such the new driver framework for pcm devices, which is now a part of FreeBSD. I propose the framework for midi devices, which supports a sequcncer, a midi interface and a synthesizer chip.

1.2.   Supported devices

The driver supports two devices at this moment:

I am working on some more devices including:

The driver supports two devices at this moment:

1.3.   The Overview of the Framework

Fig 1 shows the overview of the new midi driver framework for FreeBSD.


Figure 1: The overview of the midi driver framework

[image]


The framework consists of three functional elements: a sequencer, a midi synthesizer interface and a midi driver. A user process writes sequencer events to a sequencer through /dev/sequencer. A sequencer interprets wait events to realize gate times and durations, and demultiplexes events to an apropriate midi synthesizer interface. A midi synthesizer interface is a set of entries to several synthesizer functions, translating the sequencer events not processed by a sequencer into midi messages or the operations against a synthesizer chip. Finally a midi driver handles I/O against the midi interface or the synthesizer chip, to have a sound to hear. A midi driver for a midi interface(SB Midi, MPU401, etc.) also accepts raw midi messages via /dev/midi to output directly to the midi interface.

A sequencer is device-independent, while a midi synthesizer interface and a midi driver depend on the device to drive. Writing a new midi device driver genelrally involves implementing a new midi synthesizer intreface and midi driver, except the midi synthesizer interface for a midi interface can be shared easily among midi drivers because of the device-independency of the translation from a sequencer event into a midi message.

Due to the device-dependency of a driver, a midi synthesizer intreface and a midi driver reside in a single device driver. A sequencer does not have a hardware to drive, hence is a pseudo-device driver.

1.4.   Configuring a Kernel

My framework accepts two kinds of drivers to be configured into a kernel. Table 1 shows them.

Table 1: The drivers that can be configured

Driver Name Function Device or Pseudo
midi A midi synthesizer intreface, and a midi driver Device
seq A sequencer Pseudo

A configuration entry for an ISA midi driver looks like the following:

device midi0 at isa? port P irq N flags F

where:

If you have more than one midi devices, you can have multiple entries. In that case, each entry should have the different unit number. The framework gives the unit number of a device in the order of probe/attach. The unit number may differ from the one configured in a kernel. /dev/sndstat gives you the unit number assigned on each device.

An entry for a sequencer driver should look like the following:

pseudo-device seq 1

A sequencer is a pseudo-device, hence it does not have any hardware resources to use. You can increase the number to have multiple sequencers.

1.5.   The Roles of the Elements

1.5.1.  A Sequencer
1.5.2.  A Midi Synthesizer Interface
1.5.3.  A Midi Driver
1.5.4.  Miscellaneous Elements

In this section we discuss the functions of the elements, and how they interacts to the other elements.

1.5.1.   A Sequencer

A sequencer resides in sys/i386/snd/sequencer.c. It interacts to a user process through /dev/sequencer. A user process writes sequencer events to operate a sequencer. A sequencer event consists of four or eight bytes. The first byte tells the event type, and the size of the event. An event type of up to 127 has four bytes, while an event of 128 or greater has eight bytes. The sequencer events are defined in sys/i386/include/soundcard.h. To make your source tidy and readable, you can use the macros defined in sys/i386/include/soundcard.h to prepare sequencer events to write.

A sequencer is something more functional and abstruct than a simple Midi intreface or a synthesizer chip. Saying more specifically, a sequencer realizes the following functions:

Timing the sequencer is achived by the timeout(9) mechanism provided by the kernel. A user proccess first obtains the per-second-resolution of the sequencer timer via SNDCTL_SEQ_CTRLRATE ioctl(9). Then a timing event can be given to a sequencer using the tick count value of the sequencer timer from the beginning of a sequence. Unlike VoxWare, we do not use timeout(9) to wait for the filled queue in a midi driver to have a space. Any operation to queue data into the buffer of a midi driver must not block or wait for a notify by a midi driver via seq_intr(). This assures seq_playevent() to be non-blocking, so that seq_timer() invoked by timeout(9) can call seq_playevent() safely.

1.5.2.   A Midi Synthesizer Interface

A midi synthesizer interface consists of several methods to translate the sequencer events not processed by a sequencer into the corresponding set of operations against the midi driver. The interface functions for a generic midi interface are found in sys/i386/isa/snd/midisynth.c, while the ones for a specific midi interface or a synthesizer chip live in thesame file as the midi driver.

A midi synthesizer interface is defined as a structure synthdev_info in sys/i386/include/soundcard.h, shown in fig 2.


Figure 2: The definition of a midi synthesizer interface.

/* This is a midi synthesizer interface and state. */
struct _synthdev_info {
    /*
     * Here are the methods to translate synthesizer events
     * into midi driver operations.
     */
    mdsy_killnote_t *killnote;
    mdsy_setinstr_t *setinstr;
    mdsy_startnote_t *startnote;
    mdsy_reset_t *reset;
    mdsy_hwcontrol_t *hwcontrol;
    mdsy_loadpatch_t *loadpatch;
    mdsy_panning_t *panning;
    mdsy_aftertouch_t *aftertouch;
    mdsy_controller_t *controller;
    mdsy_patchmgr_t *patchmgr;
    mdsy_bender_t *bender;
    mdsy_allocvoice_t *allocvoice;
    mdsy_setupvoice_t *setupvoice;
    mdsy_sendsysex_t *sendsysex;
    mdsy_prefixcmd_t *prefixcmd;
    mdsy_volumemethod_t *volumemethod;

    /* Below here are the states of an interface. */
    struct voice_alloc_info alloc; /* Voice allocation. */
    struct channel_info chn_info[16]; /* Channel information. */

    u_char prev_out_status; /* Previous status. */
    int sysex_state; /* State of sysex transmission. */
};
typedef struct _synthdev_info synthdev_info;
	 

Each method in synthdev_info corresponds to a sequencer event, shown in table 2.

Table 2: The translation table from a sequencer event to a synthesizer interface method

Sequencer Event Extended Sequencer Event Midi Channel Voice Message Midi Channel Common Message Others Method
SEQ_NOTEOFF SEQ_NOTEOFF MIDI_NOTEOFF killnote
SEQ_NOTEON SEQ_NOTEON MIDI_NOTEON startnote
SEQ_PGMCHANGE SEQ_PGMCHANGE MIDI_PGMCHANGE setinstr
SEQ_AFTERTOUCH MIDI_KEY_PRESSURE aftertouch
SEQ_BALANCE panning
SEQ_CONTROLLER MIDI_CTL_CHANGE controller
SEQ_VOLMODE volumemethod
MIDI_PITCH_BEND bender
EV_SYSEX sendsysex
SEQ_PRIVATE hwcontrol
SEQ_MIDIPUTC writeraw
SEQ_FULLSIZE loadpatch

Note that at this moment synthdev_info does not have writeraw. It is initially in snddev_info. I will move writeraw to synthdev_info soon.

The methods not described in table 2 are called indirectly on processing a sequencer event. patchmgr is responsible for synthesizer patches. allocvoice and setupvoice manages the voices of a synthesizer. These three methods are called under the sequencer mode two, which is not implemented yet (or even under VoxWare). prefixcmd determines whether a midi device accepts or rejects a given midi status.

alloc and chn_info manage voice allocation and channel states for a synthesizer chip. The definition is shown in fig 3:


Figure 3: The definition of struct voice_alloc_info and struct channel_info.

#define SYNTH_MAX_VOICES	32 /* XXX What about a synthesizer chip with more than 32 voices? */

/* This is the voice allocation state for a synthesizer. */
struct voice_alloc_info {
	int max_voice;
	int used_voices;
	int ptr;		/* For device specific use */
	u_short map[SYNTH_MAX_VOICES]; /* (ch << 8) | (note+1) */
	int timestamp;
	int alloc_times[SYNTH_MAX_VOICES];
};

/* This is the channel information for a synthesizer. */
struct channel_info {
	int pgm_num;
	int bender_value;
	u_char controllers[128];
};
	 

A sequencer interface contains 16 struct channel_infos, each one of them corresponding to a single midi channel. pgm_num, bender_value, and controllers hold the instrument number, pitch bend and controller respectively, used to write the parameters down to a synthesizer chip. The state information of each voices in a synthesizer chip live in struct voice_alloc_info. max_voice has the maximum number of the audible voices. map records the assignment of a voice against a midi channel, as well as the note played on the voice.

Note that the methods in a midi synthesizer interface must not block, instead return EAGAIN to tell a sequencer to wait for a midi driver to come ready to output. A synthesizer chip generally accepts an operation at any time, so any methods in a midi synthesizer interface for a synthesizer chip should not return EAGAIN.

1.5.3.   A Midi Driver

A midi driver is the lowest-level driver in the framework, found in a device-specific file. (serial port driver in sys/i386/isa/snd/uartsio.c, OPL3 driver in sys/i386/isa/snd/opl.c, and so on) A midi driver for a midi interface (a serial port, SB MIDI, MPU401, etc) should provide an interface against a user process via /dev/midi. While one for a synthesizer chip (OPL3, EMU8000, etc) are not necessary to handle I/O against /dev/midi, it might be a good idea to offer a midi interface emulator to translate the midi messages written to /dev/midi into the methods in a midi synthesizer interface or a direct operation to a synthesizer chip, so that we can have a low-level access against a chip. We need further discussion on this issue.

For a midi interface device, the driver holds its own queues to read or write. It implements open(2), close(2), read(2), write(2), ioctl(2), and poll(2). The entries to these functions are held in a structure snddev_info, copied to midi_info during the attach. open(2) and close(2) can be called from a sequencer as well, to pass an event to play.

For a synthesizer chip driver, the mandatory functions are open(2), close(2), and ioctl(2). The other functions can simply ignore a request. Also, to improve the readability a synthesizer chip driver should provide a set of functions to write a command to or read a status from a synthesizer chip, like opl_command and opl_status. (which does really not exist, just for an example)

Another task of a midi driver is handling an interrupt from a midi interface device. We cannot always output a midi message through a midi interface due to time-consuming processes in it, hence a midi driver and a sequencer queue their events or data in their own output queue, to output or process when it comes ready. An interrupt handler in a midi interface driver restarts to output midi messages, followed by pumping it up to a sequencer to resume processing the events in a queue.

1.5.4.   Miscellaneous Elements

We also have some helper elements to support our framework.

1.6.   Writing a New Driver

1.6.1.  Probe and Attach
1.6.2.  I/O Operation
1.6.3.  Midi Synthesizer Interface

So, you are writing a driver for a new device? Great!

We now describe the guideline to write a driver for a new device. You should first read section 1.4 before beginning your work.

We have some general rules to provide good readability to a driver.

1.6.1.   Probe and Attach

The first functions to implement are probe() and attach(), called from midiprobe() and midiattach(). Fig 4 shows the typical sequences to probe and attach a device named newdev.


Figure 4: The typical sequence to prove and attach a device.

static int
newdev_probe(struct isa_device *dev)
{
    int unit, iobase;
    sc_p scp;

    /* Adjust the unit number to nmidi + nsynth. */
    unit = dev->id_unit = nmidi + nsynth;

    DDB(printf("newdev%d: probing at port %x.\n", unit, dev->id_iobase));

    /* Is the unit correct? */
    if (unit > NMIDI) {
	printf("newdev%d: bad unit number.\n", unit);
	return (0);
    }
    /* Is it already attached? */
    scp = sca[unit];
    if (scp != (sc_p)NULL) {
	printf("newdev%d: already attached.\n", unit);
	return (0);
    }

    iobase = dev->id_iobase;

    /* Now probe the device. If *dev has some missing fields, fill them with the fair defaults. */

    bzero(&midi_info[dev->id_unit], sizeof(midi_info[dev->id_unit]));

    DDB(printf("newdev%d: probed at port %x.\n", unit, dev->id_iobase));

    return (1);
}

static int
newdev_attach(struct isa_device *dev)
{
    int unit, iobase;
    sc_p scp;

    unit = dev->id_unit;

    DDB(printf("newdev%d: attaching at port %x, irq %d.\n", unit, dev->id_iobase, dev->id_irq));

    /* Allocate the softc. */
    scp = malloc(sizeof(*scp), M_DEVBUF, M_NOWAIT);
    if (scp == (sc_p)NULL) {
	printf("newdev%d: softc allocation failed.\n", unit);
	return (0);
    }
    bzero(scp, sizeof(*scp));
    sca[unit] = scp;

    /* Fill the softc for this unit. */
    sca[unit]->dev = dev;

    /* Fill the midi_info. */
    midi_info[unit].flags &= ~SND_F_BUSY;
    bcopy(&midisynth_op_desc, &midi_info[unit].synth, sizeof(midisynth_op_desc));

    /* Initialize the device. You may want to do this before filling the softc or the midi_info. */

    /* Increase the midi device number. */
    nmidi++; /* Or nsynth++; if we are driving a synthesizer chip. */

    DDB(printf("newdev%d: attached at port %x, irq %d.\n", unit, dev->id_iobase, dev->id_irq));

    return (1);
}
	 

On probing, first adjust the unit number to avoid crashing into the attached device. nmidi and nsynth are the numbers of the attached midi and synthesizer drivers respectively. Next make sure that we have a valid unit number. Then we can probe the device. Some fields in dev might be empty(-1), in which case the driver is responsible to fill them with the appropriate default value. probe should return zero if a device is not probed. Otherwise, clear midi_info[dev->id_unit] and return nonzero.

1.6.2.   I/O Operation

1.6.3.   Midi Synthesizer Interface

1.7.   Conclusion