io7m | single-page | multi-page | Aradine User Manual

Aradine User Manual

CREATOR Mark Raynsford
DATE 2023-02-26T20:18:22+00:00
DESCRIPTION Documentation for the Aradine system.
IDENTIFIER ded07ce8-21a9-4226-bfb8-261e7a1895f7
LANGUAGE en
RIGHTS Public Domain
TITLE Aradine User Manual
The aradine package provides a modular system for digital signal processing with an emphasis on musical applications.
This section of the documentation describes the design and implementation of the aradine package.
The aradine package works in terms of a standard discretized sampled audio model. Informally, the system generates a buffer of audio samples at a fixed rate determined by the underlying audio hardware.
A continuous signal in the time domain can be considered as a function from time to amplitude. At any given time t, we can evaluate a continuous time domain signal A(t) to obtain the amplitude of the signal A at time t.
A discrete signal can be considered as a discretized form of a continuous signal. In digital audio systems, in modern computing environments, both time and amplitude are treated as discrete values. Time is discretized by dividing it into a fixed sample rate (typically specified in samples per second). Amplitude values are discretized by expressing them as either machine integer values of a chosen size, or floating-point values of a chosen size. The aradine package expresses amplitude values as 64-bit IEEE754 floating point values.
As an example, a continuous signal A could be discretized as the discrete signal Q as follows:
A sample is a single amplitude value in a discrete signal.
The sample rate of a discrete signal is the number of samples that comprise one second of the signal. The unit of measurement is hertz (hz).
At the time of writing, most consumer audio hardware works in terms of a 48000 hz sample rate.
The sample period of a discrete signal is the length of time represented by one sample in the signal. This is a function of the sample rate of the signal. In a signal with sample rate r, the sample period p of the signal is p = 1 / r.
For example, in a signal that has a 48000 hz sample rate, the sample period is 1 / 48000 ≈ 0.00002083 seconds ≈ 20.83 microseconds.
The physical realities of digital audio hardware and software mean that audio signals are not typically produced one-sample-at-a-time. There are many different limitations at all parts of the hardware and software stack that ultimately mean that it is more efficient to produce a buffer of samples less frequently, than a single sample more frequently. At a configurable (but constant) rate, the audio hardware and software will request a buffer of samples from applications. The buffer size specifies the number of samples that must be produced in a single request, and is typically configurable (although most hardware will have a minimum buffer size that it can handle).
The buffer duration of a buffer is simply the number of samples in the buffer multiplied by the sample period of the samples.
Buffering increases latency in the system. If an audio system is configured such that the buffer duration equals 10 milliseconds, then each time the system requests a buffer of samples, the system is essentially saying "Produce the most recent 10 milliseconds of audio for playback right now". This means that a listener is necessarily going to hear the audio 10 milliseconds later than the events that actually produced it. There is, therefore, a tension between specifying a larger buffer size in order to reduce the hardware and software overhead of producing the audio, and specifying a smaller buffer size in order to reduce the effective latency of the system.
For example, in a system with sample duration P and a buffer duration B, at Time = 8, the listener is going to hear the contents of the sample buffer SB0, even though the first sample in that buffer occurred at Time = 0:
At the time of writing, on most consumer and professional hardware, typical buffer sizes are in the 64-512 range. This implies a typical latency of around 2 milliseconds, which is mostly below the threshold of human perception.
A frame index is an integer index that refers to a set of samples across a set of buffers. A frame index is a non-negative relative index, with a value of 0 referring to the first sample in a buffer.
As an example, assume a buffer size of 8, and three signals S0, S1, and S2. At Time = 16, a frame index of 4 refers to the sample values in the buffers at Time = 20:
The term frame comes from the general term sampling frame in DSP. In other systems, audio signals are often divided into multiple channels. For example, stereo audio is divided into left and right channels. In an audio signal with n channels, the individual sample values in each of the channels at a given time are collectively referred to as a sampling frame (or frame, in short). For example, in a stereo audio signal A, the sample value x in the left channel at time 0, and the sample value y in the right channel at time 0 can be viewed as the frame (x, y) at time 0. Conceptually, the audio signal A can be seen as a function A(t) = (p, q), where t is time, and (p, q) is the sampling frame at time t. The aradine package only works with audio signals that have a single channel, but re-uses the term frame to refer to samples across coincident audio buffers.
Much of the practical aspect of digital signal processing is concerned with avoiding buffer underrun. This occurs when the operations required to produce a buffer of samples take longer to execute than the buffer duration. Essentially, the system has asked for a buffer of samples, but it has taken the application code longer than this duration to actually produce the buffer. Therefore, the buffer of samples is not ready in time and the audio system has nothing to play! This manifests as audible glitches in the audio output.
In the aradine package, the underlying audio system periodically requests that a buffer of samples be produced for each audio output in the system. We refer to this request as the audio callback. More precisely, we say that the underlying audio system periodically calls the aradine audio callback, and the code in the aradine package referenced by the callback performs all of the required processing and then writes out the resulting buffer of samples.
The aradine package collects DSP code into instruments, and instruments can be connected together arbitrarily to form an ensemble.
Within an ensemble, the ports of the instruments are connected together to form a graph. A target port may be connected to at most one source port at any given time. An source port may be connected to zero or more target ports at any given time. The types of ports must match; it is not permitted to try to connect a port of an audio type to a port of a note type, for example.
Instruments have zero or more ports. A port may either be a source port or a target port. A target port consumes data, while a source port produces data.
Ports have a type. Currently, ports may have either an audio type or a note type.
A target port I of an audio type will, at audio callback time, consume a buffer of audio samples that was produced by whatever source port O is connected to I. If no source port is connected to I, then I will be presented with a buffer of audio samples where every sample value is equal to 0.0.
A target port I of a note type will, at audio callback time, consume a buffer of note events that was produced by whatever source port O is connected to I. If no source port is connected to I, then I will be presented with an empty buffer.
A source port O of a audio type will, at audio callback time, produce a buffer of audio samples that is then consumed by each of the target ports connected to O.
A source port O of a note type will, at audio callback time, produce a buffer of note events that is then consumed by each of the target ports connected to O.
An event is a discrete, typed, timestamped value delivered to an instrument.
An event may be one of the following base types:

2.3.3.3. Event Base Types

  • A note event type.
  • A configuration event type.
A note-typed event may be one of the following types:

2.3.3.5. Note Event Types

  • A note-on event type. This event type is published when the user presses a key on a musical keyboard.
  • A note-off event type. This event type is published when the user releases a key on a musical keyboard.
  • A pitch-bend event type. This event type is published when the user touches the pitch bend control on a musical keyboard.
A configuration-typed event may be one of the following types:

2.3.3.7. Configuration Event Types

  • A buffer-size-changed event type. This event type is published when the underlying audio system is reconfigured to use a different buffer size.
  • A sample-rate-changed event type. This event type is published when the underlying audio system is reconfigured to use a different sample rate.
  • A parameter-changed event type. This event type is published when the value of a parameter is changed on an instrument.
Events are typically delivered to instruments via an event buffer. An event buffer is an opaque buffer type that, given a frame index, returns all events that occurred at that frame index.
Instruments have zero or more parameters. A parameter is effectively a named and typed mutable variable, the value of which can be read and/or modified (atomically) at any time. Parameters have unique (within an instrument) integer identifiers, and humanly-readable string labels. Additionally, parameters that are of numeric types are declared with minimum and maximum value range constraints. All parameters also have defined default values.
A parameter may be of one of the following types:

2.3.4.3. Parameter Types

  • 64-bit integer.
  • 64-bit floating point.
  • Sample map.
The aradine package provides a command-line interface for performing tasks such as managing the local inventory, checking instrument files, and etc. The base aradine-cli command is broken into a number of subcommands which are documented over the following sections.

3.1.2. Command-Line Overview

aradine-cli: usage: aradine-cli [command] [arguments ...]

  Modular programmable synthesis (Command-line).

  Use the "help" command to examine specific commands:

    $ aradine-cli help help.

  Command-line arguments can be placed one per line into a file, and
  the file can be referenced using the @ symbol:

    $ echo help > file.txt
    $ echo help >> file.txt
    $ aradine-cli @file.txt

  Commands:
    help          Show usage information for a command.
    info          Display system information.
    instrument    Instrument commands.
    inventory     Inventory commands.
    version       Show the application version.

  Documentation:
    https://www.io7m.com/software/aradine
All subcommands accept a --verbose parameter that may be set to one of trace, debug, info, warn, or error. This parameter sets the lower bound for the severity of messages that will be logged. For example, at debug verbosity, only messages of severity debug and above will be logged. Setting the verbosity to trace level effectively causes everything to be logged, and will produce large volumes of debugging output.
The aradine-cli command-line tool uses quarrel to parse command-line arguments, and therefore supports placing command-line arguments into a file, one argument per line, and then referencing that file with @. For example:

3.1.5. @ Syntax

$ aradine-cli instrument check --file sampler.jar
{
  "Message" : "Instrument passed all checks.",
  "Level" : "INFO"
}

$ (cat <<EOF
instrument
check
--file
sampler.jar
EOF
) > args.txt

$ aradine-cli @args.txt
{
  "Message" : "Instrument passed all checks.",
  "Level" : "INFO"
}
All subcommands, unless otherwise specified, yield an exit code of 0 on success, and a non-zero exit code on failure.
help - Show usage information for a command.
The help command shows the help for a given command.

3.2.3.1. Example

$ aradine-cli help instrument check
aradine-cli: usage: check [named-arguments ...]

  Check an instrument file.

  Named parameters:
  * --file
      Description       : The instrument jar file.
      Type              : Path
      Cardinality       : [1]; Specify exactly once.
      Syntax            : <platform-specific path syntax>
    --verbose
      Description       : Set the logging level of the application.
      Type              : QLogLevel
      Cardinality       : [1]; Specify exactly once, or use the default.
      Default value     : info
      Syntax            : trace|debug|info|warn|error

  The command does not accept any positional arguments.
info - Display system information.
The info command displays information about the current system.

3.3.3.1. --verbose

Attribute Value
Name --verbose
Type com.io7m.quarrel.ext.logback.QLogLevel
Default Value info
Cardinality [1, 1]
Description Set the logging level of the application.

3.3.4.1. Example

{
  "CacheDirectory" : "/opt/aradine/com.io7m.aradine/cache",
  "ConfigurationDirectory" : "/opt/aradine/com.io7m.aradine/config",
  "DataDirectory" : "/opt/aradine/com.io7m.aradine/data",
  "InventoryDatabase" : "/opt/aradine/com.io7m.aradine/data/inventory/inventory.db",
  "InventoryBlobDirectory" : "/opt/aradine/com.io7m.aradine/data/inventory/blobs"
}
check - Check an instrument file.
The check command checks a given instrument file for validity.

3.4.3.1. --file

Attribute Value
Name --file
Type java.nio.file.Path
Default Value
Cardinality [1, 1]
Description The instrument jar file.

3.4.3.2. --verbose

Attribute Value
Name --verbose
Type com.io7m.quarrel.ext.logback.QLogLevel
Default Value info
Cardinality [1, 1]
Description Set the logging level of the application.

3.4.4.1. Example

$ aradine-cli instrument check --file sampler.jar
{
  "Message" : "Instrument passed all checks.",
  "Level" : "INFO"
}
codegen - Generate code for an instrument file.
The codegen command generates code from an instrument description.

3.5.3.1. --file

Attribute Value
Name --file
Type java.nio.file.Path
Default Value
Cardinality [1, 1]
Description The instrument description file.

3.5.3.2. --output-resource-directory

Attribute Value
Name --output-resource-directory
Type java.nio.file.Path
Default Value
Cardinality [1, 1]
Description The output resource directory.

3.5.3.3. --output-source-directory

Attribute Value
Name --output-source-directory
Type java.nio.file.Path
Default Value
Cardinality [1, 1]
Description The output Java source directory.

3.5.3.4. --package-name

Attribute Value
Name --package-name
Type com.io7m.lanark.core.RDottedName
Default Value
Cardinality [1, 1]
Description The Java package name.

3.5.3.5. --verbose

Attribute Value
Name --verbose
Type com.io7m.quarrel.ext.logback.QLogLevel
Default Value info
Cardinality [1, 1]
Description Set the logging level of the application.

3.5.4.1. Example

$ aradine-cli instrument codegen \
--package-name com.io7m.example
--output-source-directory src
--output-resource-directory res
--file sampler.json

{
  "Message" : "Generated class: src/com/io7m/example/Parameters.java",
  "Level" : "INFO"
}
{
  "Message" : "Generated class: src/com/io7m/example/Ports.java",
  "Level" : "INFO"
}
{
  "Message" : "Generated instrument file: res/com/io7m/example/instrument.json",
  "Level" : "INFO"
}
install-instrument - Install instruments into the local inventory.
The install-instrument command installs an instrument into the local inventory.

3.6.3.1. --file

Attribute Value
Name --file
Type java.nio.file.Path
Default Value
Cardinality [1, 1]
Description The instrument jar file.

3.6.3.2. --verbose

Attribute Value
Name --verbose
Type com.io7m.quarrel.ext.logback.QLogLevel
Default Value info
Cardinality [1, 1]
Description Set the logging level of the application.

3.6.4.1. Example

$ aradine-cli inventory install-instrument --file sampler.jar
install-sample-map - Install sample maps into the local inventory.
The install-sample-map command installs a sample map into the local inventory.

3.7.3.1. --file

Attribute Value
Name --file
Type java.nio.file.Path
Default Value
Cardinality [1, 1]
Description The sample map file.

3.7.3.2. --verbose

Attribute Value
Name --verbose
Type com.io7m.quarrel.ext.logback.QLogLevel
Default Value info
Cardinality [1, 1]
Description Set the logging level of the application.

3.7.4.1. Example

$ aradine-cli inventory install-sample-map --file drums.aam
list-instruments - List instruments in the local inventory.
The list-instruments command lists instruments installed in the local inventory.

3.8.3.1. --verbose

Attribute Value
Name --verbose
Type com.io7m.quarrel.ext.logback.QLogLevel
Default Value info
Cardinality [1, 1]
Description Set the logging level of the application.

3.8.4.1. Example

$ aradine-cli inventory list-instruments
[ {
  "Type" : "Instrument",
  "Group" : "com.io7m.aradine",
  "Name" : "com.io7m.aradine.instrument.sampler_m0",
  "Version" : "0.0.2",
  "Identifier" : "com.io7m.aradine:com.io7m.aradine.instrument.sampler_m0:0.0.2",
  "Title" : "Sampler M0",
  "Description" : "The simplest monophonic sampler."
} ]
list-sample-maps - List sample maps in the local inventory.
The list-sample-maps command lists sample maps installed in the local inventory.

3.9.3.1. --verbose

Attribute Value
Name --verbose
Type com.io7m.quarrel.ext.logback.QLogLevel
Default Value info
Cardinality [1, 1]
Description Set the logging level of the application.

3.9.4.1. Example

$ aradine-cli inventory list-sample-maps
[ {
  "Type" : "SampleMap",
  "Group" : "com.io7m.example_group",
  "Name" : "com.io7m.example",
  "Version" : "1.0.0",
  "Identifier" : "com.io7m.example_group:com.io7m.example:1.0.0",
  "Title" : "",
  "Description" : "",
  "MIMEType" : "application/vnd.com.io7m.aurantium.sample_map",
  "Size" : "7139504"
} ]
uninstall-instrument - Uninstall instruments from the local inventory.
The uninstall-instrument command uninstalls an instrument from the local inventory.

3.10.3.1. --id

Attribute Value
Name --id
Type com.io7m.aradine.api.instrument.ARInstrumentID
Default Value
Cardinality [1, 1]
Description The instrument identifier.

3.10.3.2. --verbose

Attribute Value
Name --verbose
Type com.io7m.quarrel.ext.logback.QLogLevel
Default Value info
Cardinality [1, 1]
Description Set the logging level of the application.

3.10.4.1. Example

$ aradine-cli inventory uninstall-instrument --id com.io7m.aradine:com.io7m.aradine.instrument.sampler_m0:0.0.2
uninstall-sample-map - Uninstall sample maps from the local inventory.
The uninstall-sample-map command uninstalls a sample map from the local inventory.

3.11.3.1. --id

Attribute Value
Name --id
Type com.io7m.aradine.api.sample_map.ARSampleMapID
Default Value
Cardinality [1, 1]
Description The sample map identifier.

3.11.3.2. --verbose

Attribute Value
Name --verbose
Type com.io7m.quarrel.ext.logback.QLogLevel
Default Value info
Cardinality [1, 1]
Description Set the logging level of the application.

3.11.4.1. Example

$ aradine-cli inventory uninstall-sample-map --id com.io7m.example_group:com.io7m.example:1.0.0
version - Show the application version.
The version command shows the application version.

3.12.3.1. Example

$ aradine-cli version
com.io7m.aradine.cmdline 0.0.2-SNAPSHOT 776ce9dc06476debd406813b753e27d8b64b0754
The com.io7m.aradine.envelope.table1 module contains a table-based envelope implementation.
The envelope supports a number of different interpolation functions to produce amplitude values between breakpoints. For a hypothetical breakpoint B at frame frame(B) with amplitude amplitude(B), and a hypothetical breakpoint C at frame frame(C) with amplitude amplitude(C), where frame(B) < frame(C), the interpolation function specified for B is used to produce interpolated amplitude values for all frames greater than frame(B) and less than frame(C).
The definitions below are explained in terms of a pair of breakpoints B and C where frame(B) = 0, amplitude(B) = 0, frame(C) = 1000, and amplitude(C) = 1.
If a breakpoint B uses the interpolation function CONSTANT_CURRENT, then all frames between B and C will have an amplitude equal to amplitude(B).
If a breakpoint B uses the interpolation function CONSTANT_NEXT, then all frames between B and C will have an amplitude equal to amplitude(C).
If a breakpoint B uses the interpolation function COSINE, then all frames between B and C will have amplitudes produced by a cosine interpolation of amplitude(B) and amplitude(C).
If a breakpoint B uses the interpolation function EXPONENTIAL, then all frames between B and C will have amplitudes produced by an exponential interpolation of amplitude(B) and amplitude(C).
This is equivalent to linear interpolation where the interpolation factor is the square of the normal linear factor.
If a breakpoint B uses the interpolation function LINEAR, then all frames between B and C will have amplitudes produced by a linear interpolation of amplitude(B) and amplitude(C).
If a breakpoint B uses the interpolation function LOGARITHMIC, then all frames between B and C will have amplitudes produced by a logarithmic interpolation of amplitude(B) and amplitude(C).
This is equivalent to linear interpolation where the interpolation factor is the square root of the normal linear factor.
The com.io7m.aradine.filter.biquad1 module contains biquad filter implementations.
Biquad filters can be composed to produce stronger filters. The output signal of one filter is fed into the input of another filter in a process known as cascading. The main filter implementation included in the com.io7m.aradine.filter.biquad1 module is a filter of order 2 (specifically, it has two poles, and two zeroes). It would be theoretically possible to use the same algorithm with more filter coefficients to produce a single filter of order N, but in practice it is better to cascade N / 2 filters of order 2; the resulting filter will be much more numerically stable and less subject to issues such as those involving floating point precision.
For example, the following code uses four filters of order 2 to produce an effective filter of order 8:

4.2.2.1.3. Order 8

final var stage0 = new ARBQ1BiquadBPFO2();
final var stage1 = new ARBQ1BiquadBPFO2();
final var stage2 = new ARBQ1BiquadBPFO2();
final var stage3 = new ARBQ1BiquadBPFO2();

final var s0 = stage0.processOneFrame(input);
final var s1 = stage1.processOneFrame(s0);
final var s2 = stage2.processOneFrame(s1);
final var out = stage3.processOneFrame(s2);
Biquad filters are relatively suitable for use in EQ applications, or when the filter cutoff value is not expected to be modulated over time. For rapidly modulating filter cutoff values, a state variable filter may be more suitable.
The filter implementation is loosely based upon the C++ implementation provided at https://www.earlevel.com/main/2012/11/26/biquad-c-source-code/.
The com.io7m.aradine.filter.recursive1 module contains simple recursive filter implementations.
The filter implementations provided in the com.io7m.aradine.filter.recursive1 module are extremely simple 1-pole filters. They represent the bare minimum in terms of filter implementations and may be useful in terms of eliminating low frequency grounds hum from audio, or reducing high frequency noise.
The filter implementations are based on the descriptions given in The Scientist and Engineer's Guide to Digital Signal Processing by Steven W. Smith (ISBN-13 978-0966017632).
The com.io7m.aradine.filter.statevar1 module contains state variable filter implementations.
The filter implementations provided in the com.io7m.aradine.filter.statevar1 module are fairly direct translations of the hardware filters used on many analogue synthesizers. They offer good resolution at all frequencies, but can become unstable at certain frequencies unless oversampling is used.
State variable filters accept separate Q and C parameters, where Q is a positive real value, and C is a real value in the range [0, 1]. Filters may become unstable in the sense that particular combinations of Q and C values may cause the filter to start oscillating and massively increase the gain of the signal passing through it. This can be directly observed by running the filter on a sample of white noise, picking various Q and C values, and seeing if the resulting output contains one or more frequency bands where the amplitude is greater than or equal to 0.6. In an unfiltered white noise sample, every frequency band should have an amplitude of roughly 0.5, so any band with a value much greater than this can indicate that the filter has begun to self oscillate. The following diagram shows an increasing Q downwards along the Y axis, and an increasing C rightwards along the X axis:
In the diagram, a green square indicates that no frequency band in the output had an amplitude of greater than or equal to 0.6, whilst a red square indicates that one or more frequency bands had an amplitude of greater than or equal to 0.6. As should be fairly obvious, much of the parameter space is essentially unusable. Fortunately, this problem can be mitigated through oversampling; simply evaluate the filter multiple times per input sample, and discard all but the output sample from the last evaluation.

4.4.2.2.4. Oversampling

final var input = sampleBuffer.frameGetExact(index);
for (int oversample = 0; oversample < sampleCount; ++oversample) {
  f.processOneFrame(input);
}
final var output = f.highPassOutput();
Applying the exact same Q and C values as in the original diagram, but evaluating the filter four times instead of one, yields the following output instead:
The resulting audio output is not drastically different than for the single sample case, but it is clear from the diagram that a much wider range of Q and C values have become usable.
The filter implementation is derived from the description given in Musical Applications Of Microprocessors by Hal Chamberlin (ISBN 0-8104-5768-7).
The entire aradine API is documented the included JavaDoc.
io7m | single-page | multi-page | Aradine User Manual