examples/examples.md,proc_dict.cfg,main.cfg : Added 'proc_suffix_07' and updated documentation.

This commit is contained in:
kevin 2024-06-01 11:51:15 -04:00
parent 5308bce0cd
commit f1a4d44818
3 changed files with 202 additions and 46 deletions

View File

@ -47,18 +47,18 @@ caw example.cfg sine_file_01
__caw__ programs specify and run a network of virtual processors. The network is
described in the `procs` dictionary.
The line beginning with `osc: {` defines an instance of a `sine_tone` processor
named `osc`. The line beginning with `af: {` defines an instance of a `audio_file_out` processor
The line `osc: { ... }` defines an instance of a `sine_tone` processor
named `osc`. The line `af: { ... }` defines an instance of a `audio_file_out` processor
named `af`.
In the language of __caw__ `osc` and `af` are refered to as `processor instances` or
__procs__.
In the language of __caw__ `osc` and `af` are refered to as _processor instances_ or
sometimes just _processors_.
`osc` and `af` are connected together using the `in:{}` statement in the `af`
`osc` and `af` are connected together using the `in:{ ... }` statement in the `af`
instance description. The `in` statement connects `osc.out` to `af.in` and
thereby directs the output of the signal generator into the audio file.
The `args:{}` statment lists instance specific arguments used to create the
The `args:{ ... }` statment lists processor specific arguments used to create the
`af` instance. In this case `af.fname` names the output file. The use of the
`$` prefix on the file name indicates that the file should be written to
the _project directory_ which is formed by joining `base_dir` with the program name.
@ -67,7 +67,7 @@ The _project directory_ is automatically created when the program is run.
### Processor Class Descriptions
- __procs__ are collections of named __variables__ which are defined in the
- _processors_ are collections of named __variables__ which are defined in the
processor class file named by the `proc_dict` system parameter field.
Here are the class specifications for `sine_tone` and `audio_file_out`.
@ -118,17 +118,18 @@ TODO:
Link to proc class desc reference.
2. more about presets.
3. variables may be a source for multiple inputs but only be connected to a single source.
4. change `sine_tone.chCnt` to `ch_cnt`.
### Example 02: Modulated Sine Signal
This example is an extended version of `sine_file_01` where a low frequency oscillator
This example is an extended version of `sine_file_01` where a low frequency oscillator (LFO)
is formed using a second `sine_tone` processor and a sample and hold unit. The output
of the sample and hold unit is then used to modulate the frequency of an audio
frequency `sine_tone` oscillator.
Note that the LFO output is a 3 Hertz sine signal
with a gain of 110 (220 peak-to-peak amplitude) and an offset
of 440. The signal is therefore sweeping an amplitude
of 440. The LFO output signal is therefore sweeping an amplitude
between 330 and 550 which will be treated as frequency values by `osc`.
```
@ -180,8 +181,9 @@ TODO: change the name of the 'ftime' sample and hold variable.
One of the fundamental features of __caw__ is the ability to build
presets which can set the network, or a given processor, to a particular state.
`mod_sine_02` showed the use of a class preset on the audio oscillator.
`presets_03` shows how presets can be specified and applied for the entire network.
`mod_sine_02` showed the use of a class preset to set the number of
audio channels generated by the audio oscillator. `presets_03` shows
how presets can be specified and applied for the entire network.
In this example four network presets are specified in the `presets` statement
and the "a" preset is automatically applied once the network is created
@ -219,13 +221,13 @@ Audio signals in __caw__ can contain an arbitrary number of signals.
As shown by the `sine_tone` class the count of output channels (`sine_tone.chCnt`)
is up to the network designer. Processors that receive and process incoming
audio will often expand the count of internal audio processors to match
the count of channels they must handle. The processor variables must
then be duplicated for each channel if each channel is to be controlled
the count of channels they must handle. The processor variables are
then automatically duplicated for each channel so that each channel can be controlled
independently.
One of the simplest ways to address the individual channels of a
processor is by providing a list of values in a preset specification.
Several examples of this are shown in the presets contained in network
Several examples of this are shown in the presets contained in then network
`presets` dictionary in `presets_03`. For example the preset
`a.lfo.dc` specifies that the DC offset of first channel of the LFO
should be 880 and the second channel should be 770.
@ -243,6 +245,12 @@ may be overwritten when the network preset is applied. For example,
when network preset "a" is applied `lfo.hz` will be overwritten with 1.0 and the
two channels of `lfo.dc` will be overwritten with 880 and 770 respectively.
When a preset is specified as a list of three values then it is interpretted
as a 'dual' preset. The applied value of 'dual' presets are found by
interpolating between the matching values of the presets named in the
first two elements of the list using the third element as the interpolation
coefficient.
Preset "d" specifies an interpolation between two presets "a" and "b"
where the point of interpolation is set by the third parameter, in this case 0.5.
In the example the values applied will be:
@ -254,8 +262,9 @@ lfo.hz | 1 | 1.75 | a.lfo.hz[0] + (b.lfo.hz[1] - a.lfo.hz[0]) * 0.5
lfo.dc | 0 | 550.00 | a.lfo.dc[0] + (b.lfo.dc[0] - a.lfo.dc[0]) * 0.5
lfo.dc | 1 | 495.00 | a.lfo.dc[1] + (b.lfo.dc[0] - a.lfo.dc[1]) * 0.5
Notice that the interpolation algorithm attempts to match channels between the presets,
however if one of the channels does not exist then it uses channel 0.
Notice that the interpolation algorithm attempts to find matching channels between
the variables named in the presets, however if one of the channels does not exist
on either preset then it uses the value from channel 0.
TODO: Check that this accurately describes preset interpolation.
@ -268,24 +277,24 @@ program_04: {
network {
procs: {
tmr: { class: timer, args:{ period_ms:1000.0 }},
cnt: { class: counter, in: { trigger:tmr.out }, args:{ min:0, max:3, inc:1, init:0, mode:modulo } },
print: { class: print, in: { in:cnt.out, eol_fl:cnt.out }, args:{ text:["my","count"] }}
tmr: { class: timer, args:{ period_ms:1000.0 }},
cnt: { class: counter, in: { trigger:tmr.out }, args:{ min:0, max:3, inc:1, init:0, mode:modulo } },
log: { class: print, in: { in:cnt.out, eol_fl:cnt.out }, args:{ text:["my","count"] }}
}
}
}
```
This program demonstrates how __caw__ passes messages between processors.
In this case a timer is generates a pulse every 1000 milliseconds
which in turn increments a modulo 3 counter the value of which is
printed to the console.
In this case a timer generates a pulse every 1000 milliseconds
which in turn increments a modulo 3 counter. The output of the counter
is then printed to the console by the `print` processor.
This program should output:
```
: my : 0.000000 : count
info: : Entering runtime.
info: : Entering runtime.
: my : 1.000000 : count
: my : 2.000000 : count
: my : 0.000000 : count
@ -298,17 +307,21 @@ info: : Entering runtime.
: my : 1.000000 : count
```
Notice that the __print__ processor has an _eol_fl_ variable. When this
variable receives any input it prints the last value in the _text_ list
and then a new line.
Notice that the __print__ processor has an _eol_fl_ variable. When
this variable receives any input it prints the last value in the
_text_ list and then a newline. In this example, although `log.in`
and `log.eol_fl` both receive values from `cnt.out`, since the
`eol_fl` connection is listed second in the `in:{...}` statement it
will receive data after the `log.in`. The newline will therefore
always print after the value received by `log.in`.
### Example 05: Processors with __mult__ inputs
### Example 05: Processors with expandable numbers of inputs
`mult_inputs_05` extends `program_04` by including a __number__ and __add__ processor.
The __number__ processor acts like a register than can hold a single value.
As used here the __number__ processor simply holds the constant value '3'.
The __add__ processor sums the output of _cnt_ and _numb_.
The __add__ processor then sums the output of _cnt_ and _numb_.
```
mult_inputs_05: {
@ -329,7 +342,7 @@ mult_inputs_05: {
The notable new concept introduced by this program is the concept of
__mult__ variables. These are variables which can be instantiated
multiple times by referencing them in the `in:{}` statement and
multiple times by referencing them in the `in:{...}` statement and
including an integer suffix. The _in_ variable of both __add__ and
__print__ have this attribute specified in their class descriptions.
In this program both of these processors have two `in` variables:
@ -339,6 +352,10 @@ network designer requires.
### Example 06: Connecting __mult__ inputs
This example shows how the `in:{...}` statement notation can be used
to easily create and connect many `mult` variables in a single
connection expression.
```
mult_conn_06: {
@ -346,7 +363,7 @@ mult_conn_06: {
network: {
procs: {
osc: { class: sine_tone, args: { chCnt:6, hz:[110,220,440,880,1760, 3520] }},
osc: { class: sine_tone, args: { chCnt:6, hz:[110,220,440,880,1760,3520] }},
split: { class: audio_split, in:{ in:osc.out }, args: { select:[ 0,0, 1,1, 2,2 ] } },
// Create merge.in0,in1,in2 by iterating across all outputs of 'split'.
@ -367,20 +384,139 @@ mult_conn_06: {
}
```
The audio source for this network is a six channel signal generator,
where the frequency is each channel is incremented by an octave.
The _split_ processor then splits the audio signal into three
signals where the channels are distributed to the output signals
based on the map given in the `select` list. The _split_ processor
therefore has a a single input variable `in` and three output
variables `out0`,`out1` and `out2`.
The __audio_split__ class takes a single signal and splits it into multiple signals.
The __audio_merge__ class takes multple signals and concatenates them into a single signal.
Each of the three merge processor (merge_a,merge_b,merge_c) in `mult_conn_06`
demonstrates a slightly different ways of selecting multiple signals to merge
in with a single `in:{...}` statement expression.
1. Connect to all available source variables.
```
merge_a: { class: audio_merge, in:{ in_:split.out_ } },
```
`merge_a` creates three input variables (`in0`,`in1` and `in2`) and connects them
to three source variables (`split.out0`,`split.out1`, and `split.out2`).
The completely equivalent, and equally correct way of stating the same construct is:
`merge_a: { class: audio_merge, in:{ in0:split.out0, in1:split.out1, in2:split.out2 } }`
Aside from being more compact, the only other advantage to using the `_` (underscore)
suffix notation is that the connections will expand and contract with
the count of outputs on _split_ should they change without having to change
the code.
2. Connect to a select set of source variables.
```
merge_b: { class: audio_merge, in:{ in_:split.out0_2 } },
```
`merge_b` uses the `in:{...}` statement _begin_,_count_ notation
to select the source variables for the connection. This statement
is equivalent to: `merge_b: { class: audio_merge, in:{ in0:split.out0, in1:split.out1 } },`.
This notations takes the integer preceding the suffix underscore
to select the first source variable (i.e. `split.out0`) and
the integer following the underscore as the count of successive
variables - in this case 2. To select `split.out1` and `split.out2`
the `in:{...}` statemennt could be changed to `in:{ in_:split.out1_2 }`.
Likewise `in:{ in_:split.out_ }` can be seen as equivalent to:
`in:{ in_:split.out0_3 }` in this example.
3. Create and connect to a selected variables.
The _begin_,_count_ notation can also be used on the destination
side of the `in:{...}` statment expression.
```
merge_c: { class: audio_merge, in:{ in0_2:split.out1 } },
```
`merge_c` shows how to create two variables `merge_c.in0` and `merge_c.in1`
and connect both to `split.out1`. Note that creating and connecting
using the _begin_,_count_ notation is general. `in:{ in1_3:split.out0_2 }`
produces a different result than the example, but is equally valid.
TODO:
- poly_merge and audio_merge are identical except for the default input gain.
Change the default input gain to default to 1 and then manually set the initial
input gain to 0 when poly_merge is used to cross fade.
- Add the 'no_create' attribute to the audio_split.out.
- If a proc inst label has an integer suffix it should be taken as the label-sfx-id
this would allow for using 'mult' connections to multiple source procs without using a poly.
- What happens if the same input variable is referenced twice in an `in:{}` statement?
An error should be generated.
### Example 07: Processor suffix notiation
As demonstrated in `mult_conn_06` variables are identified by their label
and an integer suffix id. By default, for singular variable the suffix id is set to 0.
Using the `in:{...}` statement however variables that have the 'mult' attribute
can be instantiated multiple times with each instance having a different suffix id.
Processors instances use a similar naming scheme; they have both a text label
and a suffix id.
```
proc_suffix_07: {
durLimitSecs: 5.0,
network: {
procs: {
osc: { class: sine_tone, args: { chCnt:6, hz:[110,220,440,880,1760, 3520] }},
split: { class: audio_split, in:{ in:osc.out }, args: { select:[ 0,0, 1,1, 2,2 ] } },
g0: { class:audio_gain, in:{ in:split0.out0 }, args:{ gain:0.9} },
g1: { class:audio_gain, in:{ in:split0.out1 }, args:{ gain:0.5} },
g2: { class:audio_gain, in:{ in:split0.out2 }, args:{ gain:0.2} },
merge: { class: audio_merge, in:{ in_:g_.out } },
af: { class: audio_file_out, in:{ in:merge.out }, args:{ fname:"$/out_a.wav" }}
}
}
}
```
g0 : { class: audio_gain, in:{ in:osc.out0 }, args: { gain:0.5}},
g1 : { class: audio_gain, in:{ in:osc.out1 }, args: { gain:0.25}},
g2 : { class: audio_gain, in:{ in:osc.out2 }, args: { gain:0.125}},
merge: { class: audio_merge, in:{ in_:g_.out } },
```
Note that this will have problems if done inside a poly.
In this example three __audio_gain__ processors are instantiated with
the same label 'g' and are then differentiated by their suffix id's:
0,1, and 2. The merge processor is then able to connect to them using
a single `in:{...}` expression, `in_:g_.out` which iterates over the
gain processors suffix id. This expression is very similar to the
`merge_a` connection expression in `mult_conn_06`: `in_:split.out_`
which iterated over the label suffix id's of the `split.out`. In this
case the connection is iterating over the label suffix id's of the
networks processors rather than over a processors variables.
Note also that the _begin_,_count_ notation that allows specific
variables to be selected can also be used here to select specific
ranges of processors.
__Beware__ however that when a processor is created with a
specified suffix id it will by default attempt to connect to source
processor with the same suffix id. This accounts for the fact that
the __audio_gain__ `in:{...}` statements must explicitely set the suffix id of
_split_ to 0. (e.g. `in:split0.out0` ).
TODO:
- Using suffix id's this way will have cause problems if done inside a poly. Investigate.
- Should we turn off the automatic 'same-label-suffix' behaviour except when inside a `poly` network?
- How general is the 'in' statement notation? Can underscore notation be
used simultaneously on both the processor and the variable?
### Some Invariants
- A given variable instance may only be connected to a single source.
- Once a processor is instantiated the count and data types of the variables is fixed.
- Once a processor is instantiated no new connections can be created or removed.
- If a variable has a source connection then it cannot be assigned a value.

View File

@ -49,11 +49,11 @@
audio_split: {
vars: {
in: { type:audio, flags:["src"], doc:"Audio input." },
select: { type:cfg, doc:"Give a list of integers where each integer selects an output channel for the associated input channel." }
igain: { type:coeff, value:1.0, doc:"Audio gain for each input channel." }
ogain: { type:coeff, value:1.0, doc:"Audio gain for each output channel." }
out: { type:audio, doc:"Audio output." },
in: { type:audio, flags:["src"], doc:"Audio input." },
select: { type:cfg, flags:["init"], doc:"Give a list of integers where each integer selects an output channel for the associated input channel." }
igain: { type:coeff, value:1.0, doc:"Audio gain for each input channel." }
ogain: { type:coeff, value:1.0, doc:"Audio gain for each output channel." }
out: { type:audio, doc:"Audio output." },
}
presets:

View File

@ -114,6 +114,26 @@
}
}
proc_suffix_07: {
durLimitSecs: 5.0,
network: {
procs: {
osc: { class: sine_tone, args: { chCnt:6, hz:[110,220,440,880,1760, 3520] }},
split: { class: audio_split, in:{ in:osc.out }, args: { select:[ 0,0, 1,1, 2,2 ] } },
g0: { class:audio_gain, in:{ in:split0.out0 }, args:{ gain:0.9} },
g1: { class:audio_gain, in:{ in:split0.out1 }, args:{ gain:0.5} },
g2: { class:audio_gain, in:{ in:split0.out2 }, args:{ gain:0.1} },
merge: { class: audio_merge, in:{ in_:g_.out } },
af: { class: audio_file_out, in:{ in:merge.out }, args:{ fname:"$/out_a.wav" }}
}
}
}