Introduction

Generative music is always fun and engaging, so I decided to build a simple MIDI sequencer to mess around. In this project, the sequencer sends a random note within a given octave every quarter bar (4/4) Because it’s so simple, the code provided will be very easy to extend as you please. See Bonus

Setup project

$ mkdir gen_seq
$ cd gen_seq
$ go mod init Sequencer
$ touch main.go

Install dependencies

We’ll be using gomidi with the rtmidi backend

$ go get gitlab.com/gomidi/midi/v2@latest

Create MIDI ports

We need to create:

  1. MIDI-in to sync our sequencer with the master clock
  2. MIDI-out for the sequencer to send notes to.
// main.go
package main

import (
    "fmt"

	"gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
)

func main() {
	driver, err := rtmididrv.New()
	if err != nil {
		panic(err)
	}
	defer driver.Close()

	inPort, err := driver.OpenVirtualIn("sequencer_in")
	outPort, err := driver.OpenVirtualOut("sequencer_out")
	if err != nil {
		panic(err)
    }
    // Need to use the iables otherwise the code won't compile
    fmt.Println(inPort, outPort)
}

Note:
if you’re using a synth instead of a VST, you can set the outPort to the synth directly. I use the Korg Monologue.

$ amidi -l
Dir Device    Name
IO  hw:0,0    Virtual Raw MIDI (16 subdevices)
IO  hw:0,1    Virtual Raw MIDI (16 subdevices)
IO  hw:0,2    Virtual Raw MIDI (16 subdevices)
IO  hw:0,3    Virtual Raw MIDI (16 subdevices)
IO  hw:4,0,0  monologue MIDI 1
IO  hw:4,0,1  monologue MIDI 2

package main

import (
    "fmt"

	"gitlab.com/gomidi/midi/v2"
	
	"gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
	_"gitlab.com/gomidi/midi/v2/drivers/rtmididrv" // autoregisters driver
)

func main() {
	driver, err := rtmididrv.New()
	if err != nil {
		panic(err)
	}
	defer driver.Close()

	inPort, err := driver.OpenVirtualIn("sequencer_in")
	outPort, err := midi.FindOutPort("monologue MIDI 1")
	if err != nil {
		panic(err)
    }
    // Need to use the iables otherwise the code won't compile
    fmt.Println(inPort, outPort)
}

You can follow the rest of the tutorial as-is with these changes. Just make sure the new imports remain.

Let’s see if it works

$ go build
$ ./Sequencer
sequencer_in sequencer_out
# Nice!

Get clock data

MIDI clock is essential for a sequencer. We need to stay in sync with other instruments and sequence notes at the right time. According to the MIDI clock specification, a quarter of a bar has 24 pulses.

package main

import (
	"fmt"

	"gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
)

func main() {
	driver, err := rtmididrv.New()
	if err != nil {
		panic(err)
	}
	defer driver.Close()
	inPort, err := driver.OpenVirtualIn("sequencer_in")
	outPort, err := driver.OpenVirtualOut("sequencer_out")
	if err != nil {
		panic(err)
	}
	fmt.Println(inPort, outPort)
	pulses := 0
	pulsesPerQuarter := 24
	pulsesPerBar := pulsesPerQuarter * 4 // assuming it's 4/4
	bars := 1
}

Now let’s read clock data from sequencer_in and count pulses and bars

package main

import (
	"fmt"

	"gitlab.com/gomidi/midi/v2"
	"gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
	_ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv" // autoregisters driver

)

func main() {
	driver, err := rtmididrv.New()
	if err != nil {
		panic(err)
	}
	defer driver.Close()
	inPort, err := driver.OpenVirtualIn("sequencer_in")
	outPort, err := driver.OpenVirtualOut("sequencer_out")
	if err != nil {
		panic(err)
	}
	fmt.Println(inPort, outPort)
	pulses := 0
	pulsesPerQuarter := 24
	pulsesPerBar := pulsesPerQuarter * 4 // assuming it's 4/4
	bars := 1
	_, err = midi.ListenTo(inPort, func(msg midi.Message, timestampms int32) {
		switch msg.Type() {
		case midi.TimingClockMsg:
			// a "timing message" is a "pulse"
			pulses++
			if pulses == pulsesPerBar {
				// reset pulses every bar
				bars++
				pulses = 0
				fmt.Printf("Bar %d\n", bars)
			}
		case midi.StopMsg:
			// if we get a stop message, reset our counters
			pulses = 0
			bars = 1
		}
	}, midi.UseTimeCode()) // we declare that we only want timecode messages
	if err != nil {
		panic(err)
	}
	// infinite loop to listen to messages forever
	for true {}
}

Lets try it out!

$ go build
$ ./Sequencer

Open your DAW, in my case it’s Ardour. You should see the sequencer_in ports in your MIDI settings.
MIDI Port
Set MIDI clock out to sequencer_in
Set MIDI clock out
Press play in your DAW and if all goes well, you should see the bars printing in your terminal.
Bar messages

Nice!

Play random notes

Now, let’s add a getRandomNote function

package main

import (
	"fmt"
	"math/rand"
	"time"

	"gitlab.com/gomidi/midi/v2"
	"gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
	_ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv" // autoregisters driver
)

func getRandomInt(a int, b int) uint8 {
	rand.Seed(time.Now().UnixNano())
	return uint8(a + rand.Intn(b-a+1))
}

func getRandomNote(octave uint8) uint8 {
	notes := [12]uint8{
		midi.C(octave),
		midi.Db(octave),
		midi.D(octave),
		midi.Eb(octave),
		midi.E(octave),
		midi.F(octave),
		midi.Gb(octave),
		midi.G(octave),
		midi.Ab(octave),
		midi.A(octave),
		midi.Bb(octave),
		midi.B(octave),
	}
	return notes[getRandomInt(0, 11)]
}

func main() {
	driver, err := rtmididrv.New()
	if err != nil {
		panic(err)
	}
	defer driver.Close()
	inPort, err := driver.OpenVirtualIn("sequencer_in")
	outPort, err := driver.OpenVirtualOut("sequencer_out")
	if err != nil {
		panic(err)
	}
	fmt.Println(inPort, outPort)
	pulses := 0
	pulsesPerQuarter := 24
	pulsesPerBar := pulsesPerQuarter * 4 // assuming it's 4/4
	bars := 1
	// add from here
	_, err = midi.ListenTo(inPort, func(msg midi.Message, timestampms int32) {
		switch msg.Type() {
		case midi.TimingClockMsg:
			// a "timing message" is a "pulse"
			pulses++
			if pulses == pulsesPerBar {
				// reset pulses every bar
				bars++
				pulses = 0
				fmt.Printf("Bar %d\n", bars)
			}
		case midi.StopMsg:
			// if we get a stop message, reset our counters
			pulses = 0
			bars = 1
		}
	}, midi.UseTimeCode()) // we declare that we only want timecode messages
	if err != nil {
		panic(err)
	}
	// infinite loop to listen to messages forever
	for true {}
}

Now, let’s play a random note every quarter of a bar

package main

import (
	"fmt"
	"math/rand"
	"time"

	"gitlab.com/gomidi/midi/v2"
	"gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
	_ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv" // autoregisters driver
)


func getRandomInt(a int, b int) uint8 {
	rand.Seed(time.Now().UnixNano())
	return uint8(a + rand.Intn(b-a+1))
}

func getRandomNote(octave uint8) uint8 {
	notes := [12]uint8{
		midi.C(octave),
		midi.Db(octave),
		midi.D(octave),
		midi.Eb(octave),
		midi.E(octave),
		midi.F(octave),
		midi.Gb(octave),
		midi.G(octave),
		midi.Ab(octave),
		midi.A(octave),
		midi.Bb(octave),
		midi.B(octave),
	}
	return notes[getRandomInt(0, 11)]
}

func isPulseQuarter(pulse int) bool {
	// Remember that every quarter bar is 24 pulses
	if pulse % 24 == 0 {
		return true
	}
	return false
}

func main() {
	driver, err := rtmididrv.New()
	if err != nil {
		panic(err)
	}
	defer driver.Close()
	inPort, err := driver.OpenVirtualIn("sequencer_in")
	outPort, err := driver.OpenVirtualOut("sequencer_out")
	if err != nil {
		panic(err)
	}
	fmt.Println(inPort, outPort)
	pulses := 0
	pulsesPerQuarter := 24
	pulsesPerBar := pulsesPerQuarter * 4 // assuming it's 4/4
	bars := 1
	var midiChannel uint8 = 1
	previousNote := getRandomNote(3)
	_, err = midi.ListenTo(inPort, func(msg midi.Message, timestampms int32) {
		switch msg.Type() {
		case midi.TimingClockMsg:
			// a "timing message" is a "pulse"
			pulses++
			if pulses == pulsesPerBar {
				// reset pulses every bar
				bars++
				pulses = 0
				fmt.Printf("Bar %d\n", bars)
			}
			if isPulseQuarter(pulses) {
				note := getRandomNote(3)
				fmt.Printf("playing note %d\n", note)
				// send Note Off message before sending new note
				outPort.Send(midi.NoteOff(midiChannel, previousNote))
				// channel 1, velocity 127
				outPort.Send(midi.NoteOn(midiChannel, note, 127))
				previousNote = note
			}
		case midi.StopMsg:
			// if we get a stop message, reset our counters
			pulses = 0
			bars = 1
		}
	}, midi.UseTimeCode()) // we declare that we only want timecode messages
	if err != nil {
		panic(err)
	}
	// infinite loop to listen to messages forever
	for true {}
}

Voila! Let’s compile and run this

$ go build 
$ ./Sequencer
# press play from your DAW

And you should see… Playing Note

Make sure:

  1. To adjust the midiChannel parameter in midi.NoteOn and midi.NoteOff if your synth/VST takes input from another MIDI channel.
  2. To relay the MIDI to your Synth/VST!

Reflections

The code can definitely be cleaner, but I feel that the way it is written makes it fairly easy to understand.

References

gomidi docs
rtmididrv docs

Bonus

Decide whether to play or not

When checking if pulseIsQuarter to see if we should play, we can add another conditional

if isPulseQuarter(pulses) && getRandomInt(0, 1) == 1 {
	note := getRandomNote(3)
// ....

Arpeggiator

Modify the isPulseQuarter function

func isPulseQuarter(pulse int) bool {
	// Try changing the 24 to 6 or 12
	if pulse % 24 == 0 {
		return true
	}
	return false
}

Random octave

When calling getRandomNote, we supply an octave paramter. Let’s make that random (within a range)

if isPulseQuarter(pulses) {
	note := getRandomNote(getRandomInt(3, 6))
// .....

Random velocity

Randomize note velocity

outPort.Send(midi.NoteOn(midiChannel, note, getRandInt(10, 127)))