Cabbage Logo
Back to Cabbage Site

New MIDI file opcodes

Updated on the 27/03/23

The ability to read MIDI files within an instrument has always been missing in Csound, until now :slight_smile: Last week I pushed two new Cabbage opcodes to the code base. You can try them out with the latest dev build of Cabbage. They are relatively untested, so be gentle! I will add them to the docs once they have been properly tested.


Opcodes described below.

cabbageMidiFileInfo Opcode

This opcode takes a path to a MIDI file and prints some useful debug information about the file, including the file type, the last time stamp, the number of tracks, a list and all time changes and a list of all time signature changes, listed by timestamps in seconds

The channels can hold strings or numbers, but only numeric channels work with the optional threshold arguments.

Added in Cabbage v2.9.5

Syntax

cabbageMidiFileInfo SMidifile

Initialization

  • SMidifile – midi filename, must be an absolute path

cabbageMidiFileReader Opcode

This opcode outputs an array of MIDI events in the form of numbers (ala midiin, alongside a trigger signal to indicate an event has been received. The number of events received in each k-cycle is also output.

Added in Cabbage v2.9.5

Syntax

kStatus[], kChan[], kNote[], kVel[], kNumEvents, kTrig cabbageMidiFileReader SMidifile, iTrackNum, kPlay, kLoop, kSpeedFactor, kReset [, iSkipTime]

Initialization

  • SMidifile – midi filename, must be an absolute path
  • iTrackNum – the track index you wish to read. (Use the cabbageMidiFileInfo opcode to determine the number of tracks if you are sure). The first track is track 0.
  • iSkipTime(optional) – sets the initial skip time in second, defaults to 0.

Performance

  • kPlay – set to 1 start playback, 0 to pause.
  • kLoop – set to 1 enable looping.
  • kSpeedFactor – used to increase/decrease playback speed. This basically acts as a timestamp multiplier. Larger values will slow down playback, while smaller values will increase playback speed.
  • kReset – a trigger signal (a 1 followed immediately by a 0) will cause playback to return to the start.

Example

<Cabbage>
form caption("Midi playback") size(400, 100), colour(40), guiMode("queue") pluginId("def1")
rslider bounds(114, 8, 60, 60) channel("transpose") range(-24, 24, 0, 1, 1), text("Transpose")
checkbox bounds(8, 8, 100, 30) channel("play"), text("Play")
checkbox bounds(8, 42, 100, 30) channel("loop"), text("Loop")
rslider bounds(178, 8, 60, 60) channel("speed") range(0.5, 2, 1, 1, 0.001), text("Speed")
button bounds(242, 8, 81, 28) channel("reset"), text("Reset")
</Cabbage>
<CsoundSynthesizer>
<CsOptions>
-n -d
</CsOptions>
<CsInstruments>
; Initialize the global variables. 
ksmps = 32
nchnls = 2
0dbfs = 1

instr 1
    SCsdPath chnget "CSD_PATH"
    //file lives in same dir as the .csd file..
    SMidifile init sprintf("%s/happyBirthday.mid", SCsdPath)
    cabbageMidiFileInfo SMidifile
    kEventIndex = 0
    kRes, kResetTrigger cabbageGetValue "reset"
    kStatus[], kChan[], kNote[], kVel[], kNumEvents, kTrig cabbageMidiFileReader SMidifile, 1, cabbageGetValue("play"), cabbageGetValue("loop"), cabbageGetValue("speed"), kResetTrigger, 0
    //printing this many events can have a serious impact on performance..
    //printk2 kNumEvents


    if kTrig == 1 then
        while kEventIndex < kNumEvents do
            if (kStatus[kEventIndex] == 128 || kVel[kEventIndex] == 0) then            //note off
                turnoff2 2, 1, 1
            elseif (kStatus[kEventIndex] == 144) then        //note on 
                event "i", 2, 0, 10, kNote[kEventIndex]+cabbageGetValue:k("transpose"), kVel[kEventIndex]/127       
            endif
            kEventIndex += 1
        od
    endif
endin

instr 2
    kEnv madsr 0.01, .2, .6, .1 
    a1 oscili kEnv, cpsmidinn(p4)
    outs a1*0.1, a1*0.1
endin

</CsInstruments>
<CsScore>
;causes Csound to run for about 7000 years...
f0 z
;starts instrument 1 and runs it for a week
i1 0 z
;i2 0 10
</CsScore>
</CsoundSynthesizer>

Nice job, Rory, it works for me.

There’s a small typo in your example: “kStatus[]” should be “kstatus[]”

kSpeedFactor seems a bit odd though. I’m not sure what’s going on, I’ll try it with a simpler midi file…

Thank Iain, example updated :+1: Yeah, I’m not sure how to call refer to the speed control. It basically just multiplies each time stamp by a given value. So the following time sequence: 1, 2, 3, 4, 5, 6, becomes 2, 4, 6, 8, 10, 12 when the speed value is 2. I’m open to suggestions about how to refer to it, maybe note spacing is a little more intuitive? I have to say, after all these years of using Csound it feels very strange to be able to load MIDI files :rofl:

Well there has always been the ability to play MIDI files using the -F command line flag, but this is limited to simple playback of one file at a time. What you are doing here is what I’ve always thought would be more useful - MIDI files as assets within the orchestra. I wrote an example before to convert MIDI files to Csound score but this is a much better solution.

I never found this option to be that useful. Having no control over the playback, or tempo, and anything like that was always problematic for me.

Are you planning any options such as looping? I could see that as being a good option to start with, that way you could load up several patterns and use them as loops. If you could control the tempo of several different midi files/patterns playing, you could do a lot of Reich-ish phase playing. Would multiple instances be possible? Would they all share the same tempo?

Also, have you thought about making it a general midi player, and allow someone to build a sequence in real time? Version 2?

The option to loop hadn’t even come into my head. Good idea. Easily added I think. And you can already give each opcode its own independent tempo.

1 Like

I just added looping controls, and updated the description above :+1: Also, when it stops, it goes back to the start, I guess a pause control might also be useful?

Thanks for this! Seems to open amazing possibilities!

This looks unnecessary in the example, becaus kNoteIndex is never used. Or am i blind?

Correct, and updated :slight_smile:

How cool is that! I can see a workflow with lilypond code generating small MIDI files to loop in Cabbage.

It immediately brings to mind the question of doing the data conversion the opposite direction. Say you generated some data to stored in a quartet of arrays kStatus[], kChan[], kNote[], kVel[] with some other method and the data was all in the appropriate numerical range. Would it be useful to have an opcode that would read those four arrays and save it all to disk as a .mid MIDI file?

To be sure, you could already do this with existing opcodes (midiout) and with the --midioutfile option

However, these new opcodes allow working with any number of different MIDI files as inputs in one performance. But --midioutfile at most could give you one MIDI output file per performance

I’m not sure how tricky it would be, but I can look into it :slight_smile:

Nice, but I have something you probably want to look into first

I had to spin up a midi file reader mini, straight up stole the trickiest part from your code above too:

midi_file.csd (2.0 KB)

And I copied your GUI and kept the channel names the same. Everything pretty much works the same as your example.

I can’t upload the midi file but just in case, it is a four-bar midi phrase with some velocity changes and a tempo of 120? It was made with lilypond.

But, anyway, it works! :partying_face:

Just not perfectly? In particular, the knob that changes the speed the file plays. That works, BUT it doesn’t change the frequency at which the loop loops.

The behavior is like this: The sequence itself gets faster or slower as expected. But, if you put the knob to the left (fast), you get a increasingly long pause between loops (because the loop finishes, and waits a long time for the next one to start). And if you put the knob to the right (slow), the loop gets cut off early by the next loop starting. If you put the knob on 1.00, the loop is timed perfectly. So, it seems that the speed variable is not correctly updating the start time (and only the start time) of the loop itself.

Also the reset button doesn’t do anything, and unchecking “Play” resets the loop.

Another issue I have: In order to obtain the new MIDI cabbage opcodes, I re-installed Cabbage from your latest dev build, and when running the installer, I had it also install Csound (I wasn’t sure but I thought maybe that was required to get the new opcodes). Problem is, now my system’s version of Csound doesn’t have the sterrain opcode! I tested it with csound -z from the terminal.

Thanks, I’ll take a look at those issue, these opcodes are not very well tested. Reset should reset the read position back to 0. It was working when I last tried it. Maybe the .csd is off. I’ll take a look.

I’ve pushed a fix for the looping issue. It should now begin its loop as soon as the initial play through ends. I also tested the reset button, it works just as advertised, i.e., it will reset the play pointer right back to the start of the MIDI file. It can be called during playback or when the opcode is not outputting any MIDI.

1 Like