This functor defines an abstraction for using numerical functions as envelopes (function envelopes, or "fenvs"), and provides a rich set of functions/methods to generate, combine and transform these envelopes.
See testing/Fenv-test.oz for examples (using a Gnuplot interface for envelope visualisation).
NB: This functor aims for a high degree of flexilibity in envelope creation and manipulation instead of efficiency. But nowadays, machines are rather fast...
Functor
Import
- GUtils at "x-ozlib://anders/strasheela/source/GeneralUtils.ozf"
- LUtils at "x-ozlib://anders/strasheela/source/ListUtils.ozf"
- Score at "x-ozlib://anders/strasheela/source/ScoreCore.ozf"
- Out at "x-ozlib://anders/strasheela/source/Output.ozf"
- GPlot(plot:Plot) at "x-ozlib://anders/strasheela/Gnuplot/Gnuplot.ozf"
Export
Define
Defines a data structure for envelopes based on the notion of numeric functions (a function envelope or "fenv").
class Fenv
feat !FenvType
- init(env:Env max:Mx min:Mn rangeIsForArgumentFun:RangeIsForArgumentFun)
- getEnv($)
- y($ X)
- toList($ N)
- toList_Int($ N add:Add mul:Mul)
- toPairs($ N)
- plot(n:N ...)
end
fun{IsFenv X}
Returns true if X is a Fenv instance and false otherwise.
fun{FenvSeq FenvsAndPoints}
Combines an arbitrary number of fenvs to a single fenv. Expects its args as a list in the form [fenv num fenv num ... fenv]. The numbers between the fenvs specify the start resp. end point of a certain fenv. All numbers should be between 0--1 (exclusive).
fun{FuncsToFenv Funcs Args}
Converts a list of unary numeric functions to a single fenv. The arguments min and max a given for all functions and the functions are equally spaced in the fenv.
fun{Osciallator MyFenv N}
Defines a new fenv by repeating givenm fenv n times.
fun{PointsToFenv Func Points}
Converts a list of points into a single env. A point is an x-y-pair as [Xi Yi]. X values of the points range from 0--i (including), e.g., [[0.0 Y1] [X2 Y2] ... [1.0 Yn]]. The function Func defines the shape of the fenv segments and must return a fenv. It expects a list of four numeric arguments, which describe the start and end points of the segment in the form [X1 Y1 X2 Y2].
fun{LinearFenv Points}
Defines a fenv which interpolates the given points by a linear function. Expects a list of x-y-pairs as [[0.0 Y1] ... [1.0 Yn]].
fun{SinFenv Points}
Defines a fenv which interpolates the given points by a sin function, using a full wave length. This results in a fenv without edges, however, this fenv is rather 'curvy'. Expects a list of x-y-pairs as [[0.0 Y1] ... [1.0 Yn]].
NB: in the lisp library, this was macro sin-env1.
fun{SinFenv2 Points}
Defines a fenv which interpolates the given points by a sin function. Using only the intervals [0,pi/2] and [pi, 3pi/4], which results in edges but is less 'curvy' than SinFenv. Expects a list of x-y-pairs as [[0.0 Y1] ... [1.0 Yn]].
NB: in the lisp library, this was macro sin-env.
fun{ConstantFenv Y}
Returns Fenv which outputs Y (a float) for any X.
fun{SinOsc N Args}
Defines a fenv of sin shape with n periods. Args are mul and add, as for ScaleFenv.
fun{Saw N Args}
Defines a fenv of saw shape (ascending) with n periods. Args are mul and add, as for ScaleFenv.
fun{Triangle N Args}
Defines a fenv of triangle shape with n periods. Args are mul and add, as for ScaleFenv.
fun{Square N Args}
Defines a fenv of square shape with n periods. Args are mul and add, as for ScaleFenv.
fun{Pulse N Args}
Defines a fenv of pulse shape with n periods. Args are min (lowest value), max (highest value), and width (pulse width between 0.0 and 1.0). The oscillator starts with the highest value.
fun{ReverseFenv MyFenv}
Reverses MyFenv (i.e. flips it at x=0.5).
NB: ReverseFenv is defined only for the valid Fenv domain 0.0 .. 1.0.
fun{InvertFenv MyFenv}
Inverses MyFenv (i.e. flips it at y=0.0).
fun{Reciprocal MyFenv}
Returns a Fenv which is the reciprocal of the given Fenv, i.e., 1/fenv.
fun{CombineFenvs CombiFunc Fenvs}
Returns a fenv which combines the given fenvs with an n-ary numeric function. Fenvs is a list which consists of fenvs and floats (representing constant fenvs) in any order. The combine-func expects a list with as many floats as correspond to Fenv values (in their order and at the same x), and returns a float.
fun{ScaleFenv MyFenv Args}
Scale MyFenv with Args: arg mul is factor and arg add is summand (addend).
fun{RescaleFenv MyFenv Args}
Returns a new Fenv which rescales the given y-range of MyFenv (defaults: oldmin:~1.0, oldmax:1.0) into a new range (defaults: newmin:0.0, newmax:1.0).
All these four arguments can be fenvs as well.
!! NB: RescaleFenv is buggy. Problems with neg. numbers (see examples).
fun{Waveshape Fenv1 Fenv2}
Returns a fenv which reads Fenv1 'through' Fenv2: the y value of Fenv2 (at a given x value) is used as x for Fenv1. to access the y of Fenv1 (the y of Fenv1 is returned). Compared with waveshaping in signal processing, Fenv1 is the "transfer function" and Fenv2 is the "input signal".
NB: Take care to keep the output of fenv2 in interval [0,1].
NB: for more simple use, I should think about more complex def which allows for Fenv2 values going beyond the interval [0,1] (or be automatically scaled into that interval). I could use a plain function as transfer function, but using the tools for generating fenvs can be helpful. Alternatively, I can simply remove the condition which restricts fenvs to [0,1].
fun{FenvSection MyFenv Args}
Returns fenv which is a section of given fenv. y value at 0/1 of returned fenv is y value of given fenv at min/max. Both min and max must be in the interval [0, 1].
fun{Integrate MyFenv Step}
Returns the integral fenv of fenv.
Performs numerical integration internally whenever a value of the returned fenv is accessed, which can be computationally expensive.
Step (a float in [0.0 0.5]) specifies the resolution of the numeric integration: the smaller Step, the more accurate the integration and the more expensive the computation. Step=0.01 results in 100 "function slices".
Note: implementation currently always uses Simpson's rule rule for the approximation (based on a polynomial of order 2, pretty good :), it case this is too computationally expensive, could be made user-controllable if necessary (see implementation).
fun{TempoCurveToTimeMap MyFenv Step}
Transforms a fenv expressing a normalised tempo curve into a fenv expressing a normalised time map. Step (a float) specifies the precision (and efficiency!) of the transformation, see Integrate's doc for details. A tempo curve expresses a tempo factor, i.e., f(x) = 1 results in no tempo change. A normalised time map maps score time to performance time.
Private Terminology: normalised time shift functions, time map functions and tempo curves: fenvs where x values denote the score time (usually of a temporal container) which is mapped into [0,1]: 0 corresponds to the container's start time, and 1 corresponds to the container's end time. See ContainerFenvY.
NB: normalised time map fenvs cannot be combined by function combination (x values for fenvs are always in [0,1]). Instead, either combine tempo curve and time shift fenvs, or combine plain and un-normalised time map functions (i.e. no fenvs).
fun{TempoCurveToTimeShift MyFenv Step}
... this is probably not a good idea, but works for certain cases.
?? BUG: slower tempo changes between fixed min/max tempo values result in larger [absolute] tempo changes.
fun{TempoCurveToTimeShift_KeepingDur MyFenv Args}
[Experimental] Transforms a fenv expressing a normalised tempo curve into a fenv expressing a normalised time shift function. However, the resulting time shift function is deformed such that it always ends in 0.0 (so it ends at score time).
NB: the resulting time shift function does not faithfully express the tempos of the given tempo curve (this depends on the arg mul, see below), but the overall shape is similar and therefore (hopefully) simplifies creating natural time shift functions expressing tempo changes.
Args:
step (default 0.01): stepsize for integration, see there.
mul (default 1.0): scaling factor for the resulting fenv. Try setting it to some specific note duration... Depends on intended tempo change and also on duration of phrase time shifted.
?? BUG: slower tempo changes between fixed min/max tempo values result in larger [absolute] tempo changes.
fun{TimeShiftToTimeMap TS}
Expects a fenv representing a normalised time shift function and returns a fenv representing a normalised time map function. A time shift function expresses how much is added to a score time to yield a performance time, i.e., f(x) = 0 causes performance time to be score time. A normalised time map maps score time to performance time.
Private Terminology: normalised time shift functions, time map functions and tempo curves: fenvs where x values denote the score time (usually of a temporal container) which is mapped into [0,1]: 0 corresponds to the container's start time, and 1 corresponds to the container's end time. See ContainerFenvY.
NB: normalised time map fenvs cannot be combined by function combination (x values for fenvs are always in [0,1]). Instead, either combine tempo curve and time shift fenvs, or combine plain and un-normalised time map functions (i.e. no fenvs).
fun{TimeMapToTimeShift MyFenv}
... this is perhaps not a good idea, but works for certain cases.
fun{ConcatenateTempoCurves Specs}
Concatenates a sequence of successive tempo curve fenvs. Specs is a list of pairs and has the form [Fenv1#Dur1 Fenv2#Dur2 ... FenvN#DurN], where FenvI is a tempo curve fenv and DurI (a float) is the score time duration of this tempo curve. Returned is a single tempo curve fenv.
NB: in most use-cases the sequence of successive tempo curve fenvs should start at score time 0 and span over the entire score so that the global tempo curve fenv is the result. If you concatenate a tempo curve sequence which does not start at score time 0, you should decide whether the resulting tempo curve fenv starts at the performance or score start time of its first sub-tempo curve (i.e., whether a smooth continuation of previous tempo changes is intended or not).
fun{TemporalFenvY MyFenv Start Duration MyTime}
Accesses the y-value of MyFenv which starts at time point Start (a float) for time interval Duration (a float). The fenv x-value 0.0 corresponds to the start time and the fenv x-value 1.0 coresponds to the resulting end time. MyTime (a float) is any time between the start and end time. All times are score times measured in the same time unit.
fun{ItemFenvY MyFenv MyItem MyTime}
Accesses the y-value of MyFenv which is associated with a temporal item MyItem. The fenv x-value 0.0 corresponds to the item's start time and the fenv x-value 1.0 coresponds to the item's end time. MyTime (a float) is any time between MyItem's start and end time. MyTime is a score time measured in the time unit of MyItem.
fun{FenvToMidiCC MyFenv N Track StartTime EndTime Channel Controller}
Transforms a Fenv into a list of continuous MIDI controller events. N events are output between StartTime and EndTime (two ints, given in MIDI ticks) at Channel (an int).
Controller denotes which controller is output. Possible values are one of the atoms pitchbend, and channelAftertouch, or one of the pairs cc#Number (Number is the controller number) and polyAftertouch#Note (Note denotes the note pitch).
Finally, Controller can be a function expecting 4 arguments and returning a MIDI event. For example, the volume Controller can be defined as follows
fun {$ Track Time Channel Value}
{Out.midi.makeCC Track Time Channel 7 Value}
end
NOTE: no implicit support for any tempo curves etc. Instead, adapt StartTime and EndTime (and possibly transform MyFenv) outside FenvToMidiCC.
fun{ItemFenvToMidiCC MyFenv N Track MyItem Channel Controller}
Like FenvToMidiCC, but here the Fenv is associated with a temporal item MyItem, whose start and end times are taken.
NOTE: no support for any tempo curves etc.
fun{ItemFenvsToMidiCC MyItem Args}
Expects a temporal item which defines fenvs in an info-tag 'fenvs', and returns a list of continuous MIDI controller events for all its fenvs. Each fenvs is defined by a pair Controller#Fenv, where Controller can take all values defined for FenvToMidiCC. Fenvs directly specify the controller values (e.g., if Controller is pitchBend, then the Fenv range is 0.0 to 16383.0, and the value 8192.0 means no pitchbend). Note that for any controller only a single Fenv should be defined at any time (otherwise they conflict with each other).
Example: fenvs((cc#1)#{Fenv.linearFenv [[0.0 0.0] [1.0 127.0]]})
Args:
ccsPerSecond: how many CC events are created per second (a float).
track: MIDI track to output, default 2 (suitable for more cases)
channel: midi channel to output, default nil (if nil, MIDI note object CCs are output to its channel and all other to channel 0)
Timeshift fenvs affect the start and end of the continuous MIDI controller events, but not their "spacing".
fun{ItemTempoCurveToMidi MyItem Args}
Expects a temporal item which defines a tempo curve Fenv in a info-tag 'globaltempo', and returns a list of MIDI tempo events. Returns nil in case MyItem defines no tempo curve. The tempo fenv values are in beats per minute. Due to restrictions of the MIDI protocoll, only a single global tempo is supported (note that sequencers may restrict the import of such data in a MIDI files). If multiple tempi are defined "in parallel" or nested, then "conflicting" MIDI tempo events are output.
Example: globaltempo({Fenv.linearFenv [[0.0 30.0] [1.0 240.0]]})
Args:
ccsPerSecond: how many tempo events are created per second (a float).
track: MIDI track to output, default 2 (suitable for more cases)
Time shift fenvs affect the start and end of the tempo events, but not their "spacing".
proc{RenderAndPlayMidiFile MyScore Args}
This procedure is like Out.midi.renderAndPlayMidiFile, but it additional supports continuous controllers and a global tempo curve, expressed in the score by fenvs. Also, microtonal music is supported (using pitchbends).
Supported score format:
The info-tag 'channel', given to a note or temporal container, sets the MIDI channel for this item and all contained items. Example: channel(0). If a channel is defined multiple times, then a setting in a lower hierarchical level overwrites higher-level settings.
The info-tag 'program', given to a note or temporal container, results in a program change message with the specified program number at the beginning of the item. Example: program(64) given to a sequential container changes the program at the beginning of the container (note that programs are not automatically sitched back). Remember that many MIDI instruments number their patches (programs) from 1 to 128 rather than the range 0 to 127 actually used within MIDI files. When interpreting ProgramNum values, note that they may be one less than the patch numbers given in an instrument's documentation.
With the info-tag 'fenvs', given to a note or temporal container, you can specify a tuple of continuous controllers for the duration this item. Each Fenv spec is a pair Controller#Fenv, where Controller is defined as for Fenv.fenvToMidiCC. Example: fenvs((cc#1)#MyFenv). Fenvs directly specify the controller values (e.g., if Controller is pitchBend, then the Fenv range is 0.0 to 16383.0, and the value 8192.0 means no pitchbend). Note that for any controller only a single Fenv should be defined at any time (otherwise they conflict with each other).
The info-tag 'timeshift': see doc of Out.midi.outputMidiFile.
The info-tag 'globaltempo', given to a temporal container, specifies a tempo curve (a fenv) and is output as MIDI tempo events. Example: globaltempo(MyTempoFenv). Tempo values are specified in BPM. Due to restrictions of the MIDI protocoll, only a single global tempo is supported (note that sequencers may restrict the import of such data in a MIDI files). If multiple tempi are defined "in parallel" or nested, then "conflicting" MIDI tempo events are output.
All arguments of Out.midi.renderAndPlayMidiFile are supported. RenderAndPlayMidiFile is defined by calling Out.midi.renderAndPlayMidiFile with special clauses (namely for the tests isNote, and Score.isTemporalContainer). Clauses given to RenderAndPlayMidiFile are again appended at the beginning of the list of clauses (and so potentially overwrite the clauses defined by this procedure).
Args:
'ccsPerSecond' (float, default 10.0): how many continuous controller events are created per second for every Fenv (the spacing of CC events may be affected).
'ccsPerEvent' (float or false, default false): how many continuous controller events are created per event. If this argument is not false, then its setting overwrites arg 'ccsPerSecond'.
'resolution' (default 2): pitchbend resolution in semitones. Its default value 2 corresponds to the standard pitch bend range of -2..2 semitones, i.e., 4096 steps/100 cents.
'channelDistributions' (record of lists with only int features including 0, default unit): Microtonal pitches are detuned by pitch bend, i.e. always all notes of a given channel are detuned. This arg specifies which score channel (midi note param, info tag or default 0) is output to which actual output channel. For example, for distributing the score channel 0 over the actual channels 0-7 set channelDistributions to unit(0: [0 1 2 3 4 5 6 7]). The number of output channels (8 in the example) should correspond to the maximum number of simultaneous notes in the score channel (0 in the example). Score channels for which no output channels are specified are output to themselves and thus are only suitable for a single monophonic voice. By default, no output channels are specified at all, so pitchbend always changes all notes of a channel (fine for microtonal music with only a monophonic voice per MIDI channel). Currently, only 16 MIDI chans are supported in total (no multiple ports).
Additionally, the args of Out.midi.outputMidiFile are supported.
NOTE: microtonal pitchbend and pitchbend given explicitly as fenv overwrite each other.
fun{MakeRenderAndPlayMidiFile_Scala TemperamentMapping Args}
MakeRenderAndPlayMidiFile_Scala returns a procedure for outputting microtonal music for MIDI instruments with statically detuned MIDI pitch classes (e.g. with a Scala file). For outputting more than 12 tones per octave, MakeRenderAndPlayMidiFile_Scala assumes that pitches are distributed over multiple MIDI channels with different tunings. The returned function customises RenderAndPlayMidiFile.
Supported score format: see RenderAndPlayMidiFile doc. The behaviour for the following info-tags is changed compared with Fenv.renderAndPlayMidiFile.
The info-tag 'channel', given to a note or temporal container, sets the MIDI channels (plural!) for this item and all contained items. Because the function returned by MakeRenderAndPlayMidiFile distributes microtonal pitches over multiple MIDI channels, a list of multiple channels should be specified. Example: channel([0 1]). If a channel is defined multiple times, then a setting in a lower hierarchical level overwrites higher-level settings.
The argument TemperamentMapping (record with int feats and pair values, required arg) specifies the mapping of tempered Strasheela pitch classes (unit of measurement depends on PitchesPerOctave) to pairs MidiPC#ChanOffset, where MidiPC is the corresponding MIDI pitch class and ChanOffset is the channel offset that should be added to output this pitch class to the correctly retuned channel. For example, if you implement 24-TET with your MIDI instrument by 12-TET in a first channel and a second 12-TET transposed up by 50 cent in a second channel, then the 24-TET PC 1 (C raised by 50 cent) would be mapped to the MidiPC#ChanOffset pair of 0#1 (PC 0 on the second channel, which is raised by 50 cent). In other words, TemperamentMapping for 24-TET could be defined as follows
unit(0:0#0 1:0#1 2:1#0 3:1#1 4:2#0 ...)
Optionally, an octave offset value can be given, so that a mapping specification becomes MidiPC#ChanOffset#OctaveOffset. For example, if OctaveOffset is ~1, then the resulting MIDI note will be transposed down by an octave.
optional Args (to both MakeRenderAndPlayMidiFile_Scala and the returned procedure, see doc of Fenv.renderAndPlayMidiFile for further args):
'file_postfix' (default nil): VS that is appended at end of the output filename. It is intended to clearly denote the Scale tuning file for the resulting MIDI file.
NOTE: MakeRenderAndPlayMidiFile_Scala should not be used with MIDI notes (instances of Out.midi.midiNoteMixin), as their channel parameter would confuse the channel mapping.
End