The behaviour of the DetablockerBuf UGen I wrote is based on Dave’s Betablocker system. Therefore the UGen has the following structure:
- a heap of 256 8bit values,
- an instruction table,
- one thread consisting of
- a program counter, pointing to an address in the heap, and
- a stack of eight 8bit values.
I decided to implement it as a Demand UGen which, when triggered, makes one computational step and returns the topmost element of the stack. A SuperCollider Buffer object represents the heap, allowing to share it between several DetablockerBuf UGens. This is a very simple example on how to use it:
// load a random program into a buffer
b = Buffer.alloc(s, 256);
b.loadCollection({256.rand}!256);
// play it at a rate of 20000 operations per second
{Demand.ar(Impulse.ar(20000), 0, DetaBlockerBuf(b.bufnum, 0))}.play;
heap
This is the place where the program and its data are located. In line to the von Neumann architecture, it is possible (and intended) to alter the program while it is running, i.e. interpreting it as data rather than an instruction set. The actual representation of a heap is an array of 256 8bit values. Each value can be interpreted either as an instruction, an address, or an actual number.
thread
A thread is something that executes code stored on the heap. It has a current position (the instruction it is currently evaluating), a stack (some sort of storage, see below), and a timer, determining when it will move its program counter to the heap’s next address.
stack
A stack serves as a temporary storage for a thread. It can be accessed only from top by either (a) pop an item from the stack, (b) return the top_most item without removing it, and (c) _push a value to the stack.
instructions
These are the implemented instructions:
NOP – do nothing ORG – define relative origin address for this thread EQU – compare first two elements on the stack, pop them off the stack, and push the result JMP – jump to the address specified right after this instruction JMPZ – jump only if stack returns 0 PSHL – push value on address specified right after this instruction to the stack PSH – push value on address specified by the value on the address specified right after this instruction to the stack PSHI – like push but one more encapsulation POP – pop item from stack to the point in the heap following this instruction POPI – pop item from stack and write the value at that address in the heap to the address following this instruction ADD – perform an addition on the first two elements on the stack, pop them off the stack, and push the result SUB – perform a subtraction on the first two elements on the stack, pop them off the stack, and push the result INC – increment value on stack DEC – decrement value on stack AND – perform a bit-wise "and" on the first two elements on the stack, pop them off the stack, and push the result OR – perform a bit-wise "or"" on the first two elements on the stack, pop them off the stack, and push the result XOR – perform a bit-wise "xor" on the first two elements on the stack, pop them off the stack, and push the result NOT – perform a bit-wise "not" on the first element on the stack, pop it off the stack, and push the result ROR – perform a right shift operation on the first two elements on the stack, pop them off the stack, and push the result ROL – perform a left shift operation on the first two elements on the stack, pop them off the stack, and push the result PIP – increments value specified by the next value on the heap PIP – decrements value specified by the next value on the heap DUP – push a duplicate of the topmost value of the stack to the stack NOTE – usually play a note (here: like NOP) NOTE – usually play a vox (here: like NOP) STOP – usually stop program (here: like NOP)
This is their actual implementation:
switch(instr)
{
case NOP: break;
case ORG: m_start=m_start+m_pc-1; m_pc=1; break;
case EQU: push(pop()==pop()); break;
case JMP: m_pc=peek(m,m_pc++); break;
case JMPZ: m_pc++; if (pop()==0) m_pc=peek(m,m_pc); break;
case PSHL: push(peek(m,m_pc++)); break;
case PSH: push(peek(m,peek(m,m_pc++))); break;
case PSHI: push(peek(m,peek(m,peek(m,m_pc++)))); break;
case POP: poke(m,peek(m,m_pc++),pop()); break;
case POPI: poke(m,peek(m,peek(m,m_pc++)),pop()); break;
case ADD: push(pop()+pop()); break;
case SUB: push(pop()-pop()); break;
case INC: push(pop()+1); break;
case DEC: push(pop()-1); break;
case AND: push(pop()&pop()); break;
case OR: push(pop()|pop()); break;
case XOR: push(pop()^pop()); break;
case NOT: push(~pop()); break;
case ROR: push(pop()>>peek(m,m_pc++)); break;
case ROL: push(pop()<<peek(m,m_pc++)); break;
case PIP:
{
u8 d=peek(m,m_pc++);
poke(m,d,peek(m,d)+1);
} break;
case PDP:
{
u8 d=peek(m,m_pc++);
poke(m,d,peek(m,d)-1);
} break;
case DUP: push(top()); break;
case NOTE:
{
// m_pitch=pop();
// m_played_sound=m_pitch;
// m_sound->play(m_instrument,m_pitch);
} break;
case VOX:
{
// m_instrument=pop();
// m_played_sound=m_pitch;
// m_sound->play(m_instrument,m_pitch);
} break;
case STOP: /*m_active=false;*/ break;
default : break;
};