musikinformatik / SuperDirt

Tidal Audio Engine
GNU General Public License v2.0
536 stars 77 forks source link

MIDI via superdirt #68

Closed yaxu closed 6 years ago

yaxu commented 6 years ago

Currently tidal-midi works separately from tidal, generally works well but needs some bugfixes as it can glitch and go out of time. Would it be easy to send MIDI via superdirt instead? That could have some advantages in terms of getting accurate sync, and taking advantage of the well developed+used MIDI support there. Perhaps it wouldn't be a lot of code to achieve the basics?

telephon commented 6 years ago

sure, that would be quite easy to do this. I don't use midi devices myself, so the hardest bit would be to know what is needed on the midi side. Maybe @lennart would like to join?

bgold-cosmos commented 6 years ago

I think the main thing to watch out for would be that there are a few Tidal->MIDI translation quirks that are currently handled in tidal-midi that SuperDirt would have to pick up. Mostly the stuff around translating duration-related Tidal params (sustain, dur, legato) into timing for Note On/Note Off messages.

It would be very nice if the parameter->MIDI CC mapping necessary to add support for this/that hardware MIDI synth could be done on the fly in SuperDirt.

yaxu commented 6 years ago

Perhaps for most cases simply being able to specify ccs by number rather than assigned name is enough, e.g. d1 $ n "c e f g" # s "m1" # cc3 sine # cc42 0.5 to send those notes to the first midi channel, with values sent for cc number 3 and 42. Then people can make aliases easily enough if they want to e.g. let wackymodulation = cc42

telephon commented 6 years ago

The standard way to do it in sclang is that you specify a midicmd as a parameter (e.g. # midicmd polyTouch), and the related arguments, e.g. # chan 2 # polyTouch 34. The midinote is automatically retrieved from freq or note, and amp (velocity) is also derived from what we already have.

Here is a list of supported parameters and their arguments (I haven't tested them myself):

polyTouch: chan, midinote, polyTouch
program: chan, progNum
bend: chan, val
sysex: uid, array
noteOn: chan, midinote, amp
allNotesOff: chan
smpte: frames, seconds, minutes, hours, frameRate
songPtr: songPtr
control: chan, ctlNum, control
touch: chan, val
noteOff: chan, midinote, amp
yaxu commented 6 years ago

Do you mean that this will already work from tidal @telephon ?

telephon commented 6 years ago

well, we need a little bit of tweaking, not too much. I haven't been able to run tidal for a while, some mess-up with haskell and atom maybe. Right now I have no time to look at it.

Can you try the following? Run this bit in sclang and see if you can make tidal to send the parameters above.

~dirt.soundLibrary.addSynth(\midi, 
     (play: { 
        "testing-----".postln; 
        currentEnvironment.postln 
     })
);

you should be able to call this event via tidal if you use sound "midi". The other parameters are formatted like I wrote above.

If this works, I can do the other end ...

yaxu commented 6 years ago

@telephon I don't seem to have ~dirt.soundLibrary:

ERROR: Message 'soundLibrary' not understood.
RECEIVER:
Instance of SuperDirt {    (0xb5c1bc8, gc=D8, fmt=00, flg=00, set=04)
  instance variables [15]
    numChannels : Integer 2
    server : instance of Server (0x2b68318, size=32, set=5)
    buffers : instance of Event (0x18fdbde8, size=5, set=3)
    vowels : instance of Event (0x12b60868, size=5, set=3)
    orbits : instance of Array (0x11b5c988, size=5, set=3)
    modules : instance of Array (0xb282238, size=13, set=4)
    port : Integer 57120
    senderAddr : instance of NetAddr (0xefd0a28, size=4, set=2)
    replyAddr : nil
    netResponders : instance of List (0xec07ef8, size=1, set=2)
    fileExtensions : instance of Array (0x2cb6ac0, size=4, set=2)
    receiveAction : nil
    warnOutOfOrbit : true
    verbose : false
    maxLatency : Integer 42
}
ARGS:
CALL STACK:
    DoesNotUnderstandError:reportError   0x11f92f18
        arg this = <instance of DoesNotUnderstandError>
    Nil:handleError   0x11f92ba8
        arg this = nil
        arg error = <instance of DoesNotUnderstandError>
    Thread:handleError   0x11f922b8
        arg this = <instance of Thread>
        arg error = <instance of DoesNotUnderstandError>
    Object:throw   0x11f91f48
        arg this = <instance of DoesNotUnderstandError>
    Object:doesNotUnderstand   0x11f8fe48
        arg this = <instance of SuperDirt>
        arg selector = 'soundLibrary'
        arg args = [*0]
    < closed FunctionDef >  (no arguments or variables)
    Interpreter:interpretPrintCmdLine   0xcc152b8
        arg this = <instance of Interpreter>
        var res = nil
        var func = <instance of Function>
        var code = "    ~dirt.soundLibrary.addSynth..."
        var doc = nil
        var ideClass = <instance of Meta_ScIDE>
    Process:interpretPrintCmdLine   0x11f6e448
        arg this = <instance of Main>
^^ The preceding error dump is for ERROR: Message 'soundLibrary' not understood.
yaxu commented 6 years ago

Ah I switched to 1.0-dev and now it works

testing-----
( 'length': 1, 'speed': 1, 'amp': 0.4, 'sustain': 0.999, 
  'pan': 0, 'fadeInTime': 0, 's': midi, 'cycle': 101269, 'fadeTime': 0.001, 
  'cps': 1, 'delta': 1, 'endSpeed': 1, 'note': 0, 'freq': an UnaryOpFunction, 
  'latency': 0.2949357600339, 'delayAmp': 0 )
yaxu commented 6 years ago

(when I switch to 1.0-dev git tells me I'm 'in 'detached HEAD' state', slightly alarming!)

telephon commented 6 years ago

https://i.ytimg.com/vi/6Na_RSniI2w/maxresdefault.jpg

telephon commented 6 years ago

probably because of these git refs/tags, I keep having problems with them. I'll have to understand better how to use them. https://git-scm.com/book/en/v2/Git-Internals-Git-References

telephon commented 6 years ago

@yaxu here is a first test, try with your mididevice if you get note on and off


(
// substitute your own device here
MIDIClient.init;
~midiOut = MIDIOut.newByName("FastLane USB", "Port A");
~midiOut.latency_(~dirt.server.latency);
)

(
var p = (
    type:\midi,
    midicmd: \noteOn,
    chan: 0,
    midiout: ~midiOut
);
~dirt.soundLibrary.addSynth(\midi,
     (play: {
        var e = (note: ~note, sustain: ~sustain.value);
        e.proto = p;
        e.play;
     })
);
)
d1 $ sound "midi*3" # n "5 7 1"
yaxu commented 6 years ago

Yes this works well @telephon! I just had to change the latency to ~midiOut.latency_(0.4);, to get it to match with superdirt sample playback. I can see the noteoffs going out, and they reflect the legato/sustain parameter correctly.

telephon commented 6 years ago

I've added a bit of code here, try and see if that is what we need. The default parameter names for midi in sclang I personally find a bit intrusive, but I left them for now.

https://github.com/musikinformatik/SuperDirt/commit/3f6351e29ee0a86506e1573c041387fa9e38a32d

telephon commented 6 years ago

@yaxu, if you think it should be easier to configure, I could wrap this script into a method, so given that you have a midi-device name, you could write e.g. ~dirt.initMIDI(\midi, "device name", (chan: 5)). Just let me know.

yaxu commented 6 years ago

Thanks @telephon, I'm away from any midi devices for a couple of nights but this looks great. I think an initMIDI method would be very handy. I wonder if it would be better to make chan count from 1. Being able to send MIDI clock start/tick/stop could be useful too.

telephon commented 6 years ago

Being able to send MIDI clock start/tick/stop could be useful too.

Probably works already, you just send # midicmd "start" etc.

I wonder if it would be better to make chan count from 1.

The protocol should be transparent, just pass through to the end what is there. What is the midi default channel?

yaxu commented 6 years ago

Righto. I believe the first channel (0) is default, with the tenth channel (9) used for percussion (in general midi at least).

telephon commented 6 years ago

btw I think that the following are also automatically supported (I haven't tested any of this):

    midiClock {
        this.write(1, 16rF0, 16r08);
    }
    start {
        this.write(1, 16rF0, 16r0A);
    }
    continue {
        this.write(1, 16rF0, 16r0B);
    }
    stop {
        this.write(1, 16rF0, 16r0C);
    }
    reset {
        this.write(1, 16rF0, 16r0F);
    }

to be used from tidal by ways of:

sound "midi" # midicmd "start" sound "midi" # midicmd "stop" sound "midi" # midicmd "continue"

etc.

arraybercov commented 6 years ago

I tested this out today on Win 10 w SuperDirt 3.8 and cabal Tidal 0.96. I updated my Quark's list through the GUI and grabbed the Version called "push" because I noticed it had the tidal-midi.scd in the scripts folder so I'm guessing that's the latest. I'm using @telephon 's script without modification, except the midi port ,that he linked here: https://github.com/musikinformatik/SuperDirt/commit/3f6351e29ee0a86506e1573c041387fa9e38a32d

and I also tried the tidal-midi.scd included with the "push" release.

I'm getting unexpected rhythmic patterns. In the two videos below, you'll see I have three notes per cycle playing, but something on the SuperDirt/Supercollider side is spacing them out in an unexpected way.

https://drive.google.com/open?id=1R2tJOZ7o5TzL5iAqNMH_XTlj5jwCwQ4x

https://drive.google.com/open?id=1CwHhz7cTyJyyqU_P1Ww5ffE-k54gCDFo

d1 $ n "c2 c3 c5" # sound "midi" is rhythmically messed up

m1 $ n "c2 c3 c5" works as intended

Because of this I'm fairly certain it has nothing to do with my loopMIDI/Ableton setup.

telephon commented 6 years ago

thank you, @lagauche. I might have fixed it now, try updating the quark.

The new version of the patch should start with

// version 0.2
arraybercov commented 6 years ago

What file would I find // version 0.2 in to make sure I'm using the latest?

I removed my SuperDirt folder, checked for updates and grabbed "push" again 5 minutes ago.

Since I'm getting new errors, I think I might have it, but I want to make sure. Unfortunately I'm still getting weird funky rhythmic patterns like in the video I posted.

It's almost as if SuperDirt is applying some kind of Euclidian rhythm function.

These are the new console errors I get after SuperDirt successfully boots:

CheckBadValues: unknown found in Synth 1016, ID 0
CheckBadValues: unknown found in Synth 1016, ID 0
CheckBadValues: unknown found in Synth 1012, ID 0
CheckBadValues: unknown found in Synth 1012, ID 0
CheckBadValues: unknown found in Synth 1008, ID 0
CheckBadValues: unknown found in Synth 1008, ID 0
CheckBadValues: unknown found in Synth 1004, ID 0
CheckBadValues: unknown found in Synth 1004, ID 0
CheckBadValues: unknown found in Synth 1000, ID 0
CheckBadValues: unknown found in Synth 1000, ID 0
CheckBadValues: unknown found in Synth 1044, ID 0
CheckBadValues: unknown found in Synth 1044, ID 0
CheckBadValues: unknown found in Synth 1040, ID 0
CheckBadValues: unknown found in Synth 1040, ID 0
CheckBadValues: unknown found in Synth 1036, ID 0
CheckBadValues: unknown found in Synth 1036, ID 0

And I get errors when playing SuperDirt samples like "bd" (the sound still plays fine):

CheckBadValues: normal found in Synth 1000, ID 0 (previous 5650154 values were unknown)
CheckBadValues: normal found in Synth 1000, ID 0 (previous 5650154 values were unknown)
CheckBadValues: unknown found in Synth 1000, ID 0 (previous 12553 values were normal)
CheckBadValues: unknown found in Synth 1000, ID 0 (previous 12553 values were normal)

I get no console errors when evaluating the new midi feature such as in: d1 $ n "c2 c3 c5 c2 c3" # sound "midi"

But it sounds like this: https://drive.google.com/open?id=1GqHUrxpsgnTbjwEy9xjySTjkKXqtpxo9

The timing is rhythmically/mathematically off.

When I change SuperDirt Version to "Latest" these errors go away, but of course I can't eval the tidal-midi.scd script.

P.S. I just deleted my second post regarding some errors on evaling tidal-midi.scd but it turns out I just needed to boot SuperDirt first. Everything above is still happening though.

telephon commented 6 years ago

can you try and use the version called "1.0-alpha"? I don't know what "push" is …

telephon commented 6 years ago

… ah I see, "push" is a tag that shouldn't be there.

telephon commented 6 years ago

I'm not sure if the versions that are displayed in the GUI are correct. If in doubt, just download the 1.0-dev branch from here.

yaxu commented 6 years ago

I'm using this to get sync with the sequencer on this volca keys synth. Works well! d1 $ midicmd "[start/4,midiClock*24]" # s "midi" # nudge 0.1

arraybercov commented 6 years ago

@telephon I downloaded the 1.0dev from github, put it into my downloaded-quarks folder, opened the GUI and ticked the checkbox to install. I didn't realize it was that easy.

I believe you fixed it! Timing seems accurate now and it's also syncing up with normal Tidal without any changes to the default latency setting 0.0.

I'm still getting these errors over and over though:

CheckBadValues: unknown found in Synth 1000, ID 0 (previous 12553 values were normal)
CheckBadValues: normal found in Synth 1000, ID 0 (previous 10967 values were unknown)
kindohm commented 6 years ago

I would really love to test Control Change (e.g. "CC") params once they are supported in this.

kindohm commented 6 years ago

given this proposed code:

~dirt.initMIDI(\midi, "device name", (chan: 5))

How would I set up multiple synths on multiple channels? e.g. synth1 on channel 1, synth2 on channel 15, and synth3 on channel 16? How would they then be played?

arraybercov commented 6 years ago

@kindohm I have multiple channels working using the tidal-midi.scd provided in the SuperDirt 1.0dev and I'm playing them with all the standard Tidal functions as long as I have # s "midi"

Set your device here (taken from the provided script): ~midiOut = MIDIOut.newByName("loopMIDI Port 2", "loopMIDI Port 2"); There's no need to specify a channel since we'll do that in Tidal.

eval (midichan, midichan_p) = pF "midichan" (Nothing) in Haskell

then do # s "midi" # midichan n. This seems to work perfectly.

I still have no idea how to add multiple ports/devices in Supercollider though and then access them in Tidal. Hopefully it's as simple as duplicating ~midiOut with a new port/device and then creating another function like midichan to select it. Anyone have any ideas?

kindohm commented 6 years ago

Support for multiple devices would be an important feature for me. Would be really great to have that.

telephon commented 6 years ago

@kindohm

with the current version, you can try this the following way:


(
~midiOut1 = MIDIOut.newByName("loopMIDI Port 2", "loopMIDI Port 2");
~midiOut2 = MIDIOut.newByName("loopMIDI Port 2", "loopMIDI Port 2"); // replace by your devices
var addMidi = { |name, device|

    ~dirt.soundLibrary.addSynth(name,
        (
            play: {
                var e = (
                    type: \midi,
                    midiout: device,
                    lag: ~lag + ~latency, // in the midi event, lag is used as latency
                    note: ~note ? 0,
                    sustain: ~sustain.value,
                    midicmd: ~midicmd ? \noteOn,
                    chan: ~midichan ? 0,
                );
                e.play;
            }
        )
    );
};

addMidi.(\midi1, ~midiOut1);
addMidi.(\midi2, ~midiOut2);
)
jarmitage commented 6 years ago

This looks really promising, thanks for giving it a go @telephon!

I've just got everything hooked up and going to give it a bit of a stress test. I have one question now though, I don't quite understand the final block of the tidal-midi.scd script:

// the following midicmds and their parameters are then supported
// the usual note/freq and legato/sustain parameters are taken into account
polyTouch: chan, midinote, polyTouch
program: chan, progNum
bend: chan, val
sysex: uid, array
noteOn: chan, midinote, amp // default
allNotesOff: chan
smpte: frames, seconds, minutes, hours, frameRate
songPtr: songPtr
control: chan, ctlNum, control
touch: chan, val
noteOff: chan, midinote, amp // sent automatically, after sustain.

Currently I'm just doing things like d1 $ n "0 1 2 3" # s "midi" # midichan "0", but how would I use e.g. amp (which I assume is note on velocity)? I can't do # amp 0.5 as that is not listed in the script as required for usage in Tidal. Same goes for the rest of these params. Do I need to define them in Tidal too?

And actually one more q: why does chan not work but midichan does?

Very exciting!

jarmitage commented 6 years ago

Still trying to understand how to send the additional params

If I do d1 $ midicmd "control" # s "midi" I get

ERROR: Message 'asInteger' not understood.
RECEIVER:
   nil
ARGS:
CALL STACK:
    DoesNotUnderstandError:reportError   0x11b549fd8
        arg this = <instance of DoesNotUnderstandError>
    Nil:handleError   0x11b56f828
        arg this = nil
        arg error = <instance of DoesNotUnderstandError>
    Thread:handleError   0x1197668e8
        arg this = <instance of Thread>
        arg error = <instance of DoesNotUnderstandError>
    Object:throw   0x11ea14228
        arg this = <instance of DoesNotUnderstandError>
    Object:doesNotUnderstand   0x11b540388
        arg this = nil
        arg selector = 'asInteger'
        arg args = [*0]
    MIDIOut:control   0x11a804778
        arg this = <instance of MIDIOut>
        arg chan = 0
        arg ctlNum = nil
        arg val = 125
    Function:awake   0x11a6914f8
        arg this = <instance of Function>
        arg beats = 4088.1130239488
        arg seconds = 4088.1130239488
        arg clock = <instance of Meta_SystemClock>
        var time = 4088.1130239488
^^ The preceding error dump is for ERROR: Message 'asInteger' not understood.
RECEIVER: nil

If I do d1 $ midicmd "program" # midichan 1 # progNum 5 # s "midi"

I get this (time/source/message/channel/data) - the progNum doesn't seem to work

23:19:03.350    To tidal    Program 2   1
23:19:04.350    From tidal  Program 2   1

If I define (amp, amp_p) = pF "amp" (Nothing) and then do d1 $ n "0 1 2 3" # midicmd "noteOn" # midichan 0 # amp "0.8" # s "midi" the amp param doesn't have an effect.

jarmitage commented 6 years ago

For a stress test I did this running MIDI into 8 software instruments, no timing problems whatsoever

d1 $ fast 16 $ n (scaleP "major" (run 16)) # s "midi" # midichan "[0 1 2 3 4 5 6 7]"
arraybercov commented 6 years ago

@telephon any idea why I'm getting errors like these CheckBadValues: unknown found in Synth 1176, ID 0 when I boot SuperDirt and when playing samples? Everything seems to be working nonetheless.

Could it have anything to do with the fact that I downloaded the 1.0dev from github but then I hit install in the Quarks gui? I'm doubting it because I also tried re-installing the 1.0 alpha from the gui and same error persists.

telephon commented 6 years ago

@jarmitage

I don't quite understand the final block of the tidal-midi.scd script

this is not code, just documentation

I can't do # amp 0.5 as that is not listed in the script as required for usage in Tidal.

should work despite not being listed. not sure why it doesn't.

And actually one more q: why does chan not work but midichan does?

it's just a matter of the bindings in tidal. I thought midichan is better, but, well, we'll better discuss naming in en extra step.

If I do d1 $ midicmd "control" # s "midi" I get ERROR: Message 'asInteger' not understood.

ah, maybe a default parameter not defined, probably ctlNum.

telephon commented 6 years ago

@lagauche

what is the tidal code that produces the bad values?

arraybercov commented 6 years ago

@telephon For example the most basic tidal code causes it to occur every time a sample sound plays : d1 $ s "bd"

kindohm commented 6 years ago

What is the code I need to add to the midi synth in order for it to receive Control Change params? I want to send random CC values to control #100 on my synth:

d1 $ sound "midi" # n "0" # midichan "0" # ctlNum "100" # control (rand)

^^ not even sure that's correct.

~dirt.soundLibrary.addSynth(\midi,
    (
        play: {
            var e = (
                note: ~note ? 0,
                sustain: ~sustain.value,
                midicmd: ~midicmd ? \noteOn,
                chan: ~midichan ? 0,
            );
            e.proto = p;
            e.play;
        }
    )
);
yaxu commented 6 years ago

That should be enough Mike, along with the initialisation stuff above https://github.com/musikinformatik/SuperDirt/blob/1.0-dev/scripts/tidal-midi.scd

I'm not sure what the range for these parameters is offhand. You might want to send to ctlNum 99, if it starts counting at 0, or on the other hand it might be scaled to 0 .. 1, or (maybe more likely) control might be scaled to 0 .. 127.

kindohm commented 6 years ago

I can't get it to work. I've tried indexing from both 0 and 1 for the ctlNum value, and have tried scaling values from both 0..1 and 0..127. I also can't get the amp param to work either, as @jarmitage also reported (maybe it's related?).

telephon commented 6 years ago

you need to specify the midicmd, in this case # midicmd "control"

telephon commented 6 years ago

Can you check if this works for you now?

I've wrapped the MIDI function into a method.

https://github.com/musikinformatik/SuperDirt/commit/d0e3786e6c83b969c31dbbd1ecedd9e7d52a09a1

yaxu commented 6 years ago

Yes that works Shall I release a new tidal version with the parameters available?

kindohm commented 6 years ago

I'm still not seeing control changes:

d1 $ sound "midi*4" # midinote "0 1 2 3" # midichan "0" # ctlNum "35" # midicmd "[control, noteOn]" # control (scale 0 120 $ sine)

I've tried zero-indexing ctlNum and have also tried scaling the control values from 0..1. I also tried swapping ctlNum for control, thinking I might have them backwards, but that didn't help either.

telephon commented 6 years ago

for debugging, can you try and run this:

(
fork {
    20.do {
        ~midiOut.control(chan: 0, ctlNum:35, val:126.rand);
        0.5.wait;
    }
}
)
jarmitage commented 6 years ago

@yaxu when you say this is working for you, which parts are working, and can you post Tidal code? I couldn't get anything other than note and channel data working.

yaxu commented 6 years ago

@jarmitage I inly got note and midi clock data working. I tried cc last night, but with a midi snooper (no synth to hand). I could see control messages but couldn't see control values changing.

jarmitage commented 6 years ago

I’m not sure it’s worth adding to a Tidal release until CCs and the parameters listed above are confirmed to be working. Also the suggested syntax doesn’t feel very Tidal-y to me somehow.

... Or maybe it is worth getting a minimal thing out there 😅