Closed tehn closed 4 years ago
See issue #53 for discussion of auto-discovery & indexing of units in a multi-crow setup.
@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?
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
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!)
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.
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.
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!).
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.
ii
to allow arbitrary code chunks be sent over iiThis 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!
@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.
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 :)
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.
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
andar
seem preferable because it describes the action of the command, which makes extending tolfo
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?
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
For anyone else interested, the syntax discussion has been moved here https://github.com/monome/crow/issues/258
LATER cross-crow commands ie send lua from one crow to anotherie:
cv(5, value)
and cv command is passed via i2c to crow#2 (which has cv position 5)