/* scripting language for fm synthesis via opl3(1) http://www.fit.vutbr.cz/~arnost/opl/opl3.html The eighteen opl3 channels are renumbered for a more convenient layout: Each channel has two operators. Normally a channel is in one of either fm mode (op0 modulates op1) or am mode (op0 and op1 are summed). Channels 0–5 may use the four-operator algorithms, which link the operators of Channel x and Channel x + 6 in various ways (see below). Percussion Mode makes use of Channels 12, 14 & 16 when enabled. A string of decimal digits represents a number ∈ [0, 65535]. A note name like C4, Cb4, or C#4 represents a so-called F-Number. These F-Numbers are an internal representation of pitch, not the actual frequeny in hertz, so performing arithmetic on them is not recommended: instead, use the c- and c+ commands to adjust pitches, or else arithmetic followed by the hz command, which converts frequency to F-Number. Any other string is the name of either a command or a macro. Macro names should not contain the brace characters [({})]. Macros do not override commands. Numbers collect on the main stack and are used and modified by commands. The following RPN arithmetic commands should work as expected: + - * / % << >> & | ^ ~ ! < <= == != >= > Arithmetic which results in a number ∉ [0, 65535] is an error. Stk Cmd Resulting Stack n hz (F-Number representing n Hz) m n rand (random number ∈ [m, n]) m n exch n m m n pop m n dup n n Variable storage is provided in the form of named stacks. Since they are always prefixed by a special rune, stacks may have any name that does not contain the brace characters [({})]. ←var pop a number from the main stack and push it onto var →var pop a number from var and push it onto the main stack $var peek at var and push a copy of its top number onto the main stack n ⇐var do ←var n times n ⇒var do →var n times One use for these is setting up melodies in advance, eg: C4 E4 G4 B4 4 ⇐arpeggio Creates a stack called arpeggio containing the numbers B4, G4, E4, C4, ready for a macro to pop off each frequency number when it is time to play that note. Braced Commands: ( comment ) [ macro which a later instance of str will invoke ]str n { loops and conditionals: pop n off the main stack; if n == 0, skip this section otherwise, push n - 1 onto the stack, play this section, loop } Timing commands: n m spin for n milliseconds n t set tick length to n milliseconds . spin for one tick T put tick length on the stack Synthesizer settings default to zero unless otherwise noted. Global commands: dt toggle deep tremolo dv toggle deep vibrato pm toggle percussion mode Channel commands: 0–17 c set channel for subsequent commands, set operator to 0 f n play the pitch corresponding to F-Number f (usually specified as a note name) Shortcut: a string like xC4, xCb4, or xC#4 plays a note on channel x without having to switch to/away from channel x, ie. without altering the effect of the c command. n c+ increase frequency by n cents (hundredths of a semitone) n c- decrease frequency by n cents no note off 0–7 fb feedback fm op0 → op1 (default: op0 = modulator, op1 = carrier) am op0 + op1 (both operators are carriers) fmfm op0 (ch x) → op1 (ch x) → op0 (ch x+6) → op1 (ch x+6) amfm op0 (ch x) + op1 (ch x) → op0 (ch x+6) → op1 (ch x+6) fmam op0 (ch x) → op1 (ch x) + op0 (ch x+6) → op1 (ch x+6) amam op0 (ch x) + op1 (ch x) → op0 (ch x+6) + op1 (ch x+6) left L mono [proper panning can only be approximated by mixing two…] rght R mono […identical channels, one L, one R, at different levels] both L = R stereo (default) mute Operator commands: o toggle operator for subsequent commands (c resets this to 0) 0–15 mul frequency multiplier (0 = ½) trem toggle tremolo vib toggle vibrato sus toggle sustaining ksr toggle envelope attenuation per octave 0–15 a attack rate 0–15 d decay rate 0–15 s sustain level 0–15 r release rate 0–3 ksl volume attenuation per octave 0–63 v volume (default: 63) sine operator waveform = sine: ^v^v^v^v (default) half half sine: ^-^-^-^- abs absolute sine: ^^^^^^^^ puls pulse sine: ´´´´´´´´ even even sine: ^v--^v-- eabs even absolute sine: ^^--^^-- sq square: ¯_¯_¯_¯_ desq derived square: ╮╯╮╯╮╯╮╯ Percussion mode commands: bd bass drum sd snare drum tt tom tom hh hi hat cy cymbal These trigger the drums. Tuning them is a little tricky: Channel 12 is a normal fm mode channel representing the bass drum. Channel 14: pitch = snare drum pitch op0 = hi hat envelope op1 = snare drum envelope Channel 16: pitch = tom tom pitch op0 = tom tom envelope op1 = cymbal envelope The pitch/character of the hi hat and cymbal is determined by the ratio between the pitches of the snare drum and tom tom. This is the only place where ratios are not bound to simple integers, so some interesting sounds are possible, though two pitches control four "instruments", so have fun with that! For more detail and a nice diagram: http://midibox.org/forums/topic/18625-opl3-percussion-mode-map/ */ #include #include typedef struct Stack Stack; struct Stack{ char *name; ushort val[64]; int top; Stack *next; }stack; typedef struct Macro Macro; struct Macro{ char *name, *loc; Macro *next; }*macro; int p[2]; char *script, *s, *e; uchar bd, ra[18], rb[18], rc[18], r2[54], r4[54], r6[54], r8[54]; u32int four; void err(char *m){ sysfatal("%s: %s:#%d", m, e, utfnlen(script, s - script)); } void flush(uchar *b, int n){ if(p[0] == p[1]) /* debug mode */ while(n){ print("0x%0.2x, 0x%0.2x, 0x%0.2x, 0x%0.2x, 0x%0.2x,\n", b[0], b[1], b[2], b[3], b[4]); b += 5; n -= 5; } else write(p[0], b, n); } void cmd(uchar r0, uchar r1, ushort v){ static uchar b[250] = { 1, 0, 0x20, 0, 0, /* wave form enable */ 5, 1, 1, 0, 0, /* OPL3 enable */ }; static int i = 10; if(r0 == 0){ if(i == 0){ b[0] = b[1] = b[2] = 0; i = 5; } b[i - 2] = v; b[i - 1] = v >> 8; flush(b, i); b[i - 2] = b[i - 1] = 0; i = 0; return; } b[i] = r0; b[i + 1] = r1; b[i + 2] = v; if(i == 245){ flush(b, 250); i = 0; }else i += 5; } void push(Stack *k, int n){ if(n < 0 || n > 65535) sysfatal("stack value %d ∉ [0, 65535]: %s:#%d", n, e, utfnlen(script, s - script)); if(k->top == nelem(k->val)) sysfatal("stack overflow: %d values: %s:#%d", k->top, e, utfnlen(script, s - script)); k->val[k->top++] = n; } int pop(Stack *k, int min, int max){ if(!k->top--) err("stack is empty"); if(k->val[k->top] < min || k->val[k->top] > max) sysfatal("stack value %d ∉ [%d, %d]: %s:#%d", k->val[k->top], min, max, e, utfnlen(script, s - script)); return k->val[k->top]; } void togfour(int i){ four ^= 1 << i; cmd(4, 1, four & 33 | four >> 1 & 2 | four >> 2 & 4 | four << 2 & 8 | four << 1 & 16); } int hztofnum(double f){ /* an F-Number's bits are: 000bbbff ffffffff, where f / 2^(20 - b) = hz / 49716 */ int b; f /= 49716.0; /* input is already hz * 2^20 */ for(b = 0; b < 7 && f >= 1024.0; b++) f /= 2.0; return b << 10 | (int)f & 0x3ff; } int strtofnum(char *t, int n){ if(n < 2 || n > 3 || t[0] < 'A' || t[0] > 'G' || t[n - 1] < '0' || t[n - 1] > '9' || n == 3 && t[1] != 'b' && t[1] != '#') return 0; n = *t - 'A' << 1; if(*t > 'E') n--; if(*t > 'B') n -= 13; t++; if(*t == 'b') t++, n--; if(*t == '#') t++, n++; n += 12 * (20 + *t - '0' - 4); return hztofnum(440.0 * pow(2.0, n / 12.0)); } void pitch(int ch, int f, int trig){ if(trig){ if(rb[ch] & 0x20) cmd(0xb0 | ch >> 1, ch & 1, rb[ch] & 0x1f); rb[ch] |= 0x20; } cmd(0xa0 | ch >> 1, ch & 1, ra[ch] = f); cmd(0xb0 | ch >> 1, ch & 1, rb[ch] = rb[ch] & 0x20 | f >> 8); } #define tok(c) c&&c!=' '&&c!='\t'&&c!='\n'&&c!='('&&c!=')'&&c!='{'&&c!='}'&&c!='['&&c!= ']' int toklen(char *t){ char *e = t; while(*e && tok(*e)) e++; return e - t; } int cmp(char *a, char *b){ if(a == b) return 0; while(*a == *b && tok(*a)) a++, b++; return tok(*a) || tok(*b); } void main(int n, char **a){ Stack *var; Macro *mac; int i, ch, op, drums, tick; if(n != 2 && (n != 3 || a[1][0] != '-' || a[1][1] != 'd' || a[1][2])) sysfatal("usage: %s [-d] script", a[0]); if((op = open(e = a[n - 1], OREAD)) < 0) sysfatal("open: %r"); tick = ch = 0; while(9){ if(ch - tick < 1024 && (script = realloc(script, ch += 8192)) == nil) sysfatal("realloc: %r"); if((i = read(op, script + tick, ch - tick)) < 1) break; tick += i; } if(i) sysfatal("read: %r"); close(op); if((s = script = realloc(script, tick + 1)) == nil) sysfatal("realloc: %r"); script[tick] = 0; if(n == 2){ if(pipe(p) < 0) sysfatal("pipe: %r"); switch(fork()){ case -1: sysfatal("fork: %r"); case 0: close(p[0]); dup(p[1], 0); close(p[1]); execl("/bin/games/opl3", "opl3", "-r", "1000", nil); sysfatal("execl: %r"); } close(p[1]); } for(i = 0; i < 18; i++) cmd(0xc0 | i >> 1, i & 1, rc[i] = 0x30); /*all channels stereo */ drums = ch = op = 0; tick = 1; srand(time(0)); while(9){ while(*s == ' ' || *s == '\t' || *s == '\n') s++; switch(*s){ case '(': for(s += n = 1; n; s++) switch(*s){ case 0: err("unmatched ("); case '(': n++; break; case ')': n--; break; } continue; case '[': for(s++, n = 0; n || *s != ']'; s++) switch(*s){ case 0: err("unmatched ("); case '[': err("macro definitions may not nest"); case '(': n++; break; case ')': n--; break; } s++; for(mac = macro; mac != nil && cmp(s, mac->name); mac = mac->next) ; if(mac == nil){ if((mac = malloc(sizeof *mac)) == nil) sysfatal("malloc: %r"); mac->next = macro; macro = mac; } mac->name = s; s += toklen(s); continue; case ']': s++; for(mac = macro; mac != nil && cmp(s, mac->name); mac = mac->next) ; if(mac == nil) err("unmatched ]"); s = mac->loc; continue; case '{':/*}*/ s++; if(i = pop(&stack, 0, 65535)) push(&stack, i - 1); else for(n = 1; i || n; s++) switch(*s){ case 0: err("unmatched {");/*}*/ case '{': if(!i) n++; break; case '}': if(!i) n--; break; case '(': i++; break; case ')': i--; break; } continue; case /*{*/'}': if(i = pop(&stack, 0, 65535)){ push(&stack, i - 1); i = 0; n = 1; while((i || n) && --s >= script) switch(*s){ case '{': if(!i) n--; break; case '}': if(!i) n++; break; case '(': i--; break; case ')': i++; break; } if(s < script) /*{*/err("unmatched }"); } s++; continue; } switch(n = toklen(s)){ case 0: close(p[0]); waitpid(); if(stack.top) sysfatal("%d values still on stack: top one is %d", stack.top, stack.val[stack.top - 1]); exits(nil); default: i = 0; break; case 1: i = s[0]; break; case 2: i = s[0] << 8 | s[1]; break; case 3: i = s[0] << 16 | s[1] << 8 | s[2]; break; case 4: i = s[0] << 24 | s[1] << 16 | s[2] << 8 | s[3]; break; } switch(i){ default: for(i = 0; s[i] >= '0' && s[i] <= '9'; i++) ; if(i == n){ for(n = 0; *s >= '0' && *s <= '9'; s++) n = n * 10 + *s - '0'; push(&stack, n); continue; } if(i = strtofnum(s, n)) push(&stack, i); else if(*s >= '0' && *s <= '9' && (i = strtofnum(s + 1, n - 1))) pitch(*s - '0', i, 1); else if(s[0] == '1' && s[1] >= '0' && s[1] <= '7' && (i = strtofnum(s + 2, n - 2))) pitch(10 + s[1] - '0', i, 1); else if(s[0] == '$' || (s[0] & 0xff) == 0xe2 && (s[1] & 0xfe) == 0x86 && (s[2] & 0xfd) == 0x90){ s += s[0] == '$' ? 1 : 3; for(var = stack.next; var != nil && cmp(s, var->name); var = var->next) ; if(var == nil){ if(s[-1] == '$' || s[-1] & 2) err("no such stack"); if((var = malloc(sizeof *var)) == nil) sysfatal("malloc: %r"); var->name = s; var->top = 0; var->next = stack.next; stack.next = var; } if(s[-1] == '$'){ n--; i = pop(var, 0, 65535); push(var, i); push(&stack, i); }else{ n -= 3; i = s[-2] & 1 ? pop(&stack, 1, 65535) : 1; if(s[-1] & 2) while(i--) push(&stack, pop(var, 0, 65535)); else while(i--) push(var, pop(&stack, 0, 65535)); } }else{ for(mac = macro; mac != nil && cmp(s, mac->name); mac = mac->next) ; if(mac == nil) err("unrecognized string"); mac->loc = s + n; for(s = mac->name - 1; s[-1] != '['; s--) ; continue; } break; case 'm': i = pop(&stack, 1, 65535); if(0) case '.': i = tick; if(drums){ if(~bd & 0x20) err("drums cued but not in percussion mode"); cmd(0xbd, 0, bd); cmd(0xbd, 0, bd | drums); drums = 0; } cmd(0, 0, i); break; case '!': push(&stack, !pop(&stack, 0, 65535)); break; case '!' << 8 | '=': i = pop(&stack, 0, 65535); push(&stack, pop(&stack, 0, 65535) != i); break; case '=' << 8 | '=': i = pop(&stack, 0, 65535); push(&stack, pop(&stack, 0, 65535) == i); break; case '<': i = pop(&stack, 0, 65535); push(&stack, pop(&stack, 0, 65535) < i); break; case '<' << 8 | '=': i = pop(&stack, 0, 65535); push(&stack, pop(&stack, 0, 65535) <= i); break; case '>' << 8 | '=': i = pop(&stack, 0, 65535); push(&stack, pop(&stack, 0, 65535) >= i); break; case '>': i = pop(&stack, 0, 65535); push(&stack, pop(&stack, 0, 65535) > i); break; case '~': push(&stack, ~pop(&stack, 0, 65535) & 65535); break; case '+': i = pop(&stack, 0, 65535); push(&stack, pop(&stack, 0, 65535) + i); break; case '-': i = pop(&stack, 0, 65535); push(&stack, pop(&stack, 0, 65535) - i); break; case '*': i = pop(&stack, 0, 65535); push(&stack, pop(&stack, 0, 65535) * i); break; case '/': i = pop(&stack, 1, 65535); push(&stack, pop(&stack, 0, 65535) / i); break; case '%': i = pop(&stack, 1, 65535); push(&stack, pop(&stack, 0, 65535) % i); break; case '&': i = pop(&stack, 0, 65535); push(&stack, pop(&stack, 0, 65535) & i); break; case '|': i = pop(&stack, 0, 65535); push(&stack, pop(&stack, 0, 65535) | i); break; case '^': i = pop(&stack, 0, 65535); push(&stack, pop(&stack, 0, 65535) ^ i); break; case '<' << 8 | '<': i = pop(&stack, 1, 15); push(&stack, pop(&stack, 0, 65535) << i & 65535); break; case '>' << 8 | '>': i = pop(&stack, 1, 15); push(&stack, pop(&stack, 0, 65535) >> i); break; case 'a': cmd(0x60 | op & 31, op >> 5, r6[op] = r6[op] & 0x0f | pop(&stack, 0, 15) << 4); break; case 'a' << 16 | 'b' << 8 | 's': cmd(0xe0 | op & 31, op >> 5, 2); break; case 'a' << 8 | 'm': cmd(0xc0 | ch >> 1, ch & 1, rc[ch] |= 1); if(four >> ch & 1) togfour(ch); break; case 'a' << 24 | 'm' << 16 | 'a' << 8 | 'm': case 'a' << 24 | 'm' << 16 | 'f' << 8 | 'm': case 'f' << 24 | 'm' << 16 | 'a' << 8 | 'm': case 'f' << 24 | 'm' << 16 | 'f' << 8 | 'm': cmd(0xc0 | ch >> 1, ch & 1, rc[ch] = rc[ch] & 254 | s[0] == 'a'); cmd(0xc0 | ch + 6 >> 1, ch & 1, rc[ch + 6] = rc[ch + 6] & 254 | s[2] == 'a'); if(~four >> ch & 1) togfour(ch); break; case 'b' << 8 | 'd': drums |= 16; break; case 'b' << 24 | 'o' << 16 | 't' << 8 | 'h': cmd(0xc0 | ch >> 1, ch & 1, rc[ch] |= 0x30); break; case 'c': ch = pop(&stack, 0, 17); op = ch / 2 + ch / 6 * 5 | ch << 5 & 32; break; case 'c' << 8 | '+': i = (rb[ch] << 8 & 0x300 | ra[ch]) << (rb[ch] >> 2 & 7); pitch(ch, hztofnum(i * 49716.0 * pow(2.0, pop(&stack, 1, 12000) / 1200.0)), 0); break; case 'c' << 8 | '-': i = (rb[ch] << 8 & 0x300 | ra[ch]) << (rb[ch] >> 2 & 7); pitch(ch, hztofnum(i * 49716.0 * pow(2.0, -pop(&stack, 1, 12000) / 1200.0)), 0); break; case 'c' << 8 | 'y': drums |= 2; break; case 'd': cmd(0x60 | op & 31, op >> 5, r6[op] = r6[op] & 0xf0 | pop(&stack, 0, 15)); break; case 'd' << 24 | 'e' << 16 | 's' << 8 | 'q': cmd(0xe0 | op & 31, op >> 5, 7); break; case 'd' << 8 | 't': cmd(0xbd, 0, bd ^= 0x80); break; case 'd' << 16 | 'u' << 8 | 'p': i = pop(&stack, 0, 65535); push(&stack, i); push(&stack, i); break; case 'd' << 8 | 'v': cmd(0xbd, 0, bd ^= 0x40); break; case 'e' << 24 | 'a' << 16 | 'b' << 8 | 's': cmd(0xe0 | op & 31, op >> 5, 5); break; case 'e' << 24 | 'v' << 16 | 'e' << 8 | 'n': cmd(0xe0 | op & 31, op >> 5, 4); break; case 'e' << 24 | 'x' << 16 | 'c' << 8 | 'h': pop(&stack, 0, 65535); i = pop(&stack, 0, 65535); push(&stack, stack.val[stack.top + 1]); push(&stack, i); break; case 'f' << 8 | 'b': cmd(0xc0 | ch >> 1, ch & 1, rc[ch] = rc[ch] & 0xf1 | pop(&stack, 0, 7) << 1); break; case 'f' << 8 | 'm': cmd(0xc0 | ch >> 1, ch & 1, rc[ch] &= 254); if(four >> ch & 1) togfour(ch); break; case 'h' << 24 | 'a' << 16 | 'l' << 8 | 'f': cmd(0xe0 | op & 31, op >> 5, 1); break; case 'h' << 8 | 'h': drums |= 1; break; case 'h' << 8 | 'z': push(&stack, hztofnum(pop(&stack, 0, 65535) * pow(2, 20))); break; case 'k' << 16 | 's' << 8 | 'l': cmd(0x40 | op & 31, op >> 5, r4[op] = r4[op] & 0x3f | pop(&stack, 0, 3) << 6); break; case 'k' << 16 | 's' << 8 | 'r': cmd(0x20 | op & 31, op >> 5, r2[op] ^= 0x10); break; case 'l' << 24 | 'e' << 16 | 'f' << 8 | 't': cmd(0xc0 | ch >> 1, ch & 1, rc[ch] = rc[ch] & 0x0f | 0x10); break; case 'm' << 16 | 'u' << 8 | 'l': cmd(0x20 | op & 31, op >> 5, r2[op] = r2[op] & 0xf0 | pop(&stack, 0, 15)); break; case 'm' << 24 | 'u' << 16 | 't' << 8 | 'e': cmd(0xc0 | ch >> 1, ch & 1, rc[ch] &= 0x0f); break; case 'n': pitch(ch, pop(&stack, 0, 65535), 1); break; case 'n' << 8 | 'o': cmd(0xb0 | ch >> 1, ch & 1, rb[ch] &= 0x1f); break; case 'o': i = ch / 2 + ch / 6 * 5 | ch << 5 & 32; op = op == i ? i + 3 : i - 3; break; case 'p' << 8 | 'm': cmd(0xbd, 0, bd ^= 0x20); break; case 'p' << 16 | 'o' << 8 | 'p': pop(&stack, 0, 65535); break; case 'p' << 24 | 'u' << 16 | 'l' << 8 | 's': cmd(0xe0 | op & 31, op >> 5, 3); break; case 'r': cmd(0x80 | op & 31, op >> 5, r8[op] = r8[op] & 0xf0 | pop(&stack, 0, 15)); break; case 'r' << 24 | 'a' << 16 | 'n' << 8 | 'd': i = pop(&stack, 0, 65535); i -= pop(&stack, 0, 65535); if(i <= 0) err("rand arguments out of order"); push(&stack, stack.val[stack.top] + nrand(i)); break; case 'r' << 24 | 'g' << 16 | 'h' << 8 | 't': cmd(0xc0 | ch >> 1, ch & 1, rc[ch] = rc[ch] & 0x0f | 0x20); break; case 's': cmd(0x80 | op & 31, op >> 5, r8[op] = r8[op] & 0x0f | pop(&stack, 0, 15) << 4); break; case 's' << 8 | 'd': drums |= 8; break; case 's' << 24 | 'i' << 16 | 'n' << 8 | 'e': cmd(0xe0 | op & 31, op >> 5, 0); break; case 's' << 8 | 'q': cmd(0xe0 | op & 31, op >> 5, 6); break; case 's' << 16 | 'u' << 8 | 's': cmd(0x20 | op & 31, op >> 5, r2[op] ^= 0x20); break; case 'T': push(&stack, tick); break; case 't': tick = pop(&stack, 1, 65535); break; case 't' << 24 | 'r' << 16 | 'e' << 8 | 'm': cmd(0x20 | op & 31, op >> 5, r2[op] ^= 0x80); break; case 't' << 8 | 't': drums |= 4; break; case 'v': cmd(0x40 | op & 31, op >> 5, r4[op] = r4[op] & 0xc0 | 63 - pop(&stack, 0, 63)); break; case 'v' << 16 | 'i' << 8 | 'b': cmd(0x20 | op & 31, op >> 5, r2[op] ^= 0x40); break; } s += n; } }