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:
- MIDI-in to sync our sequencer with the master clock
- 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.
Set MIDI clock out to sequencer_in
Press play in your DAW and if all goes well, you should see the bars printing in your terminal.
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…
Make sure:
- To adjust the
midiChannel
parameter inmidi.NoteOn
andmidi.NoteOff
if your synth/VST takes input from another MIDI channel. - 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
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)))