Scott Cheloha <[email protected]> wrote:
> speaker(4) is a whimsical thing, but I don't think we should have a
> dedicated chiptune interpreter in the kernel.
> 
> This patch unhooks the driver and the manpage from the build.  The
> driver is built for alpha, amd64, and i386.
> 
> A subsequent patch will move all relevant files to the attic and clean
> up manpage cross references.
> 
> Nothing in base or xenocara includes <dev/spkrio.h>.
> 
> I see a couple SPKRTONE and SPKRTUNE symbols in ports, but I imagine
> those ports don't use the symbols if they are missing.
> 
> ok?

Hello tech@,

So, after the proposal to remove speaker(4), it came up that parsing the
play string in the kernel might not be the nicest thing. Here there is a
first approach to do that. spkrcat is a CLI that takes the play string
as an argument, parses it, and use the appropiate ioctl to play it.

Name is inspired by aucat.

DIFFERENCES WITH spkr.c IMPLEMENTATION

- slightly verbose identifiers names.

- more chatty.

- sound duration math is slightly different:
  - for dotted notes, this is not computed correctly in kernelland: for
    example, with default articulation (7/8th play time, 1/8th rest
    time) and one dot, total length is computed as 25/16th of original
    duration instead of 3/2th (== 24/16th).
    After compiling with SPKRDEBUG, with 'T60 L4 c.', the total duration
    should be 1500ms: spkrcat does this in 1313ms of sound + 187ms of
    silence. spkr(4) computes this as 1375ms of sound + 187ms of
    silence.
  - even for non-dotted notes, I perceived sound duration slightly
    different. Maybe related to "rounding errors" in kernelland?
    After compiling with SPKRDEBUG, with 'T30 cbdcbdcbd', spkrcat does
    59ms sound + 8ms rest, while spkr(4) does 58ms sound + 8ms. The
    ideal total duration would be 66.6666666ms. spkrcat does round away
    from zero, spkr(4) doesn't.

- legato is (almost) fully smooth: adjacent tones with the same
  frequency are combined into one tone_t with the sum of the durations
  (as long as it doesn't overflow an int), which results in a single
  call to pcppi_bell.

- instead of using the default value on invalid inputs, clamp the value
  to the valid range.

- unveil.

CURIOSITIES CARRIED OVER FROM spkr.c

- bounds are checked in playnote / addsound:
  - "O0C-" has the same effect as "P"
  - "O6B+" is a noop

MISSING / QUESTIONS

- I have absolutely no clue how many / which of the license headers /
  comments from spkr.c should be carried over. As I'm in doubt, I
  included all of them.

- no manpage yet: shouldn't be too difficult as it's mostly moving the
  description from spkr(4), but haven't found the time to do so.

- ideally, this should be a bit more privsep'd, in a fashion similar to
  file(1): a parsing process + a ioctl process. The parsing process can
  be heavily pledge'd, as right now there is no way to pledge spkrcat
  and use the appropiat ioctl interface. The other option is adding
  ioctl(SPKR{TONE,TUNE}) to audio pledge. In any case, am unfamiliar
  with both imsg and pledge internals, but I don't mind learning it once
  an approach is set.

- no diff against the tree: I don't know where to place this between
  usr.bin/ and usr.sbin/, and some part of me even wonders if it even
  makes sense to include it there and not in games, or to even include
  it in base and not distribute it as a port. The main reason I see for
  including it in base (and in particular one of usr.{,s}bin/) is that
  it provides an interface for a feature that currently resides in
  kernelland.

- no code removal from spkr(4) yet: to be done after knowing in where to
  place spkrcat.

Feedback and comments welcome.
-Lucas

# Copyright (c) 2022 Lucas Gabriel Vuotto <[email protected]>
#
# 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.

PROG=   spkrcat
NOMAN=  spkrcat

.include <bsd.prog.mk>
/* $OpenBSD$ */
/*      $NetBSD: spkr.c,v 1.1 1998/04/15 20:26:18 drochner Exp $        */

/*
 * Copyright (c) 2022 Lucas Gabriel Vuotto <[email protected]>
 *
 * 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.
 */

/*
 * Copyright (c) 1990 Eric S. Raymond ([email protected])
 * Copyright (c) 1990 Andrew A. Chernov ([email protected])
 * Copyright (c) 1990 Lennart Augustsson ([email protected])
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. All advertising materials mentioning features or use of this software
 *    must display the following acknowledgement:
 *      This product includes software developed by Eric S. Raymond
 * 4. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

/* -- BEGIN spkr.c comments ---------------------------------------- */
/*
 * spkr.c -- device driver for console speaker on 80386
 *
 * v1.1 by Eric S. Raymond ([email protected]) Feb 1990
 *      modified for 386bsd by Andrew A. Chernov <[email protected]>
 *      386bsd only clean version, all SYSV stuff removed
 *      use hz value from param.c
 */

/**************** PLAY STRING INTERPRETER BEGINS HERE **********************
 *
 * Play string interpretation is modelled on IBM BASIC 2.0's PLAY statement;
 * M[LNS] are missing and the ~ synonym and octave-tracking facility is added.
 * Requires tone(), rest(), and endtone(). String play is not interruptible
 * except possibly at physical block boundaries.
 */
/* -- END spkr.c comments ------------------------------------------ */

#include <dev/isa/spkrio.h>
#include <sys/ioctl.h>
#include <sys/queue.h>

#include <ctype.h>
#include <err.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <vis.h>

#define _PATH_SPEAKER   "/dev/speaker"

#define nitems(_a)      (sizeof((_a)) / sizeof((_a)[0]))
#define MAX(x, y)       ((x) > (y) ? (x) : (y))
#define MIN(x, y)       ((x) < (y) ? (x) : (y))
#define CLAMP(x, a, b)  ((x) < (a) ? (a) : (x) > (b) ? (b) : (x))

/*
 * 1 minute in milliseconds (60_000) * 4 quarters per whole note.
 * Used as a factor in tempo calculation: this represents how much a whole note
 * will last at 1 BPM. Then, at a tempo of N BPM, a whole note will last
 * WHOLE_NOTELEN_1_BPM / N milliseconds.
 */
#define WHOLE_NOTELEN_1_BPM     240000

#define NOTELEN_DEFAULT         4
#define NOTELEN_MIN             1
#define NOTELEN_MAX             64
#define TEMPO_DEFAULT           120
#define TEMPO_MIN               32
#define TEMPO_MAX               255
#define OCTAVE_DEFAULT          4
#define SEMITONES_PER_OCTAVE    12

/* letter to half-tone:   A  B   C  D  E  F  G */
static int notetab[7] = { 9, 11, 0, 2, 4, 5, 7 };

/*
 * This is the American Standard A440 Equal-Tempered scale with frequencies
 * rounded to nearest integer. Thank Goddess for the good ol' CRC Handbook...
 * our octave 0 is standard octave 2.
 */
static int pitchtab[] =
{
/*        C     C#    D     D#    E     F     F#    G     G#    A     A#    B*/
/* 0 */   65,   69,   73,   78,   82,   87,   93,   98,  103,  110,  117,  123,
/* 1 */  131,  139,  147,  156,  165,  175,  185,  196,  208,  220,  233,  247,
/* 2 */  262,  277,  294,  311,  330,  349,  370,  392,  415,  440,  466,  494,
/* 3 */  523,  554,  587,  622,  659,  698,  740,  784,  831,  880,  932,  988,
/* 4 */ 1047, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1975,
/* 5 */ 2093, 2217, 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951,
/* 6 */ 4186, 4435, 4698, 4978, 5274, 5588, 5920, 6272, 6644, 7040, 7459, 7902,
};
#define NOCTAVES (nitems(pitchtab) / SEMITONES_PER_OCTAVE)

struct tones {
        SIMPLEQ_ENTRY(tones)    entries;
        tone_t                  tone;
};
SIMPLEQ_HEAD(, tones) head;

enum fillmode {
        FILL_LEGATO,    /* whole duration of the note */
        FILL_NORMAL,    /* 7/8th duration of the note */
        FILL_STACCATO,  /* 3/4th duration of the note */
};

static int verbose = 0;

/*
 * Calculate x / y, rounding away from zero if both are positive.
 */
static long
roundeddiv(long x, long y)
{
        return (x + y / 2) / y;
}

static void
addtone(tone_t t)
{
        static struct tones *last = NULL;
        struct tones *e;

        /*
         * Merge new tone into last one if the new one matches its frequency
         * and it won't overflow duration.
         */
        if (!SIMPLEQ_EMPTY(&head) && last->tone.frequency == t.frequency &&
            last->tone.duration <= INT_MAX - t.duration) {
                last->tone.duration += t.duration;
        } else {
                e = malloc(sizeof(*e));
                if (e == NULL)
                        err(1, NULL);
                e->tone = t;
                SIMPLEQ_INSERT_TAIL(&head, e, entries);
                last = e;
        }
}

static void
addsound(long pitch, long playlength, enum fillmode fill)
{
        tone_t t;
        long sound, silence;

        if (playlength <= 0 || playlength > INT_MAX)
                return;

        if (pitch == -1) {
                t.frequency = 0;
                t.duration = (int)playlength;
                addtone(t);
        } else if (pitch >= 0 && pitch < nitems(pitchtab)) {
                switch (fill) {
                case FILL_LEGATO:
                        sound = playlength;
                        break;
                case FILL_NORMAL:
                        sound = roundeddiv(playlength * 7, 8);
                        break;
                case FILL_STACCATO:
                        sound = roundeddiv(playlength * 3, 4);
                        break;
                }
                silence = playlength - sound;

                t.frequency = pitchtab[pitch];
                t.duration = sound;
                addtone(t);
                if (silence > 0) {
                        t.frequency = 0;
                        t.duration = silence;
                        addtone(t);
                }
        } else
                warnx("invalid pitch %ld", pitch);
}

static void
parsetones(const char *sp)
{
        char *endp;
        char visbuf[5];
        enum fillmode fill;
        long wholedur, num, denom, pitch, lastpitch, notelen, octave, v;
        int invalid_char, octave_track, octave_override;
        char c;

        fill = FILL_NORMAL;
        lastpitch = -1;
        notelen = NOTELEN_DEFAULT;
        octave = OCTAVE_DEFAULT;
        octave_override = 1;
        octave_track = 0; /* act as though there was an initial O(n) */
        wholedur = roundeddiv(WHOLE_NOTELEN_1_BPM, TEMPO_DEFAULT);

        for (;;) {
                while (*sp != '\0' && isspace(*sp))
                        sp++;
                if (*sp == '\0')
                        break;

                /*
                 * We use strtol() because we allow for partial convertions and
                 * endp is helpful for advancing sp. errno is ignored all in
                 * every call:
                 * - EINVAL can't be returned as we always use 10 as base
                 * - ERANGE can be returned but we don't care because we always
                 *   clamp the values
                 */

                invalid_char = 0;
                c = *sp++;
                switch (toupper(c)) {
                case 'A':
                case 'B':
                case 'C':
                case 'D':
                case 'E':
                case 'F':
                case 'G':
                        pitch = notetab[toupper(c) - 'A'] +
                            octave * SEMITONES_PER_OCTAVE;

                        if (*sp == '#' || *sp == '+') {
                                pitch++;
                                sp++;
                        } else if (*sp == '-') {
                                pitch--;
                                sp++;
                        }

                        /*
                         * If octave tracking is on and we haven't modified the
                         * octave with any of O(n), < or >, find the pitch
                         * closest to the previous pitch. This will make "bc"
                         * go higher in pitch, and "cb" go lower in pitch.
                         */
                        if (octave_track && !octave_override) {
                                if (labs(pitch - lastpitch) >
                                    labs(pitch + SEMITONES_PER_OCTAVE -
                                    lastpitch)) {
                                        octave++;
                                        pitch += SEMITONES_PER_OCTAVE;
                                }
                                if (labs(pitch - lastpitch) >
                                    labs(pitch - SEMITONES_PER_OCTAVE -
                                    lastpitch)) {
                                        octave--;
                                        pitch -= SEMITONES_PER_OCTAVE;
                                }
                        }
                        octave_override = 0;

                        if (isdigit(*sp)) {
                                v = strtol(sp, &endp, 10);
                                if (v < NOTELEN_MIN || v > NOTELEN_MAX)
                                        warnx("invalid note length: %ld", v);
                                v = CLAMP(v, NOTELEN_MIN, NOTELEN_MAX);
                                sp = endp;
                        } else
                                v = notelen;

                        num = wholedur;
                        denom = v;
                        for (; *sp == '.'; sp++) {
                                num *= 3;
                                denom *= 2;
                        }

                        addsound(pitch, roundeddiv(num, denom), fill);
                        lastpitch = pitch;
                        break;

                case 'O':
                        if (toupper(*sp) == 'L') {
                                octave_track = 1;
                                sp++;
                        } else if (toupper(*sp) == 'N') {
                                octave_track = 0;
                                octave_override = 0;
                                sp++;
                        } else if (isdigit(*sp)) {
                                v = strtol(sp, &endp, 10);
                                if (v < 0 || v > NOCTAVES)
                                        warnx("invalid octave: %ld", v);
                                octave = CLAMP(v, 0, NOCTAVES);
                                octave_override = 1;
                                sp = endp;
                        } else
                                goto invalid_input;
                        break;

                case '>':
                        v = octave + 1;
                        if (v > NOCTAVES)
                                warnx("invalid octave: %ld", v);
                        octave = MIN(v, NOCTAVES);
                        break;

                case '<':
                        v = octave - 1;
                        if (v < 0)
                                warnx("invalid octave: %ld", v);
                        octave = MAX(v, NOCTAVES);
                        break;

                case 'N':
                        if (!isdigit(*sp))
                                goto invalid_input;

                        v = strtol(sp, &endp, 10);
                        if (v < 0 || v > nitems(notetab))
                                warnx("invalid pitch: %ld", v);
                        pitch = CLAMP(v, 0, nitems(notetab));
                        sp = endp;

                        num = wholedur;
                        denom = notelen;
                        for (; *sp == '.'; sp++) {
                                num *= 3;
                                denom *= 2;
                        }

                        addsound(pitch - 1, roundeddiv(num, denom), fill);
                        /* N(n) does not overwrite lastpitch. */
                        break;

                case 'L':
                        v = strtol(sp, &endp, 10);
                        if (v < NOTELEN_MIN || v > NOTELEN_MAX)
                                warnx("invalid note length: %ld", v);
                        notelen = CLAMP(v, NOTELEN_MIN, NOTELEN_MAX);
                        sp = endp;
                        break;

                case 'P':
                case '~':
                        if (isdigit(*sp)) {
                                v = strtol(sp, &endp, 10);
                                if (v < NOTELEN_MIN || v > NOTELEN_MAX)
                                        warnx("invalid note length: %ld", v);
                                v = CLAMP(v, NOTELEN_MIN, NOTELEN_MAX);
                                sp = endp;
                        } else
                                v = notelen;

                        num = wholedur;
                        denom = notelen;
                        for (; *sp == '.'; sp++) {
                                num *= 3;
                                denom *= 2;
                        }

                        addsound(-1, roundeddiv(num, denom), fill);
                        break;

                case 'T':
                        v = strtol(sp, &endp, 10);
                        if (v < TEMPO_MIN || v > TEMPO_MAX)
                                warnx("invalid tempo: %ld", v);
                        v = CLAMP(v, TEMPO_MIN, TEMPO_MAX);
                        sp = endp;
                        wholedur = roundeddiv(WHOLE_NOTELEN_1_BPM, v);
                        break;

                case 'M':
                        if (toupper(*sp) == 'L') {
                                sp++;
                                fill = FILL_LEGATO;
                        } else if (toupper(*sp) == 'N') {
                                sp++;
                                fill = FILL_NORMAL;
                        } else if (toupper(*sp) == 'S') {
                                sp++;
                                fill = FILL_STACCATO;
                        } else
                                goto invalid_input;
                        break;

                default:
                        invalid_char = 1;
 invalid_input:
                        if (invalid_char) {
                                (void)vis(visbuf, c, VIS_WHITE, 0);
                                warnx("invalid command: %s", visbuf);
                        } else {
                                (void)vis(visbuf, *sp, VIS_WHITE, 0);
                                warnx("invalid argument for %c: %s", c,
                                    visbuf);
                        }
                }
        }
}

static __dead void
usage(void)
{
        fprintf(stderr, "Usage: %s [-v] tune\n", getprogname());
        exit(1);
}

int
main(int argc, char *argv[])
{
        tone_t *ts, *tsp;
        struct tones *e;
        size_t count;
        int ch, speaker_fd;

        while ((ch = getopt(argc, argv, "v")) != -1) {
                switch (ch) {
                case 'v':
                        verbose++;
                        break;
                default:
                        usage();
                }
        }
        argc -= optind;
        argv += optind;

        if (argc != 1)
                usage();

        if (unveil(_PATH_SPEAKER, "w") == -1)
                err(1, "unveil(" _PATH_SPEAKER ", \"w\")");
        if (unveil(NULL, NULL) == -1)
                err(1, "unveil(NULL, NULL)");

        SIMPLEQ_INIT(&head);
        parsetones(argv[0]);

        count = 0;
        SIMPLEQ_FOREACH(e, &head, entries)
                count++;
        if (count == 0)
                return 0;

        /* make room for ending { 0, 0 } */
        count++;

        ts = reallocarray(NULL, count, sizeof(*ts));
        if (ts == NULL)
                err(1, NULL);

        tsp = ts;
        while (!SIMPLEQ_EMPTY(&head)) {
                e = SIMPLEQ_FIRST(&head);
                *tsp++ = e->tone;
                SIMPLEQ_REMOVE_HEAD(&head, entries);
                free(e);
        }
        *tsp = (tone_t){ 0, 0 };

        if (verbose) {
                for (size_t i = 0; i < count; i++) {
                        if (ts[i].frequency == 0 && ts[i].duration == 0)
                                fprintf(stderr, "end\n");
                        else if (ts[i].frequency == 0)
                                fprintf(stderr, "rest for %dms\n",
                                    ts[i].duration);
                        else
                                fprintf(stderr, "%dHz for %dms\n",
                                    ts[i].frequency, ts[i].duration);
                }
        }

        speaker_fd = open(_PATH_SPEAKER, O_WRONLY);
        if (speaker_fd == -1)
                err(1, "open " _PATH_SPEAKER);
        if (ioctl(speaker_fd, SPKRTUNE, ts) == -1)
                err(1, "ioctl SPKRTUNE");
        (void)close(speaker_fd);

        free(ts);

        return 0;
}

Reply via email to