💾 Archived View for runjimmyrunrunyoufuckerrun.com › src › opltracker.c captured on 2021-12-17 at 13:26:06.

View Raw

More Information

-=-=-=-=-=-=-

#include <u.h>
#include <libc.h>
#include <draw.h>
#include <thread.h>
#include <mouse.h>
#include <keyboard.h>
enum{
	TremVibSusEnvMult0, TremVibSusEnvMult1,
	AttenuationLevel0, AttenuationLevel1,
	AttackDecay0, AttackDecay1,
	SustainRelease0, SustainRelease1,
	GlideWaves, TrigPanFbSynth, Note, INSTRLEN
};
char *pitch[] = {"   ",
	"C₀ ", "C♯₀", "D₀ ", "D♯₀", "E₀ ", "F₀ ", "F♯₀", "G₀ ", "G♯₀", "A₀ ", "A♯₀", "B₀ ",
	"C₁ ", "C♯₁", "D₁ ", "D♯₁", "E₁ ", "F₁ ", "F♯₁", "G₁ ", "G♯₁", "A₁ ", "A♯₁", "B₁ ",
	"C₂ ", "C♯₂", "D₂ ", "D♯₂", "E₂ ", "F₂ ", "F♯₂", "G₂ ", "G♯₂", "A₂ ", "A♯₂", "B₂ ",
	"C₃ ", "C♯₃", "D₃ ", "D♯₃", "E₃ ", "F₃ ", "F♯₃", "G₃ ", "G♯₃", "A₃ ", "A♯₃", "B₃ ",
	"C₄ ", "C♯₄", "D₄ ", "D♯₄", "E₄ ", "F₄ ", "F♯₄", "G₄ ", "G♯₄", "A₄ ", "A♯₄", "B₄ ",
	"C₅ ", "C♯₅", "D₅ ", "D♯₅", "E₅ ", "F₅ ", "F♯₅", "G₅ ", "G♯₅", "A₅ ", "A♯₅", "B₅ ",
	"C₆ ", "C♯₆", "D₆ ", "D♯₆", "E₆ ", "F₆ ", "F♯₆", "G₆ ", "G♯₆", "A₆ ", "A♯₆", "B₆ ",
	"C₇ ", "C♯₇", "D₇ ", "D♯₇", "E₇ ", "F₇ ", "F♯₇", "G₇ ", "G♯₇", "A₇ ", "A♯₇", "B₇ ",
	"C₈ ", "C♯₈", "D₈ ", "D♯₈", "E₈ ", "F₈ ", "F♯₈", "G₈ ", nil
};
uchar instr[256 - nelem(pitch) + 1 - 3][INSTRLEN], reg[18][INSTRLEN], wiggles, four;
char instrname[nelem(instr)][32];
int op[] = {0, 8, 1, 9, 2, 10, 0, 8, 1, 9, 2, 10, 16, 17, 18, 16, 17, 18};
int ch[] = {0, 3, 1, 4, 2, 5, 0, 3, 1, 4, 2, 5, 6, 7, 8, 6, 7, 8};
int ext[] = {0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1};
Rune rowtxt[16][5 + 3 * 18 + 8];
char hex[] = "0123456789ABCDEF";
char header[] = "     A  B  C  D  E  F  G  H  I  J  K  L  M  N  O  P  Q  R ";
char *toggle[] = {"off", "on", nil};
char *fmam[] = {"FM", "AM", nil};
char *pan[] = {"Silent", "Left", "Right", "Stereo", nil};
char *level[] = {
	"3F", "3E", "3D", "3C", "3B", "3A", "39", "38", "37", "36", "35", "34", "33", "32", "31", "30",
	"2F", "2E", "2D", "2C", "2B", "2A", "29", "28", "27", "26", "25", "24", "23", "22", "21", "20",
	"1F", "1E", "1D", "1C", "1B", "1A", "19", "18", "17", "16", "15", "14", "13", "12", "11", "10",
	"0F", "0E", "0D", "0C", "0B", "0A", "09", "08", "07", "06", "05", "04", "03", "02", "01", "00",  nil
};
char *nibble[] = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", nil};
char *mult[] = {"½", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "A", "C", "C", "F", "F", nil};
char *attenuate[] = {"no", "1.5 dB/oct", "3.0 dB/oct", "6.0 dB/oct", nil};
char *wave[] = {
	/* OPL2 */	"Sine", "Half Sine", "Absolute Sin", "Pulse Sine",
	/*OPL3 */	"Even Sine", "Even Abs Sin", "Square", "Derived Sq", nil
};
char *feedback[] = {"0", "¹⁄₁₆", "⅛", "¼", "½", "1", "2", "4", nil};
char *trig[] = {"inherit", "key down", "key up", "retrigger", nil};
/*TODO "portamento",
	"⤵₁₂", "⤵₁₁", "⤵₁₀", "⤵₉", "⤵₈", "⤵₇", "⤵₆", "⤵₅", "⤵₄", "⤵₃", "⤵₂", "⤵₁", "",
	"⤴¹", "⤴²", "⤴³", "⤴⁴", "⤴⁵", "⤴⁶", "⤴⁷", "⤴⁸", "⤴⁹", "⤴¹⁰", "⤴¹¹", "⤴¹²", nil
};*/
char *globals[] = {
	"", "§ 0", "§ 1", "§ 2", "§ 3", "§ 4", "§ 5", "§ 6", "§ 7", "§ 8", "§ 9",
	"§ A", "§ B", "§ C", "§ D", "§ E", "§ F", "§ G", "§ H", "§ I", "§ J", "§ K", "§ L", "§ M",
	"§ N", "§ O", "§ P", "§ Q", "§ R", "§ S", "§ T", "§ U", "§ V", "§ W", "§ X", "§ Y", "§ Z",
[37]
	"", "|:", ":|", ":|³", ":|⁴", ":|⁵", ":|⁶", ":|⁷", ":|⁸", "[",
[47]
	"", "dt off", "dt on", "dv off", "dv on",
[52]
	"", "AB 2-op", "CD 2-op", "EF 2-op", "GH 2-op", "IJ 2-op", "KL 2-op",
[59]
	"", "AB 4-op", "CD 4-op", "EF 4-op", "GH 4-op", "IJ 4-op", "KL 4-op",
[66]
	"", "1 rpb", "2 rpb", "3 rpb", "4 rpb", "5 rpb", "6 rpb",
	"7 rpb", "8 rpb", "9 rpb", "10 rpb", "11 rpb", "12 rpb",
	"13 rpb", "14 rpb", "15 rpb", "16 rpb", "17 rpb", "18 rpb",
	"19 rpb", "20 rpb", "21 rpb", "22 rpb", "23 rpb", "24 rpb",
[91]
	"", "fadeout",
[93]
	"", "20 bpm", "22 bpm",
	"24 bpm", "26 bpm", "28 bpm", "30 bpm", "32 bpm", "34 bpm", "36 bpm", "38 bpm",
	"40 bpm", "42 bpm", "44 bpm", "46 bpm", "48 bpm", "50 bpm", "52 bpm", "54 bpm",
	"56 bpm", "58 bpm", "60 bpm", "62 bpm", "64 bpm", "66 bpm", "68 bpm", "70 bpm",
	"72 bpm", "74 bpm", "76 bpm", "78 bpm", "80 bpm", "82 bpm", "84 bpm", "86 bpm",
	"88 bpm", "90 bpm", "92 bpm", "94 bpm", "96 bpm", "98 bpm", "100 bpm", "102 bpm",
	"104 bpm", "106 bpm", "108 bpm", "110 bpm", "112 bpm", "114 bpm", "116 bpm", "118 bpm",
	"120 bpm", "122 bpm", "124 bpm", "126 bpm", "128 bpm", "130 bpm", "132 bpm", "134 bpm",
	"136 bpm", "138 bpm", "140 bpm", "142 bpm", "144 bpm", "146 bpm", "148 bpm", "150 bpm",
	"152 bpm", "154 bpm", "156 bpm", "158 bpm", "160 bpm", "162 bpm", "164 bpm", "166 bpm",
	"168 bpm", "170 bpm", "172 bpm", "174 bpm", "176 bpm", "178 bpm", "180 bpm", "182 bpm",
	"184 bpm", "186 bpm", "188 bpm", "190 bpm", "192 bpm", "194 bpm", "196 bpm", "198 bpm",
	"200 bpm", "202 bpm", "204 bpm", "206 bpm", "208 bpm", "210 bpm", "212 bpm", "214 bpm",
	"216 bpm", "218 bpm", "220 bpm", "222 bpm", "224 bpm", "226 bpm", "228 bpm", "230 bpm",
	"232 bpm", "234 bpm", "236 bpm", "238 bpm", "240 bpm", "242 bpm", "244 bpm", "246 bpm",
	"248 bpm", "250 bpm", "252 bpm", "254 bpm", "256 bpm", "258 bpm", "260 bpm", "262 bpm",
	"264 bpm", "266 bpm", "268 bpm", "270 bpm", "272 bpm", "274 bpm", "276 bpm", "278 bpm",
	"280 bpm", "282 bpm", "284 bpm", "286 bpm", "288 bpm", "290 bpm", "292 bpm", "294 bpm",
	"296 bpm", "298 bpm", "300 bpm", "302 bpm", "304 bpm", "306 bpm", "308 bpm", "310 bpm",
	"312 bpm", "314 bpm", "316 bpm", "318 bpm", "320 bpm", "322 bpm", "324 bpm", "326 bpm",
	"328 bpm", "330 bpm", "332 bpm", "334 bpm", "336 bpm", "338 bpm", "340 bpm", "342 bpm", nil
};
short freq[] = {0,
0x159, 0x16d, 0x183, 0x19a, 0x1b2, 0x1cc, 0x1e8, 0x205, 0x224, 0x244, 0x267, 0x28b,
0x2b2, 0x2db, 0x306, 0x334, 0x365, 0x399, 0x3cf, 0x1810, 0x1811, 0x1812, 0x1813, 0x1814,
0x1816, 0x1817, 0x1818, 0x181a, 0x181b, 0x181d, 0x181e, 0x1820, 0x1822, 0x1824, 0x1826, 0x1829,
0x182b, 0x182e, 0x1830, 0x1833, 0x1836, 0x183a, 0x183d, 0x1841, 0x1844, 0x1849, 0x184d, 0x1851,
0x1856, 0x185b, 0x1861, 0x1867, 0x186d, 0x1873, 0x187a, 0x1881, 0x1889, 0x1891, 0x189a, 0x18a3,
0x18ac, 0x18b7, 0x18c2, 0x18cd, 0x18d9, 0x18e6, 0x18f4, 0x1902, 0x1912, 0x1922, 0x1933, 0x1946,
0x1959, 0x196d, 0x1983, 0x199a, 0x19b3, 0x19cc, 0x19e8, 0x1a05, 0x1a23, 0x1a44, 0x1a67, 0x1a8b,
0x1ab2, 0x1adb, 0x1b06, 0x1b34, 0x1b65, 0x1b99, 0x1bcf, 0x1e05, 0x1e23, 0x1e44, 0x1e67, 0x1e8b,
0x1eb2, 0x1edb, 0x1f06, 0x1f34, 0x1f65, 0x1f99, 0x1fcf, 0x1fff};
uchar ev[0x10000][18], global[0x1000];	/* lazy… */
Image *bg, *cursor, *fg, *grey;
Rectangle horiz, vert;
Point orig;
int playing, bpm, rpb, instrmode, em, row, col, rows = 0x40, skip = 8, oct = 5;
vlong incr;
void
drawrows(void){
	int i;
	Point o;
	draw(screen, screen->r, bg, nil, ZP);
	draw(screen, horiz, cursor, nil, ZP);
	draw(screen, vert, cursor, nil, ZP);
	o = orig;
	string(screen, o, fg, ZP, font, header);
	for(i = row - 7; o.y += font->height, i < rows && i <= row + 8; i++)
		runestring(screen, o, fg, ZP, font, rowtxt[i & 15]);
	flushimage(display, 1);
}
void
resize(void){
	orig.x = screen->r.min.x + screen->r.max.x - nelem(rowtxt[0]) * em >> 1;
	orig.y = screen->r.min.y + screen->r.max.y - 17 * font->height >> 1;
	horiz.min = Pt(screen->r.min.x, orig.y + 8 * font->height);
	horiz.max = Pt(screen->r.max.x, horiz.min.y + font->height);
	vert.min = Pt(orig.x + (5 + 3 * col) * em, screen->r.min.y);
	vert.max = Pt(vert.min.x + 2 * em, screen->r.max.y);
	drawrows();
}
void
fmtrow(int n){
	Rune *r;
	int i;
	r = rowtxt[n & 15];
	if(n < 0 || n >= rows){
		*r = 0;
		return;
	}
	r[0] = hex[n >> 12];
	r[1] = hex[n >> 8 & 15];
	r[2] = hex[n >> 4 & 15];
	r[3] = hex[n & 15];
	r[4] = ' ';
	for(r += 5, i = 0; i < 18; i++, r+= 3)
		if(ev[n][i] < nelem(pitch) - 1){
			r[0] = pitch[ev[n][i]][0];
			chartorune(r + 2, pitch[ev[n][i]] + 1 + chartorune(r + 1, pitch[ev[n][i]] + 1));
		}else if(ev[n][i] == nelem(pitch) - 1){
			r[0] = r[1] = '^';
			r[2] = ' ';
		}else if(ev[n][i] == nelem(pitch)){
			r[0] = r[1] = '-';
			r[2] = ' ';
		}else if(ev[n][i] == nelem(pitch) + 1){
			r[0] = r[1] = '+';
			r[2] = ' ';
		}else{
			r[0] = hex[ev[n][i] - nelem(pitch) - 2 >> 4];
			r[1] = hex[ev[n][i] - nelem(pitch) - 2 & 15];
			r[2] = ' ';
		}
	for(i = 0; globals[global[n]][i];)
		i += chartorune(r++, globals[global[n]] + i);
	*r = 0;
}
void
fmtrows(void){
	int i;
	for(i = row - 7; i <= row + 8; i++)
		fmtrow(i);
}
void
drawinstr(int n){
	char s[42];
	draw(screen, screen->r, bg, nil, ZP);
	sprint(s, " instr %.2X %s", n, instrname[n]);
	string(screen, orig, fg, ZP, font, s);	
	sprint(s, "  tremolo %13-s%s",
		toggle[instr[n][TremVibSusEnvMult0] >> 7],
		toggle[instr[n][TremVibSusEnvMult1] >> 7]);
	string(screen, Pt(orig.x, orig.y + font->height), fg, ZP, font, s);	
	sprint(s, "  vibrato %13-s%s",
		toggle[instr[n][TremVibSusEnvMult0] >> 6 & 1],
		toggle[instr[n][TremVibSusEnvMult1] >> 6 & 1]);
	string(screen, Pt(orig.x, orig.y + 2 * font->height), fg, ZP, font, s);	
	sprint(s, "  sustain %13-s%s",
		toggle[instr[n][TremVibSusEnvMult0] >> 5 & 1],
		toggle[instr[n][TremVibSusEnvMult1] >> 5 & 1]);
	string(screen, Pt(orig.x, orig.y + 3 * font->height), fg, ZP, font, s);	
	sprint(s, " envscale %13-s%s",
		toggle[instr[n][TremVibSusEnvMult0] >> 4 & 1],
		toggle[instr[n][TremVibSusEnvMult1] >> 4 & 1]);
	string(screen, Pt(orig.x, orig.y + 4 * font->height), fg, ZP, font, s);	
	sprint(s, " multiply %13-s%s",
		mult[instr[n][TremVibSusEnvMult0] & 0x0F],
		mult[instr[n][TremVibSusEnvMult1] & 0x0F]);
	string(screen, Pt(orig.x, orig.y + 5 * font->height), fg, ZP, font, s);	
	sprint(s, "   attack %13-s%s",
		nibble[instr[n][AttackDecay0] >> 4],
		nibble[instr[n][AttackDecay1] >> 4]);
	string(screen, Pt(orig.x, orig.y + 6 * font->height), fg, ZP, font, s);	
	sprint(s, "    decay %13-s%s",
		nibble[instr[n][AttackDecay0] & 0x0F],
		nibble[instr[n][AttackDecay1] & 0x0F]);
	string(screen, Pt(orig.x, orig.y + 7 * font->height), fg, ZP, font, s);	
	sprint(s, "  sustain %13-s%s",
		nibble[instr[n][SustainRelease0] >> 4],
		nibble[instr[n][SustainRelease1] >> 4]);
	string(screen, Pt(orig.x, orig.y + 8 * font->height), fg, ZP, font, s);	
	sprint(s, "  release %13-s%s",
		nibble[instr[n][SustainRelease0] & 0x0F],
		nibble[instr[n][SustainRelease1] & 0x0F]);
	string(screen, Pt(orig.x, orig.y + 9 * font->height), fg, ZP, font, s);	
	sprint(s, " waveform %13-s%s",
		wave[instr[n][GlideWaves] & 7],
		wave[instr[n][GlideWaves] >> 3 & 7]);
	string(screen, Pt(orig.x, orig.y + 10 * font->height), fg, ZP, font, s);	
	sprint(s, "attenuate %13-s%s",
		attenuate[instr[n][AttenuationLevel0] >> 6],
		attenuate[instr[n][AttenuationLevel1] >> 6]);
	string(screen, Pt(orig.x, orig.y + 11 * font->height), fg, ZP, font, s);	
	sprint(s, "    level %13-s%s",
		level[instr[n][AttenuationLevel0] & 0x3F],
		level[instr[n][AttenuationLevel1] & 0x3F]);
	string(screen, Pt(orig.x, orig.y + 12 * font->height), fg, ZP, font, s);	
	sprint(s, "synth/pan %13-s%s",
		fmam[instr[n][TrigPanFbSynth] & 1],
		pan[instr[n][TrigPanFbSynth] >> 4 & 3]);
	string(screen, Pt(orig.x, orig.y + 13 * font->height), fg, ZP, font, s);	
	sprint(s, " fb/glide %13-s%s",
		feedback[instr[n][TrigPanFbSynth] >> 1 & 7],
		toggle[instr[n][GlideWaves] >> 6 & 1]);
	string(screen, Pt(orig.x, orig.y + 14 * font->height), fg, ZP, font, s);	
	sprint(s, "note/trig %13-s%s",
		pitch[instr[n][Note]],
		trig[instr[n][TrigPanFbSynth] >> 6 & 3]);
	string(screen, Pt(orig.x, orig.y + 15 * font->height), fg, ZP, font, s);	
	string(screen, Pt(orig.x, orig.y + 16 * font->height), fg, ZP, font,
		"          zerox instr  rename instr");	
	flushimage(display, 1);
}
char *
instrmenugen(int n){
	static char buf[3 + sizeof(instrname[0])] = {0, 0, ' '};
	if(n < 0 || n >= nelem(instrname))
		return nil;
	buf[0] = hex[n >> 4];
	buf[1] = hex[n & 15];
	strcpy(buf + 3, instrname[n]);
	return buf;
}
void
sendinstr(int fd, int c, int i){
	static uchar out[55];
	out[0] = 0x20 | op[c];
	out[1] = ext[c];
	out[2] = reg[c][TremVibSusEnvMult0] = instr[i][TremVibSusEnvMult0];
	out[5] = out[0] + 3;
	out[6] = ext[c];
	out[7] = reg[c][TremVibSusEnvMult1] = instr[i][TremVibSusEnvMult1];
	out[10] = 0x60 | op[c];
	out[11] = ext[c];
	out[12] = reg[c][AttackDecay0] = instr[i][AttackDecay0];
	out[15] = out[10] + 3;
	out[16] = ext[c];
	out[17] = reg[c][AttackDecay1] = instr[i][AttackDecay1];
	out[20] = 0x80 | op[c];
	out[21] = ext[c];
	out[22] = reg[c][SustainRelease0] = instr[i][SustainRelease0];
	out[25] = out[20] + 3;
	out[26] = ext[c];
	out[27] = reg[c][SustainRelease1] = instr[i][SustainRelease1];
	reg[c][GlideWaves] = instr[i][GlideWaves];
	out[30] = 0xE0 | op[c];
	out[31] = ext[c];
	out[32] = reg[c][GlideWaves] & 7;
	out[35] = out[30] + 3;
	out[36] = ext[c];
	out[37] = reg[c][GlideWaves] >> 3 & 7;
	out[40] = 0x40 | op[c];
	out[41] = ext[c];
	out[42] = reg[c][AttenuationLevel0] = instr[i][AttenuationLevel0];
	out[45] = out[40] + 3;
	out[46] = ext[c];
	out[47] = reg[c][AttenuationLevel1] = instr[i][AttenuationLevel1];
	out[50] = 0xC0 | ch[c];
	out[51] = ext[c];
	reg[c][TrigPanFbSynth] = instr[i][TrigPanFbSynth] & 0xC0 ? instr[i][TrigPanFbSynth]
		: reg[c][TrigPanFbSynth] & 0xC0 | instr[i][TrigPanFbSynth];
	out[52] = instr[i][TrigPanFbSynth] & 0x3F & (header[3 * c + 6] == 'm') - 1;
	write(fd, out, 55);
}
void
sendrow(int fd){
	static uchar buf[(18 * 3 + 2) * 5];
	uchar *b;
	int i, bend, f, from[18], to[18];
	b = buf;
	bend = 0;
	switch(global[row]){
	case 48:	/* Deep Tremolo off */
		b[0] = 0xBD;
		b[1] = 0;
		b[2] = wiggles &= 0x7F;
		b += 5;
		break;
	case 49:	/* Deep Tremolo on */
		b[0] = 0xBD;
		b[1] = 0;
		b[2] = wiggles |= 0x80;
		b += 5;
		break;
	case 50:	/* Deep Vibrato off */
		b[0] = 0xBD;
		b[1] = 0;
		b[2] = wiggles &= 0xBF;
		b += 5;
		break;
	case 51:	/* Deep Vibrato on */
		b[0] = 0xBD;
		b[1] = 0;
		b[2] = wiggles |= 0x40;
		b += 5;
		break;
	case 53: case 54: case 55: case 56: case 57: case 58: /* ch pair 2-op */
		b[0] = 0x04;
		b[1] = 1;
		b[2] = four &= 0xFF ^ 1 << global[row] - 53;
		b += 5;
		break;
	case 60: case 61: case 62: case 63: case 64: case 65: /* ch pair 4-op */
		b[0] = 0x04;
		b[1] = 1;
		b[2] = four |= 1 << global[row] - 60;
		b += 5;
		break;
	}
	for(i = 0; i < 18; i++){
		to[i] = 0;
		if(global[row] == 92){ /* fadeout */
			if(reg[i][AttenuationLevel1] & 0x3F ^ 0x3F){
				b[0] = 0x40 | op[i] + 3;
				b[1] = ext[i];
				b[2] = ++reg[i][AttenuationLevel1];
				b += 5;
			}
			if(reg[i][TrigPanFbSynth] & 1 && reg[i][AttenuationLevel0] & 0x3F ^ 0x3F){
				b[0] = 0x40 | op[i];
				b[1] = ext[i];
				b[2] = ++reg[i][AttenuationLevel0];
				b += 5;
			}
		}
		if(!ev[row][i])
			continue;
		if(ev[row][i] == nelem(pitch) - 1){ /* ^^ key up */
			b[0] = 0xB0 | ch[i];
			b[1] = ext[i];
			b[2] = freq[reg[i][Note]] >> 8;
			b += 5;
			continue;
		}
		if(ev[row][i] == nelem(pitch)){ /* -- ch off */
			b[0] = 0xC0 | ch[i];
			b[1] = ext[i];
			b[2] = 0;
			b += 5;
			continue;
		}
		if(ev[row][i] == nelem(pitch) + 1){ /* ++ ch on */
			b[0] = 0xC0 | ch[i];
			b[1] = ext[i];
			b[2] = reg[i][TrigPanFbSynth] & 0x3F & (header[3 * i + 6] == 'm') - 1;
			b += 5;
			continue;
		}
		from[i] = freq[reg[i][Note]];
		if(ev[row][i] > nelem(pitch) + 1){
			sendinstr(fd, i, ev[row][i] - nelem(pitch) - 2);
			if(!instr[ev[row][i] - nelem(pitch) - 2][Note])
				continue;
			reg[i][Note] = instr[ev[row][i] - nelem(pitch) - 2][Note];
		}else
			reg[i][Note] = ev[row][i];
		if((reg[i][TrigPanFbSynth] >> 6) == 3){
			b[0] = 0xB0 | ch[i];
			b[1] = ext[i];
			b[2] = 0;
			b += 5;
		}
		if(reg[i][GlideWaves] & 0x40){
			bend = 1;
			to[i] = reg[i][TrigPanFbSynth] << 7 & 0x2000 | freq[reg[i][Note]];
			if(from[i])
				from[i] |= to[i] & 0x2000;
			else
				from[i] = to[i];
			continue;
		}
		b[0] = 0xB0 | ch[i];
		b[1] = ext[i];
		b[2] = reg[i][TrigPanFbSynth] >> 1 & 0x20 | freq[reg[i][Note]] >> 8;
		b[5] = 0xA0 | ch[i];
		b[6] = ext[i];
		b[7] = freq[reg[i][Note]];
		b += 10;
	}
	if(b == buf)
		buf[0] = buf[1] = buf[2] = 0;
	else
		b -= 5;
	if(global[row] >= 94){
		bpm = 10 + global[row] - 94 << 1;
		incr = playing / bpm / rpb;
	}
	else if(global[row] > 66 && global[row] < 91){
		rpb = global[row] - 66;
		incr = playing / bpm / rpb;
	}
	if(!bend){
		b[3] = incr;
		b[4] = incr >> 8;
		write(fd, buf, b - buf + 5);
		b[3] = b[4] = 0;
		return;
	}
	write(fd, buf, b - buf + 5);
	for(; bend <= incr; bend++){
		b = buf;
		for(i = 0; i < 18; i++)
			if(to[i]){
				f = from[i] + (to[i] - from[i]) * bend / incr;
				b[0] = 0xB0 | ch[i];
				b[1] = ext[i];
				b[2] = f >> 8;
				b[5] = 0xA0 | ch[i];
				b[6] = ext[i];
				b[7] = f;
				b += 10;
			}
		b[-2] = !!incr;
		write(fd, buf, b - buf);
		b[-2] = 0;
	}
}
int
oplinit(void){
	int fd, p[2];
	fd = open("/dev/audio", OWRITE);
	if(fd < 0 || pipe(p) < 0 && close(fd))
		return -1;
	switch(fork()){
	case -1:
		close(fd);
		close(p[0]);
		close(p[1]);
		return -1;
	case 0:
		close(p[0]);
		dup(p[1], 0);
		close(p[1]);
		dup(fd, 1);
		close(fd);
		execl("/bin/games/opl3", "opl3", "-r", "1000", nil);
		sysfatal("execl: %r");
	}
	close(fd);
	close(p[1]);
	write(p[0], "\1\0 \0\0\5\1\1\n", 10);
	return p[0];
}
int
readinstr(char *f){
	uchar *bank;
	int fd, n;
	fd = open(f, OREAD);
	if(fd < 0)
		return -1;
	bank = malloc(8 + 175 * 36 + 175 * 32);
	if(bank == nil)
		sysfatal("malloc: %r");
	n = read(fd, bank, 8 + 175 * 36 + 175 * 32);
	close(fd);
	if(n != 8 + 175 * 36 + 175 * 32 || memcmp(bank, "#OPL_II#", 8))
		sysfatal("readinstr: not an OPL2 bank?");
	for(n = 0; n < nelem(instr); n++){
		instr[n][TremVibSusEnvMult0] = bank[8 + n * 36 + 4];
		instr[n][TremVibSusEnvMult1] = bank[8 + n * 36 + 4 + 7];
		instr[n][AttackDecay0] = bank[8 + n * 36 + 5];
		instr[n][AttackDecay1] = bank[8 + n * 36 + 5 + 7];
		instr[n][SustainRelease0] = bank[8 + n * 36 + 6];
		instr[n][SustainRelease1] = bank[8 + n * 36 + 6 + 7];
		instr[n][GlideWaves] = bank[8 + n * 36 + 7] | bank[8 + n * 36 + 7 + 7] << 3;
		instr[n][AttenuationLevel0] = bank[8 + n * 36 + 8] | bank[8 + n * 36 + 9];
		instr[n][AttenuationLevel1] = bank[8 + n * 36 + 8 + 7] | bank[8 + n * 36 + 9 + 7];
		instr[n][TrigPanFbSynth] = 0xC0
			| bank[8 + n * 36 + 1] << 4 & 0x30 ^ 0x30 | bank[8 + n * 36 + 10];
		memcpy(instrname[n], bank + 8 + 175 * 36 + n * 32, 31);
	}
	free(bank);
	return 0;
}
void
usage(void){
	sysfatal("usage: %s [-i instr] [file]", argv0);
}
void
threadmain(int argc, char **argv){
	Mousectl *⌖;
	Keyboardctl *⌨;
	Menu ≡;
	Rune k;
	char path[ERRMAX] = {0}, buf[ERRMAX];
	int i = -1, rep, fd;
	uchar *b, snarf = 0;
	Point snarfp = ZP;
	Rectangle snarfr = ZR;
	vlong t, zzz;
	enum{RESIZE, MOUSE, KEYBOARD};
	Alt a[] = {
		[RESIZE]{nil, nil, CHANRCV},
		[MOUSE]{nil, nil, CHANRCV},
		[KEYBOARD]{nil, &k, CHANRCV},
		{nil, nil, CHANEND}
	};
	ARGBEGIN{
	case 'i':
		if(i = readinstr(EARGF(usage())))
			sysfatal("read: %r");
		break;
	default:
		usage();
	}ARGEND
	if(argc){
		fd = open(*argv, OREAD);
		if(fd < 0)
			sysfatal("open: %r");
		if(read(fd, buf, 8) != 8
		|| buf[0] != 'O' || buf[1] != 'P' || buf[2] != 'L'
		|| buf[3] != 'T' || buf[4] != 'R' || buf[5] != 'K')
			sysfatal("bad track format");
		rows = (uchar)buf[6] | (uchar)buf[7] << 8;
		for(i = 0; i < rows; i++)
			if(read(fd, ev[i], 18) != 18)
				sysfatal("short track read");
		if(read(fd, global, rows) != rows)
			sysfatal("short track read");
		for(i = 0; i < nelem(instr); i++)
			if(read(fd, instrname[i], sizeof(instrname[0])) != sizeof(instrname[0])
			|| read(fd, instr[i], INSTRLEN) != INSTRLEN)
				sysfatal("short instr read");
		close(fd);
		strecpy(path, path + sizeof(path), *argv);
	}
	else if(i < 0 && readinstr("/mnt/wad/genmidi"))
		for(i = 0; i < nelem(instr); i++){
			instr[i][12] = 2;
			sprint(instrname[i], "instrument 0x%.2X", i);
		}
	if(initdraw(nil, nil, argv0) < 0)
		sysfatal("initdraw: %r");
	bg = allocimagemix(display, DPalebluegreen, DWhite);
	cursor = allocimage(display, Rect(0,0,1,1), screen->chan, 1, DPalegreygreen);
	fg = display->black;
	grey = allocimage(display, Rect(0, 0, 1, 1), screen->chan, 1, DPurpleblue);
	if(bg == nil || cursor == nil || fg == nil || grey == nil)
		sysfatal ("allocimage: %r");
	if((⌖ = initmouse(nil, screen)) == nil)
		sysfatal("initmouse: %r");
	a[RESIZE].c = ⌖->resizec;
	a[MOUSE].c = ⌖->c;
	a[MOUSE].v = &⌖->Mouse;
	if((⌨ = initkeyboard(nil)) == nil)
		sysfatal("initkeyboard: %r");
	a[KEYBOARD].c = ⌨->c;
	fmtrows();
	em = stringwidth(font, "M");	/* yep, better be monospaced… */
	resize();
	≡.gen = instrmenugen;
	while(9)
		switch(alt(a)){
		case MOUSE:
			if(instrmode){
				if(⌖->buttons != 1 || ⌖->xy.x < orig.x + 10 * em
				|| ⌖->xy.x >= orig.x + 36 * em || ⌖->xy.y < orig.y)
					continue;
				switch((⌖->xy.y - orig.y) / font->height){
				case 0:
					≡.item = nil;
					≡.lasthit = i;
					menuhit(1, ⌖, &≡, nil);
					i = ≡.lasthit;
					break;
				case 1:
					≡.item = toggle;
					b = instr[i] + TremVibSusEnvMult0 + (⌖->xy.x >= orig.x + 23 * em);
					≡.lasthit = *b >> 7;
					menuhit(1, ⌖, &≡, nil);
					*b = *b & 0x7F | ≡.lasthit << 7;
					break;
				case 2:
					≡.item = toggle;
					b = instr[i] + TremVibSusEnvMult0 + (⌖->xy.x >= orig.x + 23 * em);
					≡.lasthit = *b >> 6 & 1;
					menuhit(1, ⌖, &≡, nil);
					*b = *b & 0xBF | ≡.lasthit << 6;
					break;
				case 3:
					≡.item = toggle;
					b = instr[i] + TremVibSusEnvMult0 + (⌖->xy.x >= orig.x + 23 * em);
					≡.lasthit = *b >> 5 & 1;
					menuhit(1, ⌖, &≡, nil);
					*b = *b & 0xDF | ≡.lasthit << 5;
					break;
				case 4:
					≡.item = toggle;
					b = instr[i] + TremVibSusEnvMult0 + (⌖->xy.x >= orig.x + 23 * em);
					≡.lasthit = *b >> 4 & 1;
					menuhit(1, ⌖, &≡, nil);
					*b = *b & 0xEF | ≡.lasthit << 4;
					break;
				case 5:
					≡.item = mult;
					b = instr[i] + TremVibSusEnvMult0 + (⌖->xy.x >= orig.x + 23 * em);
					≡.lasthit = *b & 0x0F;
					menuhit(1, ⌖, &≡, nil);
					*b = *b & 0xF0 | ≡.lasthit;
					break;
				case 6:
					≡.item = nibble;
					b = instr[i] + AttackDecay0 + (⌖->xy.x >= orig.x + 23 * em);
					≡.lasthit = *b >> 4;
					menuhit(1, ⌖, &≡, nil);
					*b = *b & 0x0F | ≡.lasthit << 4;
					break;
				case 7:
					≡.item = nibble;
					b = instr[i] + AttackDecay0 + (⌖->xy.x >= orig.x + 23 * em);
					≡.lasthit = *b & 0x0F;
					menuhit(1, ⌖, &≡, nil);
					*b = *b & 0xF0 | ≡.lasthit;
					break;
				case 8:
					≡.item = nibble;
					b = instr[i] + SustainRelease0 + (⌖->xy.x >= orig.x + 23 * em);
					≡.lasthit = *b >> 4;
					menuhit(1, ⌖, &≡, nil);
					*b = *b & 0x0F | ≡.lasthit << 4;
					break;
				case 9:
					≡.item = nibble;
					b = instr[i] + SustainRelease0 + (⌖->xy.x >= orig.x + 23 * em);
					≡.lasthit = *b & 0x0F;
					menuhit(1, ⌖, &≡, nil);
					*b = *b & 0xF0 | ≡.lasthit;
					break;
				case 10:
					≡.item = wave;
					b = instr[i] + GlideWaves;
					if(⌖->xy.x < orig.x + 23 * em){
						≡.lasthit = *b & 7;
						menuhit(1, ⌖, &≡, nil);
						*b = *b & 0xF8 | ≡.lasthit;
					}else{
						≡.lasthit = *b >> 3 & 7;
						menuhit(1, ⌖, &≡, nil);
						*b = *b & 0xC7 | ≡.lasthit << 3;
					}
					break;
				case 11:
					≡.item = attenuate;
					b = instr[i] + AttenuationLevel0 + (⌖->xy.x >= orig.x + 23 * em);
					≡.lasthit = *b >> 6;
					menuhit(1, ⌖, &≡, nil);
					*b = *b & 0x3F | ≡.lasthit << 6;
					break;
				case 12:
					≡.item = level;
					b = instr[i] + AttenuationLevel0 + (⌖->xy.x >= orig.x + 23 * em);
					≡.lasthit = *b & 0x3F;
					menuhit(1, ⌖, &≡, nil);
					*b = *b & 0xC0 | ≡.lasthit;
					break;
				case 13:
					b = instr[i] + TrigPanFbSynth;
					if(⌖->xy.x < orig.x + 23 * em){
						≡.item = fmam;
						≡.lasthit = *b & 1;
						menuhit(1, ⌖, &≡, nil);
						*b = *b & 0xFE | ≡.lasthit;
					}else{
						≡.item = pan;
						≡.lasthit = *b >> 4 & 3;
						menuhit(1, ⌖, &≡, nil);
						*b = *b & 0xCF | ≡.lasthit << 4;
					}
					break;
				case 14:
					if(⌖->xy.x < orig.x + 23 * em){
						b = instr[i] + TrigPanFbSynth;
						≡.item = feedback;
						≡.lasthit = *b >> 1 & 7;
						menuhit(1, ⌖, &≡, nil);
						*b = *b & 0xF1 | ≡.lasthit << 1;
					}else{
						b = instr[i] + GlideWaves;
						≡.item = toggle;
						≡.lasthit = *b >> 6 & 1;
						menuhit(1, ⌖, &≡, nil);
						*b = *b & 0xBF | ≡.lasthit << 6;
					}
					break;
				case 15:
					if(⌖->xy.x < orig.x + 23 * em){
						≡.item = pitch;
						≡.lasthit = instr[i][Note];
						menuhit(1, ⌖, &≡, nil);
						instr[i][Note] = ≡.lasthit;
					}else{
						b = instr[i] + TrigPanFbSynth;
						≡.item = trig;
						≡.lasthit = *b >> 6 & 3;
						menuhit(1, ⌖, &≡, nil);
						*b = *b & 0x3F | ≡.lasthit << 6;
					}
					break;
				case 16:
					if(⌖->xy.x < orig.x + 23 * em){
						≡.item = nil;
						≡.lasthit = i;
						if(menuhit(1, ⌖, &≡, nil) < 0)
							continue;
						memmove(instr[i], instr[≡.lasthit], INSTRLEN);
						strcpy(instrname[i], instrname[≡.lasthit]);
					}else
						enter("rename", instrname[i], sizeof(instrname[i]), ⌖, ⌨, nil);
					break;
				default:
					continue;
				}
				drawinstr(i);
				break;
			}
			switch(⌖->buttons){
			case 1:
				while(readmouse(⌖), ⌖->buttons);
				i = ⌖->xy.x - orig.x - 9 * em / 2;
				if(i >= 0 && i < 3 * 18 * em){
					col = i / 3 / em;
					vert.min.x = orig.x + (5 + 3 * col) * em;
					vert.max.x = vert.min.x + 2 * em;
				}
				i = row - 8 + (⌖->xy.y - orig.y) / font->height;
				if(i >= 0 && i < rows)
					row = i;
				fmtrows();
				break;
			case 4:
				≡.item = globals;
				≡.lasthit = global[row];
				menuhit(3, ⌖, &≡, nil);
				global[row] = ≡.lasthit;
				fmtrow(row);
				break;
			default:
				continue;
			}
			drawrows();
			break;
		case RESIZE:
			if(getwindow(display, Refnone) < 0)
				sysfatal("getwindow: %r");
			if(instrmode){
				orig.x = screen->r.min.x + screen->r.max.x - 36 * em >> 1;
				orig.y = screen->r.min.y + screen->r.max.y - 17 * font->height >> 1;
				drawinstr(i);
			}else
				resize();
			break;
		case KEYBOARD:
			if(instrmode){
				switch(k){/*TODO: notes?*/
				case Kdel: case Kbs: instr[i][Note] = 0; break;
				case 'c': instr[i][Note] = oct * 12 + 1; goto testnote;
				case 'C': instr[i][Note] = oct * 12 + 2; goto testnote;
				case 'd': instr[i][Note] = oct * 12 + 3; goto testnote;
				case 'D': instr[i][Note] = oct * 12 + 4; goto testnote;
				case 'e': instr[i][Note] = oct * 12 + 5; goto testnote;
				case 'f': instr[i][Note] = oct * 12 + 6; goto testnote;
				case 'F': instr[i][Note] = oct * 12 + 7; goto testnote;
				case 'g': instr[i][Note] = oct * 12 + 8; goto testnote;
				case 'G': instr[i][Note] = oct * 12 + 9; goto testnote;
				case 'a': instr[i][Note] = oct * 12 + 10; goto testnote;
				case 'A': instr[i][Note] = oct * 12 + 11; goto testnote;
				case 'b': instr[i][Note] = oct * 12 + 12; goto testnote;
				case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8':
					oct = k - '0';
					if(!instr[i][Note] || oct == 8 && (instr[i][Note] - 1) % 12 > 7){
						sprint(buf, "input octave set to %d", oct);
						string(screen, screen->r.min, fg, ZP, font, buf);
						flushimage(display, 1);
						continue;
					}
					instr[i][Note] = 12 * oct + (instr[i][Note] - 1) % 12 + 1;
				case ' ': testnote:
					fd = oplinit();
					if(fd < 0){uhoh:
						*buf = 0;
						errstr(buf, ERRMAX);
						enter(buf, nil, 0, ⌖, ⌨, nil);
						continue;
					}
					sendinstr(fd, 0, i);
					buf[0] = 0xA0;
					buf[1] = 0;
					buf[2] = freq[instr[i][Note]];
					buf[3] = 0;
					buf[4] = 0;
					buf[5] = 0xB0;
					buf[6] = 0;
					buf[7] = 0x20 | freq[instr[i][Note]] >> 8;
					buf[8] = 0;
					buf[9] = 2 * 4; /* ~ 2 seconds */
					write(fd, buf, 10);
					close(fd);
					break;
				case '\n':
					ev[row][col] = nelem(pitch) + 2 + i;
					fmtrow(row);
					instrmode = 0;
					resize();
					continue;
				}
				drawinstr(i);
				break;
			}
			switch(k){
			case 'q':
				draw(screen, screen->r, bg, nil, ZP);
				string(screen,
					Pt(screen->r.min.x + screen->r.max.x - em >> 1,
					screen->r.min.y + screen->r.max.y - 12 * font->height >> 1),
					fg, ZP, font, "☠");
				string(screen,
					Pt(screen->r.min.x + screen->r.max.x - 12 * em >> 1,
					screen->r.min.y + screen->r.max.y - 8 * font->height >> 1),
					fg, ZP, font, "Say q again.");
				string(screen,
					Pt(screen->r.min.x + screen->r.max.x - 23 * em >> 1,
					screen->r.min.y + screen->r.max.y - 4 * font->height >> 1),
					fg, ZP, font, "Say q again! I dare ya!");
				string(screen,
					Pt(screen->r.min.x + screen->r.max.x - 32 * em >> 1,
					screen->r.min.y + screen->r.max.y >> 1),
					fg, ZP, font, "I double dare you, motherfucker!");
				string(screen,
					Pt(screen->r.min.x + screen->r.max.x - 29 * em >> 1,
					screen->r.min.y + screen->r.max.y + 4 * font->height >> 1),
					fg, ZP, font, "Say q one more goddamn time!");
				string(screen,
					Pt(screen->r.min.x + screen->r.max.x - em >> 1,
					screen->r.min.y + screen->r.max.y + 8 * font->height >> 1),
					fg, ZP, font, "☠");
				flushimage(display, 1);
				recv(⌨->c, &k);
				if(k == 'q')
					threadexitsall(nil);
				break;
			case 'w':
				enter("write", path, sizeof(path), ⌖, ⌨, nil);
				fd = create(path, OWRITE, 0666);
				if(fd < 0)goto uhoh;
				buf[0] = 'O'; buf[1] = 'P'; buf[2] = 'L';
				buf[3] = 'T'; buf[4] = 'R'; buf[5] = 'K';
				buf[6] = rows; buf[7] = rows >> 8;
				if(write(fd, buf, 8) != 8){close(fd); goto uhoh;}
				for(i = 0; i < rows; i++)
					if(write(fd, ev[i], 18) != 18){close(fd); goto uhoh;}
				if(write(fd, global, rows) != rows){close(fd); goto uhoh;}
				for(i = 0; i < nelem(instr); i++)
					if(write(fd, instrname[i], sizeof(instrname[0])) != sizeof(instrname[0])
					|| write(fd, instr[i], INSTRLEN) != INSTRLEN){
						close(fd);
						goto uhoh;
					}
				close(fd);
				break;
			case ']':
				*buf = 0;
				enter("insert how many rows?", buf, sizeof(buf), ⌖, ⌨, nil);
				k = atoi(buf);
				if(k < 1)
					continue;
				if(rows + k >= sizeof(global)){
					enter("can't fit 'em!", nil, 0, ⌖, ⌨, nil);
					continue;
				}
				for(i = rows - 1; i >= row; i--){
					memmove(ev[i + k], ev[i], 18);
					global[i + k] = global[i];
				}
				rows += k;
				for(k += row; row < k; row++){
					memset(ev[row], 0, 18);
					global[row] = 0;
				}
				fmtrows();
				break;
			case '[':
				*buf = 0;
				enter("delete how many rows?", buf, sizeof(buf), ⌖, ⌨, nil);
				k = atoi(buf);
				if(k < 1)
					continue;
				rows -= k;
				if(row >= rows){
					row -= !!row;
					rows = row + 1;
				}else
					for(i = row; i < rows; i++){
						memmove(ev[i], ev[i + k], 18);
						global[i] = global[i + k];
					}
				if(snarfr.max.y >= rows){
					snarfp = ZP;
					snarfr = ZR;
				}
				fmtrows();
				break;
			case 's':
				snarfr = canonrect(Rect(snarfp.x, snarfp.y, col, row));
				snarfp = Pt(col, row);
			case 'S':
				snprint(buf, sizeof(buf), "snarf rect: rows %.4X–%.4X, cols %c–%c",
					snarfr.min.y, snarfr.max.y, 'A' + snarfr.min.x, 'A' + snarfr.max.x);
				stringbg(screen, screen->r.min, fg, ZP, font, buf, bg, ZP);
				flushimage(display, 1);
				continue;
			case 'P': case 'p':
				t = snarfr.min.y - row;
				zzz = Dx(snarfr) + 1;
				if(col + zzz > 18)
					zzz = 18 - col;
				if(t < 0){
					i = row + Dy(snarfr) + 1;
					if(i >= rows)
						i = rows;
					while(--i >= row){
						memmove(ev[i] + col, ev[i + t] + snarfr.min.x, zzz);
						if(zzz == 18)
							global[i] = global[i + t];
					}
				}else{
					for(i = snarfr.min.y; i <= snarfr.max.y; i++){
						memmove(ev[i - t] + col, ev[i] + snarfr.min.x, zzz);
						if(zzz == 18)
							global[i - t] = global[i];
					}
				}
				if(k == 'P'){
					zzz = Dx(snarfr) + 1;
					for(i = snarfr.min.y; i <= snarfr.max.y; i++)
						memset(ev[i] + snarfr.min.x, 0, zzz);
					if(zzz == 18)
						for(i = snarfr.min.y; i <= snarfr.max.y; i++)
							global[i] = 0;
				}
				fmtrows();
				break;
			case Kdel: case Kbs: snarf = ev[row][col]; ev[row][col] = 0; fmtrow(row); break;
			case Kesc: ev[row][col] = snarf; fmtrow(row); break;
			case 'x': case '^': snarf = ev[row][col]; ev[row][col] = nelem(pitch) - 1; fmtrow(row); break;
			case '-': snarf = ev[row][col]; ev[row][col] = nelem(pitch); fmtrow(row); break;
			case '+': case '=': snarf = ev[row][col]; ev[row][col] = nelem(pitch) + 1; fmtrow(row); break;
			case 'c': snarf = ev[row][col]; ev[row][col] = oct * 12 + 1; fmtrow(row); break;
			case 'C': snarf = ev[row][col]; ev[row][col] = oct * 12 + 2; fmtrow(row); break;
			case 'd': snarf = ev[row][col]; ev[row][col] = oct * 12 + 3; fmtrow(row); break;
			case 'D': snarf = ev[row][col]; ev[row][col] = oct * 12 + 4; fmtrow(row); break;
			case 'e': snarf = ev[row][col]; ev[row][col] = oct * 12 + 5; fmtrow(row); break;
			case 'f': snarf = ev[row][col]; ev[row][col] = oct * 12 + 6; fmtrow(row); break;
			case 'F': snarf = ev[row][col]; ev[row][col] = oct * 12 + 7; fmtrow(row); break;
			case 'g': snarf = ev[row][col]; ev[row][col] = oct * 12 + 8; fmtrow(row); break;
			case 'G': if(oct == 8) continue;
				snarf = ev[row][col]; ev[row][col] = oct * 12 + 9; fmtrow(row); break;
			case 'a': if(oct == 8) continue;
				snarf = ev[row][col]; ev[row][col] = oct * 12 + 10; fmtrow(row); break;
			case 'A': if(oct == 8) continue;
				snarf = ev[row][col]; ev[row][col] = oct * 12 + 11; fmtrow(row); break;
			case 'b': if(oct == 8) continue;
				snarf = ev[row][col]; ev[row][col] = oct * 12 + 12; fmtrow(row); break;
			case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8':
				oct = k - '0';
				if(!ev[row][col] || ev[row][col] >= nelem(pitch) - 1
				|| oct & 8 && (ev[row][col] + 1) % 12 > 7){
					sprint(buf, "input octave set to %d", oct);
					string(screen, screen->r.min, fg, ZP, font, buf);
					flushimage(display, 1);
					continue;
				}
				snarf = ev[row][col];
				ev[row][col] = 12 * oct + (ev[row][col] - 1) % 12 + 1;
				fmtrow(row);
				break;
			case '\n': case 'i':
				snarf = ev[row][col];
				i = ev[row][col] >= nelem(pitch) + 2 ? ev[row][col] - nelem(pitch) - 2 : 0;
				instrmode = 1;
				orig.x = screen->r.min.x + screen->r.max.x - 36 * em >> 1;
				orig.y = screen->r.min.y + screen->r.max.y - 19 * font->height >> 1;
				drawinstr(i);
				continue;
			case Kleft:
				col = (col + 17) % 18;
				vert.min.x = orig.x + (5 + 3 * col) * em;
				vert.max.x = vert.min.x + 2 * em;
				break;
			case Kright:
				col = (col + 1) % 18;
				vert.min.x = orig.x + (5 + 3 * col) * em;
				vert.max.x = vert.min.x + 2 * em;
				break;
			case Kup:
				if(row == 0)
					continue;
				fmtrow(--row - 7);
				break;
			case Kdown:
				if(row + 1 == rows)
					continue;
				fmtrow(++row + 8);
				break;
			case Kpgup:
				if(row - skip < 0)
					continue;
				row -= skip;
				fmtrows();
				break;
			case Kpgdown:
				if(row + skip >= rows)
					continue;
				row += skip;
				fmtrows();
				break;
			case Khome:
				row = 0;
				fmtrows();
				break;
			case Kend:
				row = rows - 1;
				fmtrows();
				break;
			case 'm':
				header[3 * col + 6] ^= 'm' ^ ' ';
				break;
			case 'k':
				stringbg(screen, screen->r.min, fg, ZP, font,
					"press an alphanum to set mark", bg, ZP);
				flushimage(display, 1);
				recv(⌨->c, &k);
				if(k >= '0' && k <= '9')
					global[row] = k - '0' + 1;
				else if(k >= 'a' && k <= 'z')
					global[row] = k - 'a' + 11;
				else if(global[row] < 11 + 26)
					global[row] = 0;
				fmtrow(row);
				break;
			case 'j':
				stringbg(screen, screen->r.min, fg, ZP, font,
					"press an alphanum to jump to mark", bg, ZP);
				flushimage(display, 1);
				recv(⌨->c, &k);
				if(k >= '0' && k <= '9')
					k = k - '0' + 1;
				else if(k >= 'a' && k <= 'z')
					k = k - 'a' + 11;
				else
					continue;
				for(i = row + 1; i < rows && global[i] != k; i++);
				if(i == rows)
					for(i = 0; i < row && global[i] != k; i++);
				if(global[i] != k)
					continue;
				row = i;
				fmtrows();
				break;
			case KF | 1: case KF | 2: case KF | 3: case KF | 4: case KF | 5: case KF | 6:
			case KF | 7: case KF | 8: case KF | 9: case KF | 10: case KF | 11: case KF | 12:
				skip = k & 15;
				snprint(buf, sizeof(buf), "PgUp/PgDn = %2.-d", skip);
				stringbg(screen, screen->r.min, fg, ZP, font, buf, bg, ZP);
				flushimage(display, 1);
				continue;
			case ' ':
				fd = oplinit();
				if(fd < 0)
					goto uhoh;
				for(i = 0; i < 18; i++)
					memset(reg[i], 0, INSTRLEN);
				bpm = 120;
				rpb = 4;
				playing = incr = rep = wiggles = four = 0;
				i = row;
				for(row = 0; row < i; row++)
					sendrow(fd);
				playing = 60000;
				incr = playing / bpm / rpb;
				t = nsec();
				for(sendrow(fd); row < rows; drawrows()){
					while(nbrecv(⌖->c, &⌖->Mouse) && !⌖->buttons);
					if(⌖->buttons)
						break;
					if(nbrecv(⌨->c, &k)){
						if(k == Kleft){
							col = (col + 17) % 18;
							vert.min.x = orig.x + (5 + 3 * col) * em;
							vert.max.x = vert.min.x + 2 * em;
						}else if(k == Kright){
							col = (col + 1) % 18;
							vert.min.x = orig.x + (5 + 3 * col) * em;
							vert.max.x = vert.min.x + 2 * em;
						}else if(k == 'm'){
							header[3 * col + 6] ^= 'm' ^ ' ';
							buf[0] = 0xC0 | ch[col];
							buf[1] = ext[col];
							buf[2] = 0x30 & (header[3 * col + 6] == 'm') - 1;
							buf[3] = buf[4] = 0;
							write(fd, buf, 5);
						}else
							break;
					}
					t += incr * 1000000;
					if(global[row] == 46){
						if(rep == 1){
							do ++row; while(global[row] < 39 || global[row] > 45);
							row++;
							rep = 0;
							fmtrows();
						}else
							fmtrow(++row + 8);
					}
					else if(global[row] >= 39 && global[row] <= 45){
						if(rep == 1){
							fmtrow(++row + 8);
							rep = 0;
						}else{
							rep += rep ? -1 : global[row] - 39 + 1;
							for(row -= !!row; row && (global[row] < 38 || global[row] > 45); row--);
							fmtrows();
						}
					}
					else
						fmtrow(++row + 8);
					if(row < rows)
						sendrow(fd);
					zzz = (t - nsec()) / 1000000;
					if(zzz > 1)
						sleep(zzz);
				}
				if(k != Kesc)
					row = i;
				fmtrows();
				close(fd);
				break;
			}
			drawrows();
			break;
		default:
			sysfatal("labyrinthus: hic habitat minotaurus");
		}
}