Seigo
Tanimura
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.
The current VoxWare audio driver for FreeBSD has the following problems:
Complexity of kernel configuration
In VoxWare, a sound card itself has the related device driver. This results in increasing the number of the device drivers (sbmidi, sscape, mpu, etc). A user has to determine the name of a sound card, and select the matching driver.
Complexity and limitation of the device drivers
Since VoxWare is initially developed under Linux, the framework for the device drivers does not match the BSD one. Also, the drivers hold some of the device information into static variables, to prevent a user from configuring multiple devices using the same driver.
To overcome the problems shown above, a new audio driver for FreeBSD should have the following design goals:
Automated device determination
The device driver framework probes the type of device to attach the apropriate one. A user simply configures an abstruct device(pcm or midi).
BSD-compliant device driver model
A device driver holds the state information of the device in per-device structures (struct softc or equivalent). This allows driving multiple devices.
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.
The driver supports two devices at this moment:
Serial port at 38.4kbps
Recent midi modules like SC-55mkII, SC-88, MU-100 etc have a {computer, PC} port to connect a module to a serial port of a PC. A {computer, PC} port looks like a serial port on a Mac. You can send/receive midi messages to such the modules via a serial port. If you have other midi instruments, connect them to the midi output/thru port. The midi module passes the midi messages from the PC to the output/thru port, and vice versa.
A serial-midi interface adoptor like Portman does not seem to work. (I have received no reports yet, including worked or not)
OPL3 synthesizer
A SB16 and later SBs have OPL3 on it, which sounds quite well. One in ViBRA16 might also work. Note that you have to enable the EMU8000 hack if you have AWE. We can get rid of it after we have EMU8000 supported.
I hope you have speakers, headphones or whatever that sound.
I am working on some more devices including:
EMU8000 wavetable synthesizer
This is the AWE engine. The VoxWare driver by Takashi Iwai is under GPL, so I am rewriting the driver under the BSD license. I have the official EMU8000 manual by E-mu and the unofficial one by Vince Vu.
MPU-401 midi interface
We have two kinds of MPU-401: the one on a sound card(including ViBRA16 and other SB compatibles) and a standalone interface card. MPU-401 on a sound card generally has only UART mode, while a standalone interface supports the native mode, providing a hardware timer, automatic timestamp and many other features. We are working on automatic probe of both types, which seems to work. Sharing an interrupt with the DSP is the issue at this moment.
If you have a SB, you need to connect a midi/joystick cable to the card and the instrument. For an onboard sound, the motherboard should have a midi/joystick connector.
The driver supports two devices at this moment:
USB-Midi interface adoptor
Roland Super-MPU
Fig 1 shows the overview of the new midi driver framework for FreeBSD.
Figure 1: The overview of the midi driver framework
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.
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:
port P
The I/O port address.
irq N
The interrupt request line number.
flags F
The 32bit flags to be passed to the driver.
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.
In this section we discuss the functions of the elements, and how they interacts to the other elements.
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:
Synchronization and timing of events
A sequencer interprets a timing event to wait for the time specified in it before processing the following event. A user process has to only include timing events to make gate times and durations, with no need to hold its own timer.
Demultiplexing to multiple Midi devices
You can include into your sequencer events the unit number of the Midi device you desire to send them to. A sequencer reads the unit number, passing the event to the appropriate Midi device.
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.
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.
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.
We also have some helper elements to support our framework.
A Midi Data Buffer
A sequencer and a midi driver use to queue input and output data a pair of midi data buffers, defined in sys/i386/isa/snd/midibuf.c. A midi data buffer is based on the DSP buffer proposed and implemented by Luigi Rizzo, with some expansion to support process synchronization. midibuf_seqwrite, midibuf_uiowrite, midibuf_seqread, and midibuf_uioread blocks in case the buffer has no data to operate, resuming when they are ready. midibuf_output_intr, and midibuf_input_intr do not block. We also have midibuf_seqcopy to only peek a buffer, without dequeueing.
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.
Follow style(9).
Style(9) is the general guide for the kernel sources in FreeBSD. In addition, a midi driver should include i386/isa/snd/sound.h.
Prefix the functions with the device name.
The functions in the driver have plenty of identical root names. To avoid confusion, a function should have the name of a device name in the prefix, eg opl_probe.
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.