caw/examples/examples.md

956 lines
38 KiB
Markdown
Raw Normal View History

### Example 01 - Write a sine signal to an audio file.
__caw__ programs are described using a slightly extended form of JSON.
In this example the program is contained in the dictionary labeled `sine_file_01` and
the preceeding fields (e.g. `base_dir`,`proc_dict`,`subnet_dict`, etc.) contain
system parameters that the program needs to compile and run the program.
2024-09-13 13:49:48 +00:00
``` javascript
{
2024-09-17 21:35:10 +00:00
base_dir: "~/src/caw/examples/io",
proc_dict: "~/src/caw/examples/proc_dict.cfg",
mode: non_real_time,
programs: {
2024-09-17 21:35:10 +00:00
ex_01_sine_file: {
dur_limit_secs:5.0, // Run the network for 5 seconds
network: {
procs: {
// Create a 'sine_tone' oscillator.
osc: { class: sine_tone },
// Create an audio output file and fill it with the output of the oscillator.
af: { class: audio_file_out, in: { in:osc.out } args:{ fname:"$/out.wav"} }
}
}
}
}
}
```
2024-09-17 21:35:10 +00:00
![Example 0](svg/00_osc_af.svg "`ex_01_sine_file` processing network")
2024-09-10 15:26:07 +00:00
When executed this program will write a five second sine signal to an audio file
named `~/src/caw/examples/sine_file_01/out.wav`. The output file name
is formed by joining the value of the system parameter `base_dir` with
the name of the program `sine_file_01`.
Run the program like this:
```
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 `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
sometimes just _processors_.
`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 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.
The _project directory_ is automatically created when the program is run.
### Processor Class Descriptions
- _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`.
```yaml
sine_tone: {
vars: {
srate: { type:srate, value:0, doc:"Sine tone sample rate. 0=Use default system sample rate"}
ch_cnt: { type:uint, value:2, doc:"Output signal channel count."},
hz: { type:coeff, value:440.0, doc:"Frequency in Hertz."},
phase: { type:coeff, value:0.0, doc:"Offset phase in radians."},
dc: { type:coeff, value:0.0, doc:"DC offset applied after gain."},
gain: { type:coeff, value:0.8, doc:"Signal frequency."},
out: { type:audio, flags['no_src'], doc:"Audio output" },
}
presets: {
a220 : { hz:220 },
a440 : { hz:440 },
a880 : { hz:880 },
mono: { ch_cnt:1, gain:0.75 }
}
}
audio_file_out: {
vars: {
fname: { type:string, doc:"Audio file name." },
bits: { type:uint, value:32u, doc:"Audio file word width. (8,16,24,32,0=float32)."},
in: { type:audio, flags:["src"], doc:"Audio file input." }
}
}
```
The class definitions specify the names, types and default values for
2024-09-17 21:35:10 +00:00
each variable. Since the `sine_tone` instance in `ex_01_sine_file`
doesn't override any of the the variable default values the generated
audio file must be a stereo (`ch_cnt`=2), 440 Hertz signal with an
amplitude of 0.8.
Note that unless stated otherwise all variables can be either input or
output ports for their processor. The `no_src` attribute on
`sine_tone.out` indicates that it is an output-only variable. The
`src` attribute on `audio_file_out.in` indicates that it must be
2024-09-13 13:49:48 +00:00
connected to a source variable or the processor cannot be instantiated -
and therefore the network it is contained by cannot be instantiated.
Note that this isn't to say that it can't be an output variable - only
that it must be connected.
Here is a complete list of possible variable attributes.
Attribute | Description
----------|-------------------------------------------------------
src | This variable must be connected to a source variable or the processor instantiation will fail.
no_src | This variable cannot be connected to a source variable (it is write-only, or output only).
init | This variable is only read at processer instantiation time, changes during runtime will be ignored.
2024-09-17 21:35:10 +00:00
mult | This variable may be instantiated multiple times. See `ex_05_mult_input` below.
out | This is a subnet output variable.
__caw__ uses types and does it's best at converting between types where the conversion will
not lose information.
Here are the list of built-in types:
Type | Description
---------|-----------------------------------
bool | true | false
uint | C unsigned
int | C int
float | C float
double | C double
string | Array of bytes.
time | POSIX timespec
cfg | cw object (JSON object)
audio | multi-channel audio array
spectrum | multi-channel spectrum in comlex or rect. coordinates.
midi | MIDI message array.
runtime | 'no_src' variable whose type is determined by the types of the other variables. See the 'list' processor.
numeric | bool | uint | int | float | double
all | This variable can be any type. Commonly used for variables which act as triggers. See the 'counter' processor.
A few type aliases are defined to help document the intended purpose of a given variable.
Type aliases:
Alias | Type | Description
---------|--------|----------------------------
srate | float | This is an audio sample rate value.
sample | float | This value is calculated from audio sample values (e.g. RMS )
coeff | float | This value will operate (e.g. add, multiply) on an audio signal.
ftime | double | Fractional time in seconds or milliseconds.
Also notice that the processor class has named presets. During
processor instantiaion these presets can be used to set the
2024-09-17 21:35:10 +00:00
initial state of the processor. See `ex_02_mod_sine` below for
an example of a class preset used this way.
### Example 02: Modulated Sine Signal
2024-09-17 21:35:10 +00:00
This example is an extended version of `ex_01_sine_file` 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 LFO output signal is therefore sweeping an amplitude
between 330 and 550 which will be treated as frequency values by `osc`.
``` json
2024-09-17 21:35:10 +00:00
ex_02_mod_sine: {
dur_limit_secs:5.0,
network: {
procs: {
lfo: { class: sine_tone, args:{ hz:3, dc:440, gain:110 }}
sh: { class: sample_hold, in:{ in:lfo.out } }
osc: { class: sine_tone, preset:mono, in:{ hz:sh.out } },
af: { class: audio_file_out, in:{ in:osc.out } args:{ fname:"$/out.wav"} }
}
}
}
```
2024-09-17 21:35:10 +00:00
![Example 2](svg/02_mod_sine.svg "`ex_02_mod_sine` processing network")
The `osc` instance in this example uses a `preset` statement. This will have
the effect of applying the class preset `mono` to the `osc` when it is
instantiated. Based on the `sine_tone` class description the `osc` will then
have a single audio channel with an amplitude of 0.75.
In this example the sample and hold unit is necessary to convert the audio signal to a scalar
value which is suitable as a `coeff` type value for the `hz` variable of the audio oscillator.
Here is the `sample_hold` class description:
```
sample_hold: {
vars: {
in: { type:audio, flags:["src"], doc:"Audio input source." },
2024-09-10 15:26:07 +00:00
period_ms: { type:ftime, value:50.0, doc:"Sample period in milliseconds." },
out: { type:sample, value:0.0, doc:"First value in the sample period." },
mean: { type:sample, value:0.0, doc:"Mean value of samples in period." },
}
}
```
2024-06-10 20:29:33 +00:00
The `sample_hold` class works by maintaining a buffer of the previous `period_ms` millisecond
samples. It then outputs two values based on this buffer. `out` is simply the first
value from the buffer, and 'mean' is the average of all the values in the buffer.
### Example 03: Presets
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.
2024-09-17 21:35:10 +00:00
`ex_02_mod_sine` showed the use of a class preset to set the number of
audio channels generated by the audio oscillator. `ex_03_presets` 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
but before it starts to execute.
If this example was run in real-time it would also be possible to apply
the the presets while the network was running.
``` JSON
2024-09-17 21:35:10 +00:00
ex_03_presets: {
dur_limit_secs:5.0,
preset: "a",
network: {
procs: {
lfo: { class: sine_tone, args:{ hz:3, dc:440, gain:[110 120] }},
sh: { class: sample_hold, in:{ in:lfo.out } },
osc: { class: sine_tone, in:{ hz:sh.out } },
af: { class: audio_file_out, in: { in:osc.out } args:{ fname:"$/out.wav"} }
}
presets: {
a: { lfo: { hz:1.0, dc:[880 770] }, osc: { gain:[0.95,0.8] } },
b: { lfo: { hz:[2.0 2.5], dc:220 }, osc: { gain:0.75 } },
c: { lfo: a880 },
d: [ a,b,0.5 ]
}
}
}
```
This example also shows how to apply `args` or `preset` values per channel.
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.ch_cnt`)
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 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 then network
2024-09-17 21:35:10 +00:00
`presets` dictionary in `ex_03_presets`. 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.
Any processor variable that has multiple channels may be set with a
list of values. If only a single value is given (e.g. `b.lfo.dc`) then
the same value is applied to all channels.
Note that if a processor specifies a class preset with a `preset`
2024-09-17 21:35:10 +00:00
statement, as in the `osc` processor in `ex_02_mod_sine`, or sets
initial values with an `args` statement, these
values will be applied to the processor when it is instantiated, but
may be overwritten when the network preset is applied. For example,
`osc` will be created with the values specified in `args`, however
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:
variable | channel | value | equation
---------|---------|--------|-------------------------------------------------
lfo.hz | 0 | 1.50 | a.lfo.hz[0] + (b.lfo.hz[0] - a.lfo.hz[0]) * 0.5
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 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.
2024-06-10 20:29:33 +00:00
### Example 04 : Event Programming
```
2024-09-17 21:35:10 +00:00
ex_04_program: {
dur_limit_secs: 10.0,
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 } },
log: { class: print, in: { in:cnt.out, eol_fl:cnt.out }, args:{ text:["my","count"] }}
}
}
}
```
2024-09-17 21:35:10 +00:00
![Example 4](svg/04_program.svg "`ex_04_program` processing network")
This program demonstrates how __caw__ passes messages between processors.
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.
: my : 1.000000 : count
: my : 2.000000 : count
: my : 0.000000 : count
: my : 1.000000 : count
: my : 2.000000 : count
: my : 0.000000 : count
: my : 1.000000 : count
: my : 2.000000 : count
: my : 0.000000 : count
: 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 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 expandable numbers of inputs
2024-09-17 21:35:10 +00:00
`ex_05_mult_inputs` extends `ex_04_program` 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 then sums the output of _cnt_ and _numb_.
```
2024-09-17 21:35:10 +00:00
ex_05_mult_inputs: {
dur_limit_secs: 10.0,
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 } },
numb: { class: number, args:{ value:3 }},
sum: { class: add, in: { in0:cnt.out, in1:numb.value } },
print: { class: print, in: { in0:cnt.out, in1:sum.out, eol_fl:sum.out }, args:{ text:["cnt","add","count"] }}
}
}
}
```
2024-09-17 21:35:10 +00:00
![Example 5](svg/05_mult_inputs.svg "`ex_05_mult_inputs` processing network")
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
including an integer suffix. The _in_ variable of both __add__ and
__print__ have the __mult__ attribute specified in their class descriptions.
In this program both of these processors have two `in` variables:
`in0` and `in1`. In practice they may have as many inputs as the
network designer requires.
The ability to define processors with a programmable count of inputs or output
of a given type is a key feature to any data flow programming scheme.
For example consider an audio mixer. The count of signals that it may
need to combine can only be determined from the context in which it is used.
Likewise, as in this example, a summing processor should be able to
form a sum of any number of inputs.
### 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.
```
2024-09-17 21:35:10 +00:00
ex_06_mult_conn: {
dur_limit_secs: 5.0,
network: {
procs: {
osc: { class: sine_tone, args: { ch_cnt: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'.
merge_a: { class: audio_merge, in:{ in_:split.out_ } },
af_a: { class: audio_file_out, in:{ in:merge_a.out }, args:{ fname:"$/out_a.wav" }}
// Create merge.in0,in1 and connect them to split.out0 and split.out1
merge_b: { class: audio_merge, in:{ in_:split.out0_2 } },
af_b: { class: audio_file_out, in:{ in:merge_b.out }, args:{ fname:"$/out_b.wav" }}
// Create merge.in0,in1 and connect them both to split.out1
merge_c: { class: audio_merge, in:{ in0_2:split.out1 } },
af_c: { class: audio_file_out, in:{ in:merge_c.out }, args:{ fname:"$/out_c.wav" }}
}
}
}
```
2024-09-17 21:35:10 +00:00
![Example 6](svg/06_mult_conn.svg "`ex_-6_mult_conn` processing network")
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.
2024-09-17 21:35:10 +00:00
Each of the three merge processor (merge_a,merge_b,merge_c) in `ex_06_mult_conn`
demonstrates three different ways of selecting multiple signals to merge
in with a single `in:{...}` statement expression.
The goal of this example is to show that fairly complex connections
between a source and destination processor can be achieved with
a single `in:{...}` statement. This syntax results in concise
network descriptions that are easier to read and modify then making lists
of individual connections between source and destination variables.
#### Connect to all available source variables on a single source processor.
```
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 equivalent but more verbose 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.
#### Connect to a select set of source variables on a single source processor.
```
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.
#### Connect multiple destination variable to a single source processor variable.
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:
- Add the 'no_create' attribute to the audio_split.out.
- What happens if the same input variable is referenced twice in an `in:{}` statement?
An error should be generated.
### Example 07: Processor suffix notiation
2024-09-17 21:35:10 +00:00
As demonstrated in `ex_-6_mult_conn` variables are identified by their label
and an integer suffix id. By default, for non __mult__ variables, 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.
```
2024-09-17 21:35:10 +00:00
ex_07_proc_suffix: {
dur_limit_secs: 5.0,
network: {
procs: {
osc: { class: sine_tone, args: { ch_cnt: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" }}
}
}
}
```
2024-09-17 21:35:10 +00:00
![Example 7](svg/07_proc_suffix.svg "`ex_07_proc_suffix` processing network")
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
2024-09-17 21:35:10 +00:00
`merge_a` connection expression in `ex_06_mult_conn`: `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.
2024-06-10 20:29:33 +00:00
__Beware__ however that when a processor is created with a specified
suffix id it will by default attempt to connect to a 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` ). Without the explicit
processor label suffix id (e.g. `in:split.out0`) in `g1: {...}` and
`g2: {...}` the interpretter would attempt to connect to the
non-existent procesor `split1` and `split2` - which would trigger a
compilation error.
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?
### Example 08: Instantiating variables from the `args:{...}` statement.
Previous examples showed how to instantiate __mult__ variables in the `in:{...}` statement.
This example shows how to instantiate __mult__ variables from the `args:{...}` statement.
An `audio_mix` processor is the perfect motivator for this feature.
A mixer is a natural example of a processor that requires a context dependent number of inputs.
The slight complication however is that every input also has a gain coefficient
associated with it that the user may want to set.
```
2024-09-17 21:35:10 +00:00
ex_08_mix: {
non_real_time_fl:true,
dur_limit_secs:5.0,
network: {
procs: {
osc0: { class: sine_tone, args: { hz:110 } },
osc1: { class: sine_tone, args: { hz:220 } },
// Instantiate gain:0 and gain:1 to control the input gain of in:0 and in:1.
mix: { class: audio_mix, in: { in_:osc_.out }, args:{ igain0:[0.8, 0], igain1:[0, 0.2] } },
af: { class: audio_file_out, in: { in:mix.out } args:{ fname:"$/out.wav"} }
}
}
}
```
2024-09-17 21:35:10 +00:00
![Example 8](svg/08_mix.svg "`ex_08_mix` processing network")
2024-09-17 21:35:10 +00:00
Notice that the `mix` processor instantiates two stereo input channels in the `in:{...}` statement
and then assigns initial gain values to each individual channel. If a scalar value was given instead of a
list (e.g. `igain0:0.8`) then the scalar value would be assigned to all channels
### Example 09: Polyphonic subnet
This example introduces the __poly__ construct. In previous examples when the
network used multiple copies of the same processor they were manually constructed - each with
2024-09-13 13:49:48 +00:00
a unique suffix id. The __poly__ construct allows whole sub-networks to be duplicated
2024-09-17 21:35:10 +00:00
and automatically assigned unique suffix id's.
```
2024-09-17 21:35:10 +00:00
ex_09_simple_poly: {
non_real_time_fl:true,
dur_limit_secs: 5.0,
network: {
procs: {
2024-09-17 21:35:10 +00:00
// LFO gain parameters - one per poly voice
g_list: { class: list, args: { in:0, list:[ 110f,220f,440f ]}},
2024-09-17 21:35:10 +00:00
// LFO DC offset parameters - one per poly voice
dc_list: { class: list, args: { in:0, list:[ 220f,440f,880f ]}},
osc_poly: {
class: poly,
args: { count:3 }, // Create 3 instances of 'network'.
network: {
procs: {
lfo: { class: sine_tone, in:{ _.dc:_.dc_list.value_, _.gain:_.g_list.value_ } args: { ch_cnt:1, hz:3 }},
sh: { class: sample_hold, in:{ in:lfo.out }},
osc: { class: sine_tone, in:{ hz: sh.out }},
}
}
}
// Iterate over the instances of `osc_poly.osc_.out` to create one `audio_merge`
// input for every output from the polyphonic network.
merge: { class: audio_merge, in:{ in_:osc_poly.osc_.out}, args:{ gain:1, out_gain:0.5 }},
af: { class: audio_file_out, in:{ in:merge.out } args:{ fname:"$/out.wav"} }
}
}
}
```
2024-09-13 13:49:48 +00:00
2024-09-17 21:35:10 +00:00
![Example 9](svg/09_simple_poly.svg "`ex_09_simple_poly` processing network")
This program instantiates three modulated sine tones each with a different set of parameters.
Notice the _lfo_ `in:{...}` statement for the `dc` variable
connection. The statement contains three underscores. The first
underscore indicates that the connection should be made to all of the
_lfo_ processors in the subnet (i.e. `lfo0`,`lfo1`,`lfo2`). The second
underscore indicates that the source is located outside the subnet.
The compiler will iterate through the network, in execution order,
looking for a processor named `dc_list` as the source. The last
underscore indicates that that the connections should begin with
`g_list.value0` and iterate forward to locate `glist.value1` and
`glist.value2` to locate the other source variables.
One subtle characteristic of the the _poly_ subnet is that the
internal connections (`lfo->sh->osc`) do not specifiy processor
id's. By default the `in:{...}` assumes that source processors
and desination processors share the same id. This allows the
suffix id to be dropped from the source processor and thereby to simplify
the syntax for connecting sub-network processors.
2024-09-17 21:35:10 +00:00
Also note that _poly_ to external connections are simply made
by referring to the poly source by name to locate the source processor.
This is shown in the `merge` input statement `in:{ in_:osc_poly.osc_.out}`.
2024-09-17 21:35:10 +00:00
The final characteristic to note about the poly construct
is the use of the 'parallel_fl' attribute. When this flag is
set the subnets will run concurrently in separate threads.
Since no connections between subnets is possible, and no other
processors can run, while the subnets are running this is
always safe.
### Example 10: Heterogeneous polyphonic subnet
In the previous example each of the three voices shared the same network
structure. In this example there are two voice with different
networks.
```
ex_10_hetero_poly: {
non_real_time_fl:true,
dur_limit_secs: 5.0,
network: {
procs: {
g_list: { class: list, args: { in:0, list:[ 110f,220f,440f ]}},
dc_list: { class: list, args: { in:0, list:[ 220f,440f,880f ]}},
osc_poly: {
class: poly,
args: { parallel_fl:true },
network: [
// network 0
{
procs: {
lfo: { class: sine_tone, in:{ _.dc:_.dc_list.value_, _.gain:_.g_list.value_ } args: { ch_cnt:1, hz:3 }},
sh: { class: sample_hold, in:{ in:lfo.out }},
osc_a: { class: sine_tone, in:{ hz: sh.out }},
}
},
// network 1
{
procs: {
osc_b: { class: sine_tone, args:{ hz:55 }},
}
}
]
}
// Iterate over the instances of `osc_poly.osc_.out` to create one `audio_merge`
// input for every output from the polyphonic network.
merge: { class: audio_merge, in:{ in0:osc_poly.osc_a.out, in1:osc_poly.osc_b.out}, args:{ gain:1, out_gain:0.5 }},
af: { class: audio_file_out, in:{ in:merge.out } args:{ fname:"$/out.wav"} }
}
}
}
```
Since the structure of the two subnets is different the very compact
`in:{...}` statements used to connect the `merge` processor to
the output of each of the `osc` processors is no longer possible.
However, this example demonstrates how `caw`can be used to
run heterogeneous networks concurrently thereby makeing better
use of available hardware cores.
2024-09-17 21:35:10 +00:00
### Example 11: Feedback
```
2024-09-17 21:35:10 +00:00
ex_11_feedback: {
non_real_time_fl:true,
max_cycle_count: 10,
network: {
procs: {
a: { class: number, log:{out:0}, args:{ in:1 }},
b: { class: number, log:{out:0}, args:{ in:2 }},
sum: { class: add, in: { in0:a.out, in1:b.out }, out: {out:b.in }, log:{out:0} }
}
}
}
```
2024-09-17 21:35:10 +00:00
![Example 11](svg/11_feedback.svg "`ex_11_feedback` processing network")
This example demonstrates how to achieve a feedback connection using
the `out:{...}` statement. Until now all the examples have been
forward connections. That is processors outputs act as sources to
processes that execute later. The `out:{...}` statement allows
2024-09-13 13:49:48 +00:00
connections to processes that occur earlier in the execution chain. The trick to making this
work is to be sure that the destination processor does not depend on
the variable receiving the feedback having a valid value on the
very first cycle of network execution - prior to the source processor
executing. One way to achieve this is to set the value of the
variable receiving the feedback to a default value in the `args:{...}`
statement. This approach is used here with the `b.in` variable.
2024-09-13 13:49:48 +00:00
This example also introduces the `log:{...}` statement.
The `log:{...}` statement has the form `log:{ <var>:<suffix_id>, <var>:<suffix_id> ... }`.
Any variables included in the statement will be logged to the console
whenever the variable changes value. This is often a more convenient
way to monitor the changing state of the network then using calls to `print`.
2024-09-13 13:49:48 +00:00
The output is somewhat cryptic but contains most of information to debug a program.
```
: exe cycle: process: id: variable: id vid ch : : : type:value : destination
: ---------- ----------- ----- --------------- -- --- ----- ------------: -------------
: 0 : a: 0: out: 0 vid: 2 ch: -1 : : : <invalid>:
: 0 : a: 0: out: 0 vid: 2 ch: -1 : : : i:1 :
: 0 : b: 0: out: 0 vid: 2 ch: -1 : : : <invalid>:
: 0 : b: 0: out: 0 vid: 2 ch: -1 : : : i:2 :
: 0 : add: 0: out: 0 vid: 0 ch: -1 : : : d:3.000000 : dst:b:0.in:0:
info: : Entering runtime.
: 0 : a: 0: out: 0 vid: 2 ch: -1 : : : i:1 : dst:add:0.in:0:
: 0 : b: 0: out: 0 vid: 2 ch: -1 : : : i:2 : dst:add:0.in:1:
: 0 : add: 0: out: 0 vid: 0 ch: -1 : : : d:3.000000 : dst:b:0.in:0:
: 1 : b: 0: out: 0 vid: 2 ch: -1 : : : i:3 : dst:add:0.in:1:
: 1 : add: 0: out: 0 vid: 0 ch: -1 : : : d:4.000000 : dst:b:0.in:0:
: 2 : b: 0: out: 0 vid: 2 ch: -1 : : : i:4 : dst:add:0.in:1:
: 2 : add: 0: out: 0 vid: 0 ch: -1 : : : d:5.000000 : dst:b:0.in:0:
: 3 : b: 0: out: 0 vid: 2 ch: -1 : : : i:5 : dst:add:0.in:1:
```
2024-09-13 13:49:48 +00:00
#### Log Column Descriptions
Column | Description
------------|---------------------------------------------------------------------
exe cycle | The `exe cycle` value give the execution cycle index.
| Each time the network completes a cycle this index advances.
process id | The process label and suffix id of the variable
variable id | The variable label and variable suffix id.
vid | System assigned unique (per process) id.
ch | Channel index or -1 if the variable is not channelized.
| Variables may be channelized if the audio signal they are applied to have multiple channels.
type:value | Data type and current value of the variable.
### Example 11: Audio Feedback
Coming soon.
### Example 12: User Defined Processor
__caw__ user defined processor act somewhat like functions in a procedural programming language.
They allow a network designer to create an arbitrarily complex network but then
implement a well defined interface to it. The network can then be instantiated just like
a built-in process with a limited set of inputs and outputs. This hides the complexity of
the implementation of the network from the user and makes for simpler top level networks.
Here is a user defined processor which implements an oscillator with internal amplitude and frequency modulators.
```
mod_osc: {
vars: {
hz: { proxy:hz_lfo.dc, doc:"Audio frequency" },
hz_mod_hz: { proxy:hz_lfo.hz, doc:"Frequency modulator hz" },
hz_mod_depth: { proxy:hz_lfo.gain, doc:"Frequency modulator depth" },
amp_mod_hz: { proxy:amp_lfo.hz, doc:"Amplitude modulator hz" },
amp_mod_depth: { proxy:amp_lfo.gain, doc:"Amplutide modulator depth."},
mo_out: { proxy:ogain.out flags:[out] doc:"Oscillator output."},
},
network: {
procs: {
// Frequency modulating LFO
hz_lfo: { class: sine_tone, args: { ch_cnt:1 }}
hz_sh: { class: sample_hold, in:{ in:hz_lfo.out }}
// Amplitude modulating LFO
amp_lfo: { class: sine_tone, args: { ch_cnt:1 }}
amp_sh: { class: sample_hold, in:{ in:amp_lfo.out }}
// Audio oscillator
osc: { class: sine_tone, in:{ hz: hz_sh.out }}
ogain: { class: audio_gain, in:{ in:osc.out, gain:amp_sh.out}}
}
presets: {
net_a: { hz_lfo: { dc:220, gain:55 }, amp_lfo: { gain:0.8 } },
net_b: { hz_lfo: { dc:110, gain:25 }, amp_lfo: { gain:0.7 } },
}
}
}
```
The structure of a user defined procedure is the same as a __caw__ top level program with the
addition of the __vars__ dictionary. The elements of the __vars__ dictionary are a simplified
version of the variable descriptions from the processor class description record.
Each user defined __var__ element creates an input or output port for the user defined process using
the `proxy` keyword. Output ports are distinguished from input ports by the `out` attribute as
shown in the `mo_out` port.
The `mod_osc` user defined processor is instantiated and used just like a built-in processor.
```
2024-09-17 21:35:10 +00:00
user_defined_proc_12 : {
2024-09-13 13:49:48 +00:00
non_real_time_fl: true,
dur_limit_secs: 5,
network: {
procs: {
sub_osc: { class: mod_osc args:{ hz:220, hz_mod_hz:3, hz_mod_depth:55, amp_mod_hz:2, amp_mod_depth:0.5 }},
af: { class: audio_file_out, in:{ in:sub_osc.mo_out } args:{ fname:"$/out.wav"}}
}
}
}
```
2024-10-12 19:39:55 +00:00
### Example 13: Global Variables
2024-09-13 13:49:48 +00:00
2024-10-12 19:39:55 +00:00
See the sampler wavetable.
### Appendix:
#### Network Parameters:
Label | Description
----------------------|------------------------------------------------------------------------
`non_real_time_fl` | Run the program in non-real-time for `max_cycle_count` cycles.
`frames_per_cycle` | Count of audio sample frames per network execution cycle.
`sample_rate` | Default system audio sample rate.
`max_cycle_count` | Maximum count of network cycles to execute in non-real-time mode.
`dur_limit_secs` | Set `max_cycle_count` as (`sample_rate` * `dur_limit_secs`)/`frames_per_cycle`.
`print_class_dict_fl` |
`print_network_fl` |
2024-09-13 13:49:48 +00:00
#### `log:{...}` statement data type abbreviations:
Type | Description
-----|-------------
b | bool
u | unsigned
i | int
f | float
d | double
s | string
t | time
c | cfg
abuf | audio
fbuf | spectrum
mbuf | MIDI
### Execution Model
### Some Invariants
2024-06-10 20:29:33 +00:00
#### Network 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.
2024-06-10 20:29:33 +00:00
- Once a processor is instantiated no new connections can be created or removed. (except for feedback connections?)
- If a variable has a source connection then it cannot be assigned a value.
2024-06-10 20:29:33 +00:00
- Processors always execute in order from top to bottom.
#### Internal Proc Invariants
- The _value() function will be called once for every new value received by a variable.