monome / crow

Crow speaks and listens and remembers bits of text. A scriptable USB-CV-II machine
GNU General Public License v3.0
166 stars 34 forks source link

i2c chaining #5

Closed tehn closed 4 years ago

tehn commented 6 years ago

ie:

trentgill commented 5 years ago

See issue #53 for discussion of auto-discovery & indexing of units in a multi-crow setup.

trentgill commented 4 years ago

@tehn thinking about syntax for the basic i/o expander setup (ie one crow is leader, and uses extra crows for adding ins & outs:

-- direct i2c call style (currently {secretly} supported)
ii.crow.output(1,2.3) -- set remote crow's first output to 2.3 volts

-- can be extended to multiple devices with forthcoming multi-device support
ii.crow[2].output(1,2.3) -- as above
ii.crow[3].output(1,3.4) -- a third crow's first output is set to 3.4 volts

-- we could use the input/output metamethods to implement:
output[1].volts = 1.1 -- regular old output 1 to 1.1volts
output[5].volts = 2.2 -- forwards to second device over i2c
output[5].slew = 0.1 -- slew is also a default supported message

-- this breaks down when you try and use asl
output[5].action = ar() -- will not work

-- for inputs:
ii.crow.get('input')
-- triggers ->
ii.crow.event(e,data)
  if e == 'input' then
    -- do things
  end
end

-- this could be extended to allow something like the stream/change events
input[3].mode('stream',0.1) -- note: no event supported. must use the default which will be i2c comm'n
-- triggers another added function
input[3].stream(d)
  -- do things with the value sent over i2c
end

This last idea with inputs seems really nice, but involves a fair bit of magic. I'm not opposed to that, but don't want to jump into implementing this stuff unless it seems like the appropriate path forward.

The indexing of the input & output libs seems nice, but it's a little tough because inputs are %2 and outputs are %4, so it's not immediately obvious which device you're communicating with.

I guess we should be asking, what are people trying to get out of a multi-crow setup in the first place?

tehn commented 4 years ago

i like all of these ideas-- follows the way i was thinking TT expands via ansible(s).

for the use as a norns/max expander, having two crows side-by-side means it's not too weird to have the ins/outs count up a different rates (2/4).

not sure if you're suggesting implementing both ie ii.crow[1].output and output[5], but i don't think it'd be bad to offer both syntaxes, since ascending-count might not make sense for every use case.

curious what the performance/throughput will look like. might be worth considering ahead of too ambitious of a design

trentgill commented 4 years ago

Currently ii.crow.output already works, and ii.crow[2].output works with the #260 PR. I think they should stay. Agreed that sequential indexing doesn't always make sense, but it does seem to cover the 'big array of i/o' use case.

Throughput is an interesting question. I've had 2 crows talking back and forth and i think it was roughly 1000 request/responses per second. That's probably 4~5bytes per side, so in the 4-5kB/s. Mind you, that's happening via the event system on both crow's, so that's simultaneous to outputs being generated etc. Not enough for streaming audio, but certainly fine for streaming a few continuous cv channels i think. (Agreed more thorough testing is an important thing here though!)

simonvanderveldt commented 4 years ago

I guess we should be asking, what are people trying to get out of a multi-crow setup in the first place?

I've been using two crows as input/output modules for norns for a bit now. In this setup I'm using the second crow simply as an IO extension where I'd like to use the same functionality as on the primary crow. For me currently this means outputting gates for triggering envelopes and pitch/voltage for two voices, two gates for triggering drum voices and a gate for syncing stuff. This means I currently have one spare output. I'll probably use this for another gate (either for clock/sync or for a third drum voice) or alternatively I'd like to use it as clock synced LFO. In the future I might want to replace some of the gate outputs with envelopes. I currently have no plans to add another crow for my use case. Input wise the only thing I currently need is a way to get time/clock sync information from crow into norns. In the future I might want to add some additional controls/knobs connected to those inputs to give some more hands-on control to the script running on norns.

Being able to set pitch/voltage over ii is already possible but there's no way to easily trigger gates or envelopes. I think there are two or maybe three approaches to this: Add some simplified functionality, similar to the ii methods that are available for Ansible/Kria, use/abuse the callN functions to do this or try to make a generic thing that would allow executing anything including ASL. I don't like the second option because it means I need to write a matching script for the second crow whereas with the other two solutions I can just use it as-is. And I'm not sure the last option is achievable.

What I've done so far is use the second crow that's connected over ii purely for pitch/voltage output and handle all the gates on the primary crow.

P.S. It's not directly related to this, but ii in general can serve as an alternative for requiring more IO because instead of having to for example send out a gate and pitch voltage that information can be sent over ii freeing up two output channels on crow. The ii ecosystem is missing sound sources though to make this a viable option. AFAIK there are only Just Friends, W/ and the ER-301 and TXo in oscillator mode, although I believe the latter one is a bit limited in functionality.

trentgill commented 4 years ago

I'll try and add some of my perspective to the 3 options. I know you've probably already thought about this ideas, but just fleshing them out so we have real code to look at and hopefully further inform a direction goal.

Add baked in ii follower commands ala Ansible

The clear benefit is ease of use and more idiomatic ii usage within the existing ecosystem. The cons are 1) limited functionality, 2) requires a crow firmware update to add new commands.

Main questions are which additional commands we want to see? For your use case I'd imagine:

ii.crow.output[1].pulse()
ii.crow.output[2].ar()
ii.crow.output[3].lfo()

These are 3 basic ones to start. Would they be fully parameterized like the asllib versions (ie optional args for times, ranges, shapes?). This will require some metatable magic to allow the indexing of the outputs, so we could add more magic to allow the shapes (eg. logarithmic) to be sent as enum integers & decoded on the receiving end.

I like this approach as an end point for ii support, not as a jumping off point. It's not a sustainable way to allow inter-device access to the whole asl syntax (just look at the ridiculous magic we had to add to make norns able to speak to crow, not to mention ii!).

Use callN

This is the most clunky option for sure, but it's also the most flexible. You can do pretty much anything this way and therein lies the problem (no universal system is ever elegant for every usage).

One workaround to maintaining multiple scripts is to wrap all the working code in conditionals based on the ii address. This gets weird if you want your followers to do more complicated things with events, but that seems not to be the case for you.

function init()
  local myaddress = ii.get_address()
  if myaddress == 1 then
    -- leader init
    -- setup timers etc that calls ii.crow.callN()
  else
    -- follower init
  end
end

ii.self.call1(arg)
  -- will only be called on the follower device
end

A second part could be to have manual init style functions (that aren't in the actual init) and then call them explicitly from norns / callN.

--- norns calls `crow.send("myinit()")` on script start
function myinit()
  ii.crow.call1(0) -- inits the attached crow
  -- leader device init goes here >>
end

ii.self.call1 = function(arg)
  -- follower device init goes here
end

This feels awkward, but has the benefit of only requiring a single script that can run on both devices.

Extend ii to allow arbitrary code chunks be sent over ii

This is the best & only real way to allow a leading crow to take full control over remote devices without adding a huge amount of lua code. Unfortunately it requires completely rethinking how the i2c driver works.

It's absolutely possible, but will be a substantial undertaking. I don't think it's the greatest use of my time (ie there are other desirable features that will take substantially less effort), so it will likely need to be picked up by someone who is passionate about this issue. (that said, it's entirely possible that i'll drink enough coffee one day that i will need to think about it further).

Before even thinking how it would work, I really want to see examples of how it would be used. Here's me walking through some ideas.

-- send some generic code to be executed on a remote device
ii.crow.send( 'output[1]( lfo(2,3,"log") )' )

-- to do that from norns looks *really weird*
crow.ii.crow.send( 'output[1]( lfo(2,3,"log") )' )

But this doesn't even feel like what we want to do. Sure it allows total flexibility, but it's the kind of thing i want to instantly build a library to make more natural & terse.

I want to write

-- on crow
output[5]( lfo(2,3,"log") )

-- on norns
crow.output[5]( lfo(2,3,"log") )

Unfortunately this would require a huge amount of metamethod magic on crow that is unsustainable and difficult to maintain.


I think the first option above is really the way to go. We can then add some lightweight metamethods to allow the ins & outs to be referred to sequentially (output[5].volts = 3).

Clearly I haven't thought this all through yet though!

simonvanderveldt commented 4 years ago

@trentgill Thanks for sharing your thoughts on this. From my current understand I agree with your final assessment that the first option is probably the way to go, especially for now/the short term. It's achievable, we have some existing examples we can follow and it would already enable a bunch of use-cases which could provide valuable input for potentially a better system that supports arbitrary code chunks.

Regarding syntax, personally I think I prefer ii.crow[x].output[y] over ii.crow.output[5] because it's easier to relate it to the physical devices.

Regarding the functions to expose: I think we can look at the existing functionality of other ii devices as well as crow's own API and choose what we prefer. We could mimic for example Ansible's API, with functions like actions.ansible.trigger or actions.ansible.trigger_pulse which immediately do something or stay closer to crow's API by having a separate "configure" function similar to crow.output[i].action which would support a fixed list of actions and a "execute the configured action" function similar to crow.output[i].execute. Since this will be pretty much a simplified API I'd prefer the former, the latter seems pointless for this purpose. The examples you posted look good to me.

ii.crow.output[1].pulse()
ii.crow.output[2].ar()
ii.crow.output[3].lfo()

Maybe prefixing the pulse and ar ones with trigger_ or tr_ as that seems to be somewhat of a standard (naming is hard! ;)).

Would they be fully parameterized like the asllib versions (ie optional args for times, ranges, shapes?). This will require some metatable magic to allow the indexing of the outputs, so we could add more magic to allow the shapes (eg. logarithmic) to be sent as enum integers & decoded on the receiving end.

That's a good question, I'm not sure tbh. It would of course be very nice, but I'd expect it would also be quiet a bit more work, so maybe for the initial implementation they should be fixed? This way we can get some experience and learnings and go from there.

Wall of text following below about the crow and ii plumbing code in norns

Regarding the plumbing code that is required to make crow and ii devices work on norns I think that's just a consequence of some high-level design choices. I.e. ii defines just a message format, not it's contents, so devices can and will have different names for their functions (I'm not sure if functions is the correct word in the ii context, but I hope you understand what I mean). On top of that there's no discovery mechanism defined in ii for these functions so then you're effectively left with two options: have a single send-over-ii function that simply sends the data over i2c as-is or provide some convenience wrappers which are effectively a copy of the API) to make the experience easier/better for the user, which is what we currently have. These things are addressable though, the first issue can be aided by standardization, the second can be solved by implementing a discovery mechanism. Both have their pros and cons and both will take some effort of course.

I think for crow the issue is sort of similar in that the way it works is by sending it strings of text which then get executed on the device. Here we have the same issue on norns (or in druid, etc) that we don't know which functionality is available so we end up with the same choice of adding convenience wrappers effectively duplicating the whole API or some discovery mechanism. On crow at least some (human focused) discovery mechanism is present making using druid a bit easier of course.

Hope this makes sense :)

trentgill commented 4 years ago

personally I think I prefer ii.crow[x].output[y] over ii.crow.output[5]

The second option was actually a much deeper shortcut: ii.crow[x].output[y] output[4+y]

The idea being that outputs >= 5 are accessed via ii behind-the-scenes. This would require some extensive documentation & likely a tutorial to explain what is and isn't possible for inputs beyond the first device. See this post above for examples.

Now that I look at the current scheme, we're already breaking from the native crow code, requiring input/output channel to be sent as the first argument (not as a table index). Below is the definitions copied from here:

-- actions. shouldn't return a value
ii.crow.output(chan,val)
ii.crow.slew(chan,slew)
ii.crow.call1(arg)
ii.crow.call2(a,a2)
ii.crow.call3(a,a2,a3)
ii.crow.call4(a,a2,a3,a4)

-- queries. must return 1 value
ii.crow.get('input',chan)
ii.crow.get('output',chan)
ii.crow.get('query0')
ii.crow.get('query1',a)
ii.crow.get('query2',a,a2)
ii.crow.get('query3',a,a2,a3)

If you could write out your desired functionality & syntax for this extension, I can add it to my list.

Maybe prefixing the pulse and ar ones with trigger or tr as that seems to be somewhat of a standard

This I'm less worried about. Typically the cv/tr dichotomy is a hardware one, so tr(1) and cv(1) refer to different physical jacks. In crow's case it's a single physical jack, so it seems more that the 'cv' or 'tr' would refer to the action of the command than the hardware (which is an awkward difference imo). Using pulse and ar seem preferable because it describes the action of the command, which makes extending to lfo feel more natural to me.

simonvanderveldt commented 4 years ago

The second option was actually a much deeper shortcut: ii.crow[x].output[y] output[4+y]

The idea being that outputs >= 5 are accessed via ii behind-the-scenes. This would require some extensive documentation & likely a tutorial to explain what is and isn't possible for inputs beyond the first device. See this post above for examples.

Ah, I missed that. FWIW I definitely prefer the ii.crow[x].output[y] (or ii.crow[x].output(y,arg) format. Explicit > implicit, less magic :)

Maybe prefixing the pulse and ar ones with trigger or tr as that seems to be somewhat of a standard

This I'm less worried about. Typically the cv/tr dichotomy is a hardware one, so tr(1) and cv(1) refer to different physical jacks. In crow's case it's a single physical jack, so it seems more that the 'cv' or 'tr' would refer to the action of the command than the hardware (which is an awkward difference imo). Using pulse and ar seem preferable because it describes the action of the command, which makes extending to lfo feel more natural to me.

You're totally right, they are all distinct output types and thus prefixed by their type. That doesn't apply to crow of course because all outputs are equal so there's no need for that distinction. I guess we can use the verbs from the existing ii devices without the tr/trigger prefix as a starting point?

Now that I look at the current scheme, we're already breaking from the native crow code, requiring input/output channel to be sent as the first argument (not as a table index).

Is this something we should/you'd like to change? All the actions for all devices (not only crow) listed in https://github.com/monome/norns/blob/b1395713cf7d81fd727fae57f68a81792657c798/lua/core/crow/ii_actions.lua work this way it seems.

If you could write out your desired functionality & syntax for this extension, I can add it to my list.

Taking the existing actions without a prefix as starting point, if we start with Ansible's actions (most other devices which support both CV and trigger outputs have similar names for their actions) we have this list:

actions.ansible.trigger
actions.ansible.trigger_toggle
actions.ansible.trigger_pulse
actions.ansible.trigger_time
actions.ansible.trigger_polarity
actions.ansible.cv
actions.ansible.cv_slew
actions.ansible.cv_offset
actions.ansible.cv_set

Removing the trigger_ and cv_ prefixes from this does make it maybe slightly less clear what the actions are going to do though.

actions.ansible.trigger
actions.ansible.toggle
actions.ansible.pulse
actions.ansible.time
actions.ansible.polarity
actions.ansible.cv
actions.ansible.slew
actions.ansible.offset
actions.ansible.set

I guess we'll have to choose which one of these makes sense. In any case I think something along these lines would be a good start, allowing both CV and gate control for crow's connected over ii. This also requires the least amount of thinking on names because there are already several devices out there that can serve as an example, makes life a bit easier :) Regarding my current use-case I think I only need pulse and cv.

Other things like LFOs and envelopes seem to be only supported by TXo (see https://github.com/monome/norns/blob/b1395713cf7d81fd727fae57f68a81792657c798/lua/core/crow/ii_actions.lua#L217 and https://github.com/monome/norns/blob/b1395713cf7d81fd727fae57f68a81792657c798/lua/core/crow/ii_actions.lua#L199) so there's less of a precedent on how to handle them.

One interesting thing to note is that all ii devices use .cv as their action for setting an output voltage whereas for crow we're currently using output. Do we want to change crow to match the existing convention for this?

tehn commented 4 years ago

i'm in preference of pulse, ar, and lfo and additionally the explicit ii.crow[x].output[y] syntax.

i am not particularly enthusiastic about implementing an ii "pipe" to send lua code between crows. i would highly consider this to be a completely separate feature, as currently ii does not have a string implementation.

will comment on the other issue re: finalizing the syntax

simonvanderveldt commented 4 years ago

For anyone else interested, the syntax discussion has been moved here https://github.com/monome/crow/issues/258

trentgill commented 4 years ago

258 remains open, but closing this as it's pretty well implemented already and will be 'complete' with 258.