Read Me
Pybris is a compiler written in Python using Pyparsing for the B Programming Language.
The compiler emits a variant of Bitmario RISVM assembly.
The practical goal of the project is to provide a way to develop digital signal processing (DSP) effects for the
Competent Audio library that is a friendlier alternative to writing RISVM assembly by hand.
Pybris is written for Python 2.7, but has also been tested to run with Python 3.8.10.
---- 0. Basic Usage ----
'pybris.py' compiles console RISVM applications (see section 4), while 'pybris_dsp.py' compiles DSP effects for use
with the Competent Audio Library (section 5).
To compile an run the hello world sample:
python pybris/pybris.py samples/helloworld/hello.b
python assembler/assembler.py samples/helloworld/hello.asm
risvmc/risvmc samples/helloworld/hello.bin
Alternatively, use the Java assembler (Requires Java 8 or later):
java -jar risvm-assembler.jar samples/helloworld/hello.asm -o
---- 1. Known issues ----
Error reporting is not very good.
---- 2. Deviation from the specification ----
The compiler implements the B Programming Language as specified in the 1998 edition of the B Language Reference Manual
from Thinkage Ltd., with the following known deviations:
- Manifest constants are not implemented. (Instead, you are encouraged to use a C preprocessor such as mcpp.)
- The standard library is not implemented. (The DSP RISVM comes with a different set of built-in library routines.)
- Identifiers and keywords are always case sensitive.
- The compiler uses every character of an identifier, not just the first 6.
- 32-bit words are used instead of 36-bit words.
- Octal integers are not supported.
- Hexadecimal integers are supported (in the familiar 0x notation).
- Nested ivals in global variable initialisers are not supported.
- BCD constants are not available.
- ASCII constants use 8 bits per character rather than 9.
- Only *t, *n, and *e character escapes are available.
- No source code escapes are available.
- Byte addresses are used, with the following consequences:
The identity a[i] == i[a] does not hold (those have the addresses a+4*i and i+4*a, respectively).
The identity a[i] == *(a+i) does not hold (those have the addresses a+4*i and a+i, respectively).
- Auto-variables can have initialisers, such as 'auto i = 0;'
- The first expression in a 'for' statement can be an auto declaration, e.g. 'for(auto i = 0; i<8; i++)'
- Declaring functions using extrn is optional unless the function has not been defined yet (can be disabled).
- The empty statement '{}' is legal.
- The empty statement ';' is a syntax error (however 'for(;;)' is still legal).
---- 3. RISVM modifications ----
Pybris compiles to a variant of RISVM assembly that is different from Bitmario's original specification in the
following ways:
1. The additional instructions icom, ucom, and fcom are required.
These compare ints, unsigned ints and floating point numbers, respectively.
They take 3 register operands a, b, c, such that:
a = b > c ? 1 : (b < c ? -1 : 0);
2. The following enhancements to the assembly language are required:
- Support for floating point literals.
- Labels can be used as data, e.g. '$someData word $someLabel', inserts the address of $someLabel.
- Data can be sized, e.g. '$someData dword[64] 1', pads $someData with 63 dword zeroes.
---- 4. Standard functionality ----
The 'pybris.py' compiles regular RISVM programs, which can be run using the standalone RISVM executable after
assembling. A B program must define the 'main' function to be used in this configuration.
The following built-in functions are available to regular RISVM programs.
They are not addressable, but otherwise behave as normal functions:
- printu(uint) Prints the given unsigned integer and returns it
- printi(int) Prints the given signed integer and returns it
- printf(float) Prints the given floating point number and returns it
- printc(char) Prints the given character object:
Returns the last printed character, 0 if the argument ended with a null terminator.
- print(vec) Prints the given string (vector of character objects),
and returns the address of the last printed character object.
- println() Print a newline.
- exit(int) Halt the RISVM and place the given value in the return value register.
- nargs() Returns the number of arguments passed to the current function.
- arg(int) Returns the argument at the given index (even if no such argument was declared).
---- 5. DSP functionality ----
The 'pybris_dsp.py' script emits RISVM DSP assembly suitable for use with the Competent Audio library.
A B program must define the following functions to be used in this configuration:
- init(int, int, int)
Called when the DSP is initialized (or re-initialized).
The arguments are: dspVersion (e.g. 0), sampleRate (e.g. 48000), channelCount (e.g. 2)
- initMP() (optional)
Always called immediately after init, with a variable number of arguments, which may be 0.
The application and DSP program must agree on a protocol for the arguments that are used.
If omitted, mainMP will be called instead.
- mainMP()
Called before main if the application has sent a message, following the same protocol as initMP.
- main(int, float*)
Called to process the frame data in the array of floats given by the second argument.
The number of sample frames in the array is given by the first argument.
The number of samples in each sample frame is given by the channel count passed to init.
In addition to the regular built-in functions, the following additional built-in functions are available to
DSP programs:
- cos(float) Returns the cosine of the argument
- sin(float) Returns the sine of the argument
- tan(float) Returns the tangent of the argument
- acos(float) Returns the inverse cosine of the argument
- asin(float) Returns the inverse sine of the argument
- atan(float) Returns the inverse tangent of the argument
- atan2(float, float) Returns the unique arc tangent for the two arguments
- cosh(float) Returns the hyperbolic cosine of the argument
- sinh(float) Returns the hyperbolic sine of the argument
- tanh(float) Returns the hyperbolic tangent of the argument
- exp(float) Returns e^x, where x is the argument
- log(float) Returns the natural logarithm of the argument
- log10(float) Returns the base 10 logarithm of the argument
- pow(float, float) Returns x^y, for the two respective arguments
- sqrt(float) Returns the square root of the argument
- fmod(float, float) Returns the floating point modulo of the arguments
- floor(float) Returns the argument rounded down to an integer, as a float.
- ceil(float) Returns the argument rounded up to an integer, as a float.
- round(float) Returns the argument rounded to an integer, as a float.
- fabs(float) Returns the absolute value of the argument as a float.
- abs(int) Returns the absolute value of the argument as an int.
- fsign(float) Returns the sign of the argument as a float.
- sign(int) Returns the sign of the argument as an int.
- fmin(float, float) Returns the smallest of the two floating point values.
- umin(uint, uint) Returns the smallest of the two unsigned int values.
- min(int, int) Returns the smallest of the two int values.
- fmax(float, float) Returns the biggest of the two floating point values.
- umax(uint, uint) Returns the biggest of the two unsigned int values.
- max(int, int) Returns the biggest of the two int values.
- bqNew
Creates an empty biquad filter object for a single channel.
Note: The filter is automatically deleted when the DSP is disposed, or before it is re-initialized.
The DSP must record parameter changes so it can re-apply the updated values on reinitialization.
Arguments: 1
uint - sample rate
Returns: uint - filter handle
- bqProc
Filters a single audio data channel using a biquad filter.
Arguments: 4
uint - filter handle
uint - RISVM address of audio data
uint - number of samples to process
uint - number of elements between each sample (typically the channel count)
Returns: void
- bqReset
Resets the sample buffer for the given filter. This may be useful when changing the filter type.
Arguments: 1
uint - filter handle
Returns: void
- bqLowpass
Configures the given filter as a lowpass filter
Arguments: 3
uint - filter handle
float - cutoff frequency (hz)
float - resonance
Returns: void
- bqHighpass
Configures the given filter as a highpass filter
Arguments: 3
uint - filter handle
float - cutoff frequency (hz)
float - resonance
Returns: void
- bqBandpass
Configures the given filter as a bandpass filter
Arguments: 3
uint - filter handle
float - frequency (hz, location of the peak in the frequency spectrum)
float - Q-factor (related to inverse of the peak width, use 1.0 as a starting point)
Returns: void
- bqNotch
Configures the given filter as a notch filter
Arguments: 3
uint - filter handle
float - frequency (hz, location of the peak in the frequency spectrum)
float - Q-factor (related to inverse of the peak width, use 1.0 as a starting point)
Returns: void
- bqPeaking
Configures the given filter as a peaking filter
Arguments: 4
uint - filter handle
float - frequency (hz, location of the peak in the frequency spectrum)
float - Q-factor (related to inverse of the peak width, use 1.0 as a starting point)
float - gain factor
Returns: void
- bqAllpass
Configures the given filter as an allpass filter
Arguments: 3
uint - filter handle
float - frequency (hz, location of the peak in the frequency spectrum)
float - Q-factor (related to inverse of the peak width, use 1.0 as a starting point)
Returns: void
- bqLowshelf
Configures the given filter as a low shelf filter
Arguments: 4
uint - filter handle
float - frequency (hz, location of the peak in the frequency spectrum)
float - Q-factor (related to inverse of the peak width, use 1.0 as a starting point)
float - gain factor
Returns: void
- bqHighshelf
Configures the given filter as a high shelf filter
Arguments: 4
uint - filter handle
float - frequency (hz, location of the peak in the frequency spectrum)
float - Q-factor (related to inverse of the peak width, use 1.0 as a starting point)
float - gain factor
Returns: void
- fvNew
Creates a Jezar Freeverb reverberator object with the default configuration.
Note: The reverberator is automatically deleted when the DSP is disposed, or before it is re-initialized.
The DSP must record parameter changes so it can re-apply the updated values on reinitialization.
Arguments: 8
uint - sample rate
uint - number of channels per sample frame
float - roomsize
float - damp
float - wet
float - dry
float - width
float - freezemode
Returns: uint - reverberator handle
- fvProc
Adds reverberation to audio data using a Jezar Freeverb reverberator.
Arguments: 4
uint - reverberator handle
uint - RISVM address of audio data
uint - number of sample frames to process
uint - number of samples between each frame (typically same as channel count)
Returns: void
- fvMute
Resets the given reverberator so it stops being noisy
Arguments: 1
uint - reverberator handle
Returns: void
- fvRoomSize
Sets the room size for the given reverberator
Arguments: 2
uint - reverberator handle
float - room size factor
Returns: void
- fvDamp
Sets the damping for the given reverberator
Arguments: 2
uint - reverberator handle
float - damping factor
Returns: void
- fvWet
Sets the wet signal multiplier for the given reverberator
Arguments: 2
uint - reverberator handle
float - wet factor
Returns: void
- fvDry
Sets the dry signal multiplier for the given reverberator
Arguments: 2
uint - reverberator handle
float - dry factor
Returns: void
- fvWidth
Sets the stereo width for the given reverberator
Arguments: 2
uint - reverberator handle
float - width factor
Returns: void
- fvFreezeMode
Enables or disables freeze mode for the given reverberator.
In freeze mode the the current wet reverberation signal is sustained forever,
and further inputs will not affect it.
Arguments: 2
uint - reverberator handle
float - freeze mode (enabled if value >= 0.5)
Returns: void
---- 6. Compiler Optimisations ----
The following compiler optimisations are implemented:
- A very primitive form of local register allocation is used, described in 'register allocation.txt'.
- Jump tables are used for dense switch statements.
- The basic form of constant folding is implemented:
Arithmetic using only numeric literals can be assumed to have no cost.
- Some instruction selection optimisations are present.
That aside, it should be assumed that the compiler does not optimise for you.
---- 7. Miscellaneous notes ----
Pybris supports the #line directives emitted by C preprocessors: Most errors should display the correct file and line
number even when using a preprocessor.
Character objects are emitted as 32 bit integers with the first character in the least significant byte. This results
in the assembly being more difficult to read, but it was the easiest way to ensure strings and character objects have
the same structure seen from the B program, regardless of the byte order of the target machine, since the B language
does not support handling individual characters.
In theory, RISVM assembly is byte order-agnostic, but the bytecode form is not. The Java RISVM Assembler can emit
big-endian bytecode, which should, in theory, allow the program run on big-endian machines. This is currently untested.
Stack traces are made possible (e.g. when triggering a memory error or using the exit function) by following the
Competent Audio DSP calling convention. The Java RISVM Assembler can be used to emit a line table, which allows the
RISVM to display assembly line numbers and contents in the stack trace.
Using gotos and labels, and creating pointers to variables using the & operator will degrade or prevent some of the
register allocation optimizations.
Using goto to jump to any location that is not a label in the same function will result in undefined behaviour.
Attempting to access a missing argument in a function may result in a memory error (halting the VM).