RISC-V MIDI and sampled audio

What the MIDI port and the sampled-audio output are

These two devices let a program send sound as it runs:

  • The MIDI port sends messages to a synthesizer. A program chooses an instrument, starts a note and later stops it. The synthesizer produces the sound in between.
  • The sampled-audio output plays numbers computed by the program. Each number, a sample, gives the sound wave's amplitude at one moment.

This editor adds both devices to RARS, alongside the Synth and the Input controller. The MIDI registers start at 0xffff0340 and the sampled-audio registers at 0xffff0350, after the Input controller. In RARS these addresses are plain memory, initially 0.

Before using either device, check that it is present. In assembly, use lw to read MIDI Status at 0xffff0340 + 4 and test bit 7 (0x80): it is set when the port is present. Read sampled-audio Rate at 0xffff0350 + 4: a nonzero value means the output is present. Make these checks before any stores to the devices. In C, use sim_midi_present() and sim_pcm_present().

Choose the interface for the sound you need:

To playUse
A note whose length you know when you start itservices 31 and 33, one call a note
Chip sounds you shape yourself, with waveforms and envelopesthe Synth
Music a program plays as it goes, such as a song or a game's soundtrackthe MIDI port
Any sound a program can compute: effects, speech, several sounds mixed togetherthe sampled-audio output

Click Run to enable sound. Once a program makes sound, a speaker button appears beside the run buttons to mute or unmute it.

A first MIDI program

This program assumes the MIDI port is present. It plays a C major scale on a piano, one note every 300 milliseconds. Build it and press Run.

        .data
scale:  .byte   60, 62, 64, 65, 67, 69, 71, 72  # C major, as MIDI keys
        .text
main:   li      s0, 0xffff0340  # the MIDI port
        li      a0, 0xC0        # program change, channel 1...
        jal     send
        li      a0, 0           # ...to instrument 0, a piano
        jal     send
        la      s1, scale
        li      s2, 8           # eight notes
note:   li      a0, 0x90        # note on, channel 1
        jal     send
        lbu     a0, 0(s1)       # the key
        jal     send
        li      a0, 100         # the velocity
        jal     send
        li      a0, 300         # let it sound for 300 ms
        li      a7, 32
        ecall
        li      a0, 0x80        # note off, channel 1
        jal     send
        lbu     a0, 0(s1)
        jal     send
        li      a0, 0
        jal     send
        addi    s1, s1, 1
        addi    s2, s2, -1
        bnez    s2, note
        li      a7, 10
        ecall

# send: writes the byte in a0 to the MIDI port once it can take one
send:   lw      t0, 4(s0)       # Status
        andi    t0, t0, 0x40    # bit 6: busy
        bnez    t0, send
        sb      a0, 0(s0)       # Data
        ret

Each note takes two messages, three bytes each:

  1. Note on starts a note: the byte 0x90, then the key, which note to play (60 is middle C, and each step up is a semitone), then the velocity, how hard the key is struck, from 1 to 127.
  2. Note off ends it: 0x80, the same key, and 0.

Before the notes, a program change, 0xC0 and an instrument number, picks the instrument. 0 is a piano. Try 40 for a violin or 73 for a flute.

Before sending each byte, the send routine reads Status and waits while bit 6 is set. That bit means the port is busy. A byte sent while it is busy is dropped.

The MIDI port's registers

Each register is a word at an offset from 0xffff0340.

OffsetRegisterIn CAccessWhat it holds
0x00DataSIM_MIDI_DATAwriteSends the low byte written with sb or sw. Reads 0. Bytes written while the port is busy are dropped
0x04StatusSIM_MIDI_STATUSreadBit 6 is set while the buffer is full. Bit 7 is always set because the port has no MIDI input. Reads 0x80 when ready to accept a byte
0x04CommandSIM_MIDI_COMMANDwriteShares its address with Status. Writing 0xFF empties the buffer and ends all notes. Writing 0x3F has no effect. Other values are ignored

Status and Command share one address: loads read Status, stores send a Command. This follows the MPU-401 MIDI interface used by PC sound cards. The words at 0x08 and 0x0C are reserved and read 0.

MIDI messages

A MIDI message starts with a status byte, whose bit 7 is set, followed by one or two data bytes from 0 to 127. Most status bytes carry a channel in their low four bits: 0x90 starts a note on channel 1, 0x91 on channel 2, up to 0x9F on channel 16. Each channel has its own instrument, so a program can play a melody on one and chords on another.

MessageBytes, for channel n + 1What it does
Note on0x9n, key, velocityStarts a note: key 0 to 127, 60 being middle C; velocity 1 to 127, how hard it is struck. Velocity 0 ends the note instead
Note off0x8n, key, velocityEnds a note; the velocity is usually 0
Program change0xCn, programPicks the channel’s instrument, 0 to 127
Control change0xBn, controller, valueSets one of the channel’s controllers: 7 is volume, 10 left-right position (64 the middle), 64 the sustain pedal (127 down, 0 up), 123 ends every note on the channel
Pitch bend0xEn, low 7 bits, high 7 bitsBends every note on the channel: 8192, bytes 0 and 64, is no bend; 0 and 16383 are two semitones down and up
Channel pressure0xDn, pressureHow hard held keys are pressed, which some instruments respond to
System exclusive0xF0, data…, 0xF7A message for the synthesizer itself, such as GM System On, which resets every channel
  • Channel 10 is drums. Its status bytes end in 9, such as 0x99. Each key selects a drum: 36 bass drum, 38 snare, 42 closed hi-hat, 46 open hi-hat and 49 crash cymbal.
  • Instruments follow the 128-entry General MIDI list. Lists usually number them from 1. Program change bytes start at 0. Subtract 1 from a listed number: 0 piano, 19 church organ, 24 nylon guitar, 32 acoustic bass, 40 violin, 56 trumpet, 73 flute, 80 square-wave lead.
  • Running status: after a message, more messages of the same kind on the same channel may leave out the status byte. 0x90 60 100 64 100 67 100 starts a C major chord with one status byte. A note on with velocity 0 ends a note, which keeps running status going through a melody.
  • System exclusive messages, or SysEx, start with 0xF0 and end with 0xF7, with any number of data bytes between, up to 256 bytes in all. 0xF0 0x7E 0x7F 0x09 0x01 0xF7, GM System On, puts every channel back to General MIDI's defaults.
  • Bytes from 0xF8 to 0xFF, MIDI's timing and transport messages, are allowed anywhere, even in the middle of a message, and the synthesizer ignores them.

Waiting for the port

A MIDI byte takes 0.32 milliseconds to leave the port: the cable rate is 31,250 bits a second, with ten bits per byte. The port queues up to 4,096 bytes and sends them in order at that speed. A three-byte note-on message takes about a millisecond. A note starts after its last byte leaves, so consecutive note-on messages start about a millisecond apart.

When the buffer is full, Status bit 6 is set: the port is busy. A byte written then is dropped and reported in the Log. Otherwise Status reads 0x80. Bit 7 is always set because the port has no MIDI input. Check that bit 6 is clear before every byte, as send does, even if a short melody rarely fills the buffer.

Writing 0xFF to the Command register resets the port: the bytes still waiting are thrown away and every note that is sounding ends. 0x3F, which switches a real MPU-401 into the simple mode this port is always in, is accepted and does nothing.

The MIDI port and services 31 and 33

The MIDI port has its own synthesizer and 16 channels. Notes played by services 31 and 33 use separate channels, matching RARS, where each service note gets its own synthesizer. A program change through the port does not change a service note's instrument, and a service note does not change the port's channels. Both use the instrument sounds that RARS uses on Java 21 and share the same volume control.

A first sampled-audio program

This program assumes the sampled-audio output is present. Check Rate before running it on another simulator: a missing device leaves Free at 0, so the program would wait forever. Here it plays a buzzing tone for two seconds. Build it and press Run.

        .text
main:   li      s0, 0xffff0350  # the sampled-audio output
        li      t0, 8000
        sw      t0, 4(s0)       # Rate: 8,000 samples a second
        li      t0, 1
        sw      t0, 0(s0)       # Control: on, in mono
        li      s1, 0x20002000  # a word of two samples, both 8,192
        li      s2, 0xC000C000  # what turns 8,192 into -8,192 and back
        li      s3, 8000        # 8,000 words, 16,000 samples: two seconds
        li      s4, 5           # words before the wave turns
loop:   lw      t0, 8(s0)       # Free: how many samples fit now
        bnez    t0, write
        li      a0, 10          # none: sleep 10 ms and look again
        li      a7, 32
        ecall
        j       loop
write:  sw      s1, 12(s0)      # Data: two samples
        addi    s4, s4, -1
        bnez    s4, next
        xor     s1, s1, s2      # the wave turns: high becomes low
        li      s4, 5
next:   addi    s3, s3, -1
        bnez    s3, loop
        li      a7, 10
        ecall

The program uses mono output, where one sample is one frame:

  1. It sets Rate to 8,000 frames per second.
  2. It turns the output on with bit 0 of Control.
  3. It reads Free, the number of frames the output can accept. When it is 0, it sleeps for 10 milliseconds and checks again.
  4. It writes two samples at a time to Data, as one word: here both are 8,192 for the high half of the wave, or −8,192 for the low half.

The wave goes up and down every 10 samples: 5 words high, then 5 words low, 20 samples for a whole cycle. At 8,000 samples a second that is 8,000 ÷ 20 = 400 cycles a second, a tone of 400 Hz. A wave that jumps between two levels like this is a square wave, which sounds buzzy, like an old video game.

The program exits while about a second of audio is still queued. That audio plays after the program ends.

The sampled-audio output's registers

Each register is a word at an offset from 0xffff0350. Write them with sw.

OffsetRegisterIn CAccessWhat it holds
0x00ControlSIM_PCM_CONTROLread and writeBit 0 enables output. Clearing it empties the audio buffer. Bit 1 selects stereo. Reads back these two bits. Resets to 0 at Build
0x04RateSIM_PCM_RATEread and writeFrames per second, from 4,000 to 48,000. Values outside this range are clamped. Reads back the rate in use. Resets to 8,000 at Build
0x08FreeSIM_PCM_FREEreadNumber of frames the audio buffer can accept, 0 to 8,192. Always even in mono, where each word holds two frames
0x0CDataSIM_PCM_DATAwriteWrite a whole word with sw. In mono, it holds two signed 16-bit samples, the low half first. In stereo, it holds one frame, left in the low half and right in the high half. Reads 0. Writes are dropped if the buffer has no room or output is off

At every Build the output is off, in mono, at 8,000 frames per second.

Samples, frames and words

A sample is a signed 16-bit number from −32,768 to 32,767. Zero is silence. Larger magnitudes give larger wave amplitude. A wave from −8,192 to 8,192 has about a quarter of full-scale amplitude.

A frame is what plays at one moment: one sample in mono, or simultaneous left and right samples in stereo. Control bit 1 selects stereo. Rate always counts frames per second, so stereo plays that many samples per second in each channel.

Each word written to Data holds two samples, 16 bits each:

BitsIn monoIn stereo
15–0the first samplethe left sample
31–16the second sample, played right after the firstthe right sample, played with the left

In memory a word's low half comes first, so a buffer of 16-bit samples, in the order a WAV file keeps them, can be copied to Data a word at a time. To put two samples from registers into one word:

        li      t2, 0xffff
        and     t0, a0, t2      # the first sample, in the low half
        slli    t1, a1, 16      # the second, in the high half
        or      t0, t0, t1
        sw      t0, 12(s0)      # Data

Rate accepts 4,000 to 48,000 frames per second. A value outside that range is clamped to the nearest limit. Reading Rate returns the value in use. A higher rate allows higher pitches, up to half the rate. Common rates include 8,000 for telephone audio, 22,050 for lower-bandwidth audio and 44,100 for CD audio.

The FIFO and Free

Written samples wait in a FIFO, a queue that plays the oldest first: "first in, first out". It holds 8,192 frames, about a second at 8,000 frames per second or a sixth of a second at 48,000. The output plays queued frames at Rate and plays silence when the FIFO is empty.

Free reports how many frames the FIFO can accept. A word holds two frames in mono and one frame in stereo. Write no more frames than Free reports. A word that does not fit is dropped and reported in the Log. Free is always even in mono.

  • Keeping it fed. Write a batch when Free is large, then do other work or sleep. Queue enough frames to last until the program writes again.
  • Delay. New samples play after the frames already queued. A nearly full FIFO at 8,000 frames per second adds about a second of delay. Keep less queued when a sound must respond quickly to an action.
  • Settings. Clearing Control bit 0 turns the output off and empties the FIFO. Changing Rate or the stereo setting also discards queued frames. Set both before writing samples.

From C

<sim.h> names both devices' registers and bits, and has a few helpers:

  • MIDI port registers: SIM_MIDI_DATA, SIM_MIDI_STATUS and SIM_MIDI_COMMAND, with Status's bits SIM_MIDI_BUSY and SIM_MIDI_NO_INPUT, and the commands SIM_MIDI_RESET and SIM_MIDI_UART_MODE.
  • MIDI port helpers: sim_midi_write(byte) waits until the port is not busy and sends one byte, and sim_midi_present() says whether the port is there.
  • Sampled-audio output registers: SIM_PCM_CONTROL, SIM_PCM_RATE, SIM_PCM_FREE and SIM_PCM_DATA, with Control's bits SIM_PCM_ON and SIM_PCM_STEREO, and SIM_PCM_RATE_MIN, SIM_PCM_RATE_MAX and SIM_PCM_FIFO_FRAMES.
  • Sampled-audio output helpers: sim_pcm_start(rate, stereo) sets the rate and turns the output on, sim_pcm_free() reads Free, sim_pcm_write(first, second) waits until the FIFO has room and writes two samples, and sim_pcm_present() says whether the output is there.

In mono, sim_pcm_write(first, second) plays first and then second. In stereo, it plays first in the left channel and second in the right channel at the same moment. The rate argument to sim_pcm_start is frames per second in either mode.

See Using C and C++ for compiling. This program plays the scale of the first MIDI program on a violin, then two seconds of the 400 Hz tone:

#include <sim.h>

static const unsigned char scale[] = {60, 62, 64, 65, 67, 69, 71, 72};

int main(void) {
    if (!sim_midi_present() || !sim_pcm_present()) {
        sim_print_string("This simulator has no MIDI port or sampled-audio output.\n");
        return 1;
    }
    sim_midi_write(0xC0); /* program change, channel 1 */
    sim_midi_write(40);   /* a violin */
    for (int i = 0; i < 8; i++) {
        sim_midi_write(0x90); /* note on */
        sim_midi_write(scale[i]);
        sim_midi_write(100);
        sim_sleep(300);
        sim_midi_write(0x80); /* note off */
        sim_midi_write(scale[i]);
        sim_midi_write(0);
    }

    sim_pcm_start(8000, 0); /* 8,000 samples a second, in mono */
    for (int i = 0; i < 16000; i += 2) {
        int level = (i / 10) % 2 ? -8192 : 8192; /* a square wave at 400 Hz */
        sim_pcm_write(level, level);
    }
    return 0;
}

sim_midi_write and sim_pcm_write poll Status or Free until there is room. To do other work between batches, check sim_pcm_free() and write no more than that many frames. Each call to sim_pcm_write uses two frames in mono or one in stereo.

Debugging and Testcases

  • Pause and Breakpoints leave playback running. MIDI notes continue. Queued audio plays, then the empty FIFO produces silence.
  • Step runs one instruction, so stepping over a sb or sw sends its byte or queues its samples, as in a Run.
  • Undo takes back what has not been heard yet. Stepping back over a write takes its byte out of the port's buffer, or its samples out of the FIFO, if they are still waiting. A note it started stops, and an instrument, controller or pitch bend it changed goes back to what it was. What has already been heard stays heard, and a note it ended does not start again.
  • At the end of the program, what is still waiting plays out, then every note still sounding is released.
  • Stop silences both at once. Build puts the synthesizer back to General MIDI's defaults and the sampled-audio output off.
  • In a Testcase both devices are silent but keep their normal timing on the virtual clock: 0.32 milliseconds per MIDI byte, and Rate frames per second for audio. Program waits advance that clock immediately. Polling for space also counts as a wait: reading busy MIDI Status advances to the next departing byte, and reading Free when no word fits advances until one fits. Repeated Free reads with the same result and no intervening Data write also advance playback, one word at a time. These polling loops can therefore make progress without reaching the instruction limit.
  • Under the bitmap display. A display whose base address is 0xffff0000, RARS's "memory map" base, can cover these addresses. Its words are then pixels, and the two devices are off. Move the display, or make it smaller, to use them.

On RISC-V 64

These programs also run on RV64. The addresses are unchanged: li s0, 0xffff0340 reaches the MIDI port on either target. The readable registers have bit 31 clear, so lw gives the same values on RV32 and RV64 despite sign extension.

Keep word writes as sw: sd writes eight bytes and can affect two registers. The MIDI port also accepts sb, as in the first example. In C, unsigned is 32 bits on both targets, so the SIM_MIDI_ and SIM_PCM_ macros give the same results.