musikinformatik / SuperDirt

Tidal Audio Engine
GNU General Public License v2.0
524 stars 76 forks source link

How to add a custom DirtEventType for MIDI over OSC #276

Closed jarmitage closed 1 year ago

jarmitage commented 1 year ago

I have an OSC protocol here for an instrument that sends MIDI NoteOn and NoteOff messages over OSC, plus some custom sound parameters also being sent over OSC:

I typical event might look like:

/mrp/midi Channel16NoteOn NoteNumber NoteVelocity
/mrp/quality/intensity Channel16 NoteNumber IntensityValue
/mrp/quality/brightness Channel16 NoteNumber BrightnessValue
/mrp/midi Channel16NoteOff NoteNumber

I assume to get this working with SuperDirt, I would need to add a custom DirtEventType similar to midiEvent? I tried to read the code in DirtEvent for how SuperDirt MIDI works, but I found the code really hard to read.

Any pointers would be appreciated.

yaxu commented 1 year ago

This seems like a midiesque OSC protocol rather than MIDI over OSC. Why not just send the OSC from Tidal?

jarmitage commented 1 year ago

Yeah that's right, but what I am struggling with is how would I get the NoteOff messages scheduled automatically like SuperDirt MIDI does?

https://github.com/musikinformatik/SuperDirt/blob/develop/classes/DirtEventTypes.sc#L126-L128

yaxu commented 1 year ago

How about something like this:

import Data.Maybe (fromJust)

eventHasOffset :: Event a -> Bool
eventHasOffset e | isAnalog e = False
                 | otherwise = stop (fromJust $ whole e) == stop (part e)

offsets :: Pattern a -> Pattern a
offsets pat = withEvent f $ filterEvents eventHasOffset pat
  where f (e@(Event _ Nothing _ _)) = e -- ignore analog events
        f (Event c (Just (Arc b e)) _ v) = Event c (Just a) a v
           where a = Arc e (e + (e - b))

cmd = pS "cmd"

noteonoff pat = stack [pat # cmd "Channel16NoteOn", offsets pat # cmd "Channel16NoteOff"]

noteonoff (note "a b" # velocity 50)

giving:

(0>½)|cmd: "Channel16NoteOn", note: 9.0n (a5), velocity: 50.0f
 (½>1)|cmd: "Channel16NoteOn", note: 11.0n (b5), velocity: 50.0f
 (½>1)|cmd: "Channel16NoteOff", note: 9.0n (a5), velocity: 50.0f
(1>1½)|cmd: "Channel16NoteOff", note: 11.0n (b5), velocity: 50.0f

Getting the OSC shape right from this would hopefully be straightforward, as long as the MRP accepts (and ignores) the velocity parameter on a note off message.

telephon commented 1 year ago

Btw. there is already a midi event type in SuperDirt's DirtEventTypes. For tests, you could even tweak it while running.

(
DirtEventTypes.midiEvent[\play] = {
<.... the long function ....>
}
)
yaxu commented 1 year ago

The tidal solution for sending noteoffs does have some drawbacks, e.g. if you silence the pattern then the noteoff won't get sent. I think it's better to modify the receiving end to accept a duration parameter to the noteon. Tidal will fill this in to a parameter called 'delta'.

jarmitage commented 1 year ago

Thank you for the suggestions.

A Haskell-only OSCTarget version sounds great...

@yaxu: if you silence the pattern then the noteoff won't get sent

There is an /mrp/allnotesoff command in the spec, so I could get around this with e.g. hushmrp = once $ s "mrp" $ mrp_msg "allnotesoff and equivalents for silence. I'm going to try this out with your example code...

@yaxu: I think it's better to modify the receiving end to accept a duration parameter to the noteon.

Unfortunately the receiving end is a 10+ year old C++ codebase (albeit a marvel in its own right!) that I can't build myself in XCode as of yet. That's why I was wondering about using our SuperCollider MRP client class and creating a custom DirtEventType to have more control over the messaging/scheduling.

@telephon: there is already a midi event type in SuperDirt's DirtEventTypes

Yes this is what I was originally referring to. As I mentioned, I found this quite difficult to comprehend, as I'm a novice SC user, but also there's few code comments and no .schelp for this class. I assume I would need the overall structure/logic of the midiEvent type to be able to schedule the NoteOff messages (ref https://github.com/musikinformatik/SuperDirt/blob/develop/classes/DirtEventTypes.sc#L107-L131), but to send out OSC messages instead of MIDI messages?

A thought: since our MRP class has its own MIDIdefs as inputs, could this somehow be added as a "MIDI device" in the regular SuperDirt MIDI fashion?

jarmitage commented 1 year ago

I had a quick go at the Haskell-only version. There's two head-scratchers:

The first is that the keyword-value OSC message style of Tidal means I can't send a message that complies with the keyword-less spec of the MRP (i.e. /mrp/midi Channel16NoteOn NoteNumber NoteVelocity is sent as /mrp/midi 145 48 127). If there's no simple way around this in Haskell, then I would need to parse out the keywords, probably in SC which wouldn't necessarily be a bad thing... however...

The second is that the MRP app cannot currently schedule incoming messages such as...

(0>½)|cmd: "Channel16NoteOn", note: 9.0n (a5), velocity: 50.0f
 (½>1)|cmd: "Channel16NoteOn", note: 11.0n (b5), velocity: 50.0f
 (½>1)|cmd: "Channel16NoteOff", note: 9.0n (a5), velocity: 50.0f
(1>1½)|cmd: "Channel16NoteOff", note: 11.0n (b5), velocity: 50.0f

Hopefully I will be able to build the MRP app someday, however in this case I would be less sure about designing a C++ Tidal event scheduler for it, over using what SuperDirt already has implemented.

This maybe leads back to the idea of a SuperDirt DirtEventType. Unless ~dirt.addMidi can somehow send MIDI out to the MRP SC class directly using its MIDIdefs? I guess it could simply use a virtual MIDI device as the go-between.

yaxu commented 1 year ago

Use 'arglist' to send a list of values rather than name-value pairs http://tidalcycles.org/docs/configuration/MIDIOSC/osc/#defining-osc-message-structure However it seems the only difference between a note on and note off is the lack of a velocity parameter in the latter. So you'll need to do some hackery to get around that. I'd forget about MIDI and superdirt. It'd be easier to make an OSC server for converting between tidal deltas and the MRP protocol.

yaxu commented 1 year ago

I'll close this for now as I don't think it makes sense to add OSC functionality to the MIDI code.

jarmitage commented 1 year ago

Yes that seems right.

I'm finding a way through with the Haskell OSC side now, thanks.

For posterity, here's the thing working: https://github.com/Intelligent-Instruments-Lab/iil-python-tools/tree/ja-dev/examples/mrp/tidalcycles