emuell / afseq

GNU Affero General Public License v3.0
2 stars 1 forks source link

Allow specifying note attributes (instr/vol/pan/delay) in cycles #31

Open emuell opened 2 months ago

emuell commented 2 months ago

Continuing the discussion from https://github.com/emuell/afseq/issues/13#issuecomment-2224325987

Will close that topic as I'm giving up on the mapping stuff for now.

@unlessgames said

Regarding the ability to specify vol/pan and co in cycles a la c4'maj7_#1_v0.5_p0.0_d0.5:

If it would be its own thing I'd prefer the look of the equal sign (or the minus) more than the underscore here like c4=v.5=p.0=d.5.

I wonder if making the name operator : work in a way that you could attach multiple "tags" with a normal value to any step? Like a:v.1:p.2:foo.5 b.

@emuell said

I'd prefer if the target attribute (instrument, volume, panning...) could be addressed directly with a prefix:

$: s("[bd:<v.8 v.4>]*4")
 .bank("RolandTR909")

Now, if we'd use the colon character as a separator and the #/v/p/d prefixes as an attribute selector, then you could actually do both with the same syntax:

1.

attach a single property (target) to a note:

cycle("c d:v0.2 e")

or create sub-cycles as target

2.

cycle("[c d e]:<v0.1 v0.2>")

For 1. the only necessary change in the cycle is to attach multiple targets to a value instead of a single one, so things like cycle("c d:v0.2:p0.1 e") do work.

2. likely is a bigger task?

The properties could be resolved outside of the cycle, just like the instrument target is handled right now.

For regular notes outside of the cycle we could then also allow using the colon as separator for consistency. I would not enforce it, as it's a bit harder to read and not necessay there.

note("c4:v0.2:p0.1")
-- will be the same as 
note("c4 v0.2 p0.1")

@unlessgames I could take care of 1. The nested target cycle thing should probably better implemented by you...

unlessgames commented 2 months ago

Sounds good!

I imagine the target on the Event would change to a HashMap<String, Target> and Target could be extended with other types. Then these would be inserted into that map at output time as ("v", Target::Float(0.2)) or something. Then it would be up to the outside context to map this into volume or whatever.

Although the application of Targets in this manner might be somewhat backwards, for example in the case of

a's volume would be overwritten by the outer volume setting but the other way around seems more useful in a compositional sense. So the outer value should get dropped if a value already exist.

My only issue with the generalized usecase here is that you couldn't use a number at the end of the tag's name, otherwise the syntax would be ambiguous. Maybe there could be some optional character like _ that you could insert if you needed to separate the key from the value. If not, keys like p1 p2 ... (something I could see myself using for custom parameter maps) would be unusable and string target types would be impossible to create because there would be no way to tell if the character is part of the key or the value.

emuell commented 2 months ago

I imagine the target on the Event would change to a HashMap<String, Target>

I'd collect them in a vector and apply and merge them in the cycle event trait: the trait that consumes the cycle output.

For volume I'd actually expect the result to multiply in this case:

[a:v0.2 b c d]:v0.5
-> a v0.1, b v0.5, c v0.5, d v0.5

For panning and delay, this is a bit tricky. Need to think about that.

My only issue with the generalized usecase here is that you couldn't use a number at the end of the tag's name, otherwise the syntax would be ambiguous.

If we are not mapping the attributes in the cycle, this should not really be an issue either? But yes, if it is, we indeed could introduce some new separator here. Actually would be nice to have some generic parameter attribute in note() to. Will need to think about that too...


The Target' in Cycle then should be aParameter' or `Attribute' struct then, and it should contain a list of strings, where such strings can be anything but subgroups. The raw strings then need to be parsed and evaluated by the Cycle event trait, but I think that's OK.

Alternatively, Target could hold a list of strings and nothing else. No index, no string, just:

pub struct Target {
    attributes: Vec<Rc<str>>,
   // or  values: Vec<Rc<str>>,
}
unlessgames commented 2 months ago

For volume I'd actually expect the result to multiply in this case

Interesting. I suppose that would be neat. But it would disallow or make some other uses tricky. For example if you want all your notes to be at a lower volume and add a few accents at max (a fairly common style of pattern): you'd need to be able to set any value not just normalized floats, then you'd have to think a bit more about how to get to the final value, as in

[a b c d]:v0.7 Where I'd like to have only a be at 1.0, I'd have to calculate that I need about 1.43 for it to multiply correctly. For this reason I think keeping it simpler might be better, especially since multiplication makes less sense for things other than volume.

Maybe it could be something you could choose to do explicitly, or we could come up with some syntax to essentially write expression fragments that would get combined like this :d

[a:v1.43 b c d]:*v0.7

But that's starting to look a bit too messy maybe and confusing with the other operators.

It also makes sense to not do anything besides collect and ouput the strings inside the cycle.

emuell commented 2 months ago

Hard to argue about that :)

In doubt, the operation IMHO should be an assignment, so the last argument wins and overrides previous values. Can we agree on that? I think that's how an instrument/sample assignment as target should work too.

unlessgames commented 2 months ago

That makes sense in terms of implementation and assigment in programming but in practice I think the opposite would be much more useful.

Example: I want all events to play on bass so I type in [a b c d e]:bass, Now I realize I'd like to play c on another synth, if assignment was "last wins", I'd have to modify the entire pattern like [a:bass b:bass c:synth e:bass] for it to work, if the assignment was "most specific wins" I'd just put [a b c:synth d]:bass.

I think the general goal about this notation is to write less and achieve more, "last wins" would lead to the opposite in this sense.

I agree that it is counter-intuitive but I still think it aligns better with what you typically do in composition, that is to apply a general parameter for everything and pick a few outliers later.

Also, if the general case wins over the more specific one, it allows for many possible patterns that are different in notation yet have exact same output, which is not nice imo.

"More specific wins" also keeps (on average) the relevant parameters closer to the notes so overall readability would be better. I imagine a lot of scenarios would happen where a user would try to assign something to a note in a list only to realize that some outer parameter was overriding it all from farther away.

emuell commented 2 months ago

Agreed. Maybe not the most obvious behaviour, but definitely more useful.