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:
==========================================================================
NAME
piano - musical keyboard
SYNOPSIS
piano [-p] [-a amplitude] [-d duration]
DESCRIPTION
The piano program plays sounds when you press keys on the keyboard.
Pressing a letter key produces a note for one beat. "q" produces the
lowest note and "m" produces the highest note. Notes between "q" and
"m" are each a half step apart, ordered as they appear on a QWERTY
keyboard. The space (" ") character produces a rest (silence).
Entering a digit before a note or rest causes the note or rest to be
that many beats long.
The options to piano are as follows:
-a amplitude
Volume percentage. Must be a value between 0 and 100.
-d duration
The duration (in milliseconds) of a beat.
-p Use a piano-like layout instead of the default ascending
QWERTY layout. Keys in the middle keyboard row act as the
white keys on a piano, and keys in the top row act as the
black keys.
EXAMPLES
Tetris theme:
$ echo 'z ghk hgd dhz khg ghk z h d 2d' | piano -d 150
Megalovania:
$ echo 'uul f2 d s p upsuul f2 d s p ups' | piano
==========================================================================
Compile with `cc -lm -lsndio piano.c -o piano`:
---
diff --git a/piano.c b/piano.c
new file mode 100644
index 0000000..6687bbd
--- /dev/null
+++ b/piano.c
@@ -0,0 +1,217 @@
+/*
+ * Copyright (c) 2024 Simon Schwartz and Benjamin Poulin
+ *
+ * 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 <assert.h>
+#include <ctype.h>
+#include <err.h>
+#include <math.h>
+#include <sndio.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <termios.h>
+#include <unistd.h>
+
+#define TWRT2 1.05946 /* 12th root of 2 */
+#define TWOPI (3.14159 * 2.0)
+#define MAXBEATS 9
+
+static bool piano_layout = false;
+static const char *chromatic_chars = "qwertyuiopasdfghjklzxcvbnm";
+static const char *piano_chars = "awsedftgyhujkolp;']zbxncv";
+static struct termios oldattr;
+static int beat_duration = 100;
+static unsigned int amplitude = INT16_MAX / 2;
+static size_t samps_per_beat;
+static struct sio_hdl *snd;
+static struct sio_par snd_par;
+static int16_t *sndbuf;
+
+static void
+initsound(void)
+{
+ snd = sio_open(SIO_DEVANY, SIO_PLAY, 0);
+ if (snd == NULL)
+ errx(1, "sio_open");
+
+ struct sio_par par;
+ sio_initpar(&par);
+
+ par.pchan = 1;
+ par.bits = 16;
+ par.bps = 2;
+ par.sig = 1;
+
+ if (sio_setpar(snd, &par) == 0)
+ errx(1, "sio_setpar");
+
+ if (sio_getpar(snd, &snd_par) == 0)
+ errx(1, "sio_getpar");
+
+ if (sio_start(snd) == 0)
+ errx(1, "sio_start");
+
+ samps_per_beat = (snd_par.rate * beat_duration) / 1000;
+ assert(snd_par.bps == sizeof(*sndbuf));
+ sndbuf = malloc(sizeof(*sndbuf) * samps_per_beat * MAXBEATS);
+}
+
+static void
+playsnd(size_t beats)
+{
+ assert(beats <= MAXBEATS);
+ uint8_t *ptr = (uint8_t *)sndbuf;
+ size_t nbytes = samps_per_beat * beats * sizeof(*sndbuf);
+ while (nbytes) {
+ size_t nwrite = sio_write(snd, ptr, nbytes);
+ nbytes -= nwrite;
+ ptr += nwrite;
+ }
+}
+
+static void
+beep(int freq, int beats)
+{
+ assert(beats <= MAXBEATS);
+ size_t nsamps = samps_per_beat * beats;
+ float ease_width = nsamps / 8;
+ float theta = 0;
+
+ for (size_t i = 0; i < nsamps; i++) {
+ float amp = amplitude;
+ if (i < ease_width)
+ amp *= (float)i / ease_width;
+ else if (i > nsamps - ease_width)
+ amp *= (float)(nsamps - i) / ease_width;
+
+ sndbuf[i] = powf(sin(theta), 17.8) * amp;
+ theta += TWOPI * ((float)freq) / ((float)snd_par.rate);
+ }
+ playsnd(beats);
+}
+
+static void
+rest(int beats)
+{
+ assert(beats <= MAXBEATS);
+ memset(sndbuf, 0, sizeof(*sndbuf) * samps_per_beat * beats);
+ playsnd(beats);
+}
+
+static void
+resetterm(void)
+{
+ tcsetattr(STDIN_FILENO, TCSANOW, &oldattr);
+}
+
+static void
+setterm(void)
+{
+ tcgetattr(STDIN_FILENO, &oldattr);
+
+ struct termios attr;
+ memcpy(&attr, &oldattr, sizeof(attr));
+ attr.c_lflag &= ~ICANON;
+ attr.c_cc[VMIN] = 1;
+ attr.c_cc[VTIME] = 0;
+
+ tcsetattr(STDIN_FILENO, TCSAFLUSH, &attr);
+ atexit(resetterm);
+}
+
+static int
+getfreq(char ch)
+{
+ const char *chars = piano_layout ? piano_chars : chromatic_chars;
+ const char *cptr = strchr(chars, ch);
+ if (cptr == NULL)
+ return -1;
+ size_t idx = cptr - chars;
+
+ return 185.0 * powf(TWRT2, idx);
+}
+
+static void
+parseargs(int argc, char **argv)
+{
+ int ch;
+ char *endptr;
+ unsigned int a;
+ while ((ch = getopt(argc, argv, "a:d:p")) != -1) {
+ switch (ch) {
+ case 'a':
+ a = strtoul(optarg, &endptr, 10);
+ if (*optarg == '\0' || *endptr != '\0' || a > 100)
+ errx(1, "invalid amplitude");
+ amplitude = (float)INT16_MAX * ((float)a / 100.0);
+ break;
+ case 'd':
+ beat_duration = strtoul(optarg, &endptr, 10);
+ if (*optarg == '\0' || *endptr != '\0')
+ errx(1, "invalid duration");
+ break;
+ case 'p':
+ piano_layout = true;
+ break;
+ default:
+ exit(1);
+ }
+ }
+ argc -= optind;
+ argv += optind;
+}
+
+int
+main(int argc, char **argv)
+{
+ pledge("stdio tty unix rpath", NULL);
+ parseargs(argc, argv);
+ initsound();
+
+ int is_tty = isatty(STDIN_FILENO);
+ if (is_tty) {
+ pledge("stdio tty", NULL);
+ setterm();
+ } else {
+ pledge("stdio", NULL);
+ }
+
+ char ch;
+ int nbeats = 1;
+ ssize_t nread;
+ while ((nread = read(STDIN_FILENO, &ch, sizeof(ch))) != 0) {
+ if (nread == -1)
+ err(1, "read");
+ if (is_tty && ch == oldattr.c_cc[VEOF])
+ break;
+
+ int freq;
+ if (isdigit(ch) && ch != '0') {
+ nbeats = ch - '0';
+ if (nbeats > MAXBEATS)
+ nbeats = MAXBEATS;
+ continue;
+ } else if (ch == ' ') {
+ rest(nbeats);
+ } else if ((freq = getfreq(ch)) != -1) {
+ beep(freq, nbeats);
+ }
+
+ nbeats = 1;
+ }
+}
diff --git a/harmony.sh b/harmony.sh
new file mode 100755
index 0000000..cc84ce2
--- /dev/null
+++ b/harmony.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+
+echo '
+4d2j2g2d2s2d2i2t2i3q 2t2i4i
+2 2i2d2i2p2t2y2e2i2i8q6 2d2d
+2d2d2d2i3i 2i2i2i2i2i2i3q 2d
+2j2g2d2s2d2i2t2i3q 2d2q4i2 2i
+2d2i2p2t2y2e2i2i8q' | piano -d150 -a70 &
+
+echo '
+4d2j2g2d2s2d2i2d2g3j 2d2j4g
+2 2d2j2g2d2s2p2g2d2s8d6 2j2j
+2j2d2j2j3g 2g2g2g2s2g2g3d 2d
+2j2g2d2s2d2i2d2g3j 2d2j4g2 2d
+2j2g2d2s2p2g2d2s8d' | piano -d150 -a70 &
+
+echo '
+4d2j2g2d2s2d2g2j2z3z 2j2z4z
+2 2j2z2k2j2j2k2k2j2g8j6 2j2z
+2z2j2z2z3k 2k2k2k2g2k2k3j 2d
+2j2g2d2s2d2g2j2z3z 2j2z4z2 2j
+2z2k2j2j2k2c2z2k8j' | piano -d150 -a70