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);
}

Reply via email to