On Sat, Dec 07, 2024 at 08:20:46PM -0500, Simon Schwartz wrote: > Hi all! > > My roommate bought an old all-in-one from Goodwill, which we promptly > installed OpenBSD on. We left it out in our living room, and (being a > college dorm full of computer science majors), have been having quite some > fun writing and running various silly programs on it. I figured I would > share one we've been having a lot of fun with, in the hopes that someone > else might get a kick of it too:
reminds me of playing nokia ringtones (RTTTL) /* * Copyright (c) 2009-2014 Jonathan Gray <j...@jsg.id.au> * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include <sndio.h> #include <stdio.h> #include <stdlib.h> #include <math.h> #include <unistd.h> #include <string.h> #include <fcntl.h> #include <sys/ioctl.h> #include <err.h> #include <dev/isa/spkrio.h> /* * http://arcadetones.emuunlim.com * */ #define CIRCUS \ "Circus:d=32,o=6,b=45:16c#,16c,b5,c,b5,a#5,16a5,16g#5,16g5,16g#5,16a#5,16a5,g#5,a5,g#5,g5,16f#5,16f5,16e5,16f5,16g#5,f#5,f#5,16d5,16d#5,16g#5,f#5,f#5,16d5,16d#5,c,c#,d,d#,e,f,f#,g,g#,a,a#,b,16a#,16g#" char defmelody[] = CIRCUS; int defdur = 4; int defoct = 4; int defmul = 1; int bpm = 150; enum wave { WAVE_SQUARE = 0, WAVE_TRIANGLE = 1, WAVE_SAWTOOTH = 2, WAVE_SINE = 3, WAVE_NOISE = 4, WAVE_MAX = 5, }; typedef enum wave wavet; int note2key(char *note, int octave) { int key; switch (note[0]) { case 'c': key = 0x18; break; case 'd': key = 0x1a; break; case 'e': key = 0x1c; break; case 'f': key = 0x1d; break; case 'g': key = 0x1f; break; case 'a': key = 0x21; break; case 'b': key = 0x23; break; case 'p': return (-1); break; default: warnx("unknown note '%c'\n", note[0]); return (-2); } if (note[1] == '#') key++; key += (octave * 12); // printf("translated note %s to key is %x\n", note, key); return (key); } #define MIDI_NOTEON 0x90 #define MIDI_NOTEOFF 0x80 #define MIDI_CTLCHG 0xb0 #define MIDI_PRGCHG 0xc0 #define MIDI_RESET 0xff #define MIDI_CTL_VOLUME 0x07 #define MIDI_VELOCITY 90 void midi_reset(struct mio_hdl *hdl, int bank) { int channel = 0; u_char data[] = { MIDI_RESET }; mio_write(hdl, data, sizeof(data)); /* GS bank select */ u_char ctl[] = { MIDI_CTLCHG | (channel & 0xf), 0x00, bank & 0xff }; mio_write(hdl, ctl, sizeof(ctl)); ctl[1] = 0x20; ctl[2] = (bank >> 8) & 0xff; mio_write(hdl, ctl, sizeof(ctl)); } void tgen_midi(struct mio_hdl *hdl, int key, int instrument, int dur) { u_char data[3]; int channel = 0; data[0] = MIDI_PRGCHG | (channel & 0x0f); data[1] = instrument; mio_write(hdl, data, 2); data[0] = MIDI_NOTEON | (channel & 0x0f); data[1] = key; data[2] = MIDI_VELOCITY; mio_write(hdl, data, sizeof(data)); usleep(dur); data[0] = MIDI_NOTEOFF | (channel & 0x0f); mio_write(hdl, data, sizeof(data)); } void tgen_pcspkr(int fd, int key, int dur) { tone_t tone; if (key < 0) tone.frequency = 0; else tone.frequency = 6.875 * powf(2, (3 + key) / (double)12); tone.duration = dur / 10000; ioctl(fd, SPKRTONE, &tone); } short tgen_sample(wavet wave, int t, int period) { short sample; int m, x; switch (wave) { case WAVE_TRIANGLE: m = 40000; x = t + (period / 4.0); x = (x % period) * 2 * (m / period); sample = abs(x - m) - (m / 2); break; case WAVE_SAWTOOTH: m = 30000; x = t % period; sample = ((x * m) / (period / 2)) - m; break; case WAVE_SINE: m = 20000; sample = m* (sin(M_PI + ((2 * M_PI) * (t / (double)period)))); break; case WAVE_NOISE: m = 40000; sample = arc4random_uniform(m) - (m / 2); break; default: case WAVE_SQUARE: m = 10000; sample = (t > period / 2) ? m : -m; break; } // printf("%d %d\n", t, sample); return (sample); } short tgen_adsr(short sample, int t, int period) { /* ADSR envelope */ double attack = 0.15; double decay = 0.30; double sustain = 0.65; double release = 1.0; double svol = 0.55; double pos = t / (double)period; if (pos <= attack) { double factor = t / (period * (double)attack); sample *= factor; } else if ((t / period) <= decay) { int tn = t - (attack * period); double factor = (tn * (1 - svol)) / (period * (double)(decay - attack)); factor = 1 - factor; sample *= factor; } else if ((t / period) <= sustain) { sample *= svol; } else if ((t / period) <= release) { int tn = t - (sustain * period); double factor = (tn * svol) / (period * (double)(release - sustain)); factor = svol - factor; sample *= factor; } return (sample); } void tgen_sndio(struct sio_hdl *hdl, struct sio_par par, wavet wave, int key, int msdur, int envelope) { int i, j, period, t = 0; double hz; short *p, sample; int nsamples; size_t size; short *buf; hz = 6.875 * powf(2, (3 + key) / (double)12); period = par.rate / hz; nsamples = (par.rate / 1000000.0) * msdur; /* round up to a multiple of the period */ nsamples = nsamples + period - 1 - (nsamples - 1) % period; size = nsamples * par.pchan * par.bps; buf = malloc(size); if (key < 0) { memset(buf, 0, size); goto write; } p = buf; for (i = 0; i < nsamples; i++) { sample = tgen_sample(wave, t, period); if (++t == period) t = 0; if (envelope) sample = tgen_adsr(sample, i, nsamples); for (j = 0; j < par.pchan; j++) *p++ = sample; } write: if (!sio_write(hdl, buf, size)) { fprintf(stderr, "failed writing samples\n"); exit(1); } } struct note { int dur; char note[3]; int oct; }; /* <note> := [<duration>] <note> [<scale>] [<special-duration>] <delimiter> */ void parse_note(struct note *sn, char *note, int defdur, int defoct) { size_t s; char sdur[5]; const char *errstr; int dur, oct; char *p = note; s = strspn(note, "0123456789"); if (s == 0) dur = defdur; else { memset(sdur, 0, sizeof(sdur)); memcpy(sdur, note, s); p += s; dur = strtonum(sdur, 1, 32, &errstr); if (errstr) errx(1, "invalid duration %s: %s", errstr, note); } s = strspn(p, "pcdefgab#"); if (s == 0) errx(1, "no note found in '%s'\n", p); memset(sn->note, 0, sizeof(sn->note)); memcpy(sn->note, p, s); p += s; if (*p == '.') { dur += (dur / 2); p++; } s = strspn(p, "0123456789"); if (s == 0) oct = defoct; else { oct = *p - 0x30; p += s; } // printf("dur '%d', note '%s' oct '%d' ", dur, sn->note, oct); sn->dur = dur; sn->oct = oct; return; } char * parse_header(char *str) { size_t sz; char *hdr = strchr(str, ':'); char *p, *s, *cmd; const char *errstr; int val; if (hdr == NULL) return (str); hdr++; sz = strcspn(hdr, ":"); p = malloc(sz + 1); if (p == NULL) err(1, "malloc hdr"); memcpy(p, hdr, sz); p[sz] = '\0'; // printf("found header '%s'\n", p); s = p; for (;;) { cmd= strsep(&s, ", "); if (cmd == NULL) break; if (*cmd == '\0') continue; val = strtonum(&cmd[2], 1, 300, &errstr); if (errstr) errx(1, "invalid value %s: '%s'", errstr, cmd); switch (cmd[0]) { case 'd': defdur = val; break; case 'o': defoct = val; break; case 'b': bpm = val; break; default: errx(1, "unknown header '%c'", cmd[0]); } } printf("using defaults duration %d, octave %d, bpm %d\n", defdur, defoct, bpm); free(p); return (hdr + sz + 1); } /* * 12 keys in an octive * 0-127 MIDI keys * 0-127 program/instrument changes * 16 channels (chan 10 is percussion only) * controller events * sysex messages * */ #define nitems(_a) (sizeof((_a)) / sizeof((_a)[0])) int init_sndio(struct sio_hdl **hdl, struct sio_par *par) { *hdl = sio_open(NULL, SIO_PLAY, 0); if (*hdl == NULL) { fprintf(stderr, "failed to open default audio device\n"); exit(1); } sio_initpar(par); par->bits = 16; par->bps = 2; if (!sio_setpar(*hdl, par)) { fprintf(stderr, "failed to set parameters\n"); exit(1); } if (!sio_getpar(*hdl, par)) { fprintf(stderr, "failed to get parameters\n"); exit(1); } if (par->bits != 16 || par->bps != 2 || par->le != SIO_LE_NATIVE) { fprintf(stderr, "unsupported audio format\n"); exit(1); } if (!sio_start(*hdl)) { fprintf(stderr, "failed to start playback\n"); exit(1); } return (0); } enum output { OUTPUT_PCSPKR = 0, OUTPUT_SNDIO, OUTPUT_MIDI, }; extern char *__progname; void usage(void) { fprintf(stderr, "usage: %s [-e] [-m melody-string] [-w wave-num]" " [-b bank] [-i ins] [-o output-type]\n", __progname); fprintf(stderr, "\te: enable ADSR envelope\n"); fprintf(stderr, "\bi: MIDI instrument\n"); fprintf(stderr, "\ti: MIDI instrument\n"); fprintf(stderr, "\twaves: 0 (square) 1 (triangle) 2 (sawtooth)\n"); fprintf(stderr, "\t\t3 (sine) 4 (noise)\n"); fprintf(stderr, "\toutputs: p (pc speaker) s (sndio) m (midi)\n"); exit(1); } int main(int argc, char *argv[]) { struct sio_hdl *hdl; struct mio_hdl *mhdl; struct sio_par par; struct note sn; char *snote, *input; int key = 0, instrument = arc4random() & 0x7f; int bank = 0; int fd, ch; int msdur; enum output out = OUTPUT_SNDIO; char *melody = defmelody; wavet wave = WAVE_SQUARE; const char *errstr; int envelope = 0; while ((ch = getopt(argc, argv, "b:ei:m:o:w:")) != -1) { switch (ch) { case 'b': bank = strtonum(optarg, 0, 128, &errstr); if (errstr != NULL) errx(1, "invalid bank : %s", errstr); break; case 'e': envelope = 1; break; case 'i': instrument = strtonum(optarg, 0, 127, &errstr); if (errstr != NULL) errx(1, "invalid instrument: %s", errstr); break; case 'm': melody = optarg; break; case 'o': switch (optarg[0]) { case 'm': out = OUTPUT_MIDI; break; case 'p': out = OUTPUT_PCSPKR; break; case 's': out = OUTPUT_SNDIO; break; default: errx(1, "invalid output type"); } break; case 'w': wave = strtonum(optarg, 0, WAVE_MAX - 1, &errstr); if (errstr) errx(1, "invalid wave type %s: %s", errstr, optarg); break; default: usage(); break; } } switch (out) { case OUTPUT_PCSPKR: printf("using PC speaker\n"); fd = open("/dev/speaker", O_WRONLY, 0700); if (fd == -1) err(1, "open /dev/speaker"); break; case OUTPUT_SNDIO: init_sndio(&hdl, &par); break; case OUTPUT_MIDI: mhdl = mio_open(NULL, MIO_OUT, 0); if (mhdl == NULL) errx(1, "mio_open failed"); midi_reset(mhdl, bank); printf("using bank %d instrument %d!\n", bank, instrument); break; } input = parse_header(melody); for (;;) { snote = strsep(&input, ","); if (snote == NULL) break; parse_note(&sn, snote, defdur, defoct); #ifdef DEBUG printf("note %-2s octave %d dur %d", sn.note, sn.oct - 1, sn.dur); #endif msdur = ((60 * 1000000) / (bpm * sn.dur)) * 4; key = note2key(sn.note, sn.oct - 1); #ifdef DEBUG printf(" key %x\n", key); #endif switch (out) { case OUTPUT_PCSPKR: tgen_pcspkr(fd, key, msdur); break; case OUTPUT_SNDIO: tgen_sndio(hdl, par, wave, key, msdur, envelope); break; case OUTPUT_MIDI: tgen_midi(mhdl, key, instrument, msdur); break; } } switch (out) { case OUTPUT_PCSPKR: close(fd); break; case OUTPUT_SNDIO: sio_close(hdl); break; case OUTPUT_MIDI: mio_close(mhdl); break; } return (0); }