-
-
Notifications
You must be signed in to change notification settings - Fork 133
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
External mappings for MIDI instruments #1212
Conversation
This commit adds a second argument to the midi() command: mapping. This argument should be an object containing a key-value map of MIDI controls used by an external synthesizer. If any control is used that matches the mapping, a CC message is sent.
Hi! I've added Program Change and SYSEX support to MIDI mapping. |
@felixroos : anything we can do to push this further towards a merge? |
i can test this soon |
how about making the syntax more like a registry, so you wouldn't need a variable? something like: midimap({
port: "MIDI Bus 1",
dummy: {cc: 80, channel: 1}, // channel is optional
controller: true // whether or not to emit a note
})
note("C E G C").dummy(0.2).midi('MIDI Bus 1') the midi function could check if there is a mapping for the given output and apply it if it exists. midimap('mything', {
port: "MIDI Bus 1",
dummy: {cc: 80, channel: 1}, // channel is optional
controller: true // whether or not to emit a note
})
note("C E G C").dummy(0.2).midi('mything') the upsides of using a registry are
what do you think? |
Some ideas building on the registry idea: // Generalised idea of simple way of defining a 'target' for patterns
const {dummy} = target('mything', new Midi({
port: "MIDI Bus 1",
dummy: {cc: 80, channel: 1}, // channel is optional
controller: true // whether or not to emit a note
});
// So we can e.g. make an OSC, MQTT, Serial target as well:
target('myotherthing', new OSC({
address: "127.0.0.1",
port: 6010,
schedule: "bundlestamp",
shape: ['s', 'speed', 'pan'],
handshake: false
}));
// .midi('mything') could just be .mything()
$: dummy("0.2*2 0.3(3,8)").note("<C E G C>").mything();
// It should be possible to send to more than one target
$: dummy("0.2*2 0.3(3,8)").note("<C E G C>").mything().myotherthing();
// I guess if you specify a target it doesn't go to the default unless you specify explicitly
$: dummy("0.2*2 0.3(3,8)").note("<C E G C>").mything().myotherthing().superdough(); These targets could be definable in strudel settings as a preamble to add to all patterns The above contributes to #623 but I guess don't need to solve all at once.. My idea would be to have an interface for MIDI which is general enough to be extended to these other cases. |
Maybe this would be better: $: dummy("0.2*2 0.3(3,8)").note("<C E G C>").target("mything, myotherthing, superdough") Allowing this sort of thing: $: dummy("0.2*2 0.3(3,8)").note("<C E G C>").target("mything").off(0.125, x => x.target("myotherthing")) |
my hot take would be to integrate targets into "s" at some point... but doesn't have to be in this PR |
This would be the logical conclusion to making MIDI/OSC a first class citizen of the system. I don't have much to add to your suggestions except that IMO this is peak Tidal/Strudel. Being able to pattern anything while "hiding" the subtleties of the backend used for producing pattern side effects in the real world. @felixroos, @yaxu: How do you suggest implementing the midimap registry? |
i'd maybe try to focus on midi in this PR so it doesn't get too hairy. then we could use a Map to map the names to the configs. the midi function would then check if the given name is either a config name or a device name and act accordingly. midimap('mything', {
port: "MIDI Bus 1",
lpf: {cc: 80, channel: 1, range: [0,20000], exp: 2},
controller: true // whether or not to emit a note
})
note("C E G C").lpf(400).midi('mything') with the range mapping in place, the lpf function can be used normally. the config would then remap 400 to a value within the midi cc value range. This would mean you could play the same pattern with midi or superdough in the same way. @yaxu we could do a similar thing with osc but also hide it behind a function like oscmap, then see how we generalize it internally? |
@felixroos Yep ok let focus on midi I feel like there are two things here - mapping strudel controls to CC values, and mapping midi messages to midi ports. Maybe these should be separated? Can the I guess if there are CCs and notes in one hap, then the CCs should be sent first and slightly ahead of time so as not to delay the note. |
Most definitely. It's extremely common to split instruments by channel so we should support it. Your current plan seems good. The only thing missing is that map/memory system, and it shouldn't be too hard to implement.
Indeed, but they are separated in the current implementation. The only thing that declaring a mapping does is giving a new parameter name to a CCN, with CCV as parameter. If you omit the parameter name, no CC message is sent.
Yes, and there is a condition to it: sending a note on the same MIDI Port and channel. Otherwise, we don't really care. It can sometimes happen, currently, that the CC change happens after the note. Depending on the algorithm/synth used, it can sound messy. I don't have time to tackle this in the next few days but will definitely have it at some point. I don't know if you have already planned for a 1.2 release schedule but I would really love to see this getting merged. It is quite essential to my current performance method. Let me know 📆 |
good point, when they are separated (or at least separatable), then we can have a config for a common midi controller or plugin that doesnt contain the name of the audio device the person was using when mapping the device or plugin: addMidimap('arturia-dx7', {
lpf: {cc: 80, channel: 1, range: [0,20000], exp: 2},
lpq: {cc: 81: .... },
controller: true // whether or not to emit a note
})
note("C E G C").lpf(400).midimap('arturia-dx7').midi('MIDI Bus 1') now,
seems to be a misunderstanding here? or maybe i'm the one who misunderstood when answering above..
I'd say thats important yes. Then we would have a single config that maps from strudel controls to cc messages for a given device / plugin.
should be doable to detect that edit: another question would be if it should be possible to have multiple midimaps on a single pattern.. i'd say yes? each mapping can potentially handle different controls for different channels. overlaps would need to be handled (e.g. last mapping wins) edit 2: alternative syntax, inspired by samples function: midimaps({
'arturia-dx7': {
lpf: {cc: 80, channel: 1, range: [0,20000], exp: 2},
lpq: {cc: 81: .... },
controller: true // whether or not to emit a note
}
}) edit 3: loading midi map(s) from a url: midimaps('github:felixroos/midimaps') |
Great! I think the 'midichan' should be separated/separable too then though? Also is the control mapping side MIDI-specific? It seems it could also be used for things like adding alternative curves for superdough/osc parameters (most obviously gain). So this could just be called addMapping or similar, with the parameters just mapping to control names. Where a pattern value is used, an object describes the mapping. addMapping({
'arturia-dx7': {
lpf: {ccn: 80, ccv: {range: [0,20000], exp: 2}, midichan: 10},
controller: true // whether or not to emit a note
}
}) (I'm unsure what the controller param is for) With a specification like this the params can be overridden in the pattern, e.g. if a midichannel is changed: note("c a f e").mapping("arturia-dx7").midichan("7").midi("MIDI Bus 1") Again I think adding a mapping as a method would be nice: note("c a f e").arturia_dx7().midichan("7").midi("MIDI Bus 1") A way to add mappings to all patterns would be nice: addMapping({
'ALL': {
overdrive: {distort: {range: [0,100]}, gain: {range: [0, 100]}},
}
}) This also allows one-to-many mappings as per the above.. Similar to @daslyfe's idea to add generic timbre parameters that adjust more than one synthesis parameter to superdirt. The value mappings could even be in the form of sanitised method calls, similar to the YAML strudel implementation, if that isn't too much of a security problem.. Then it would be really flexible mapping beyond range etc. |
It is not useful anymore if we carry on with the changes you are suggesting so don't worry about it 😄
Indeed, I get it now that you have both developed your ideas some more. These changes will ask for a total rewrite of this current PR. I'm not sure if this branch is useful anymore. Note that it will also break #1244. |
if we open it up to a generic remapping, the json format would be syntax sugar for an fmap operation like this: note("g1")
.lpf("<400 2000 8000>")
.fmap(
value=>({
...value, // keep lpf?
ccn: 80,
ccv: value.lpf/20000 // in reality this would be value.cutoff ... (just in case you try to run this)
})
).log() the corresponding json would looks something like: { lpf: { ccv: { range:[0,20000] }, ccn: 80 } } this type of remapping would certainly be useful in other scenarios (e.g. pitch tracked filter etc..). note("g1").lpf("<400 2000 8000>").remap("lpf:0:20000:ccv:0:1") or, using the json: note("g1").lpf("<400 2000 8000>").remap({ lpf: { ccv: { range:[0,20000] }, ccn: 80 }}) there are some considerations to make:
i fear this generalization might lead to this PR getting harder to handle.. |
could be done with |
one addition: the midi map has the specialty that the output range (ccv) is always [0, 1]. a more generic mapping would require specifying input and output range, altho the output range could default to [0, 1]. in some advanced future strudel, each control could have a range config........... |
The problem is that the hardware MIDI protocol is pretty slow and serial. So it's not just about order but throughput. If you send a load of CC values to get the instrument ready for a note, the note will happen noticeably late, unless you send the CC ahead of time. Even worse, the CC notes are separate from notes, because the MIDI committee had pianos and dead white guy music on the brain where musical events are only about pitch and duration and everything else are just properties of an instrument i.e. piano pedals. This means we can't easily map timbral dimensions to notes at pattern time. note("c e f g").lpf(300).distort(3).midi(...) If lpf maps to straight to CC params in the pattern it would end up as So with this in mind I think the mapping can't be done in a nice generic way because general MIDI is not actually general enough. I think the mapping should after all be passed to the This still means you can't have two notes happening at the same time with different CC values but that's a limitation of MIDI that I think it's not possible to get around.. On the plus side you can pattern effects separately from notes which isn't yet possible in superdough (#665). stack(note("c e f g"),
lpf(cosine.mul(3000)).distort(sine).segment(100)
).midi(...) |
we could use lists to send multiple cc pairs. i wouldnt scrap all weve discussed so far :x but yeah it makes defining a generic remapping more difficult.. id rather implement midi mapping in a non generic but useful way than trying coming up with an all encompassing format. we can refactor it later when we have collected some experience with it.. |
on the issue of sending ahead of time: the clock works in a way that any callback is around 100ms early so i dont think this is a problem (unless proven otherwise) |
Yes I thought about that but it'd need to be patterns of lists of lists. I think that would need a bit of refactoring / breaking changes to support, and I'd generally worry about infecting strudel with midi's constraints. I think it could be better to leave core patterns based on simple key/value objects and translate to noteon/noteoff/cc at the time of scheduling. Now I remember why I own some nice midi synths that I never use.. But a 'best case' way of translating to midi ccs+notes would be nice to make them a bit more useable! |
Following the MIDI mapping discussion and while I understand the urgency for the 1.2 release and @Bubobubobubobubo's upcoming performance needs. This would be a proposal fo a future update. The current midi.mjs handles MIDI messages in three distinct pipelines.
Perhaps we could consider splitting this into two packages: @strudel/midi for low-level MIDI communication (1 and 2) I see a lot of potential usecases for midimap to do sth like.
which relates to |
I accidentally included some experimental code to test a GM module in the PR #1244, For example, with a GM (General MIDI) sound module configuration which maps program changes with instrument names, note("c a f e").sound("gm_piano") // Composing with a soundfont on the go
note("c a f e").sound("gm_piano").midimap("RolandSC88").midi(0) // Performing with a vintage hardware |
ok then let's do it like this: midimaps({
'arturia-dx7': {
lpf: {cc: 80, range: [0,20000], exp: 2},
lpq: {cc: 81: .... },
}
})
// or midimaps('github:felixroos/midimaps')
note("C E G C").lpf(400).midimap('arturia-dx7').midi('MIDI Bus 1')
agree?
sounds a bit overkill at first, but let's discuss this separately then |
Sounds good! |
Sounds good indeed. Could go further with more advanced PRs after 1.2 but this is a good milestone already. |
ok here we go: #1274 gonna close this one. thanks @Bubobubobubobubo for kicking things off! |
This draft PR proposes the addition of custom external instrument MIDI mappings exposing new parameters to be patterned. A mapping currently
takes this form:
It could be much better with the following additions: