From 322778f211a58656e4250aaea17fdef230597273 Mon Sep 17 00:00:00 2001
From: rim <rozhuk.im@gmail.com>
Date: Tue, 24 Nov 2020 04:22:40 +0300
Subject: [PATCH] ao_oss: add this audio output again

Changes:
- code refactored;
- mixer options removed;
- new mpv sound API used;
- add sound devices detect (mpv --audio-device=help will show all available devices);
- only OSSv4 supported now;

Tested on FreeBSD 12.2 amd64.
---
 DOCS/man/ao.rst    |   5 +-
 audio/out/ao.c     |   4 +
 audio/out/ao_oss.c | 397 +++++++++++++++++++++++++++++++++++++++++++++
 wscript            |   5 +
 wscript_build.py   |   1 +
 5 files changed, 411 insertions(+), 1 deletion(-)
 create mode 100644 audio/out/ao_oss.c

diff --git a/DOCS/man/ao.rst b/DOCS/man/ao.rst
index cc42ec23a6..f39b2d3b23 100644
--- a/DOCS/man/ao.rst
+++ b/DOCS/man/ao.rst
@@ -14,7 +14,7 @@ in the list.
 
     See ``--ao=help`` for a list of compiled-in audio output drivers. The
     driver ``--ao=alsa`` is preferred. ``--ao=pulse`` is preferred on systems
-    where PulseAudio is used.
+    where PulseAudio is used. On BSD systems, ``--ao=oss`` is preferred.
 
 Available audio output drivers are:
 
@@ -35,6 +35,9 @@ Available audio output drivers are:
         with automatic upmixing with shared access, so playing stereo
         and multichannel audio at the same time will work as expected.
 
+``oss``
+    OSS audio output driver
+
 ``jack``
     JACK (Jack Audio Connection Kit) audio output driver.
 
diff --git a/audio/out/ao.c b/audio/out/ao.c
index 52a38b63be..7c347cb138 100644
--- a/audio/out/ao.c
+++ b/audio/out/ao.c
@@ -35,6 +35,7 @@
 #include "common/common.h"
 #include "common/global.h"
 
+extern const struct ao_driver audio_out_oss;
 extern const struct ao_driver audio_out_audiotrack;
 extern const struct ao_driver audio_out_audiounit;
 extern const struct ao_driver audio_out_coreaudio;
@@ -70,6 +71,9 @@ static const struct ao_driver * const audio_out_drivers[] = {
 #endif
 #if HAVE_WASAPI
     &audio_out_wasapi,
+#endif
+#if HAVE_OSS_AUDIO
+    &audio_out_oss,
 #endif
     // wrappers:
 #if HAVE_JACK
diff --git a/audio/out/ao_oss.c b/audio/out/ao_oss.c
new file mode 100644
index 0000000000..51ee65ff90
--- /dev/null
+++ b/audio/out/ao_oss.c
@@ -0,0 +1,397 @@
+/*
+ * OSS audio output driver
+ *
+ * Original author: A'rpi
+ * Support for >2 output channels added 2001-11-25
+ * - Steve Davies <steve@daviesfam.org>
+ * Rozhuk Ivan <rozhuk.im@gmail.com> 2020
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with mpv.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include <sys/ioctl.h>
+#include <sys/soundcard.h>
+#include <sys/stat.h>
+#if defined(__DragonFly__) || defined(__FreeBSD__)
+#include <sys/sysctl.h>
+#endif
+#include <sys/types.h>
+
+#include "config.h"
+#include "audio/format.h"
+#include "common/msg.h"
+#include "options/options.h"
+#include "osdep/endian.h"
+#include "osdep/io.h"
+#include "ao.h"
+#include "internal.h"
+
+#ifndef AFMT_AC3
+#define AFMT_AC3 -1
+#endif
+
+#define PATH_DEV_DSP "/dev/dsp"
+
+struct priv {
+    int dsp_fd;
+    bool playing;
+    double bps; /* Bytes per second. */
+};
+
+/* like alsa except for 6.1 and 7.1, from pcm/matrix_map.h */
+static const struct mp_chmap oss_layouts[MP_NUM_CHANNELS + 1] = {
+    {0},                                        /* empty */
+    MP_CHMAP_INIT_MONO,                         /* mono */
+    MP_CHMAP2(FL, FR),                          /* stereo */
+    MP_CHMAP3(FL, FR, LFE),                     /* 2.1 */
+    MP_CHMAP4(FL, FR, BL, BR),                  /* 4.0 */
+    MP_CHMAP5(FL, FR, BL, BR, FC),              /* 5.0 */
+    MP_CHMAP6(FL, FR, BL, BR, FC, LFE),         /* 5.1 */
+    MP_CHMAP7(FL, FR, BL, BR, FC, LFE, BC),     /* 6.1 */
+    MP_CHMAP8(FL, FR, BL, BR, FC, LFE, SL, SR), /* 7.1 */
+};
+
+#if !defined(AFMT_S32_NE) && defined(AFMT_S32_LE) && defined(AFMT_S32_BE)
+#define AFMT_S32_NE AFMT_S32MP_SELECT_LE_BE(AFMT_S32_LE, AFMT_S32_BE)
+#endif
+
+static const int format_table[][2] = {
+    {AFMT_U8,           AF_FORMAT_U8},
+    {AFMT_S16_NE,       AF_FORMAT_S16},
+#ifdef AFMT_S32_NE
+    {AFMT_S32_NE,       AF_FORMAT_S32},
+#endif
+#ifdef AFMT_FLOAT
+    {AFMT_FLOAT,        AF_FORMAT_FLOAT},
+#endif
+#ifdef AFMT_MPEG
+    {AFMT_MPEG,         AF_FORMAT_S_MP3},
+#endif
+    {-1, -1}
+};
+
+#define MP_WARN_IOCTL_ERR(__ao) \
+    MP_WARN((__ao), "%s: ioctl() fail, err = %i: %s\n", \
+        __FUNCTION__, errno, strerror(errno))
+
+
+static void uninit(struct ao *ao);
+
+
+static void device_descr_get(size_t dev_idx, char *buf, size_t buf_size)
+{
+#if defined(__DragonFly__) || defined(__FreeBSD__)
+    char dev_path[32];
+    size_t tmp = (buf_size - 1);
+
+    snprintf(dev_path, sizeof(dev_path), "dev.pcm.%zu.%%desc", dev_idx);
+    if (sysctlbyname(dev_path, buf, &tmp, NULL, 0) != 0) {
+        tmp = 0;
+    }
+    buf[tmp] = 0x00;
+#else
+    buf[0] = 0x00;
+#endif
+}
+
+static int format2oss(int format)
+{
+    for (size_t i = 0; format_table[i][0] != -1; i++) {
+        if (format_table[i][1] == format)
+            return format_table[i][0];
+    }
+    return -1;
+}
+
+static bool try_format(struct ao *ao, int *format)
+{
+    struct priv *p = ao->priv;
+    int oss_format = format2oss(*format);
+
+    if (oss_format == -1 && af_fmt_is_spdif(*format))
+        oss_format = AFMT_AC3;
+
+    if (oss_format == -1) {
+        MP_VERBOSE(ao, "Unknown/not supported internal format: %s\n",
+            af_fmt_to_str(*format));
+        *format = 0;
+        return false;
+    }
+
+    return (ioctl(p->dsp_fd, SNDCTL_DSP_SETFMT, &oss_format) != -1);
+}
+
+static int init(struct ao *ao)
+{
+    struct priv *p = ao->priv;
+    struct mp_chmap channels = ao->channels;
+    audio_buf_info info;
+    size_t i;
+    int format, samplerate, nchannels, reqchannels, trig = 0;
+    int best_sample_formats[AF_FORMAT_COUNT + 1];
+    const char *device = ((ao->device) ? ao->device : PATH_DEV_DSP);
+
+    /* Opening device. */
+    MP_VERBOSE(ao, "Using '%s' audio device.\n", device);
+    p->dsp_fd = open(device, (O_WRONLY | O_CLOEXEC));
+    if (p->dsp_fd < 0) {
+        MP_ERR(ao, "Can't open audio device %s: %s.\n",
+            device, mp_strerror(errno));
+        goto err_out;
+    }
+
+    /* Selecting sound format. */
+    format = af_fmt_from_planar(ao->format);
+    af_get_best_sample_formats(format, best_sample_formats);
+    for (i = 0; best_sample_formats[i]; i++) {
+        format = best_sample_formats[i];
+        if (try_format(ao, &format))
+            break;
+    }
+    if (!format) {
+        MP_ERR(ao, "Can't set sample format.\n");
+        goto err_out;
+    }
+    MP_VERBOSE(ao, "Sample format: %s\n", af_fmt_to_str(format));
+
+    /* Channels count. */
+    if (af_fmt_is_spdif(format)) {
+        /* Probably could be fixed by setting number of channels;
+         * needs testing. */
+        if (channels.num != 2) {
+            MP_ERR(ao, "Format %s not implemented.\n", af_fmt_to_str(format));
+            goto err_out;
+        }
+    } else {
+        struct mp_chmap_sel sel = {0};
+        for (i = 0; i < MP_ARRAY_SIZE(oss_layouts); i++) {
+            mp_chmap_sel_add_map(&sel, &oss_layouts[i]);
+        }
+        if (!ao_chmap_sel_adjust(ao, &sel, &channels))
+            goto err_out;
+        nchannels = reqchannels = channels.num;
+        if (ioctl(p->dsp_fd, SNDCTL_DSP_CHANNELS, &nchannels) == -1) {
+            MP_ERR(ao, "Failed to set audio device to %d channels.\n",
+                reqchannels);
+            goto err_out_ioctl;
+        }
+        if (nchannels != reqchannels) {
+            /* Update number of channels to OSS suggested value. */
+            if (!ao_chmap_sel_get_def(ao, &sel, &channels, nchannels))
+                goto err_out;
+        }
+        MP_VERBOSE(ao, "Using %d channels (requested: %d).\n",
+            channels.num, reqchannels);
+    }
+
+    /* Sample rate. */
+    samplerate = ao->samplerate;
+    if (ioctl(p->dsp_fd, SNDCTL_DSP_SPEED, &samplerate) == -1)
+        goto err_out_ioctl;
+    MP_VERBOSE(ao, "Using %d Hz samplerate.\n", samplerate);
+
+    /* Get buffer size. */
+    if (ioctl(p->dsp_fd, SNDCTL_DSP_GETOSPACE, &info) == -1)
+        goto err_out_ioctl;
+    /* See ao.c ao->sstride initializations and get_state(). */
+    ao->device_buffer = ((info.fragstotal * info.fragsize) /
+        af_fmt_to_bytes(format));
+    if (!af_fmt_is_planar(format)) {
+        ao->device_buffer /= channels.num;
+    }
+
+    /* Do not start playback after data written. */
+    if (ioctl(p->dsp_fd, SNDCTL_DSP_SETTRIGGER, &trig) == -1)
+        goto err_out_ioctl;
+
+    /* Update sound params. */
+    ao->format = format;
+    ao->samplerate = samplerate;
+    ao->channels = channels;
+    p->bps = (channels.num * samplerate * af_fmt_to_bytes(format));
+    p->playing = false;
+
+    return 0;
+
+err_out_ioctl:
+    MP_WARN_IOCTL_ERR(ao);
+err_out:
+    uninit(ao);
+    return -1;
+}
+
+static void uninit(struct ao *ao)
+{
+    struct priv *p = ao->priv;
+
+    if (p->dsp_fd == -1)
+        return;
+    ioctl(p->dsp_fd, SNDCTL_DSP_HALT, NULL);
+    close(p->dsp_fd);
+    p->dsp_fd = -1;
+    p->playing = false;
+}
+
+static int control(struct ao *ao, enum aocontrol cmd, void *arg)
+{
+    struct priv *p = ao->priv;
+    ao_control_vol_t *vol = (ao_control_vol_t *)arg;
+    int v;
+
+    if (p->dsp_fd < 0)
+        return CONTROL_ERROR;
+
+    switch (cmd) {
+    case AOCONTROL_GET_VOLUME:
+        if (ioctl(p->dsp_fd, SNDCTL_DSP_GETPLAYVOL, &v) == -1) {
+            MP_WARN_IOCTL_ERR(ao);
+            return CONTROL_ERROR;
+        }
+        vol->right = ((v & 0xff00) >> 8);
+        vol->left = (v & 0x00ff);
+        return CONTROL_OK;
+    case AOCONTROL_SET_VOLUME:
+        v = ((int)vol->right << 8) | (int)vol->left;
+        if (ioctl(p->dsp_fd, SNDCTL_DSP_SETPLAYVOL, &v) == -1) {
+            MP_WARN_IOCTL_ERR(ao);
+            return CONTROL_ERROR;
+        }
+        return CONTROL_OK;
+    }
+
+    return CONTROL_UNKNOWN;
+}
+
+static void reset(struct ao *ao)
+{
+    struct priv *p = ao->priv;
+    int trig = 0;
+
+    /* Clear buf and do not start playback after data written. */
+    p->playing = false;
+    if (ioctl(p->dsp_fd, SNDCTL_DSP_HALT, NULL) == -1 ||
+        ioctl(p->dsp_fd, SNDCTL_DSP_SETTRIGGER, &trig) == -1)
+    {
+        MP_WARN_IOCTL_ERR(ao);
+        MP_WARN(ao, "Force reinitialize audio device.\n");
+        uninit(ao);
+        init(ao);
+    }
+}
+
+static void start(struct ao *ao)
+{
+    struct priv *p = ao->priv;
+    int trig = PCM_ENABLE_OUTPUT;
+
+    if (ioctl(p->dsp_fd, SNDCTL_DSP_SETTRIGGER, &trig) == -1) {
+        MP_WARN_IOCTL_ERR(ao);
+        return;
+    }
+    p->playing = true;
+}
+
+static bool audio_write(struct ao *ao, void **data, int samples)
+{
+    struct priv *p = ao->priv;
+    ssize_t rc;
+    const size_t size = (samples * ao->sstride);
+
+    if (size == 0)
+        return true;
+
+    while ((rc = write(p->dsp_fd, data[0], size)) == -1) {
+        if (errno == EINTR)
+			continue;
+        MP_WARN(ao, "audio_write: write() fail, err = %i: %s.\n",
+            errno, strerror(errno));
+        p->playing = false;
+        return false;
+    }
+    if ((size_t)rc != size) {
+        MP_WARN(ao, "audio_write: unexpected partial write: required: %zu, written: %zu.\n",
+            size, (size_t)rc);
+        p->playing = false;
+        return false;
+    }
+
+    return true;
+}
+
+static void get_state(struct ao *ao, struct mp_pcm_state *state)
+{
+    struct priv *p = ao->priv;
+    audio_buf_info info;
+    int odelay;
+
+    if (ioctl(p->dsp_fd, SNDCTL_DSP_GETOSPACE, &info) == -1 ||
+        ioctl(p->dsp_fd, SNDCTL_DSP_GETODELAY, &odelay) == -1)
+    {
+        MP_WARN_IOCTL_ERR(ao);
+        p->playing = false;
+        memset(state, 0x00, sizeof(struct mp_pcm_state));
+        state->delay = 0.0;
+        return;
+    }
+    state->free_samples = (info.bytes / ao->sstride);
+    state->queued_samples = (ao->device_buffer - state->free_samples);
+    state->delay = (odelay / p->bps);
+    state->playing = p->playing;
+}
+
+static void list_devs(struct ao *ao, struct ao_device_list *list)
+{
+    struct stat st;
+    char dev_path[32] = PATH_DEV_DSP, dev_descr[256] = "Default";
+    struct ao_device_desc dev = {.name = dev_path, .desc = dev_descr};
+
+    if (stat(PATH_DEV_DSP, &st) == 0) {
+        ao_device_list_add(list, ao, &dev);
+    }
+
+    /* Auto detect. */
+    for (size_t i = 0, fail_cnt = 0; fail_cnt < 8; i ++, fail_cnt ++) {
+        snprintf(dev_path, sizeof(dev_path), PATH_DEV_DSP"%zu", i);
+        if (stat(dev_path, &st) != 0)
+            continue;
+        device_descr_get(i, dev_descr, sizeof(dev_descr));
+        ao_device_list_add(list, ao, &dev);
+        fail_cnt = 0; /* Reset fail counter. */
+    }
+}
+
+const struct ao_driver audio_out_oss = {
+    .name      = "oss",
+    .description = "OSS/ioctl audio output",
+    .init      = init,
+    .uninit    = uninit,
+    .control   = control,
+    .reset     = reset,
+    .start     = start,
+    .write     = audio_write,
+    .get_state = get_state,
+    .list_devs = list_devs,
+    .priv_size = sizeof(struct priv),
+    .priv_defaults = &(const struct priv) {
+        .dsp_fd = -1,
+        .playing = false,
+    },
+};
diff --git a/wscript b/wscript
index b81f1202ec..b653e3fa36 100644
--- a/wscript
+++ b/wscript
@@ -421,6 +421,11 @@ audio_output_features = [
         'desc': 'SDL2 audio output',
         'deps': 'sdl2',
         'func': check_true,
+    }, {
+        'name': '--oss-audio',
+        'desc': 'OSSv4 audio output',
+        'func': check_statement(['sys/soundcard.h'], 'int x = SNDCTL_DSP_SETPLAYVOL'),
+        'deps': 'posix && gpl',
     }, {
         'name': '--pulse',
         'desc': 'PulseAudio audio output',
diff --git a/wscript_build.py b/wscript_build.py
index 14c254e1ec..a12cbf19e1 100644
--- a/wscript_build.py
+++ b/wscript_build.py
@@ -244,6 +244,7 @@ def swift(task):
         ( "audio/out/ao_null.c" ),
         ( "audio/out/ao_openal.c",               "openal" ),
         ( "audio/out/ao_opensles.c",             "opensles" ),
+        ( "audio/out/ao_oss.c",                  "oss-audio" ),
         ( "audio/out/ao_pcm.c" ),
         ( "audio/out/ao_pulse.c",                "pulse" ),
         ( "audio/out/ao_sdl.c",                  "sdl2-audio" ),
