pedantic.software --pedantic

Overview of SNDC files

Nodes

A SNDC source file is a collection of nodes. Each node is an instance of a particular module. A node is declared like so:

nodeName: moduleName {
    /* list of inputs */
}

Each module has a set of inputs and outputs, whose type can be either FLOAT, BUFFER or STRING. Of course this is an ongoing project and further data types will be added in the future. The inputs are set like so:

    inputName: inputValue;

Some inputs are required and some are optional depending on the module. inputValue can be either a literal value, or the output of a node above the current node. Example:

newNode: myModule { /* node named "newNode", instance of "myModule" */
    input: n.out;   /* "input" is set to an output of a previous node named n */
    freq: 440;      /* "freq" is set to a literal float */
    interp: "sine"; /* "interp" is set to a literal string */
}

Let’s try it out with the simplest SNDC file that produces an audible sound, save the following script in a file named test.sndc:

o: osc {
    function: "sin";
    duration: 3;
    freq: 440;
}

Then, play it with:

sndc_play test.sndc

It should play a simple tone at 440Hz during 3 seconds.

Modules

A module is basically the “type” of a node. Modules are either built-in or can be SNDC files, as we will see later on.

In the previous example, we used the osc module, but there are many more. To get a full list of sndc’s built-in modules, simply type:

$ sndc -l

To get the description and specification of a particular module, type:

$ sndc -h <module>

For instance, sndc -h osc will give the following output:

[generator] osc - A generator for sine, saw and square waves
Inputs:
    function [STRING] [REQUIRED]
        waveform: 'sin', 'square', 'saw' or 'input'
    waveform [BUFFER] [OPTIONAL]
        buffer containing waveform, used when 'input' is specified in 'function'
    freq [BUFFER | FLOAT] [REQUIRED]
        frequency in Hz
    amplitude [BUFFER | FLOAT] [OPTIONAL]
        amplitude in unit
    p_offset [FLOAT] [OPTIONAL]
        period offset, in period (1. = full period)
    a_offset [FLOAT] [OPTIONAL]
        amplitude offset, a constant that gets added to the resulting signal
    param [FLOAT] [OPTIONAL]
        the wave parameter, relevant for 'square' and 'saw'
    duration [FLOAT] [REQUIRED]
        duration of resulting signal in seconds
    sampling [FLOAT] [OPTIONAL]
        sampling rate, def 44100Hz
    interp [STRING] [OPTIONAL]
        interpolation of the resulting buffer, 'step', 'linear' or 'sine'
Outputs:
    out [BUFFER] output signal

From the first line, we learn that the osc module is in the “generator” category, which means that it generates signals and buffers. Categories are just an informal way of ordering modules and don’t enforce anything on the module.

Then we have two lists, inputs and outputs, with their names used to reference them in SNDC fils (function, freq, etc), their types (note that one input can accept several types) and whether they are required or not. Not providing a required input will result in sndc returning an error.

Inputs are what you use within the node declaration, outputs are what you refer to in subsequent nodes' inputs.

A simple example: frequency-oscillating tone

The following SNDC file produces a sine tone with an oscillating frequency:

frequency: osc {
    function: "sin";
    freq: 1;
    duration: 5;
    amplitude: 50;

    a_offset: 440;
}

tone: osc {
    function: "sin";
    freq: frequency.out;
    duration: 5;
}

The first node is of type osc, it will output a sinusoidal function of frequency 1, amplitude 50 and centered around 440 (refer to sndc -h osc for a description of osc’s inputs).

The second node is also an osc, it will produce a sinusoidal signal but with a variable frequency given by the output of the frequency node. In the above description of osc we see indeed that freq can be either FLOAT or BUFFER.

Try it yourself by copy-pasting the above SNDC script in a file and playing it with:

$ sndc_play <file>

Processing

When sndc is invoked on a SNDC file, it will process the nodes sequentially in the same order as they appear in the file. The node will read their inputs and produce/set their outputs at once. Nodes never modify their inputs (they do not process their inputs “in-place” as the underlying data might be shared by other nodes).

Once processed, the nodes' outputs become available as inputs of the successive nodes.

Finally, by default, sndc will output the raw data contained in the last node, from the node’s output name out or will not output anything at all if no such output exists.

Visualizing signals and buffers

Visualizing what’s in the buffers is critical to debug node assemblies. While sndc doesn’t come with a graphical user interface and hence cannot plot the waveforms and other functions in buffers, it does have a way to easily export buffer content to files that can be easily plotted using specialized software like Gnuplot: the print module.

$ sndc -h print
[debug] print - Prints input buffer to specified file for plotting/analysis
Inputs:
    in [BUFFER] [REQUIRED]
        input buffer
    file [STRING] [OPTIONAL]
        output file name
Outputs:

That module takes a buffer in the in slot and a file name in the file slot, it doesn’t have any output but when processed, it will create a file of the given name containing all values in text form, one value per line, preceded by their sample number.

Example:

o: osc {
    function: "sin";
    freq: 440;
    duration: 1;
}

p: print {
    in: o.out;
    file: "o.out";
}

Calling sndc on this file won’t output anything, because print is the last node and it doesn’t have any output. However, it will create o.out which you can plot with gnuplot:

gnuplot -e 'set xrange [0:200]; plot "o.out" with lines; pause -1'

gnuplot