emuell / afseq

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

Mapping identifiers and numbers to notes in Cycles #13

Open emuell opened 1 month ago

emuell commented 1 month ago

Right now, only note strings and integers are supported in cycle. e.g: cycle("c4 c5") or cycle("48 60")

But it's possible to use arbitrary strings as identifiers, and floating point numbers in the mini notation too: cycle("bd sn")

Such identifiers are currently ignored and will emit nothing.


The easiest and probably most obvious way to map such identifiers to notes (or later other event types) could be using a map function which takes a table or function as argument:

  1. using a Lua table to map events:

table:

cycle("bd sn"):map({ bd = "c4", sn = "d4" })
  1. using a lambda function to dynamically map stuff

function:

---@param value string|number single value from the cycle
---@param emitter_context EmitterContext Runtime context as passed to `emit` functions
---Should return something that can be converted to a Note
cycle("bd sn"):map(function(value, emitter_context) 
  if value ==  "bd" then
    return emitter_context.trigger_note + 1
  elseif value ==  "sn" then
    return "d4"
  else
    return nil
  end
end)

I think 1. could be implemented quite easily using the following rules:

  1. is quite powerful, but also pretty verbose. We could either add some preconfigured map functions to for example ease doing stuff with scales or could add new map_with_scale funtions for that.

@unlessgames you had some whishes idea here, if I remember well. Any idea how this could look like?

unlessgames commented 1 month ago

My wishes were around scales, either with some scaled function that maps integers to scale degrees like

cycle("0 2 [4 5] [6 4]"):scaled("minor"):transposed("d")

Or have the scale object contain common mapping functions that can be used without lambdas, I like this a bit more because you can define a scale at one place and reuse it.

local cmin = scale("penta", "c")
cycle("0 2 [4 5] [6 4]"):map(cmin.notes)
cycle("0 2 [4 5] [6 4]"):map(cmin.chords)

It might be nice to also support something like

local cmaj = scale("major", "c")
cycle("ii V I"):map(cmaj.chords)

But the issue with these approaches I think is that just taking the chords from scales is not very interesting, you often want to invert a chord, extend it or use something out of the scale at some point. If there was some more general chord mapper that would be really handy.

For example we could make use of the Target field from events to build chords on root notes, or do something similar with strings mapped to chords without target or root note.

cycle("c4:m7 f3:7#11 c3:M9i1"):as_chords()
emuell commented 1 month ago

I like this one:

local cmin = scale("penta", "c")
cycle("1 2 [4 5] [6 4]"):map(cmin.notes)
cycle("1 2 [4 5] [6 4]"):map(cmin.chords)

This would work out of the box with the suggested mapping feature.

chords needs an extra argument though (number of notes), so it must be a member function.


But the issue with these approaches I think is that just taking the chords from scales is not very interesting, you often want to invert a chord, extend it or use something out of the scale at some point. If there was some more general chord mapper that would be really handy.

For example we could make use of the Target field from events to build chords on root notes, or do something similar with strings mapped to chords without target or root note.

cycle("c4:m7 f3:7#11 c3:M9i1"):as_chords()

Actually would be nice if the existing note chord's syntax could be directly used in cycle as well. I had borrowed that from Tidal already.

https://github.com/emuell/afseq/blob/982dee3c90490b83417aba063b5b9c13c6b6ccdb/types/nerdo/library/note.lua#L23

cycle("c4'm7 f3'7#11 c3'M9")

I guess this could be added directly to the cycle impl - without any custom mapping?


Good point about the : operator:

Both should be passed as arguments to a map lambda:

local cmin = scale("c", "minor")
local function my_scale_map(value, target) 
  local degree, note_count = value, target
  return cmin:chord(degree, note_count) 
end
cycle("I:3 V:3 IIV:5"):map(my_scale_map)

quite verbose, but I personally don't have a problem with that.

emuell commented 2 weeks ago

With the basic mapping working, I'll start looking into automatically mapping chords as c4'maj in cycles now and allowing chords as values in maps:

local cmin = scale("c", "minor")
cycle("i v vi iv"):map(function(context, degree) 
  return cmin:chord(degree) 
end)

Once that's done, we can start thinking about how to make some of the more common things, like chords, easier to map.