From 2fb2feb431fbcccbb3ce8e2dceeb26ec192e92f6 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 31 May 2024 09:04:07 -0400 Subject: [PATCH] Examples documentation related updates. --- examples/examples.md | 261 +++++++++++++++++++++++++++++++++++++++++ examples/proc_dict.cfg | 1 + src/caw/main.cfg | 62 +++++++++- 3 files changed, 321 insertions(+), 3 deletions(-) create mode 100644 examples/examples.md diff --git a/examples/examples.md b/examples/examples.md new file mode 100644 index 0000000..43644ca --- /dev/null +++ b/examples/examples.md @@ -0,0 +1,261 @@ + + + +### Example 01 - Write a sine signal to an audio file. + + + +``` +{ + base_dir: "~/src/caw/examples", + proc_dict: "~/src/caw/examples/proc_dict.cfg", + mode: non_real_time, + + programs: { + + example_01: { + + durLimitSecs:5.0, + + network: { + + procs: { + osc: { class: sine_tone }, + af: { class: audio_file_out, in: { in:osc.out } args:{ fname:"$/out.wav"} } + } + } + } + } +} + +``` + +__caw__ programs are described using a slightly extended form of JSON. +In this example the program is contained in the dictionary labeled `example_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. + +When run this program will write a five second sine signal to an audio file +named `~/src/caw/examples/example_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 `example_01`. + +Run the program like this: +``` +caw example.cfg example_01 +``` + +__caw__ 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 +named `af`. + +In the language of __caw__ `osc` and `af` are refered to as `processor instances` or +__procs__. + +`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 +`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 + +- __procs__ 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`. + +``` +sine_tone: { + vars: { + srate: { type:srate, value:0, doc:"Sine tone sample rate. 0=Use default system sample rate"} + chCnt: { 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: { chCnt: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." } + } +} +``` + +Based on the `sine_tone` class all the default values for the signal generator +are apparent. With this information it is clear that the audio file +written by `example_01` contains a stereo (`chCnt`=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 +proc. 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 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. + +TODO: +1. more about types - especially the non-obvious 'srate','coeff'. +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. + +### Example 02: Modulated Sine Signal + +This example is an extended version of `example_01` where a low frequency oscillator +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 module the frequency of an audio +frequency `sine_tone` oscillator. + +Note that the LFO output by specifies a 3 Hertz sine signal +with a gain of 110 (220 peak to peak amplitude) and an offset +of 110. The signal is therefore sweeping an amplitude +between 330 and 550 which will be treated as frequency values by `osc`. + +``` +example_02: { + + durLimitSecs: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"} } + } + } +} +``` + +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 therefore +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." }, + period_ms: { type:ftime, value:50, 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." }, + } +} +``` + +The `sample_hold` class works by maintaining a buffer of the previous `ftime` millisecond +samples it has received. The output is both the value of the first sample in the buffer (`sh.out`) +or the mean of all the values in the buffer (`sh.mean`). + + + +### 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. + +`example_02` showed the use of a class preset on the audio oscillator. +`example_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 +but before it starts to execute. + +``` +example_03: { + + durLimitSecs: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.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 +independently. + +One of the simplest ways to address the individual channels of a +processor is by providing a list of value in a preset specification. +Several examples of this are shown in the presets contained in network +`presets` dictionary in `example_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. + +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` +statement, as in the `osc` processor in `example_02`, 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. + +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 match channels between the presets, +however if one of the channels does not exist then it uses channel 0. + +TODO: Check that this accurately describes preset interpolation. diff --git a/examples/proc_dict.cfg b/examples/proc_dict.cfg index 7f49fcc..ed7300b 100644 --- a/examples/proc_dict.cfg +++ b/examples/proc_dict.cfg @@ -122,6 +122,7 @@ a220 : { hz:220 }, a440 : { hz:440 }, a880 : { hz:880 }, + mono: { chCnt:1, gain:0.75 } } } diff --git a/src/caw/main.cfg b/src/caw/main.cfg index 2003e1f..b6537be 100644 --- a/src/caw/main.cfg +++ b/src/caw/main.cfg @@ -15,14 +15,70 @@ network: { procs: { - osc: { class: sine_tone }, - af: { class: audio_file_out, in: { in:osc.out } args:{ fname:"$/out.wav"} } + osc: { class: sine_tone }, + af: { class: audio_file_out, in: { in:osc.out } args:{ fname:"$/out.wav"} } } - } + } } + example_02: { + + durLimitSecs: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"} } + } + } + } + + example_03: { + + durLimitSecs:5.0, + preset: "a", + + 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, 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 }, osc: { gain:0.95 } }, + b: { lfo: { hz:2.0, dc:220 }, osc: { gain:0.75 } }, + c: { lfo: a880 }, + d: [ a,b,0.5 ] + + } + } + } + + example_04: { + maxCycleCount: 10, + + 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:clip, out_type:uint }} + numb: { class: number, + add: { class: add, in: { in0:cnt.out, in1:cnt.out }, } + + log: { class: print, in: { in0:cnt.out, in1:add.out, eol_fl:add.out }, args:{ text:["a","b","c"] }} + } + } + } + + } } \ No newline at end of file