tmhglnd / mercury

A minimal and human-readable language and environment for the live coding of algorithmic electronic music.
http://www.timohoogland.com/mercury-livecoding
GNU General Public License v3.0
284 stars 13 forks source link

Make specifying time consistent across time(), length() and similar functions #71

Open marcora opened 3 years ago

marcora commented 3 years ago

Currently, the way time argument is interpreted is not consistent across functions that use time as an argument, for example:

time(4) -> 4 measures worth of time length(4) -> 4 milliseconds

I would suggest to make an integer or fraction represent a multiple/subdivision of a measure and add a time unit specificier if other measures of time are desired, for example:

length(4) -> 4 measures worth of time length(4ms) -> 4 milliseconds length(4s) -> 4 seconds

this could be extended to other values, for which there is a default unit, but different units could be specified, for example:

fx(filter low 4) -> filter cutoff at 4 Hz fx(filter low 4kHz) -> filter cutoff at 4000 Hz or fx(filter low 4k) to save typing

tmhglnd commented 3 years ago

Thanks! So a few things that come to mind for now:

Examples

I like time in division (since this is musically important and makes sense)

time(1/4) time(3) time(5/16 1/8)

I like envelopes in milliseconds, because this does not change in length when tempo is changed, and is usually the default time-unit with other synthesizers/software and midi-instruments

shape(5 100) shape(170 15000) length(500)

Possible Solution

So I'm not yet convinced I want to break those conventions, but an intermediate solution could be to work with units for all if you want to make sure the right unit is used.

So for example:

length(4m) -> 4 measures
length(4ms) -> 4 milliseconds
length(4s) -> 4 seconds

or instead of the m for measures some other suggestions I have are to work with / or : instead resulting in:

length(4/) -> 4 measures
length(4:) -> 4 measures

equally valid would then be:

length(3/8)
length(3:8)

another idea could be to interpret floating-points as milliseconds

length(4.) -> 4 milliseconds

This currently does break with the time, because time(0.25) is the same as time(1/4)

marcora commented 3 years ago

I see your point of having the more sensible unit being the default (without the need to specify it after the value). Now that I thought about it some more I think the problem lies with the fact that note length (which should be, by default, expressed in terms of division units) has been conflated with length of an envelope stage (attack, decay, etc) which should be expressed in ms by default. Perhaps those two concepts should be disambiguated. I haven't experimented with synth parameters much yet, but how do you specify "hold time" (note on->note off in midi terms) with synths? There should a notion of note duration (and perhaps even sample/loop duration, especially if time stretching is enabled) and that should be, by default, expressed in time division units.

tmhglnd commented 3 years ago

I've always expressed note-off duration in milliseconds as well, but that is probably because of working with pureData and Max a lot. A piano-roll in a DAW does indeed show the length as a note-timevalue.

For the synth you use the function shape() : https://tmhglnd.github.io/mercury/02-instrument.html#shape

The number of arguments determines if it is default attack+release, attack+release, attack+sustain+release.

marcora commented 3 years ago

It is difficult to specify the duration of a note (note on -> note off) in classical music notation terms (i.e., time divisions, as in piano roll or, well before that, western music notation) by specifying ADSR params. Sustain is normally a level (not time) and the release time should start after the note is released/note off event is fired. Also, by expressing note duration in ms, its time division value will change if tempo is changed.

For these reasons I think that implementing a note duration parameter as it is done in western music notation/midi makes more sense.

A possible solution, without having to mess around with units etc, would be to reserve length() to set note on/off interval in ms and reserve duration() to set note on/off interval in division units as in time().

I believe these two functions should also be available for synths and samples as well and, when specified, that the adsr envelope (or start/stop sample playback) should be applied over time based on the on/off signal generated using these functions (same as what happened in synthesizers when receiving midi note on/off or cv gate in the modular world, as opposed to cv trigger... which is how time() works in Mercury).

marcora commented 3 years ago

So I guess what I am saying is to implement the notion of a gate event (rather than just a trigger) everywhere and not only for the midi object. Not sure why most live coding systems I have tried struggle with this concept (see the way Sonic Pi approaches this in the attachment, which is still a suboptimal attempt to make a trigger ("play") work as a gate by having sustain time in the synth expressed as a fraction or multiple of one measure).

I guess what I am advocating for is that the duration of a note event (gate on->off as in midi note on->off) should be an attribute of the "play" call, not of a sound (or sample) definition. This is, in my opinion, the most natural approach given that electronic gear and daw software have used this concept for as long as I can remember.

This way the AD stages (expressed as a number of some unit of time, e.g., ms) are triggered when the gate opens up, then S stage level (expressed as a number of some unit of amplitude) is maintained until the gate closes up, at which point the R stage (also expressed as time) is triggered. Again, envelope and note duration are kept separate!

The gate time could be expressed as ms or division based on calls to different functions (e.g., duration() and length()) or by implementing some sort of unit specifier as you suggested (e.g., 4 for 4ms and 4/ for 4 measures).

Hope this makes sense.

Screen Shot 2021-01-31 at 08 52 39

marcora commented 3 years ago

I realize that this may be too onerous to implement or something you disagree with from a software design perspective, and that's totally fine. A simple solution would be to have mercury interpret the value of arguments that represet time as ms when no forward slash character or decimal point is present. For example,

4 = 4ms 4/ or 4/1 = 4 measures

  1. or 4.0 = 4 measures 1/4 = 1/4 measure .25 or 0.25 = 1/4 measure (I doubt people would need to specify fractions of a ms)

thus in practice integers are always interpreted as ms and floats (or fractions) always interpreted as a time division

In Sonic Pi time seems to always be expressed time as beats, but I like the idea of having Mercury allowing the flexibility of expressing time in either ms or time division