/** %% 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... %% */ %% %% Status: almost all of the original func-env Lisp library has been ported to Oz here. %% Missing: %% %% - transformation of fenv into a Common Music pattern, which can be nested with other CM patterns etc (that is not possible to port to Oz, as CM is a Lisp library) %% - use of fenvs for creating random distribution %% %% %% functor import % System % Browser(browse:Browse) % temp for debugging 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 Fenv IsFenv FenvSeq FuncsToFenv Osciallator PointsToFenv LinearFenv SinFenv SinFenv2 ConstantFenv SinOsc Saw Triangle Square Pulse ReverseFenv InvertFenv Reciprocal %% !! to test CombineFenvs ScaleFenv RescaleFenv Waveshape FenvSection Integrate TempoCurveToTimeMap TempoCurveToTimeShift TempoCurveToTimeShift_KeepingDur TimeShiftToTimeMap TimeMapToTimeShift ConcatenateTempoCurves TemporalFenvY ItemFenvY FenvToMidiCC ItemFenvToMidiCC ItemFenvsToMidiCC ItemTempoCurveToMidi RenderAndPlayMidiFile MakeRenderAndPlayMidiFile_Scala prepare FenvType = {Name.new} define /** %% Defines a data structure for envelopes based on the notion of numeric functions (a function envelope or "fenv"). %% */ class Fenv feat !FenvType:unit attr env /** %% Builds a env from a given numeric function. Env is a unary numeric function which expects and returns a float. If RangeIsForArgumentFun is true, then the interval [0,1] of the resulting fenv is mapped to [min, max] of the argument function. Otherwise, the interval [min,max] of the resulting fenv is mapped to [0,1] of the argument function. %% NB: init blocks as long as Env is undetermined (Env is only an optional argument because the Score.scoreObject method getInitClassesVS requires this for score archiving). %% */ %% !! accessing an envelope value outside its range is explicitly disabled here. Is that too strict? Is that too costly (done for every embedded env!)? meth init(env:Env<=_ min:Mn<=0.0 max:Mx<=1.0 rangeIsForArgumentFun:RangeIsForArgumentFun<=true) if RangeIsForArgumentFun then @env = fun {$ X} if {Not (0.0 =< X andthen X =< 1.0)} then {Exception.raiseError strasheela(failedRequirement X "Must be in [0.0, 1.0]")} end {Env Mn + (X * (Mx - Mn))} end else @env = fun {$ X} if {Not (Mn =< X andthen X =< Mx)} then {Exception.raiseError strasheela(failedRequirement X "Must be in ["#Mn#", "#Mx#"]")} end {Env (X - Mn) / (Mx - Mn)} end end end /** %% Returns the unary numeric function of self. %% */ meth getEnv($) @env end /** %% Access the y value (a float) of fenv at X (a float). %% */ meth y($ X) {@env X} end /** %% Samples the fenv from 0.0 to 1.0 (including) and collects samples in a list. N is the number of samples (an integer). If N=1, only the last env value is returned. Returns a list of floats. %% */ meth toList($ N<=100) if N==1 then [{self y($ 1.0)}] %% tmp: for i from 0 to 1 by (/ 1 (1- n)) else N1 = {IntToFloat N-1} in for I in 0..N-1 collect:C do {C {self y($ {IntToFloat I}/N1)}} end end end /** %% Same as toList, but rounds the results to integers. The output can be scaled (before the rounding) with the summand Add and factor Mul (both floats). %% */ meth toList_Int($ N<=100 add:Add<=0.0 mul:Mul<=1.0) {Map {self toList($ N)} fun {$ X} {FloatToInt X*Mul+Add} end} end /** %% Samples the fenv from 0.0 to 1.0 (including) and collects the x-y-pairs as sublists in a list: [[X1 Y1] ... [Xn Yn]]. N is the number of samples (an integer). If N=1, only the last env value is returned. %% */ meth toPairs($ N<=100) if N==1 then {self y($ 1)} %% tmp: for i from 0 to 1 by (/ 1 (1- n)) else N1 = {IntToFloat N-1} in for I in 0..N-1 collect:C do X = {IntToFloat I}/N1 in {C [X {self y($ X)}]} end end end /** %% Plots the fenv by calling gnuplot in the background. N (an integer) is the number of fenv samples (see method toList). See the documentation of the procedure Gnuplot.plot for further arguments supported (the Gnuplot.plot args x and z are not supported). %% */ %% !!?? is it more efficient to create xs values again (additional loop, IntoToFloat, and float division) or to LUtils.matTrans the output of toPairs? meth plot(n:N<=100 ...) = Args {Plot {self toList($ N)} {Adjoin {Record.subtractList Args [n z]} unit(x:local N1 = {IntToFloat N-1} in for I in 0..N-1 collect:C do {C {IntToFloat I}/N1} end end)}} end end /** %% Returns true if X is a Fenv instance and false otherwise. %% */ fun {IsFenv X} {Not {GUtils.isFS X}} andthen % undetermined FS vars block on Object.is {Object.is X} andthen {HasFeature X FenvType} end %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% %%% Fenv Generators %%% local fun {Aux Fenvs Points} % points: [0 ... 1] %% !! inefficient: for every fenv value access, a fenv is created {New Fenv init(env:fun {$ X} %% (position x points :test #'<) %% !!?? < or > Pos = {LUtils.findPosition Points fun {$ P} X < P end} MyFenv = if Pos \= nil then {New Fenv init(env:{{Nth Fenvs Pos-1} getEnv($)} min:{Nth Points Pos-1} max:{Nth Points Pos} rangeIsForArgumentFun:false)} else {List.last Fenvs} end in {MyFenv y($ X)} end)} % %% !!?? refactored above into the following -- OK?? % Pos = {LUtils.findPosition Points fun {$ P} X < P end} % Fenv = if Pos \= nil % then % {New Fenv init(env:{{Nth Fenvs Pos-1} getEnv($)} % min:{Nth Points Pos-1} % max:{Nth Points Pos} % rangeIsForArgumentFun:false)} % else {List.last Fenvs} % end % in % Fenv end in /** %% 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 {FenvSeq FenvsAndPoints} Points = {Append 0.0|{LUtils.everyNth FenvsAndPoints.2 2 0} [1.0]} % 0, , 1 Fenvs = {LUtils.everyNth FenvsAndPoints 2 0} in {Aux Fenvs Points} end /** %% 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 {FuncsToFenv Funcs Args} Defaults = unit(min:0.0 max:1.0) As = {Adjoin Defaults Args} L = {IntToFloat {Length Funcs}} Points = for I in 0..{FloatToInt L} collect:C do {C {IntToFloat I}/L} end Fenvs = {Map Funcs fun {$ F} {New Fenv init(env:F min:As.min max:As.max)} end} in {Aux Fenvs Points} end /** %% Defines a new fenv by repeating givenm fenv n times. %% */ fun {Osciallator MyFenv N} Fenvs = {Map {MakeList N} fun {$ _} MyFenv end} Nf = {IntToFloat N} Points = for I in 0..N collect:C do {C {IntToFloat I}/Nf} end in {Aux Fenvs Points} end end %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% %%% Segment fenvs %%% /** %% 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 {PointsToFenv Func Points} Xs = {Map Points fun {$ [X _]} X end} Ys = {Map Points fun {$ [_ Y]} Y end} Fenvs = {Map {LUtils.matTrans [{LUtils.butLast Xs} {LUtils.butLast Ys} Xs.2 Ys.2]} Func} in {New Fenv init(env:fun {$ X} Pos = {LUtils.findPosition Xs.2 fun {$ MyX} X =< MyX end} in {{Nth Fenvs Pos} y($ X)} end)} end /** %% 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 {LinearFenv Points} {PointsToFenv fun {$ [X1 Y1 X2 Y2]} {New Fenv init(env:fun {$ X} (Y2-Y1) * X + Y1 end min: X1 max: X2 rangeIsForArgumentFun:false)} end Points} end /** %% 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 {SinFenv Points} {PointsToFenv fun {$ [X1 Y1 X2 Y2]} {New Fenv init(env:fun {$ X} ({Sin X*GUtils.pi - 0.5*GUtils.pi} * 0.5 + 0.5) * (Y2-Y1) + Y1 end min: X1 max: X2 rangeIsForArgumentFun:false)} end Points} end /** %% 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 {SinFenv2 Points} {PointsToFenv fun {$ [X1 Y1 X2 Y2]} {New Fenv init(env:fun {$ X} {Sin (X * GUtils.pi * 0.5)} * (Y2-Y1) + Y1 end min: X1 max: X2 rangeIsForArgumentFun:false)} end Points} end /* % ;; !!! noch voellig falsch: die Steilheit stimmt nicht !!! % (defun expon-env-fn (points) % (points->env #'(lambda (x1 x2 y1 y2) % (make-env1 #'(lambda (x) % (+ (expt (/ y2 y1) x) y1 -1)) % :min x1 :max x2)) % points)) */ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% %%% %%% /* %% expects list values (which will be the result of sampling [i.e. transforming to a list] the returned Fenv by as many values as there are samples. Samples are interpolated by InterpolationFenv. %% Implementation uses tuple of samples for efficient access of the sample values relevant for given X. fun {SamplesToFenv Samples InterpolationFenv} end */ /** %% Returns Fenv which outputs Y (a float) for any X. %% */ fun {ConstantFenv Y} {New Fenv init(env:fun {$ _} Y end)} end %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% %%% A few oscillators %%% !! Implementation not necessarily most efficient... %%% !! no phase defined... %%% /** %% Defines a fenv of sin shape with n periods. Args are mul and add, as for ScaleFenv. %% */ fun {SinOsc N Args} Defaults = unit(mul:1.0 add:0.0) As = {Adjoin Defaults Args} in {ScaleFenv {New Fenv init(env:fun {$ X} {Sin X} end min:0.0 max:GUtils.pi*2.0*{IntToFloat N})} unit(mul:As.mul add:As.add)} end /** %% Defines a fenv of saw shape (ascending) with n periods. Args are mul and add, as for ScaleFenv. %% */ fun {Saw N Args} Defaults = unit(mul:1.0 add:0.0) As = {Adjoin Defaults Args} in {ScaleFenv {Osciallator {New Fenv init(env:fun {$ X} 2.0*X - 1.0 end)} N} unit(mul:As.mul add:As.add)} end /** %% Defines a fenv of triangle shape with n periods. Args are mul and add, as for ScaleFenv. %% */ fun {Triangle N Args} Defaults = unit(mul:1.0 add:0.0) As = {Adjoin Defaults Args} in {ScaleFenv {Osciallator {LinearFenv [[0.0 ~1.0] [0.5 1.0] [1.0 ~1.0]]} N} unit(mul:As.mul add:As.add)} end /** %% Defines a fenv of square shape with n periods. Args are mul and add, as for ScaleFenv. %% */ fun {Square N Args} Defaults = unit(mul:1.0 add:0.0) As = {Adjoin Defaults Args} in {ScaleFenv {Osciallator {New Fenv init(env:fun {$ X} if X < 0.5 then ~1.0 else 1.0 end end)} N} unit(mul:As.mul add:As.add)} end /** %% 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 {Pulse N Args} Defaults = unit(min:~1.0 max:1.0 width:0.5) As = {Adjoin Defaults Args} in {Osciallator {FenvSeq [{ConstantFenv As.max} As.width {ConstantFenv As.min}]} N} end /* %% Outputs a fenv composed of (evenly distributed) constant functions defined by numbers. %% fun {Steps Numbers} {FuncsToFenv {Map Numbers fun {$ Num} fun {$ _} Num end end}} end fun {RandomSteps N Args} Defaults = unit(min:~1.0 max:1.0) As = {Adjoin Defaults Args} in {Steps for _ in 1..N collect:C do %% create random number between As.min and As.max %% %% {GUtils.random Max} returns integer in interval [0, Max-1]... {C } end for N do } end */ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% %%% Fenv transformations %%% /** %% 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 {ReverseFenv MyFenv} {New Fenv init(env:fun {$ X} {MyFenv y($ 1.0-X)} end)} end /** %% Inverses MyFenv (i.e. flips it at y=0.0). %% */ fun {InvertFenv MyFenv} {New Fenv init(env:fun {$ X} {MyFenv y($ X)} * ~1.0 end)} end /** %% Returns a Fenv which is the reciprocal of the given Fenv, i.e., 1/fenv. %% */ fun {Reciprocal MyFenv} {New Fenv init(env:fun {$ X} 1.0 / {MyFenv y($ X)} end)} end /** %% 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 {CombineFenvs CombiFunc Fenvs} {New Fenv init(env:fun {$ X} {CombiFunc {Map Fenvs fun {$ MyFenv} if {IsFloat MyFenv} then MyFenv elseif {IsFenv MyFenv} then {MyFenv y($ X)} else {Exception.raiseError strasheela(failedRequirement MyFenv "Must be either float or fenv")} unit % never returned end end}} end)} end /** %% Scale MyFenv with Args: arg mul is factor and arg add is summand (addend). %% */ fun {ScaleFenv MyFenv Args} Defaults = unit(mul:1.0 add:0.0) As = {Adjoin Defaults Args} in %% !! CombineFenvs calls buggy: enter fun expecting list {CombineFenvs fun {$ [X Y]} X + Y end [As.add {CombineFenvs fun {$ [X Y]} X * Y end [As.mul MyFenv]}]} end /** %% 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). %% */ %% see CM 2.3.4 definition of rescale fun {RescaleFenv MyFenv Args} Defaults = unit(oldmin:~1.0 oldmax:1.0 newmin:0.0 newmax:1.0) As = {Adjoin Defaults Args} in {CombineFenvs fun {$ [X Y]} X + Y end [{CombineFenvs fun {$ [X Y]} X * Y end [{CombineFenvs fun {$ [X Y]} X / Y end [{CombineFenvs fun {$ [X Y]} X - Y end [As.newmax As.newmin]} {CombineFenvs fun {$ [X Y]} X - Y end [As.oldmax As.oldmin]}]} {CombineFenvs fun {$ [X Y]} X + Y end [MyFenv As.oldmin]}]} As.newmin]} end /** %% 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]. %% */ %% !! TODO: rethink this approach... fun {Waveshape Fenv1 Fenv2} {New Fenv init(env:fun {$ X} {Fenv1 y($ {Fenv2 y($ X)})} end)} end /** %% 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 {FenvSection MyFenv Args} Defaults = unit(max:1.0 min:0.0) As = {Adjoin Defaults Args} in if {Not ((0.0 =< As.min andthen As.min =< 1.0) andthen (0.0 =< As.max andthen As.max =< 1.0))} then {Exception.raiseError strasheela(failedRequirement [As.min As.max] "Must be both in [0.0, 1.0]")} end {New Fenv init(env: {MyFenv getEnv($)} min:As.min max:As.max)} end /* ;; noise... ;; hp-filter (env) ;; lp-filter (env) */ /** %% 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). %% */ %% Note: I tried to improve the efficiency by memoization, but without success. Memoizing the integration of a single "function slice" is less efficient than recomputing it, and memoizing the integration of an inceasing function interval does not work, because my memoization does not work for recursive functions (I can not overwrite the definition of a function, and recursively calling the memoized function had no effect -- I really tried this for hours!). %% NOTE: instead of 'simply' memoizing the function CompositeIntegral, I may do the internal caching manually. I would redefine CompositeIntegral to expect to integers, and change various details accrodingly: e.g., in the call to CompositeIntegral, X would be transformed into an integer {FloatToInt X/Step}. For the call to MyDefIntegral I then need floats again.. %% However, after the many attempts with memoization so far I am not sure whether this is worth the effort fun {Integrate MyFenv Step} %% select approximation % MyDefIntegral = DefiniteIntegral_Trapezoidal MyDefIntegral = DefiniteIntegral_Simpson F = {MyFenv getEnv($)} /** %% Returns the definite integral of function F in [A, B] (two floats), approximation uses Step (a float) size between A and B. %% */ %% memoize CompositeIntegral: A and B must be computed into 'grid' of Step for memoization to work -- outside this function. So, I should convert A and B to integers for the memoized function, and back to a float for calling MyDefIntegral %% However, Memo.memoize does not work recursively (I really tried!). fun {CompositeIntegral A B} B2 = B-Step in if B2 =< 0.0 then {MyDefIntegral F A B} else {MyDefIntegral F B2 B} + {CompositeIntegral A B2} end end in {New Fenv init(env:fun {$ X} {CompositeIntegral 0.0 X} end)} end % proc {IntegrateFenv MyFenv Step ?Result} % MyDefIntegral = DefiniteIntegral_Trapezoidal % select approximation % % ClearMemoFun % DefIntegral_Memo = {Memo.memoize % fun {$ [A B]} {MyDefIntegral {MyFenv getEnv($)} A B} end % % ClearMemoFun % _} % %% test: no memoization % % DefIntegral_Memo = fun {$ [A B]} {MyDefIntegral {MyFenv getEnv($)} A B} end % /** %% Returns the definite integral of function F in [A, B] (two floats), approximation uses Step (a float) size between A and B. % %% */ % fun {CompositeIntegral A B} % %% from A to B with Step-size % Xs = for X in A;X= 0 end) clauses:nil track:2 %% new args ccsPerSecond:10.0 ccsPerEvent:false resolution: 2 channelDistributions: unit) As = {Adjoin Defaults Args} FullChanDistributions = {Adjoin unit(0:[0] [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14] [15]) As.channelDistributions} FullChanDistributionLengths = {Record.map FullChanDistributions Length} LastChanPositions = {Record.toDictionary unit(0:1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1)} %% Return next channel specified for channel of N (i.e. first %% access returns first chan of corresponding distribution) fun {GetChan_RoundRobin N} ScoreChan = {Out.midi.getChannel N} DistroChans = FullChanDistributions.ScoreChan LastPos = LastChanPositions.ScoreChan NextPos = if LastPos < FullChanDistributionLengths.ScoreChan then LastPos+1 else 1 % again first specified chan end in LastChanPositions.ScoreChan := NextPos {Nth DistroChans LastPos} end fun {GetCCsPerSecond EventDur} if As.ccsPerEvent \= false then As.ccsPerEvent / EventDur else As.ccsPerSecond end end in {Out.midi.renderAndPlayMidiFile MyScore {Adjoin %% remove args not supported by Out.midi.renderAndPlayMidiFile {Record.subtractList As [ccsPerSecond resolution channelDistributions]} unit(clauses: {Append As.clauses [%% Note output: output Micro-CC message, note on/off, and all its fenvs (if defined) isNote #fun {$ N} Chan = {GetChan_RoundRobin N} Progam = {N getInfoRecord($ program)} in {LUtils.accum [if Progam==nil then nil else [{Out.midi.makeProgramChange As.track {Out.midi.beatsToTicks {N getStartTimeInSeconds($)}} Chan Progam.1}] end [{Out.midi.noteToPitchbend N unit(channel:Chan)}] {Out.midi.noteToMidi N unit(channel:Chan round:Round)} %% ?? TODO: shift pitch bend fenv {ItemFenvsToMidiCC N unit(channel:Chan ccsPerSecond: {GetCCsPerSecond {N getDurationInSeconds($)}})}] Append} end %% Container with fenv(s) output: output all its fenvs, program change messages, and tempo curve (if defined) Score.isTemporalContainer #fun {$ C} Chan = {Out.midi.getChannel C} Progam = {C getInfoRecord($ program)} CCsPerSecond = {GetCCsPerSecond {C getDurationInSeconds($)}} in {LUtils.accum [if Progam==nil then nil else [{Out.midi.makeProgramChange As.track {Out.midi.beatsToTicks {C getStartTimeInSeconds($)}} Chan Progam.1}] end {ItemFenvsToMidiCC C unit(channel:Chan ccsPerSecond:CCsPerSecond)} {ItemTempoCurveToMidi C unit(ccsPerSecond:CCsPerSecond)}] Append} end ]} )}} end local /** %% Expects note N and a TemperamentMapping (see MakeRenderAndPlayMidiFile_Scala doc). Returns a pair Pitch#ChanOffset (pair of ints), where Pitch is the MIDI note pitch to output and ChanOffset the channel offset that is added to output this Pitch to the correctly retuned channel. %% */ fun {NoteToMidiPitch_Scala N TemperamentMapping} %% PC measured in PitchesPerOctave PC = {N getPitchClass($)} Oct = {N getOctave($)} in if {Not {HasFeature TemperamentMapping PC}} then {Exception.raiseError strasheela(failedRequirement PC "Pitch class not contained in current temperament.")} nil % never returned else %% MidiPC measured in 12-TET case TemperamentMapping.PC of MidiPC#ChanOffset then ((Oct+1) * 12 + MidiPC) # ChanOffset [] MidiPC#ChanOffset#OctaveOffset then ((Oct+1+OctaveOffset) * 12 + MidiPC) # ChanOffset end end end in /** %% 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. %% */ fun {MakeRenderAndPlayMidiFile_Scala TemperamentMapping Args} Default = unit(file_postfix: nil %% file:"test" ccsPerSecond:10.0 ccsPerEvent:false clauses:nil noteOffVelocity:0 track:2) As = {Adjoin Default Args} in proc {$ MyScore Args2} As2 = {Adjoin As Args2} fun {GetCCsPerSecond EventDur} if As2.ccsPerEvent \= false then As2.ccsPerEvent / EventDur else As2.ccsPerSecond end end /** %% Expects a TemperamentMapping (see MakeRenderAndPlayMidiFile_Scala doc) and returns a MIDI note output function for a note clause. %% */ %% def copied from Fenv.renderAndPlayMidiFile, and pitch mapping added %% %% TODO: revise Args processing fun {MakeNoteToMIDI_Scala TemperamentMapping Args} fun {$ N} ChanAux = {Out.midi.getChannel N} Chans = if ChanAux==nil then 0 elseif {IsList ChanAux} then ChanAux else [ChanAux] end Progam = {N getInfoRecord($ program)} NoteOffOffset = ~0.01 MidiPitch#ChanOffset = {NoteToMidiPitch_Scala N TemperamentMapping} NoteChan = Chans.1 + ChanOffset in {LUtils.accum [if Progam==nil then nil else {LUtils.mappend Chans fun {$ Chan} [{Out.midi.makeProgramChange Args.track {Out.midi.beatsToTicks {N getStartTimeInSeconds($)}} Chan Progam.1}] end} end {Out.midi.noteToUserEvent N fun {$ Track Start _ /* Channel */} EndTime = {Out.midi.beatsToTicks {N getEndTimeInSeconds($)} + NoteOffOffset} Velocity = {FloatToInt {N getAmplitudeInVelocity($)}} in %% output a list of MIDI events [{Out.midi.makeNoteOn Track Start NoteChan MidiPitch Velocity} %% BUG: Args.noteOffVelocity -- Feat not in Args {Out.midi.makeNoteOff Track EndTime NoteChan MidiPitch Args.noteOffVelocity}] end Args} {LUtils.mappend Chans fun {$ Chan} {ItemFenvsToMidiCC N unit(channel:Chan ccsPerSecond: {GetCCsPerSecond {N getDurationInSeconds($)}})} end}] Append} end end in {RenderAndPlayMidiFile MyScore {Adjoin As2 unit(removeQuestionableNoteoffs: false file: As2.file#As2.file_postfix clauses: {Append As2.clauses [%% overwrites note clause isNote#{MakeNoteToMIDI_Scala TemperamentMapping As2} %% CC fenvs in chords are output % HS.score.isChord % #fun {$ C} % ChanAux = {Out.midi.getChannel C} % Chans = if ChanAux==nil then 0 % elseif {IsList ChanAux} then ChanAux % else [ChanAux] end % in % {LUtils.mappend Chans % fun {$ Chan} % {ItemFenvsToMidiCC C % unit(channel:Chan % ccsPerSecond: {GetCCsPerSecond {C getDurationInSeconds($)}})} % end} % end ]}) }} end end end end