#!/bin/sh
# testsnd — play a tone out of /dev/dsp, so sound can be checked in one command.
#
#   testsnd                 440 Hz for 2 s (A above middle C)
#   testsnd 1000            1000 Hz for 2 s
#   testsnd 440 5           440 Hz for 5 s
#   testsnd sweep           200 -> 2000 Hz, 3 s
#   testsnd silence 2       2 s of silence — the CONTROL, see below
#
# WHY A SCRIPT AND NOT A .WAV. There is no audio player on a base install, and a
# stored sample cannot tell you WHICH part of the path is broken: a file that
# will not play might be missing, malformed, or unreadable. A tone synthesised
# here has no inputs but /dev/dsp itself, so a failure is the device or the
# driver and nothing else.
#
# THE FORMAT IS NEGOTIATED, AND THE FIRST VERSION OF THIS SCRIPT DID NOT DO
# THAT — the correction is the whole point of this header. It used to write
# 8000 Hz mono 8-bit and issue NO ioctl at all, on the stated reasoning that
# "fd.c resets /dev/dsp to 8000/1/8 on every open, so one less thing can be
# wrong". That reasoning was measured wrong on 2026-09-06 (commit 6348df4c):
# the Intel HDA backend has NO RESAMPLER. hda_pcm_configure() snaps every
# requested rate to the nearer of 44100/48000, programs THAT, and returns it.
# So 8 kHz samples were clocked out of the DAC at 44100 and the 440 Hz tone
# came out at about 2425 Hz — which is exactly the "harsh noise with crackle"
# an owner would report, and which this tool would then have blamed on the
# driver. 6348df4c's own commit message names testsnd as a victim of it; the
# sibling tool playmp3 was repaired in 633e9e9c and this is the same repair.
#
# Reporting the true rate did NOT fix the pitch, and that distinction matters:
# the kernel now tells the truth about what it programmed, and a program that
# writes 8 kHz anyway still plays fast. The adaptation has to happen HERE.
#
# THE LESSON, which generalises past audio: ASK THE DEVICE WHAT IT DID. Do not
# assume the value you requested was honoured. Every OSS ioctl below writes back
# the value actually programmed, and the samples are synthesised at that value.
#
# RUN THE SILENCE ARM IF YOU HEAR NOTHING. `testsnd silence 2` writes the same
# number of bytes, at the same rate, with every sample at the zero level. If the
# tone is inaudible AND silence takes the same time to write, the bytes are
# reaching the device and the fault is after the DAC — amplifier, mute, or the
# codec's output pin. If the tone is inaudible and silence returns instantly,
# the writes are not reaching the hardware at all. Those are different bugs and
# this is the cheapest way to tell them apart.
#
# A CLIP SHORTER THAN THE RING PROVES NOTHING about timing: the driver prefills
# 16384 bytes, so a write smaller than that returns immediately whatever the
# hardware does. Use 5 s or more when the question is "did it play in real
# time", not the 2 s default.
set -u

DSP=${DSP:-/dev/dsp}

usage() { sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'; exit 0; }

[ "${1:-}" = "-h" ] && usage
[ "${1:-}" = "--help" ] && usage

if [ ! -c "$DSP" ]; then
    echo "testsnd: $DSP is not a character device — no audio driver bound?" >&2
    echo "  check: dmesg | grep -i 'hda\\|alc\\|codec'" >&2
    exit 1
fi

MODE=tone
FREQ=440
SECS=2

case "${1:-}" in
    sweep)   MODE=sweep; SECS=${2:-3} ;;
    silence) MODE=silence; SECS=${2:-2} ;;
    "")      ;;
    *)       FREQ=$1; SECS=${2:-2} ;;
esac

# Reject nonsense early rather than writing garbage at the codec.
case "$FREQ$SECS" in *[!0-9]*) echo "testsnd: frequency and seconds must be whole numbers" >&2; exit 2 ;; esac
[ "$SECS" -gt 0 ] 2>/dev/null || { echo "testsnd: seconds must be positive" >&2; exit 2; }
[ "$SECS" -le 30 ] || { echo "testsnd: refusing more than 30 s" >&2; exit 2; }

# ---------------------------------------------------------------------------
# The negotiating path. python3 is used for exactly one reason: an ioctl cannot
# be issued from POSIX sh, and without an ioctl there is NO WAY to learn the
# rate the card actually programmed. It also holds the fd across the ioctls AND
# the write, which a shell redirect cannot — a `> /dev/dsp` redirect opens a
# FRESH fd that never saw the negotiation, so the settings would be discarded
# before the first sample. (Same reasoning as playmp3.)
# ---------------------------------------------------------------------------
if command -v python3 >/dev/null 2>&1; then
    DSP="$DSP" MODE="$MODE" FREQ="$FREQ" SECS="$SECS" python3 - <<'EOF'
import fcntl, math, os, struct, sys

DSP  = os.environ["DSP"]
MODE = os.environ["MODE"]
FREQ = int(os.environ["FREQ"])
SECS = int(os.environ["SECS"])

SNDCTL_DSP_RESET    = 0x00005000
SNDCTL_DSP_SPEED    = 0xc0045002
SNDCTL_DSP_SETFMT   = 0xc0045005
SNDCTL_DSP_CHANNELS = 0xc0045006
AFMT_U8             = 0x00000008
AFMT_S16_LE         = 0x00000010

def ioctl_int(fd, req, val):
    """Every one of these writes back what was ACTUALLY programmed."""
    buf = fcntl.ioctl(fd, req, struct.pack("i", val))
    return struct.unpack("i", buf)[0]

fd = os.open(DSP, os.O_WRONLY)
try:
    try:
        fcntl.ioctl(fd, SNDCTL_DSP_RESET)      # start from an empty ring
    except OSError:
        pass

    # Order matters: format, then channels, then rate — a backend may clamp the
    # rate differently per format, and the LAST answer is the one that counts.
    # 8-bit unsigned mono is asked for because it is the OSS default every
    # backend here supports (ES1370 and SB16 included, not only Intel HDA);
    # whatever comes back is what gets synthesised.
    fmt  = ioctl_int(fd, SNDCTL_DSP_SETFMT,   AFMT_U8)
    chan = ioctl_int(fd, SNDCTL_DSP_CHANNELS, 1)
    rate = ioctl_int(fd, SNDCTL_DSP_SPEED,    8000)

    if fmt not in (AFMT_U8, AFMT_S16_LE):
        print("testsnd: device returned an unknown format %d — refusing to guess"
              % fmt, file=sys.stderr)
        sys.exit(2)
    if rate <= 0 or chan <= 0:
        print("testsnd: device reported rate=%d channels=%d — nonsense, refusing"
              % (rate, chan), file=sys.stderr)
        sys.exit(2)

    fmtname = "u8" if fmt == AFMT_U8 else "s16le"
    what = {"sweep":   "sweeping 200 -> 2000 Hz",
            "silence": "SILENCE (the control arm, see 'testsnd -h')"}.get(
                MODE, "%d Hz" % FREQ)
    print("testsnd: %s for %ds on %s" % (what, SECS, DSP))
    print("testsnd: device programmed %d Hz, %d channel(s), %s "
          "(asked 8000/1/u8 — the DEVICE's answer is what is synthesised)"
          % (rate, chan, fmtname))
    if rate != 8000:
        print("testsnd: NOTE the card refused 8000 Hz. A tone written at 8000 "
              "would have played %.2fx fast (this is the #605 defect)."
              % (rate / 8000.0))

    n = rate * SECS
    two_pi = 2.0 * math.pi
    buf = bytearray()
    if fmt == AFMT_U8:
        zero, amp, pack = 128, 100, None
    else:
        zero, amp, pack = 0, 12000, None

    for i in range(n):
        if MODE == "silence":
            v = zero
        else:
            f = FREQ if MODE != "sweep" else 200.0 + (1800.0 * i / n)
            v = zero + int(amp * math.sin(two_pi * f * i / rate))
        if fmt == AFMT_U8:
            if v < 0:   v = 0
            if v > 255: v = 255
            sample = bytes((v,))
        else:
            if v < -32768: v = -32768
            if v >  32767: v =  32767
            sample = struct.pack("<h", v)
        buf += sample * chan          # the same sample to every channel

    os.write(fd, bytes(buf))
    bps = rate * chan * (1 if fmt == AFMT_U8 else 2)
    print("testsnd: wrote %d bytes = %.2f s at the PROGRAMMED format — the "
          "device accepted them." % (len(buf), len(buf) / float(bps)))
    print("  If you heard nothing, run 'testsnd silence %d' and compare how "
          "long it takes." % SECS)
finally:
    try:
        fcntl.ioctl(fd, SNDCTL_DSP_RESET)   # close(2) does not stop it pre-k1571
    except OSError:
        pass
    os.close(fd)
EOF
    exit $?
fi

# ---------------------------------------------------------------------------
# The FALLBACK, for a root with no python3 — the boot initramfs is one. It
# cannot negotiate, so it states plainly that the pitch is unverified rather
# than printing a number it has not checked. This is the old behaviour, kept
# only because a tool that refuses to run on the initramfs is worse than one
# that runs and says what it does not know.
# ---------------------------------------------------------------------------
RATE=8000
echo "testsnd: WARNING — no python3, so the format CANNOT be negotiated." >&2
echo "  Writing 8000 Hz mono 8-bit blind. On Intel HDA the card runs at" >&2
echo "  44100 or 48000 and does not resample, so the tone will play about" >&2
echo "  5.5x fast and the pitch below is NOT what you will hear. Timing and" >&2
echo "  the silence control arm are still valid; the pitch is not." >&2

case "$MODE" in
    sweep)   echo "testsnd: sweeping 200 -> 2000 Hz for ${SECS}s on $DSP (blind 8000 Hz, mono, 8-bit)" ;;
    silence) echo "testsnd: ${SECS}s of SILENCE on $DSP — the control arm, see 'testsnd -h'" ;;
    *)       echo "testsnd: nominally ${FREQ} Hz for ${SECS}s on $DSP (blind 8000 Hz, mono, 8-bit)" ;;
esac

# awk writes the samples. The amplitude is 100 either side of the 128 zero
# level: loud enough on a laptop speaker without clipping into the rails.
#
# LC_ALL=C IS LOAD-BEARING AND WAS MEASURED. Under a UTF-8 locale GNU awk's
# `printf "%c"` encodes any value above 127 as a MULTI-BYTE sequence, so a
# one-second tone came out as 12001 bytes instead of 8000 — every sample above
# the zero level doubled in size. The tone would have played at the wrong rate
# and distorted, and nothing would have said so: the write still succeeds and
# the device still accepts it. busybox awk emits one byte either way, so this
# only bites on a full userland, which is the harder case to notice.
LC_ALL=C awk -v rate="$RATE" -v freq="$FREQ" -v secs="$SECS" -v mode="$MODE" '
BEGIN {
    n = rate * secs
    pi = 3.14159265358979
    for (i = 0; i < n; i++) {
        if (mode == "silence") {
            v = 128
        } else {
            f = freq
            if (mode == "sweep") f = 200 + (1800 * i / n)
            v = int(128 + 100 * sin(2 * pi * f * i / rate))
            if (v < 0)   v = 0
            if (v > 255) v = 255
        }
        printf "%c", v
    }
}' > "$DSP" 2>/dev/null

rc=$?
if [ "$rc" -ne 0 ]; then
    echo "testsnd: writing to $DSP FAILED (exit $rc)" >&2
    echo "  a write that fails is a different fault from one that succeeds silently;" >&2
    echo "  try 'testsnd silence 2' and compare." >&2
    exit "$rc"
fi

echo "testsnd: wrote $((RATE * SECS)) bytes blind — the device accepted them."
echo "  If you heard nothing, run 'testsnd silence 2' and compare how long it takes."
