root/usr.bin/beep/beep.c
/*-
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * Copyright (c) 2021 Hans Petter Selasky <hselasky@freebsd.org>
 *
 * 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.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``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 OR CONTRIBUTORS 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.
 */

#include <sys/soundcard.h>

#include <capsicum_helpers.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <math.h>
#include <paths.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define SAMPLE_RATE_DEF 48000           /* hz */
#define SAMPLE_RATE_MAX 48000           /* hz */
#define SAMPLE_RATE_MIN 8000            /* hz */

#define DURATION_DEF 150                /* ms */
#define DURATION_MAX 2000               /* ms */
#define DURATION_MIN 50                 /* ms */

#define GAIN_DEF 75
#define GAIN_MAX 100
#define GAIN_MIN 0

#define WAVE_POWER 1.25f

#define DEFAULT_HZ 440

#define DEFAULT_DEVICE _PATH_DEV "dsp"

static int frequency = DEFAULT_HZ;
static int duration_ms = DURATION_DEF;
static int sample_rate = SAMPLE_RATE_DEF;
static int gain = GAIN_DEF;
static const char *oss_dev = DEFAULT_DEVICE;
static bool background;

/*
 * wave_function_16
 *
 * "phase" should be in the range [0.0f .. 1.0f>
 * "power" should be in the range <0.0f .. 2.0f>
 *
 * The return value is in the range [-1.0f .. 1.0f]
 */
static float
wave_function_16(float phase, float power)
{
        uint16_t x = phase * (1U << 16);
        float retval;
        uint8_t num;

        /* Handle special cases, if any */
        switch (x) {
        case 0xffff:
        case 0x0000:
                return (1.0f);
        case 0x3fff:
        case 0x4000:
        case 0xBfff:
        case 0xC000:
                return (0.0f);
        case 0x7FFF:
        case 0x8000:
                return (-1.0f);
        default:
                break;
        }

        /* Apply Gray coding */
        for (uint16_t mask = 1U << 15; mask != 1; mask /= 2) {
                if (x & mask)
                        x ^= (mask - 1);
        }

        /* Find first set bit */
        for (num = 0; num != 14; num++) {
                if (x & (1U << num)) {
                        num++;
                        break;
                }
        }

        /* Initialize return value */
        retval = 0.0;

        /* Compute the rest of the power series */
        for (; num != 14; num++) {
                if (x & (1U << num)) {
                        retval = (1.0f - retval) / 2.0f;
                        retval = powf(retval, power);
                } else {
                        retval = (1.0f + retval) / 2.0f;
                        retval = powf(retval, power);
                }
        }

        /* Check if halfway */
        if (x & (1ULL << 14))
                retval = -retval;

        return (retval);
}

static void
usage(void)
{
        fprintf(stderr, "Usage: %s [-Bh] [-D duration_ms] [-F frequency_hz] "
            "[-d oss_device] [-g gain] [-r sample_rate_hz]\n"
            "\t" "-B Run in background\n"
            "\t" "-D <duration in ms, from %d ms to %d ms, default %d ms>\n"
            "\t" "-F <frequency in Hz, default %d Hz>\n"
            "\t" "-d <OSS device, default %s>\n"
            "\t" "-g <gain from %d to %d, default %d>\n"
            "\t" "-h Show usage\n"
            "\t" "-r <sample rate in Hz, from %d Hz to %d Hz, default %d Hz>\n",
            getprogname(),
            DURATION_MIN, DURATION_MAX, DURATION_DEF,
            DEFAULT_HZ,
            DEFAULT_DEVICE,
            GAIN_MIN, GAIN_MAX, GAIN_DEF,
            SAMPLE_RATE_MIN, SAMPLE_RATE_MAX, SAMPLE_RATE_DEF);
        exit(1);
}

int
main(int argc, char **argv)
{
        float *buffer;
        size_t slope;
        size_t size;
        size_t off;
        float a;
        float d;
        float p;
        int c;
        int f;

        while ((c = getopt(argc, argv, "BF:D:r:g:d:h")) != -1) {
                switch (c) {
                case 'F':
                        frequency = strtol(optarg, NULL, 10);
                        break;
                case 'D':
                        duration_ms = strtol(optarg, NULL, 10);
                        if (duration_ms < DURATION_MIN ||
                            duration_ms > DURATION_MAX)
                                usage();
                        break;
                case 'r':
                        sample_rate = strtol(optarg, NULL, 10);
                        if (sample_rate < SAMPLE_RATE_MIN ||
                            sample_rate > SAMPLE_RATE_MAX)
                                usage();
                        break;
                case 'g':
                        gain = strtol(optarg, NULL, 10);
                        if (gain < GAIN_MIN ||
                            gain > GAIN_MAX)
                                usage();
                        break;
                case 'd':
                        oss_dev = optarg;
                        break;
                case 'B':
                        background = true;
                        break;
                default:
                        usage();
                        break;
                }
        }

        if (background && daemon(0, 0) != 0)
                errx(1, "daemon(0,0) failed");

        f = open(oss_dev, O_WRONLY);
        if (f < 0)
                err(1, "Failed to open '%s'", oss_dev);

        if (caph_enter() == -1)
                err(1, "Failed to enter capability mode");

        c = 1;                          /* mono */
        if (ioctl(f, SOUND_PCM_WRITE_CHANNELS, &c) != 0)
                errx(1, "ioctl SOUND_PCM_WRITE_CHANNELS(1) failed");

        c = AFMT_FLOAT;
        if (ioctl(f, SNDCTL_DSP_SETFMT, &c) != 0)
                errx(1, "ioctl SNDCTL_DSP_SETFMT(AFMT_FLOAT) failed");

        if (ioctl(f, SNDCTL_DSP_SPEED, &sample_rate) != 0)
                errx(1, "ioctl SNDCTL_DSP_SPEED(%d) failed", sample_rate);

        c = (2 << 16);
        while ((1ULL << (c & 63)) < (size_t)(4 * sample_rate / 50))
                c++;
        if (ioctl(f, SNDCTL_DSP_SETFRAGMENT, &c))
                errx(1, "ioctl SNDCTL_DSP_SETFRAGMENT(0x%x) failed", c);

        if (ioctl(f, SNDCTL_DSP_GETODELAY, &c) != 0)
                errx(1, "ioctl SNDCTL_DSP_GETODELAY failed");

        size = ((sample_rate * duration_ms) + 999) / 1000;
        buffer = malloc(sizeof(buffer[0]) * size);
        if (buffer == NULL)
                errx(1, "out of memory");

        /* compute slope duration in samples */
        slope = (DURATION_MIN * sample_rate) / 2000;

        /* compute base gain */
        a = powf(65536.0f, (float)gain / (float)GAIN_MAX) / 65536.0f;

        /* set initial phase and delta */
        p = 0;
        d = (float)frequency / (float)sample_rate;

        /* compute wave */
        for (p = off = 0; off != size; off++, p += d) {
                float sample;

                p = p - floorf(p);
                sample = a * wave_function_16(p, WAVE_POWER);

                if (off < slope)
                        sample = sample * off / (float)slope;
                else if (off > (size - slope))
                        sample = sample * (size - off - 1) / (float)slope;

                buffer[off] = sample;
        }

        if (write(f, buffer, size * sizeof(buffer[0])) !=
            (ssize_t)(size * sizeof(buffer[0])))
                errx(1, "failed writing to DSP device(%s)", oss_dev);

        free(buffer);

        /* wait for data to be written */
        while (ioctl(f, SNDCTL_DSP_GETODELAY, &c) == 0) {
                if (c == 0)
                        break;
                usleep(10000);
        }

        /* wait for audio to go out */
        usleep(50000);
        close(f);

        return (0);
}