GETTING STARTED

Your first semitone script

If you know Octave or MATLAB®, you already know most of semitone. The key things to understand upfront:

Octave syntax subset

In most cases, semitone scripts run unmodified in Octave and MATLAB®. The idea is that you can write semitone-compatible scripts that you prototype and validate in Octave or MATLAB® and then use the same code in the semitone interpreter for real time applications (and in the future to generate C/C++ code).

Genuine scalar type

The most significant difference from Octave: a variable containing a number is internally represented as Scalar<double>, not as a 1×1 matrix. This distinction is mostly invisible to you but enables the chunked scalar execution mode.

ASCII strings

Strings are char row arrays. No Unicode — intentional for embedded use cases.

EXAMPLE 1 — SINE OSCILLATOR (runs in semitone and Octave)

% Simple sine oscillator
fs = 44100;    % sample rate
f0 = 440.0;    % A4 — concert pitch
len = 0.01;    % duration in seconds
t  = linspace(0, len, len * fs + 1);
y  = sin(2 * pi * f0 * t);
disp(y);

EXAMPLE 2 — FOR LOOP & STRUCTS

% Group parameters in a struct
osc       = struct('freq', 440.0, 'amp', 0.8);
osc.phase = 0.0;

% 2^(1/12) per semitone step
ratio = 2 ^ (1 / 12);
for k = 0:11
  f = osc.freq * ratio ^ k;
  disp(num2str(f, '%.2f'));
end
DATA TYPES

Types & containers

The most significant deviation from Octave is the existence of a genuine scalar type. In Octave, everything is a matrix; in semitone a single number is genuinely scalar. This is invisible to your code but enables the chunked scalar execution mode.

TypeInternal TypeNotes
doubleScalar<double>Default numeric type — matches Octave
singleScalar<float>Single precision scalar
int8 … int64Scalar<int8_t> …Signed integers
uint8 … uint64Scalar<uint8_t> …Unsigned integers
logicalScalar<bool>Logical scalar
charScalar<char8>Single ASCII character
matrixNDArray<T>1 or 2-dimensional, column-major, 1-based indexing
structStructNamed fields; internally, variable environment is a struct
cell arrayCellArrayHeterogeneous container
float chunkChunk<float>Audio buffer pointer — see chunked ccalar mode. Note that this type cannot be created inside a script.
BUILT-IN FUNCTIONS

Built-in functions

All built-in functions follow Octave semantics unless noted.

TRIGONOMETRY

sin(x), cos(x), tan(x)Sine, cosine, tangent (radians)
asin(x), acos(x), atan(x)Inverse trig
atan2(y, x), atan2d(y, x)Four-quadrant arctangent; degree variant
sinh(x), cosh(x), tanh(x)Hyperbolic
asinh(x), acosh(x), atanh(x)Inverse hyperbolic
sind(x), cosd(x), tand(x)Argument in degrees
sin1(x), cos1(x)Argument in cycles (0–1) — not in Octave

EXPONENTIAL & LOGARITHM

exp(x)e^x
log(x), log2(x), log10(x)Natural, base-2, base-10 logarithm
sqrt(x)Square root
abs(x)Absolute value / complex modulus

OTHER MATH OPERATIONS

ceil(x)Round toward +∞
floor(x)Round toward −∞
round(x)Round to nearest
mod(x, m)Modulo — Octave sign convention
rem(x, m)Remainder — C sign convention
min(x, y), max(x, y)Minimum / maximum (element-by-element only in v0.1)

MATRIX FACTORIES

zeros(m, n), ones(m, n)m×n zero / ones matrix
eye(n)n×n identity matrix
linspace(a, b, n)n evenly spaced points from a to b
hadamard(n)Hadamard matrix
kron(A, B)Kronecker tensor product

REDUCTION & SHAPE

sum(x), prod(x)Sum / product of elements
size(x, dim)Matrix dimensions
diff(x)Differences between consecutive elements

TYPE CONVERSION

double(x), single(x)Convert to floating-point type
int8(x) … int64(x)Convert to signed integer
uint8(x) … uint64(x)Convert to unsigned integer
logical(x)Convert to boolean
char(x)Convert to ASCII character
num2str(x, fmt)Number to string, optional printf-style format

CONSTANTS (work also as matrix factories, e.g. pi(m,n) )

piπ
eEuler's number
Inf, infPositive infinity
NaN, nanNot a number
true, falseBoolean constants; also matrix factories

BITWISE & I/O

bitand(a,b), bitor(a,b), bitxor(a,b)Bitwise AND / OR / XOR
disp(x)Display value
struct(...)Create struct from name-value pairs
cell(m, n)Create empty m×n cell array
CHUNKED SCALAR MODE

Chunked scalar mode

Chunked scalar mode is the feature that allows certain semitone scripts to run at near-native speed inside an audio plugin's process callback. It works best for scalar DSP algorithms that process one sample at a time with scalar intermediate values, without recursion. FM operators, filter coefficient computation, distortion algorithms and pixelwise operations are good examples.

Besides the overhead reduction achieved by processing entire chunks of data at a time, this mode also reduces friction for you as a developer. By treating data as individual samples, you avoid having to think of your algorithm in terms of vectors in places where this is unnecessary. For example a simple distortion function like the one below not only forces you to write many dot operators, but also to think about every operation: "is this a scalar-scalar, scalar-vector, or vector-vector operation?" and if it is scalar-vector, "does this constellation still require a dot operator?" (e.g. scalar ./ vector vs. vector / scalar).

y = (1 - d) .* x + d .* x ./ (1 + abs(x))
This is a friction in the development process many of us have come to accept, but it is friction nonetheless.

In chunked scalar mode, you write your code like this:

y = (1 - d) * x + d * x / (1 + abs(x))

and the interpreter will apply the correct operations depending on whether d and x are chunks or scalars.

At this point, matrix operations are not allowed in chunked mode. If the need arises to do (small) matrix operations on samples from an audio signal, a "matrix of chunks" datatype may be added.

How it works

The idea behind this mode is that you write your code as if everything was a scalar. Inputs holding audio signals are given to the interpreter as Chunk<float> — essentially a pointer to an audio buffer. The interpreter evaluates the script once per buffer rather than once per sample, and performs calculations involving float chunks directly in single precision.

Duplicate subexpression detection

In chunked scalar mode the interpreter applies an optimization step that analyses the AST for subexpressions that appear more than once. All subsequent occurrences are marked in the AST as having a previous computation and instead of recomputing them, the buffer containing the result is kept until the last occurrence has been consumed.

Speed

For scalar DSP expressions, with chunks sizes of a few thousand samples, execution runs at roughly 2–3× the time of -O3 compiled C++. This is fast enough for production use in a VST/AU plugin for suitable algorithms. While the chunked scalar mode benefits from many optimizations, such as loop unrolling, the fact that it works on single operations applied to entire buffers does cause more memory accesses and thus a limit to the achievable speed.

EXAMPLE — WHAT THE INTERPRETER SEES IN CHUNKED SCALAR MODE

% Your script — works on single samples in Octave:
sin(x + sin(2 * x) / x) + sin(2 * x)

% What chunked scalar mode evaluates:                Buffer  Prev. Comp.  Node ID
% block                                              1                    N8
% │  ╭─ sin()                                        2                    N5
% │  │  │  ╭─ (0.0, ..) (float chunk x)              [input]
% │  │  ╰─ +                                         2                    N4
% │  │     │  ╭─ sin()                               1                    N2
% │  │     │  │  │  ╭─ 2.0 (double)                  [scalar]
% │  │     │  │  ╰─ *                                1                    N1
% │  │     │  │     ╰─ (0.0, ..) (float chunk x)     [input]
% │  │     ╰─ /                                      2                    N3
% │  │        ╰─ (0.0, ..) (float chunk x)           [input]
% ╰─ +                                               1                    N7
%    ╰─ sin()                                        1       N2           N6
%       │  ╭─ 2.0 (double)
%       ╰─ *
%          ╰─ (0.0, ..) (float chunk x)

What about recursion?

The chunked scalar mode does not allow recursion (i.e. something like x(n+1) = a * x(n) + b * x(n-1) cannot be implemented in chunked scalar mode). While this obviously excludes certain types of algorithms, this should be mitigated in the medium term by creating builtin components for the most commonly used recursive algorithms, such as biquad filters, ADSR envelopes, etc. This will allow you to express algorithms that make use of these components in a non-recursive way compatible with the chunked scalar mode.

Since there is an infinity of possible recursive algorithms, the long-term solution is to enable user-defined components that can transpiled from semitone to C++ and that become available in chunked scalar mode (e.g. by dynamically loading the compiled components). But of course this feature is dependent on the semitone to C/C++ transpiler, which is planned only for semitone v1.0.