This manual was last updated on 10 November 1998.
This HTML-format reference manual may be freely distributed to third parties so long as it is not altered in any way. Feel free to save a copy to your own hard drive for convenient reference.
The latest version of this manual is available at the Sonic home page:
http://www.intersrv.com/~dcross/sonic/
Please address all comments, questions, or suggestions about this manual or the Sonic language to Don Cross by email at:
dcross@intersrv.com
All Sonic programs are command-line based utilities. They are told what to do using audio filenames and numeric parameters passed on the command line.
A Sonic program consists of functions that contain variable declarations and statements. Functions may call other functions or themselves. Optionally, functions may return values.
// This Sonic program makes echo sounds! program reverb ( inWave: wave, outWave: wave, delayTime: real, decayFactor: real, extendTime: real ) { outWave[c, i: inWave.n + r*extendTime] = inWave[c,i] + decayFactor * outWave[c, i - r*delayTime]; }
All Sonic programs are command line utilities. They accept arguments such as audio filenames and numeric values on the command line. These arguments are listed inside parentheses after the keyword 'program' and the name of the program. The sample program here requires 5 arguments: an input audio filename, an output audio filename, a delay time expressed in seconds, a numeric decay factor, and an amount of time by which the output should be longer than the input.
To run the above Sonic program, you would need to use a text editor to type it into a file (perhaps called 'reverb.s') and run the Sonic/C++ translator on it:
This would result in the output C++ source file called 'reverb.cpp'. Note that the output filename is derived directly from the name after the keyword 'program', not the name of the source file.sonic reverb.s
The most important feature of Sonic is that digital audio operations are performed by parametric assignment statements. This means that the output sound is described in terms of a typical output data value, identified by its channel 'c' and its sample index 'i'. The reserved symbols 'c' and 'i' are called placeholders. There is no need for an explicit loop to iterate through the samples and channels; this is implicit in the language.
The variable name appearing on the left side of the equal sign specifies where the output audio is stored. The expression appearing after 'i:' defines the number of samples that will be in the output sound. In certain circumstances it is possible to omit this duration specifier, as described in the section about wave assignments.
On the right side of the equal sign is an expression in terms of c and i. This expression defines each output data value in terms of its channel number and sample index. The sample program here adds the current input value from 'inWave' to a decayed previous output from 'outWave'. Note that Sonic allows the use of a mathematical recurrence relation in its wave assignments, so that previous outputs can be used to help calculate future outputs. For convenience, when the sample index is out of bounds, the value of a sound is defined to be zero. Therefore, there is no need to worry about array bounds checking in the sample index. The same is not true of the channel index, because such behavior would not be nearly as useful and would degrade runtime performance. Therefore, it is up to the programmer to ensure that the channel index is always valid. An out-of-bounds channel index produces undefined results, just as with array indices in C or C++.
Note that there is no need to explicitly open, read, write, and close files in Sonic. All of these low-level operations are handled automatically. This sample program knows to open outWave for write and inWave for read simply by where they appear in the assignment statement. Files are opened before the beginning of each assignment statement, and are closed after the assignment is executed.
As in C and C++, all array indices are zero-based. For example, in the array declaration above, umbrella[0,0,0] is the first array element, and umbrella[4,2,7] is the last array element. Such a subscripted array expression may appear anywhere a variable may appear, whether in an expression, function call, or left side of an assignment operator.var umbrella: real[5, 3, 8];
A non-subscripted array variable may appear on the left side of an assignment statement only when another non-subscripted array variable of identical type appears to the right of the assignment operator. For example:
Arrays may be passed to any non-program function. All arrays are passed by reference. The element type of the passed array must be identical to the declared element type in the function parameter list. All dimensions must match except for the first dimension in the function parameter list, which is ignored, just as in C and C++. Also, like C and C++, no array bounds checking is ever performed. It is up to the programmer to ensure that subscripts to arrays are within the valid range of values for each dimension.var x: real[5]; var y: real[5]; var z: real[7]; x += y; // add each element of 'y' to corresponding element of 'x'. z = x; // ERROR! don't have same number of elements.
Array variables with numeric element types are initialized to all zeroes, and arrays of boolean are initialized to all false values. Array variables are not allowed to have explicit initializer expressions.
Here is a list of the built-in constants whose default values may be overridden:
r = sampling rate expressed in Hz. [defaults is 44100]To redefine one of these built-in constants, just use the syntax constant = value;. Statements like this must appear outside a user-defined function. Here is a sample program that generates an output audio recording with 1 channel at a sampling rate of 8000 Hz. It also explicitly sets the interpolate flag, even though true is the default value anyway.m = number of channels. [defaults to 2, but may be 1..64]
interpolate = true/false; whether to linearly interpolate between samples [default = true].
When set to true, the Sonic/C++ translator will check the index parameter for being a non-integer (i.e. real) value. If the index is not an integer, the generated C++ code will call SonicWave::interp() instead of SonicWave::fetch(). This results in much cleaner sound than when interpolate is set to false, but it will execute somewhat slower. However, even if interpolate is true, the translator knows to still call SonicWave::fetch() if the index expression is of integer type, so that the code is faster and still produces the same effect. Therefore, it is recommended that you leave interpolate to its default value of true.
Here is a list of the built-in constants that cannot be re-defined:m = 1; r = 8000; interpolate = true; program tone ( output: wave ) { output[c,i:5*r] = sinewave(1,300,0); }
true = the boolean affirmative value
false = the boolean negative value
pi = the ratio of a circle's circumference to its diameter, 3.141592...
e = the base of natural logarithms, 2.7182818...
Functions may appear in any order. Note that the program function 'fmsynth' calls the non-program function 'fm', even though 'fm' is defined later in the source code.r = 22050; // set sampling rate to 22050 Hz program fmsynth ( outWave:wave, freq:real ) { outWave[c,i:r] = fm ( 5, freq, t ); } function fm ( depth:integer, freq:real, time:real ): real { if ( depth < 1 ) return sin(2*pi*freq*time); return sin ( depth*pi*freq*time + fm(depth-1,freq,time) ); }
The function 'fm' returns a real value, indicated by the ': real' appearing after the closing parenthesis of the argument list. Functions that do not return a value simply omit the ':' and the return type.
Functions may accept arguments of any type, but they may not return values of type wave. By default, parameters to a user-defined function are passed by value, except for wave arguments and import function object arguments, which are passed by reference. To force a parameter to be passed by reference, add an ampersand '&' after the formal parameter's type. When you call a function that accepts parameters by reference, those parameters must be variable names. Here is an example of a function that accepts two real parameters by reference so that it can swap their values and have the change persist in the calling code.
function swap ( x: real&, y: real& ) { var temp=x: real; x = y; y = temp; }
sinewave(A,F,P) = a sinewave with amplitude A, frequency F Hz, and phase P degrees. For example, the wave assignment
generates a five-second 440 Hz cosine wave of unit amplitude. The arguments to the sinewave pseudo-function are evaluated once before the enclosing wave assignment is executed. Note that it is highly recommended that you use the sinewave pseudo-function instead of the intrinsic function sin(x) wherever possible, because it is extremely fast in comparison. The sinewave pseudo-function is implemented in terms of trigonometric recurrence relations that require only one multiplication, one subtraction, and two floating point assignments per sample iteration. However, you still will need to use intrinsic functions like sin(x) or cos(x) whenever the argument x is not a linear function of i.outWave[c,i:5*r] = sinewave(1,440,90);
sawtooth(F) = a sawtooth (triangular) wave with frequency F Hz, having the range [-1,1].
iir ( {x_coeff_list}, {y_coeff_list}, expression ) = infinite impulse response (IIR) filter of the given expression, using the given list of x-coefficients and y-coefficients. For example, the wave assignment
is equivalent toy[c,i] = iir ( {0.3, 0.4}, {-0.1}, x[c,i] );
Note that the x-coefficient list must contain at least one expression, whereas the y-coefficient list may contain zero or more expressions. If the y-coefficient list is empty, the resulting filter is actually a finite impulse response (FIR) filter. The expression may be a function of c and i, but the x- and y-coefficient lists of expressions must not. The coefficent list expressions are evaluated once before the enclosing assignment statement is executed. The programmer may nest iir constructs to implement cascaded filters.y[c,i] = 0.3*x[c,i] + 0.4*x[c,i-1] - 0.1*y[c,i-1];
fft ( expression, fft_size, transfer_func, freq_shift )
Filters expression using the
Fast Fourier Transform
(FFT) algorithm. The input expression is split up into buffers defined by fft_size. Each buffer is fed through the FFT and the resulting frequency spectrum is processed by the specified user-defined transfer function. Afterward, the entire spectrum is shifted up or down depending on the value freq_shift. Finally, the processed spectrum is fed through the inverse FFT to obtain a processed time signal. An "overlap-add" technique is used to eliminate boundary discontinuities inherent in FFT processing.
Before the enclosing wave assignment is executed, the fft_size and freq_shift terms are evaluated once.
The fft_size term defines the size of the FFT buffers, and must evaluate to a positive integer power of 2 (for example, 1024 or 4096). If a number that is not a power of 2 is encountered, a runtime error will occur. Divide fft_size by the sampling rate to determine the time length of the FFT buffer. For example, a FFT buffer with 4096 samples divided by a sampling rate of 44100 Hz yields an FFT buffer that is 4096/44100 = 0.0929 seconds long. It is recommended that you make the buffer be longer than 0.05 seconds to keep buffer width distortion lower than audible 20 Hz (reciprocal of buffer length).
The transfer_func must be the name of a user-defined function with the following parameter types. Note that the function must not return a value:
This function will be called once for every positive frequency bin in the frequency spectrum. The first parameter is the frequency of the given bin expressed in Hz. The following two parameters are the real and imaginary components of the frequency bin. They are passed by reference so that the function may modify their values. For example, to eliminate a given frequency in the input signal, set zr and zi to zero when that frequency is encountered.function xfer ( freq:real, zr:real&, zi:real& ) { // ... }
The freq_shift term defines the amount by which the frequency spectrum is to be shifted after being processed by the user-defined function and before being fed through the inverse transform. This provides a simple method for pitch shifting. This value is expressed in Hz. If it is positive, frequencies will be shifted upward (pitch increase), and if it is negative, frequencies will be shifted downward (pitch decrease). Set this value to zero if you don't want any pitch shifting to take place.
Note that using the FFT filter will result in a signal delay equal to a full buffer width. Therefore, you may need to pad the output signal by fft_size samples to get the entire filtered signal.
There are two constraints to a C++ class that implements an import function type. First, if it is to be used inside a wave assignment, the class must implement the following member function:
A variable of an import function type will receive a call to this member function immediately before Sonic executes any wave assignment in which that variable appears. Only one call will be made to this member function for each import function variable in the wave assignment, even if the import function variable appears more than once.void reset ( int numChannels, long samplingRate );
The second constraint is that the import function class must have a name beginning with 'i_' followed by at least one more character.
Here is an example of a valid C++ declaration for an import function type:
Here is an example of how you would use the above import function type in a Sonic program:// happy.h class i_Happy { i_Happy ( double freq ); // constructor may have any number of arguments double operator() ( int c, long i, double z ); // same here double operator() ( int c, long i ); // may overload for different arg count void reset ( int numChannels, long samplingRate ); };
Another example of how to create an import function has been provided in the Sonic runtime library. See the files 'pluck.h' and 'pluck.cpp' therein. These files define an import function type called PluckedString, which is an implementation of the Karplus-Strong Plucked String Algorithm.import Happy from "happy.h"; // note class name here omits the 'i_' program JoyJoy ( outWave: wave ) { var stimpy(220), ren(440): Happy; outWave[c,i:3*r] = stimpy(c,i,5) - ren(c,i); }
Important Note: The Sonic/C++ translator does not validate the C++ header file from which a user-defined class is imported. Therefore, certain programming errors related to import classes will not be detected by the translator:
All variable declarations begin with the keyword 'var', followed by one or more comma-delimited variables. Following the list of comma-delimited variables is the symbol ':', then a data type, and finally a semicolon ';'.
A declared variable of numeric or boolean type may optionally have an initializer expression. If the initializer is omitted, variables of numeric type are implicitly initialized to zero, and variables of boolean type are implicitly initialized to false. A variable of import function type may have an initializer consisting of a comma-delimited list of expressions enclosed in parentheses, comprising the variable's constructor arguments. A variable of numeric or boolean type may have a single initializer expression either enclosed in parentheses or followed by an equal sign '='.
All variables (but not function and program arguments) of type wave are temporary; any data stored to them will be erased upon the function's return. Wave variables must never have an initializer.
All local variable declarations must occur after the function's opening curly brace '{' and before any statements inside the function.
There are special rules for variables of array types.
No local variable or user-defined function argument may have the same name as any function or any global variable. Two local variables and/or function arguments may have the same name so long as they are declared in different functions.
Here are examples of valid variable declarations.
program scooby ( outWave: wave ) { var freq1 = 440.0, freq2 = freq1 / 2 : real; var temp: wave; // ... more code here ... } var thelma = shaggy(99) : integer; function shaggy ( fred: integer ): integer { var sum: integer; // initialized to zero implicitly while ( fred > 0 ) { sum += fred^2; // add fred squared to sum fred -= 1; } return sum; }
A statement may be a simple statement which is terminated by a semicolon ';', or a compound statement which is a list of zero or more statements enclosed by curly braces '{}'. A compound statement may be used anywhere the Sonic grammar calls for a statement.
= Store the value on the right into the variable on the left.
+= Add the value on the right to the variable on the left.
-= Subtract the value on the right from the variable on the left.
*= Multiply the value on the right into the variable on the left.
/= Divide the value on the right into the variable on the left.
%= Store the remainder (integer or real) with the value on the right into the variable on the left.
<< Append the value on the right to the variable on the left (used only in wave assignments).
All of the assignment operators listed in the previous section are allowed in a simple assignment statement, with the exception of the append operator <<, which may be used only in a wave assignment statement.
Some special rules apply to assignment statements containing array variables.
Wave assignment statements are the most important and distinctive feature of the Sonic language. They allow an entire digital audio operation to be performed, often with a single line of code. There is no need to explicitly iterate through channels and sample indices in Sonic. The symbol 'c' takes on the value of all valid channels, and 'i' takes on the value of all sample indices. See the discussion about the sample program earlier in this manual for examples and a conceptual overview.
Channel Placeholder. The placeholder c takes on all values from 0 to m-1, where m is the number of channels, as defined in the section on built-in constants.
Sample Index Placeholder. The placeholder i takes on all values from 0 to N-1, where N is the number of output samples to be generated. If the string '[c,i:expression]' appears after the wave variable on the left side of the assignment operator, the expression defines the value of N. This expression is evaluated once before the wave assignment executes. If expression has non-integer value, it is truncated to the largest integer less than or equal to it. If the resulting integer is negative or zero, the output generated will be zero samples long.
If the value of N can be determined from the expression to the right of the assignment operator, i.e. the assigned expression contains other wave expressions having known length, then it is not necessary to provide an explicit number of samples to generate. This means you can just use the '[c,i]' syntax. In this case, the value of N is automatically the duration of the longest finite input to the right of the assignment operator. Note that all calls to intrinsic functions, pseudo-functions, and user-defined functions are considered to have infinite duration, so they don't count in the determination of the output's duration. All finite inputs with duration less than the longest finite input are padded with zero values (silence) at the end.
If you know how many samples you will generate, and you are not sure whether to include an explicit sample count qualifier, it is recommended that you do include it. In most cases, the Sonic/C++ translator will issue a translator error when it finds a wave assignment missing a sample count qualifier and whose number of samples cannot be determined. However, there are a few cases where such a mistake might slip by, resulting in a program that keeps generating output until your hard disk fills up! So be careful...
Rate and Time Placeholders. There are two more placeholders to learn about: 'r' and 't', representing sampling rate and current time, respectively. The symbol 'r' is a placeholder for the sampling rate being used in a particular Sonic program. It has integer value, and is expressed in samples per second, or Hz. By default, its value is 44100 Hz, but the programmer may redefine it. The programmer may use r inside or outside a wave assignment. It has constant value, unlike the other placeholders mentioned here.
A common use of r is to convert a time expressed in seconds to a number of samples. In terms of unit analysis, multiplying time in seconds by r samples/second causes the seconds to cancel, resulting in samples. For example, to generate 7.2 seconds of audio to a wave variable fred, use a wave assignment like
fred[c,i:r*7.2] = somefunc(c,i);
The symbol 't' represents the time expressed in seconds inside a wave assignment. This symbol has the same value as i*r, and is provided for convenience.
Wave Assignment Operators. All of the assignment operators previously defined may be used in a wave assignment. For example, the following wave assignment mixes a half-amplitude version of the wave variable 'mixfile' into the wave variable 'outfile':
An operator like += has the same meaning in this context with respect to individual data values that it has in a simple assignment. However, these mix-assignment operators may be applied only to wave variables on their left that already contain audio data. An attempt to do otherwise will cause a runtime error.outfile[c,i] += 0.5 * mixfile[c,i];
The Old-Data Placeholder '$'. Mix-assignment operators are useful in many cases where the programmer desires to modify the existing audio data in a wave variable. However, these operators cannot perform every possible modification. Consider a more general case where the programmer wishes to apply the sine function to the data in a wave variable, and replace the old data in that variable with the result. One might expect the Sonic code for such a situation to look like this:
This approach will not work. The surprising result is that sound would end up being a zero-length audio recording. The reason for this counter-intuitive behavior is that the appearance of the the same wave variable on both sides of = always causes the value on the right to be that of the variable during the assignment, not the value before the assignment began. This is necessary to allow recurrence relations, as was used to produce an echo effect in the sample program earlier in this manual. In other words, you cannot assign sound[c,i] to sound[c,i], because it doesn't have a value yet in that same assignment statement! Once the assignment statement begins executing, you cannot use the variable sound to refer to values it contained before the assignment started.program effect ( sound:wave ) { sound[c,i] = sin ( sound[c,i] ); // This doesn't work! }
To get around this problem, the special placeholder '$' is used to represent the old value of x[c,i], where x is the name of the variable on the left side of the assignment operator. So to correct the above code, the program function would be modified to read:
program effect ( sound:wave ) { sound[c,i] = sin($); // This does work! }
The Append Operator '<<'. The assignment operator << appends the wave expression on its right to the wave variable on its left. Note that the value of the placeholder i still starts at zero in this case, and is limited in the same manner as any other wave assignment. The operator << may appear only in a wave assignment; it is not allowed in a simple assignment. Here is an example of appending a one-second long 1kHz sinewave to the wave variable 'tones':
tones[c,i:r] << sinewave(1,1000,0);
Vector Expressions. Usually you will code a Sonic wave assignment to make a single expression apply to all channels in the output. By careful use of the placeholder c, you can use a single expression to treat different output channels differently. For example, if you were making a stereo output file, and you wanted a function leftfunc(t) to go to the left channel and a function rightfunc(t) to go to the right channel, you could write a line of code like:
However, in a wave assignment (and only there), a special syntax is allowed. The programmer may enclose comma-delimited expressions, one for each channel, inside curly braces {}. The example above could be coded as:outWave[c,i:5*r] = (1-c)*leftfunc(t) + c*rightfunc(t);
Any expression of this sort is called a vector expression. The number of subexpressions within the curly braces of a vector expression must be equal to the number of channels m. Note that you may still use all the placeholders that are usually valid in a wave expression, including c. For example, the vector expression {c,c} is equivalent to {0,1}. Vector expressions are more efficient than the equivalent use of multiplying by (1-c) and c as in the example above. Furthermore, when there are more than two channels in use, creating a single formula using c would be even more inefficient and awkward.outWave[c,i:5*r] = { leftfunc(t), rightfunc(t) };
if ( value > maxValue ) maxValue = value; else if ( -value > maxValue ) maxValue = -value; if ( x<17 & y<40 ) // note that '&' is boolean AND, not bitwise { x = SomeFunc(x); y += 3; }
function factorial ( num: integer ): integer { var product=1: integer; while ( num > 0 ) { product *= num; num -= 1; } return product; }
The initializer and update must be assignment statements, meaning they may be either simple assignment statements or wave assignment statements. The condition must be a boolean expression. The for construct is simply a shorthand notation for a while loop, as the above code is equivalent to:for ( initializer; condition; update ) statement
Here is an example of using for loops to initialize a two-dimensional array:initializer; while ( condition ) { statement; update; }
function something() { var z: real[5,3]; var row, col: integer; for ( row=0; row<5; row += 1 ) for ( col=0; col<5; col += 1 ) z[row,col] = row + noise(col); }
The repeat statement is ideal for situations like this. The syntax of a repeat statement is as follows:
The numeric_expression is evaluated only once. If it has a non-integer value, it is truncated to the closest integer smaller than it. If the resulting value is less than or equal to zero, the statement is not executed. Otherwise, the statement is executed the number of times indicated by the numeric_expression.repeat ( numeric_expression ) statement
The numeric_expression may be any expression of type real or integer, and the operation of the repeat construct has no side effects on its argument. Because the argument expression is evaluated only once, any effects that the statement body may have on variables in the repeat argument will not alter the number of iterations.
The following example function creates an output sound that contains the input sound repeated the specified number of times.
function OverAndOver ( inWave:wave, outWave:wave, numTimes:integer ) { outWave[c,i:0] = 0; // make the output sound be zero samples long repeat ( numTimes ) { outWave[c,i] << inWave[c,i]; // append inWave to outWave } }
function hypotenuse ( x:real, y:real ): real { return sqrt ( x*x + y*y ); } function AppendMaybe ( outWave:wave, inWave:wave, doit:boolean ) { if ( !doit ) return; repeat ( 5 ) outWave[c,i] << inWave[c,i]; }
Note that function arguments of type wave (unlike all other data types) are passed by reference, not value. This means that wave arguments may be modified by the called function and the modification will persist in the calling code. It also means that only a wave variable may be passed as a wave argument, not a general expression.program charro ( inWave:wave, outWave:wave ) { var temp: wave; trill ( inWave, temp, 3 ); // make 'temp' be 'inWave' repeated 3 times trill ( temp, outWave, 2 ); // make 'outWave' be 'inWave' repeated 6 times } function trill ( x:wave, y:wave, k:integer ) { y[c,i:0] = 0; // make 'y' be zero-length repeat ( k ) y[c,i] << x[c,i]; }
|
boolean OR (not bitwise)&
boolean AND (not bitwise)== < <= > >= != <>
comparison operators. The operators != and <> both mean "not equal".+ -
add, subtract two numbers.* / %
multiply, divide, modulus. Unlike in C++, the % operator allows operands to be integer or real.^
numeric exponentiation. x^y means x raised to the y power.! -
the unary operators boolean NOT and negative.. []
field selector and wave expression indexing. The left side of '.' must be a variable of type wave. The right side must be one of the following:n = The number of samples in the wave.
r = Same as global constant r.
m = Same as global constant m.
max = The maximum absolute amplitude of data in the wave.
()
Parentheses used to override operator precedence.
The second form of comment is bounded by the string /* and the string */. Such a comment may span more than one line. When this form of comment is used, all characters including and between the two strings /* and */ are ignored by the Sonic translator. Note that after /* is found, the very first occurrence of */ terminates the comment, regardless of whether another /* string is found after the beginning /*. In other words, comments of this form are not nestable. Here is an example of this kind of comment:outWave[c,i] = inWave[c,i]; // copy the input to the output
/* The following program switches the channels in the input file to produce the output file. In other words, the left channel in the input becomes the right channel in the output, and vice versa. */ m = 2; /* explicitly state that this is stereo audio */ program swap ( input:wave, output:wave ) { output[c,i] = input[1-c,i]; }
Textual symbols that appear literally in the source code are enclosed within double-quotes, e.g., "integer".
Items separated by the '|' symbol are mutually exclusive alternatives. For example, a parm_name is allowed to be either the literal string "r" or the literal string "m".
Items enclosed inside square brackets [] are optional; they may appear either zero or one times in the place indicated.
Items enclosed inside curly braces {} may appear zero or more times.
Items in italics are English descriptions of a lexical construct. I chose such descriptions when I felt they would be best understood that way.
program ::= {global} program_body {global} global ::= function_body | parm_override | import_decl | var_decl import_decl ::= "import" import_name { "," import_name } "from" string_constant ";" import_name ::= name parm_override ::= parm_name "=" integer_const ";" | "interpolate" "=" boolean_const ";" parm_name ::= "r" | "m" program_body ::= "program" name "(" func_args ")" "{" {var_decl} {statement} "}" statement ::= [function_call] ";" | assignment ";" | if_body | while_body | repeat_body | for_body "return" [b0] ";" | "{" {statement} "}" var_decl ::= "var" var_spec { "," var_spec } ":" type ";" var_spec ::= name [ var_init ] var_init ::= "=" b0 | "(" [ b0 { "," b0 } ] ")" type ::= basic_type | "wave" | import_name | array_type array_type ::= basic_type "[" integer_constant { "," integer_constant } "]" basic_type ::= "real" | "integer" | "boolean" function_call ::= name "(" [ expr { , expr } ] ")" function_body ::= "function" name "(" func_args ")" [ ":" type ] "{" {var_decl} {statement} "}" func_args ::= [ arg { "," arg } ] arg ::= name ":" type ["&"] assignment ::= lvalue assign_op expr lvalue ::= name [ "[" "c" "," "i" [ ":" term ] "]" ] | name "[" term { "," term } "]" assign_op ::= "=" | "<<" | "+=" | "-=" | "*=" | "/=" | "%=" expr ::= b0 | "{" b0 {"," b0} "}" b0 ::= b1 { "|" b1 } b1 ::= b2 { "&" b2 } b2 ::= term [ relop term ] relop ::= "==" | "<" | "<=" | ">" | ">=" | "!=" | "<>" term ::= t1 { op1 t1 } op1 ::= "+" | "-" t1 ::= t2 { op2 t2 } op2 ::= "*" | "/" | "%" t2 ::= t3 [ "^" t2 ] t3 ::= constant | var_term | function_call | "(" b0 ")" | "!" t3 | "-" t3 | "$" var_term ::= name [var_qualifier] var_qualifier ::= "." wave_field | "[" term { "," term } "]" wave_field ::= "n" | "r" | "m" | "max" constant ::= numeric_constant | string_constant | builtin builtin ::= "pi" | "e" | "i" | "c" | "n" | "t" | parm_name | boolean_const boolean_const ::= "true" | "false" if_body ::= "if" "(" b0 ")" statement [ "else" statement ] while_body ::= "while" "(" b0 ")" statement for_body ::= "for" "(" assignment ";" b0 ";" assignment ")" statement repeat_body ::= "repeat" "(" term ")" statement numeric_constant ::= integer_constant | real_constant integer_constant ::= a decimal integer number, e.g. 123 real_constant ::= a decimal real number expressed in standard (123.4) or scientific (1.234e+2) notation string_constant ::= zero or more ASCII characters inside double quotes name ::= alpha{alnum} alpha ::= any alphabetic character "a"-"z", "A"-"Z", or underscore "_" alnum ::= alpha | "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
The output filename is not derived from any of the input filenames. Instead, it is determined by the name following the program function. For example, if the program function looks likesonic barney.s wilma.s
then the output filename will be 'betty.cpp'. Although there may be any number of input source files, there must be exactly one program function amongst all of them. The source files may appear in any order without incurring errors, regardless of which functions call which others across source files.program betty ( inWave:wave, outWave:wave ) { ... }
Of course, after you translate Sonic into C++, you will need to compile and link it with the C++ compiler of your choice. To do this without getting compiler and linker errors, you will need to get a copy of the Sonic runtime library from the Sonic home page. Place the following header files from the runtime library in your C++ compiler's include path:
In addition to the source file generated by the Sonic/C++ translator, include the following source files in the build of your project:sonic.h copystr.h ddc.h riff.h fourier.h
sonic.cpp copystr.cpp riff.cpp fourierd.cpp fftmisc.cpp
I have tried to document all of the features of Sonic in this manual accurately and lucidly. However, there certainly are things that can be confusing. One suggestion I have for times of confusion is to examine the C++ code generated by the translator. I have tried to make the C++ code produced by the Sonic translator as readable as possible. All output is neatly formatted, and constructs which generate complex code such as wave assignments are commented with the original Sonic code next to the C++ code. In many cases, the programmer can experiment by trial and error, reading the code produced by the translator, to understand the Sonic language better.
Good luck, and happy acoustic hacking! If you have any problems, questions, comments, or suggestions, feel free to contact me, Don Cross, by email using the address at the beginning of this manual. Please let me know of any bugs in the translator or runtime library, or if you find parts of this manual incorrect or confusing. I would also like to hear of any interesting results you have obtained using Sonic. Feel free to send programs you have written my way, especially if you would like to contribute sample programs for publication on the Sonic home page.