💾 Archived View for thrig.me › blog › 2024 › 02 › 17 › tnats.c captured on 2024-03-21 at 15:43:11.

View Raw

More Information

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

// tnats - throw numbers at a synth
// there are some PORTABILITY concerns if you're not on OpenBSD

#include <sys/wait.h>

#include <err.h>
#include <fcntl.h>
#include <getopt.h>
#include <math.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

// a positive number here indicates the note was turned on (or has
// otherwise been blocked from being turned on)
#define PITCHMAX 128U
int pitches[PITCHMAX];

// a bad RNG, but who cares! we're throwing numbers at a synth
uint16_t seed;
#define RN_MIN 0
#define RN_MAX 0x7fff
#define RN (((seed = seed * 11109 + 13849) & RN_MAX) >> 1)

#define MS2NSEC 1000000U
#define TICKLEN 4 // milliseconds

typedef void (*notefn)(int fd, int pitchnum);

sig_atomic_t bailout;

inline static int
coinflip(void)
{
	return RN % 1000 >= 500; // RN & 1 is broken for this algo
}

inline static void
pitch_decr(int fd, int minus, notefn offcall)
{
	for (int i = 0; i < PITCHMAX; ++i) {
		int prev = pitches[i];
		pitches[i] -= minus;
		if (pitches[i] < 0) pitches[i] = 0;
		if (pitches[i] == 0 && prev) offcall(fd, i);
	}
}

static void
note_off(int fd, int note)
{
	dprintf(fd, "\x80%c%c", note, 0);
}

static void
note_on(int fd, int note, int velocity)
{
	dprintf(fd, "\x90%c%c", note, velocity);
}

void
whoops(int unused)
{
	bailout = 1;
}

int
main(int argc, char *argv[])
{
	// PORTABILITY assumes OpenBSD
	char *file = "/dev/rmidi0";
	int ch;
	while ((ch = getopt(argc, argv, "d:")) != -1) {
		switch (ch) {
		case 'd':
			file = optarg;
			break;
		default:
			err(1, "... -d mididevice [record]");
		}
	}
	argc -= optind;
	argv += optind;

	int fd = open(file, O_WRONLY);
	if (fd < 0) err(1, "open '%s'", file);

	// PORTABILITY assumes OpenBSD
	pid_t recording = 0;
	if (argc > 0 && strncmp(*argv, "rec", 3) == 0) {
		recording = fork();
		if (recording < 0) err(1, "fork");
		if (recording == 0) {
			execl("/usr/bin/aucat", "aucat", "-h", "wav", "-o",
			      "out.wav", (char *) 0);
			err(1, "exec");
		}
		signal(SIGINT, whoops);
		fprintf(stderr, "recording...\n");
	}

	// PORTABILITY non-OpenBSD systems may need some other way to
	// seed the RNG
	seed = 1 + arc4random_uniform(UINT16_MAX - 1);
	// blind, preset 93
	seed = 13019;
	printf("%u\n", seed);

	struct timespec ticklen;
	ticklen.tv_sec  = 0;
	ticklen.tv_nsec = TICKLEN * MS2NSEC;

	// stats help show if and how much your features are working
	size_t iters   = 0;
	size_t blocked = 0;
	size_t drained = 0;
	size_t onsets  = 0;

	for (int i = 0; i < 8; ++i) {
		for (int nc = 0; nc < 64; ++nc) {
			int n = RN % 128;
			if (pitches[n] == 0) {
				note_on(fd, n, 96);
				onsets++;
			} else {
				blocked++;
			}
			int plus = 8 * (1 + RN % 16); // block the note
			pitches[n] += plus;
			nanosleep(&ticklen, NULL);
			if (bailout) goto CLEANUP;
			pitch_decr(fd, 1, note_off);
			iters++;
		}
		sleep(i % 3 == 0 ? 4 : 6);
		if (coinflip()) {
			for (int j = 0; j < PITCHMAX; ++j) {
				if (pitches[j] > 0) {
					drained++;
					note_off(fd, j);
					pitches[j] = 0;
				}
			}
		}
	}
	fprintf(stderr, "draining any notes left on...\n");
	int decr = 1;
	while (1) {
		int done = 1;
		for (int i = 0; i < PITCHMAX; ++i) {
			if (pitches[i] > 0) {
				done = 0;
				break;
			}
		}
		if (done) break;
		nanosleep(&ticklen, NULL);
		if (bailout) goto CLEANUP;
		pitch_decr(fd, decr++, note_off);
	}
	if (recording) {
		int delay = 3;
		fprintf(stderr, "wait for synth... %d)\n", delay);
		sleep(delay);
		kill(recording, SIGTERM);
	}
	goto BAILOUT;
CLEANUP:
	for (int i = 0; i < PITCHMAX; ++i)
		note_off(fd, i);
	if (recording) {
		kill(recording, SIGTERM);
		fprintf(stderr, "wait for recorder to exit...\n");
		wait(0);
	}
BAILOUT:
	fprintf(stderr, "total %zu blocked %zu onsets %zu drained %zu\n", iters,
	        blocked, onsets, drained);
	exit(0);
}