< Back to IRCAM Forum

Long-winded macro expansion question:

Hello Antescofers.

First of all, I want to thank the team for this amazing piece of software, and the forum members for being so active and encouraging.

I’m having a small issue concerning text-expansion macros which I’m hoping that you all might have some ideas for. The answer is likely obvious to others, but is not to me. In short, it involves a macro to expand a TAB into a properly-formatted NIM definition.

Because of the way I use Antescofo at the moment (I’m open to new ideas, thanks to this group!), I use a LOT of NIMs in the course of a piece. And I use them almost always in the same way, as a number of evenly-spaced breakpoints as an argument to a process call. To give an example:

;==A Process==:
@proc_def myproc ($note, $vel, $length, $cutoffNIM)
{
group myproc {
$midinote := [$note, $vel]
$midioff := [$note, 0]

midipair-myproc $midinote  

curve cutoff @grain = 0.1 @action {cutoff-myproc $cutoff-myproc}  
{cutoff-myproc : $cutoffNIM}  

$length midipair-myproc $midioff  

}}  

I would currently call this thus:

NOTE 66.5 1/2 “event10”
{group myprocnote {
$length := 3 ;three beats
$CutoffNIM := NIM {0 5000, ($length/3) 10000 “sine_in”,
($length/3) 22000 “cubic_in”,
($length/3) 4000 “cubic_out”}

::myproc (65.2, 100, $CutoffNIM)  
}}  

A macro (let’s call it MrsFrizbee) to perform the text expansion of the NIM, might do something like this:

Take a TAB of the form: [a, b, c, d, …]
or even of the form: [a, [b, @opt shape1], [c, @opt shape2], [d, @opt shape3], …]

and the $length variable in the process call, expanding into something like:

let $tabdelta := ($length/(@size TAB) - 1)
NIM {0 a, $tabdelta b {shape1 if provided},
$tabdelta c {shape2 if provided},
$tabdelta d {shape3 if provided},

}}

returning the NIM.

The looping constructs provided don’t provide an obvious answer inside a text-expansion macro of this type; however I am also not the brightest programmer in the room. Also, it occurs to me that this might be impossible, because the $length variable is unavailable to the macro at expansion time, or for other—more arcane—reasons. (?) Maybe better done with a function?

The resulting process call, with the macro, might look something like this:

NOTE 66.5 1/2 “event10”
{group myprocnote {
$length := 3
$CutoffNIM := @MrsFrizbee([5000, [10000 “sine_in”], [22000 “cubic_in”], [4000, “cubic_out”])
::myproc (65.2, 100, $CutoffNIM)
}}

(or possibly this:

NOTE 66.5 1/2 “event10”
{group myprocnote {
$length := 3
$MrsFriz := @MrsFrizbee([5000, [10000 “sine_in”], [22000 “cubic_in”], [4000, “cubic_out”])
$CutoffNIM := $MrsFriz
::myproc (65.2, 100, $CutoffNIM)
}}
)

This is mostly out of laziness, rather than any grand compositional scheme that can be realised no other way; but given that I use perhaps 200 NIMs of this type in a 10min piece, many of which have 5<breakpoints<30, this could really speed me up.

Thanks in advance,

Nicholas R. Nelson
City University of New York
Brooklyn College Center for Computer Music

Ooof, sorry: forum was not kind to my indentation. Hope it’s all still clear.

Hello Atonalist.

You don’t need a macro. I propose below an approach where the NIM is computed “on the fly” in the process itself.

There is no way in the current version of Antescofo to a a variable number of argument for a process or a macro (it is possible to provide less arguments than expected in a function call, this is called currying, but it does not fit your needs). So, the idea is to pass a TAB and to analyze the TAB. Here is the code, explanations follows:

  
@fun_def ok($t) { return true }  

@fun_def extract_breakpoint($t)  
{  
    switch ($t)  
    {  
       case @is_numeric:  
            [$t, "linear"]  

       case @is_tab:  
            $t  

       case @ok:  
            print "ERROR in extraction of nim specification, got" $t  
            [0, "linear"]  
    }  
}  

@proc_def myproc ($note, $vel, $length, $cutoffTAB)  
        {  
                @local  $midinote, $midioff, $cutoff_myproc  
                @local  $nim, $delta, $a, $tmp  

                $delta := $length / @size($cutoffTAB)  
                $a := @extract_breakpoint($cutoffTAB[0])  
		$nim := NIM { 0 ($a[0]), 0 ($a[0]) }  

                forall $t in @drop($cutoffTAB, 1)  
                {  
                        $tmp := @extract_breakpoint($t)  
                        $nim := @push_back($nim, $delta, $tmp[0], $tmp[1])  
                }  
                print COMPUTED $nim  ;; just to check  
             
                $midinote := [$note, $vel]  
                $midioff := [$note, 0]  

                midipair-myproc $midinote  

                curve cutoff  
                  @grain = 0.1  
                  @action {cutoff-myproc $cutoff_myproc}  
                { $cutoff_myproc : $nim}  

                $length midipair-myproc $midioff  
        }  

::myproc(65.2, 100, 5000, [  
                7500,  
                [10000, "sine_in"],  
                [22000, "cubic_in"],  
                8000,  
                [4000, "cubic_out"]  
              ] )  

  1. As you can see, we pass to the process a tab which contains all the informations needed to build the NIM
  2. Note the TAB is specified "inline" in the process call but nothing prevent you to define the TAB as the value of a variable and to use the variable instead in the process call. In this way you can use this variable to refer to the same data elsewhere, if needed
  3. In the process, we initialize a nim with two breakpoints after a duration of 0. This is the trick to define a starting point because we cannot define a NIM with zero or only one breakpoint. The x0 value of the NIM is 0 and the y0 value is the first value in the tab.
  4. The element of the tab are either numeric value, in which case the interpolation is implicitly linear, or a tab with the expected y value and the string specifying the desired interpolation type.
  5. The function @extract_breakpoint is used to transform an element in $cutoffTAB into a tab of two elements. A case construct is used to distinguish between numeric element and fully specified element. The @ok function is used to catch all the remaining case to print a warning message (I suppose that you have a print receiver to redirect to the Max console).
  6. A forall construct is used to iterate on all elements of the $cutoffTAB except the first (the function @drop is used to drop the first element of the tab). The iteration is done in parallel but nevertheless in the right order, that is, in the tab order. Making simultaneous things in a specific order may seems strange, but there is logical and fruitful theory behind: the synchronous approach used in real-time programming (there is a section on the synchronous hypothesis in the reference manual)
  7. The function @push_back is used to ad da breakpoint at the end of the nim.
  8. The line print COMPUTED ... is used to check if the NIM just build is correct. Comment it when running in non debugging mode. The message printed for the code fragment below is: COMPUTED NIM { 0 7500, 0 7500 linear, 1000 10000 sine, 1000 22000 cubic, 1000 8000 linear, 1000 4000 cubic_out }
  9. From the code you give, note that
    1. variable in the process must be declared locals (via @local declaration). By default they are global and if you call two process that overlap, the data used by the two process will mixe!
    2. a variable name cannot have a dash inside, so use $cutoff_mypro instead of $cutoff-myproc (the latter will provoque a syntax error)
    3. do not confuse the Max receiver cutoff_mypro with the variable name $cutoff_mypro
    4. there is no need to define a group for the body of the process: the action spanned by a process are implicitly gathered in an implicit group
    5. One last point: in the process call, the tab argument is specified using several line. This is possible only because we have started to define a tab. The inside of a tab definition can span multiple line, but this is an exception: usually, message or process call (in fact atomic actions) must be specified on only one line. This explain why we open the TAB definition on the same line as the process call and why we close the process call on the same line as we close the tab definition.

Hope this will solve your problem. Do not hesitate to ask for further clarification if needed.

Jean-Louis!! WOW!!!

This is just amazing, especially the solution to the thorny issue of NIM creation with not enough arguments. Bravo!

As presented here, it works exactly as intended. I’m still interested in encapsulating this methodology outside the process definition, however.

As many of my processes might have five or six different NIM-driven curves firing all at once, constructing the NIM inside the proc_def could get a little ugly.

I’ve been doing some reading on extended expressions, and am wondering whether a “NIM generation function” could be written which accepts a TAB and a length, uses your methodology above, and returns the desired NIM. It didn’t seem like that would be too difficult, but hasn’t yet worked in practice.

  
;====FunDefs====;  

@fun_def ok($t) {return true}  

@fun_def extract_breakpoint($t)  
{  
  switch ($t)  
 {  
  case @is_numeric:  
     [$t, "linear"]  

  case @is_tab:  
      $t  

  case @ok:  
      AscoPrint "ERROR in extraction of nim spectification, got" $t  
      [0, "linear"]  
 }  
}  

@fun_def MrsFriz($nimtab, $length)  
 {  
  @local  $nim, $delta, $a, $tmp  
        $delta := $length / @size($nimtab)  
        $a := @extract_breakpoint($nimtab[0])  
		$nim := NIM { 0 ($a[0]), 0 ($a[0]) }  

         Forall $t in @cdr($nimtab)  
                {  
                        $tmp := @extract_breakpoint($t)  
                        $nim := @push_back($nim, $delta, $tmp[0], $tmp[1])  
                }  
   AscoPrint COMPUTED $nim  ;; just to check  
   
  return $nim  
 }  

;====ProcDefs===;  
@proc_def myproc ($note, $vel, $length, $cutoffTAB)  
 {  
   @local $midinote, $midioff, $cutoff_myproc  
     
   $cutoff_nim := @MrsFriz($cutoffTAB, $length)  

   $midinote := [$note, $vel]  
   $midioff  := [$note, 0]  

   midipair-myproc $midinote  

   curve myproc_cutoff  
     @grain = 0.1  
     @action {cutoff-myproc $cutoff_myproc}  
    {$cutoff_myproc : $cutoff_nim}  

    $length midipair-myproc $midioff  
 }  

BPM 60  
;==Measure 1==;  
NOTE 60 2 "event1"  
group event1 {  
 $cutoffTAB := TAB [7500, 1000, 22000, 400, 1000]  
::myproc(65.5, 100, 10, $cutoffTAB)  

}  

NOTE 61 2  
NOTE 62 2  
NOTE 63 2  

I feed, deep in my heart of hearts, that this should work: after all, it’s a function which returns a value called inside a process.

However, for some reason (likely having to do with my lack of understanding about the TAB structure (I’m always just treating them as lists in my mind), when I pass the $cutoffTAB, @extract_breakpoint is giving me the “ERROR in extortion of nim specification” error which you helpfully programmed in:

  
FromAsco: ERROR in extraction of nim spectification, got <>  
FromAsco: COMPUTED NIM \{ 0 7500\\, 0 7500 linear\\, 2 0 linear\\, 2 0 linear\\, 2 0 linear\\, 2 0 linear \}  

Any look anyone happened to take at it would be much appreciated.

Oh, and as to your helpful hints in #9: I had to hack together my example code on a machine without Asco on it: so I knew I made some schoolboy errors in there: eagle eyes to catch them!

Thanks again,
Nicholas R. Nelson
Brooklyn College Center for Computer Music

Hello Atonalist.
Your code should work. There is a bug and it will be corrected soon. I am sorry for the inconvenience: extended expressions in function are new and we are still tuning the implementation. For the moment, you can rely on the process version.

Just a comment: if you worry about the construction time of all your nims, it will be usually negligible. Nevertheless, if you know in advance the data to build the NIM, it is a good point to build it before their use. However, this is not the case with your code: the nim is build (well, when the bug will be corrected) in the function @MrsFriz which is called inside the process. So there is no difference with the previous solution. But, since you have the function to build the nim, you can build it in advance and use it latter:

  
; at the very beginning of the score  
$nim0 := @MrsFriz([7500, 1000, 22000, 400, 1000], 10)  
; ...  

; so latter you can use the pre-build nim  
::myproc(65.5, 100, 10, $nim0)  

For that, you relies on a simplified version the process definition that directly use the provided nim

  
@proc_def myproc ($note, $vel, $length, $cutoff_nim)  
 {  
   @local $midinote, $midioff, $cutoff_myproc  
     
   $midinote := [$note, $vel]  
   $midioff  := [$note, 0]  

   midipair-myproc $midinote  

   curve myproc_cutoff  
     @grain = 0.1  
     @action {cutoff-myproc $cutoff_myproc}  
    {$cutoff_myproc : $cutoff_nim}  

    $length midipair-myproc $midioff  
 }  

(note that in in your version, $cutoff_nim is not declared as local, which is probably an error).

Again, sorry for the bug. It will be corrected very soon.

Jean-Louis:

Thanks so much for taking another look at this. And no need to apologise: they call it experimental music for a reason!

In the meanwhile, I’ve rewritten the function to take advantage of the Loop extended expression, rather than the Forall and it seems to work swimmingly. (Of course, none of it works without your clever build function, so hat’s off to you!)

[This code has some leftover parts from other things I’ve tried, so ignore the variable declarations, LOL

  
@fun_def MrsFriz2($intab, $length)  
 {  
  @local  $nim, $delta, $a, $tmp, $nimtab, $t, $dim  
	  

		$nimtab := $intab   
	AscoPrint PASSEDTAB $nimtab  
		$dim := @size($nimtab)  
        $delta := $length / $dim  
        $a := @extract_breakpoint($nimtab[0])  
		$nim := NIM { 0 ($a[0]), 0 ($a[0]) }  
		$t := 1  
		  

         Loop   
                {  
					$item := $nimtab[$t]  
					AscoPrint Item $item  
					AscoPrint Result $nim  
                        $tmp := @extract_breakpoint($item)  
                        $nim := @push_back($nim, $delta, $tmp[0], $tmp[1])  
					$t := $t+1  
                } until ($t = $dim)  
     
   
  return $nim  
 }  

Thanks again!

Nick Nelson

Yes, a loop do the job equally (the forall just spare you the burden of introducing explicitly an iteration variable). As a matter of fact, the cause of the bug was the iteration variable in the forall which is not a ‘real’ variable and was handled incorrectly. The bug is corrected. A new version will be available soon, with other glitches corrected.

I mention that you can visualize the nim you build, if you have gnuplot installed on your machine (installing gnuplot is not straightforward on a mac, but you can do it easely if you already use fink or macport). Ascograph does not visualize nims because they can be the result of a computation yet to be done. Ascograph visualizes only curves which are statically defined. They are plans to visualize in Ascograph constant nim. Until it, you can do the job at run-time, for instance in play mode, with the following code snippet, assuming that gnuplot is located at path /path/to/gnuplot:

  
; tell the system where is gnuplot, in case it is not apparent from the user environment  
$gnuplot_path := "/path/to/gnuplot"  

; we suppose the nim to plot is refered by the variable $nim  

$minx := @min_key($nim) ; computes the lower boundary of the nim domain  
$maxx := @max_key($nim) ; computes the upper boundary of the nom domain  

; variable $y will be used to plot the nim. More precisely, we will plot the history of this variable.   
; So we specify an history of size 200 meaning that we can have up to 200 sampling points on the nim.  
@global 200:$y  

; this curve will make $x go from $minx to $maxx in 1s  
; During this second, each 1/100 s $y will be set to the successive values of the nim  
curve  
@grain 1/100 s ; i.e. 100 sampling points   
@action { $y := $nim($x) }  
{  
   $x { {$minx} 1s {$maxx} }  
}  

; at the end of the curve (that 1 second after its start), we plot the history of $y   
1s _ := @plot($y)  

The plot for the nim build by $nim := @MrsFriz([7500, 1000, 22000, 400, 1000], 10) is attached to this post.
This snippet of code can be improved in many ways. For instance, plotting the curve takes times (here 1 second) and we can do better.
But you get the idea.

Beware that plotting a nim may introduces time glitches into the timing of the computation. So, this is not something to do during a performance. But it can be used during the composition time, using the simulation facilities of Antescofo (the play mode), to visualize and trace what will be done.

Hi,

Wonderful world of NIM !

Just in case : A brew install gnuplot makes the install straight forward and place it here /usr/local/bin/gnuplot
(Homebrew must be configured first, no big deal, google Homebrew)

Bye

N.