From: Helge Deller <deller@kernel.org>
To: qemu-devel@nongnu.org
Cc: Richard Henderson <richard.henderson@linaro.org>,
Keith Monahan <keith@techtravels.org>,
Gerd Hoffmann <kraxel@redhat.com>, Helge Deller <deller@gmx.de>
Subject: [PATCH 1/4] hw/audio: Add HP Harmony audio (LASI audio function, CS4215/AD1849 codec)
Date: Wed, 28 Oct 2026 23:48:12 +0100 [thread overview]
Message-ID: <20261028224815.32502-2-deller@kernel.org> (raw)
In-Reply-To: <20261028224815.32502-1-deller@kernel.org>
From: Keith Monahan <keith@techtravels.org>
Found in the HP 712, HP 715/64-100, HP 725 and B132L-B180L workstations.
Register layout per the "712 I/O Subsystem ERS". Playback and record
each have a pair of 4 KiB page registers (next/current). While the codec
is out of reset a queued next page moves to current as soon as the
current one is done, leaving the "next empty" status bit (PN/RN) set;
DSTATUS.IE only gates that onto the interrupt line. A direction with no
queued page idles.
Both directions run off the codec's single sample clock.
Signed-off-by: Keith Monahan <keith@techtravels.org
Signed-off-by: Helge Deller <deller@gmx.de>
---
hw/audio/harmony.c | 892 +++++++++++++++++++++++++++++++++++++
hw/audio/trace-events | 10 +
include/hw/audio/harmony.h | 76 ++++
3 files changed, 978 insertions(+)
create mode 100644 hw/audio/harmony.c
create mode 100644 include/hw/audio/harmony.h
diff --git a/hw/audio/harmony.c b/hw/audio/harmony.c
new file mode 100644
index 0000000000..f25d7197d5
--- /dev/null
+++ b/hw/audio/harmony.c
@@ -0,0 +1,892 @@
+/*
+ * HP Harmony audio (LASI audio function, CS4215/AD1849 codec)
+ *
+ * Found in the 712, 715/64-100, 725 and B132L-B180L. Register layout per
+ * the "712 I/O Subsystem ERS". Playback and record each have a pair of
+ * 4 KiB page registers (next/current). While the codec is out of reset a
+ * queued next page moves to current as soon as the current one is done,
+ * leaving the "next empty" status bit (PN/RN) set; DSTATUS.IE only gates
+ * that onto the interrupt line. A direction with no queued page idles.
+ * Both directions run off the codec's single sample clock.
+ *
+ * Copyright (c) 2026 Keith Monahan <keith@techtravels.org>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include <math.h>
+#include "qemu/bswap.h"
+#include "qemu/log.h"
+#include "qapi/error.h"
+#include "hw/core/irq.h"
+#include "hw/core/qdev-properties.h"
+#include "hw/audio/harmony.h"
+#include "migration/vmstate.h"
+#include "system/address-spaces.h"
+#include "system/dma.h"
+#include "trace.h"
+
+#define HARMONY_ID 0x00
+#define HARMONY_RESET 0x04
+#define HARMONY_CNTL 0x08
+#define HARMONY_GAINCTL 0x0c
+#define HARMONY_PNXTADD 0x10
+#define HARMONY_PCURADD 0x14
+#define HARMONY_RNXTADD 0x18
+#define HARMONY_RCURADD 0x1c
+#define HARMONY_DSTATUS 0x20
+#define HARMONY_OV 0x24
+#define HARMONY_PIO 0x28
+#define HARMONY_DIAG 0x3c
+
+#define RESET_RST 0x00000001
+
+#define CNTL_C 0x80000000
+#define CNTL_CODEC_REV_SHIFT 20
+#define CNTL_CODEC_REV_MASK 0x0ff00000
+#define CNTL_FORMAT_SHIFT 6
+#define CNTL_FORMAT_MASK 0x000000c0
+#define CNTL_STEREO 0x00000020
+#define CNTL_RATE_MASK 0x0000001f
+
+#define FMT_S16BE 0
+#define FMT_ULAW 1
+#define FMT_ALAW 2
+#define FMT_U8 3
+
+#define GAINCTL_HE 0x08000000
+#define GAINCTL_LE 0x04000000
+#define GAINCTL_SE 0x02000000
+#define GAINCTL_IS 0x01000000
+#define GAINCTL_IN_R_SHIFT 16
+#define GAINCTL_IN_L_SHIFT 12
+#define GAINCTL_OUT_L_SHIFT 6
+#define GAINCTL_OUT_R_SHIFT 0
+
+#define DSTATUS_IE 0x80000000
+#define DSTATUS_PN 0x00000200
+#define DSTATUS_PC 0x00000100
+#define DSTATUS_RN 0x00000002
+#define DSTATUS_RC 0x00000001
+
+#define OV_OV 0x00000001
+#define PIO_PO 0x00000002
+#define DIAG_CO 0x00000001
+
+/* audio ticks with nothing to transfer before the host voices are stopped */
+#define HARMONY_IDLE_TICKS 5
+
+/* CNTL rate field to sample rate; 0 = reserved encoding */
+static const int harmony_rates[32] = {
+ [0x08] = 8000, [0x09] = 16000, [0x0a] = 27429, [0x0b] = 32000,
+ [0x0e] = 48000, [0x0f] = 9600, [0x10] = 5513, [0x11] = 11025,
+ [0x12] = 18900, [0x13] = 22050, [0x14] = 37800, [0x15] = 44100,
+ [0x16] = 33075, [0x17] = 6615,
+};
+
+/* Q12 input gain, 1.5 dB steps */
+static int32_t harmony_in_gain[16];
+
+static void harmony_init_tables(void)
+{
+ int i;
+
+ for (i = 0; i < 16; i++) {
+ harmony_in_gain[i] = lrint(4096.0 * pow(10.0, 1.5 * i / 20.0));
+ }
+}
+
+/* G.711 */
+static int16_t ulaw_to_s16(uint8_t u)
+{
+ int t;
+
+ u = ~u;
+ t = ((u & 0x0f) << 3) + 0x84;
+ t <<= (u & 0x70) >> 4;
+ return (u & 0x80) ? (0x84 - t) : (t - 0x84);
+}
+
+static int16_t alaw_to_s16(uint8_t a)
+{
+ int t, seg;
+
+ a ^= 0x55;
+ t = (a & 0x0f) << 4;
+ seg = (a & 0x70) >> 4;
+ if (seg == 0) {
+ t += 8;
+ } else {
+ t = (t + 0x108) << (seg - 1);
+ }
+ return (a & 0x80) ? t : -t;
+}
+
+/* smallest segment whose end ((base << seg) - 1) is >= val */
+static int g711_segment(int val, int base)
+{
+ int seg;
+
+ for (seg = 0; seg < 8; seg++) {
+ if (val < (base << seg)) {
+ break;
+ }
+ }
+ return seg;
+}
+
+static uint8_t s16_to_ulaw(int16_t s)
+{
+ int val = s >> 2, mask, seg;
+
+ if (val < 0) {
+ val = -val;
+ mask = 0x7f;
+ } else {
+ mask = 0xff;
+ }
+ val = MIN(val, 8159) + 33;
+ seg = g711_segment(val, 0x40);
+ if (seg >= 8) {
+ return 0x7f ^ mask;
+ }
+ return ((seg << 4) | ((val >> (seg + 1)) & 0x0f)) ^ mask;
+}
+
+static uint8_t s16_to_alaw(int16_t s)
+{
+ int val = s >> 3, mask, seg;
+ uint8_t aval;
+
+ if (val >= 0) {
+ mask = 0xd5;
+ } else {
+ mask = 0x55;
+ val = -val - 1;
+ }
+ seg = g711_segment(val, 0x20);
+ if (seg >= 8) {
+ return 0x7f ^ mask;
+ }
+ aval = seg << 4;
+ aval |= (val >> (seg < 2 ? 1 : seg)) & 0x0f;
+ return aval ^ mask;
+}
+
+static int harmony_format(HarmonyState *s)
+{
+ return (s->cntl & CNTL_FORMAT_MASK) >> CNTL_FORMAT_SHIFT;
+}
+
+static int harmony_channels(HarmonyState *s)
+{
+ return (s->cntl & CNTL_STEREO) ? 2 : 1;
+}
+
+/* guest bytes per frame */
+static int harmony_bpf(HarmonyState *s)
+{
+ return (harmony_format(s) == FMT_S16BE ? 2 : 1) * harmony_channels(s);
+}
+
+static int harmony_freq(HarmonyState *s)
+{
+ int freq = harmony_rates[s->cntl & CNTL_RATE_MASK];
+
+ return freq ? freq : 8000;
+}
+
+/*
+ * DMA does not stop when IE is cleared: HP-UX stops a stream by leaving
+ * pages queued with IE clear, and on the next open expects to find both
+ * next registers drained (PN and RN set) again.
+ */
+static bool harmony_running(HarmonyState *s)
+{
+ return !(s->reset & RESET_RST);
+}
+
+static void harmony_update_irq(HarmonyState *s)
+{
+ int level = (s->dstatus & DSTATUS_IE) &&
+ (!s->pnxt_valid || !s->rnxt_valid);
+
+ if (level != s->irq_level) {
+ s->irq_level = level;
+ trace_harmony_irq(level);
+ qemu_set_irq(s->irq, level);
+ }
+}
+
+static void harmony_set_voices(HarmonyState *s, bool out, bool in)
+{
+ if (out != s->out_active) {
+ s->out_active = out;
+ s->starved = false;
+ if (s->voice_out) {
+ audio_be_set_active_out(s->audio_be, s->voice_out, out);
+ }
+ }
+ if (in != s->in_active) {
+ s->in_active = in;
+ s->in_rd = s->in_wr;
+ if (s->voice_in) {
+ audio_be_set_active_in(s->audio_be, s->voice_in, in);
+ }
+ }
+}
+
+static bool harmony_busy(HarmonyState *s)
+{
+ return harmony_running(s) &&
+ (s->pcur_valid || s->pnxt_valid || s->rcur_valid || s->rnxt_valid);
+}
+
+/*
+ * Voices start as soon as there is a page to transfer. They are stopped
+ * only by harmony_out_cb after a sustained idle spell: drivers leave both
+ * directions empty for a moment at every page boundary, and restarting a
+ * host capture stream that often loses its input.
+ */
+static void harmony_update_active(HarmonyState *s)
+{
+ bool out;
+
+ if (!harmony_running(s)) {
+ s->rec_seen = false;
+ harmony_set_voices(s, false, false);
+ return;
+ }
+ if (s->rcur_valid || s->rnxt_valid) {
+ s->rec_seen = true;
+ }
+ out = s->out_active || harmony_busy(s);
+ harmony_set_voices(s, out, out && s->rec_seen);
+}
+
+/* Start the queued next page in any direction that has finished its last */
+static void harmony_load(HarmonyState *s)
+{
+ if (!harmony_running(s)) {
+ return;
+ }
+ if (!s->pcur_valid && s->pnxt_valid) {
+ s->pcuradd = s->pnxtadd;
+ s->poff = 0;
+ s->pcur_valid = true;
+ s->pnxt_valid = false;
+ trace_harmony_page("play", s->pcuradd);
+ }
+ if (!s->rcur_valid && s->rnxt_valid) {
+ s->rcuradd = s->rnxtadd;
+ s->roff = 0;
+ s->rcur_valid = true;
+ s->rnxt_valid = false;
+ trace_harmony_page("rec", s->rcuradd);
+ }
+ harmony_update_irq(s);
+}
+
+static int16_t harmony_get_sample(HarmonyState *s, const uint8_t *p)
+{
+ switch (harmony_format(s)) {
+ case FMT_S16BE:
+ return (int16_t)lduw_be_p(p);
+ case FMT_ULAW:
+ return ulaw_to_s16(*p);
+ case FMT_ALAW:
+ return alaw_to_s16(*p);
+ default:
+ return (int16_t)((*p - 0x80) << 8);
+ }
+}
+
+static void harmony_put_sample(HarmonyState *s, uint8_t *p, int16_t v)
+{
+ switch (harmony_format(s)) {
+ case FMT_S16BE:
+ stw_be_p(p, v);
+ break;
+ case FMT_ULAW:
+ *p = s16_to_ulaw(v);
+ break;
+ case FMT_ALAW:
+ *p = s16_to_alaw(v);
+ break;
+ default:
+ *p = (v >> 8) + 0x80;
+ break;
+ }
+}
+
+/* Fetch n playback frames from guest memory into out_buf as S16 stereo */
+static void harmony_play_fetch(HarmonyState *s, int n)
+{
+ int bpf = harmony_bpf(s);
+ int bps = harmony_format(s) == FMT_S16BE ? 2 : 1;
+ bool stereo = harmony_channels(s) == 2;
+ int32_t att_l = s->out_att[(s->gainctl >> GAINCTL_OUT_L_SHIFT) & 0x3f];
+ int32_t att_r = s->out_att[(s->gainctl >> GAINCTL_OUT_R_SHIFT) & 0x3f];
+ int i;
+
+ /*
+ * HE/LE/SE are not used to mute the host output: HP-UX 10.20 sets all
+ * three while idle and clears all three for playback, the opposite of
+ * the "enable" reading in the Linux and BSD drivers. The host output
+ * stands for whichever jack is in use; attenuation still applies.
+ */
+ if (!s->pcur_valid) {
+ memset(s->out_buf, 0, n * 4);
+ return;
+ }
+
+ dma_memory_read(&address_space_memory, s->pcuradd + s->poff,
+ s->dma_buf, n * bpf, MEMTXATTRS_UNSPECIFIED);
+ for (i = 0; i < n; i++) {
+ const uint8_t *p = s->dma_buf + i * bpf;
+ int16_t l = harmony_get_sample(s, p);
+ int16_t r = stereo ? harmony_get_sample(s, p + bps) : l;
+
+ s->out_buf[2 * i] = MIN(MAX((l * att_l) >> 15, INT16_MIN), INT16_MAX);
+ s->out_buf[2 * i + 1] = MIN(MAX((r * att_r) >> 15, INT16_MIN),
+ INT16_MAX);
+ }
+}
+
+/* Store n record frames (host input or silence) into guest memory */
+static void harmony_rec_store(HarmonyState *s, int n)
+{
+ int bpf = harmony_bpf(s);
+ int bps = harmony_format(s) == FMT_S16BE ? 2 : 1;
+ bool stereo = harmony_channels(s) == 2;
+ int32_t g_l = harmony_in_gain[(s->gainctl >> GAINCTL_IN_L_SHIFT) & 0xf];
+ int32_t g_r = harmony_in_gain[(s->gainctl >> GAINCTL_IN_R_SHIFT) & 0xf];
+ uint32_t got = MIN(s->in_wr - s->in_rd, (uint32_t)n);
+ uint32_t i;
+
+ for (i = 0; i < got; i++) {
+ uint32_t j = (s->in_rd + i) & (HARMONY_IN_RING - 1);
+
+ s->in_buf[2 * i] = s->in_ring[2 * j];
+ s->in_buf[2 * i + 1] = s->in_ring[2 * j + 1];
+ }
+ s->in_rd += got;
+ trace_harmony_rec_read(n, got, s->in_wr - s->in_rd);
+ memset(s->in_buf + 2 * got, 0, (n - got) * 4);
+
+ for (i = 0; i < n; i++) {
+ int32_t l = (s->in_buf[2 * i] * g_l) >> 12;
+ int32_t r = (s->in_buf[2 * i + 1] * g_r) >> 12;
+ uint8_t *p = s->dma_buf + i * bpf;
+
+ if (l != (int16_t)l || r != (int16_t)r) {
+ s->ov |= OV_OV;
+ l = MIN(MAX(l, INT16_MIN), INT16_MAX);
+ r = MIN(MAX(r, INT16_MIN), INT16_MAX);
+ }
+ if (stereo) {
+ harmony_put_sample(s, p, l);
+ harmony_put_sample(s, p + bps, r);
+ } else {
+ harmony_put_sample(s, p, (l + r) / 2);
+ }
+ }
+ dma_memory_write(&address_space_memory, s->rcuradd + s->roff,
+ s->dma_buf, n * bpf, MEMTXATTRS_UNSPECIFIED);
+}
+
+/*
+ * Retire finished pages and start queued ones. Returns true if a direction
+ * ran out with nothing queued: the transfer then waits for the driver to
+ * service the interrupt rather than consuming more of this audio tick.
+ */
+static bool harmony_page_end(HarmonyState *s)
+{
+ /* a format change mid-page can leave less than a frame; drop it */
+ int bpf = harmony_bpf(s);
+ bool p_end = s->pcur_valid && s->poff + bpf > HARMONY_PAGE_SIZE;
+ bool r_end = s->rcur_valid && s->roff + bpf > HARMONY_PAGE_SIZE;
+
+ if (p_end) {
+ s->pcur_valid = false;
+ }
+ if (r_end) {
+ s->rcur_valid = false;
+ }
+ harmony_load(s);
+ if ((p_end && !s->pcur_valid) || (r_end && !s->rcur_valid)) {
+ trace_harmony_starved(!s->pcur_valid, !s->rcur_valid);
+ return true;
+ }
+ return false;
+}
+
+/*
+ * Transfer up to the given number of frames, as far as the output voice
+ * accepts them. Stops early, marking the stream starved, when a direction
+ * runs out with nothing queued.
+ */
+static void harmony_fill(HarmonyState *s, int frames)
+{
+ while (frames > 0) {
+ int bpf, n;
+ size_t written;
+
+ if (harmony_page_end(s)) {
+ s->starved = true;
+ break;
+ }
+ if (!(s->pcur_valid || s->rcur_valid)) {
+ break;
+ }
+ s->starved = false;
+
+ bpf = harmony_bpf(s);
+ n = MIN(frames, HARMONY_MIX_FRAMES);
+ if (s->pcur_valid) {
+ n = MIN(n, (HARMONY_PAGE_SIZE - s->poff) / bpf);
+ }
+ if (s->rcur_valid) {
+ n = MIN(n, (HARMONY_PAGE_SIZE - s->roff) / bpf);
+ }
+
+ harmony_play_fetch(s, n);
+ written = audio_be_write(s->audio_be, s->voice_out, s->out_buf, n * 4);
+ n = written / 4;
+ if (n == 0) {
+ break;
+ }
+ if (s->pcur_valid) {
+ s->poff += n * bpf;
+ }
+ if (s->rcur_valid) {
+ harmony_rec_store(s, n);
+ s->roff += n * bpf;
+ }
+ frames -= n;
+ }
+}
+
+/*
+ * A page queued while the stream is starved is picked up at once instead
+ * of at the next audio tick. Backends that call back rarely (PulseAudio
+ * after an underrun raises its latency) would otherwise hold the stream to
+ * one page per tick. The voice only accepts what it has room for, so this
+ * cannot run ahead of the host clock. While record is in use, wait for
+ * its page too so the two directions stay in step.
+ */
+static void harmony_resume(HarmonyState *s)
+{
+ if (!s->starved || !s->out_active || !s->voice_out) {
+ return;
+ }
+ if (s->rec_seen && !s->rcur_valid) {
+ return;
+ }
+ harmony_fill(s, INT_MAX / 4);
+}
+
+static void harmony_out_cb(void *opaque, int avail)
+{
+ HarmonyState *s = opaque;
+
+ harmony_fill(s, avail / 4);
+
+ if (harmony_busy(s)) {
+ s->idle_ticks = 0;
+ } else if (++s->idle_ticks >= HARMONY_IDLE_TICKS) {
+ s->idle_ticks = 0;
+ s->rec_seen = false;
+ harmony_set_voices(s, false, false);
+ return;
+ }
+ harmony_update_active(s);
+}
+
+/*
+ * Drain the input voice into the ring. Keep at most a quarter second of
+ * backlog so a host input running faster than the codec clock doesn't
+ * build up latency.
+ */
+static void harmony_in_cb(void *opaque, int avail)
+{
+ HarmonyState *s = opaque;
+ uint32_t max_backlog = MIN(harmony_freq(s) / 4, HARMONY_IN_RING - 1);
+ int frames = avail / 4;
+
+ trace_harmony_in_cb(avail);
+ while (frames > 0) {
+ int n = MIN(frames, HARMONY_MIX_FRAMES);
+ size_t got = audio_be_read(s->audio_be, s->voice_in, s->in_buf, n * 4);
+ int i;
+
+ n = got / 4;
+ if (n == 0) {
+ break;
+ }
+ for (i = 0; i < n; i++) {
+ uint32_t j = (s->in_wr + i) & (HARMONY_IN_RING - 1);
+
+ s->in_ring[2 * j] = s->in_buf[2 * i];
+ s->in_ring[2 * j + 1] = s->in_buf[2 * i + 1];
+ }
+ s->in_wr += n;
+ frames -= n;
+ }
+ if (s->in_wr - s->in_rd > max_backlog) {
+ s->in_rd = s->in_wr - max_backlog;
+ }
+}
+
+static void harmony_open_voices(HarmonyState *s)
+{
+ struct audsettings as = {
+ .freq = harmony_freq(s),
+ .nchannels = 2,
+ .fmt = AUDIO_FORMAT_S16,
+ .big_endian = HOST_BIG_ENDIAN,
+ };
+
+ if (s->voice_out && as.freq == s->freq) {
+ return;
+ }
+ s->freq = as.freq;
+ trace_harmony_config(as.freq, harmony_format(s), harmony_channels(s));
+
+ s->voice_out = audio_be_open_out(s->audio_be, s->voice_out, "harmony.out",
+ s, harmony_out_cb, &as);
+ s->voice_in = audio_be_open_in(s->audio_be, s->voice_in, "harmony.in",
+ s, harmony_in_cb, &as);
+
+ /* reopening leaves the voices inactive; restore their state */
+ if (s->voice_out) {
+ audio_be_set_active_out(s->audio_be, s->voice_out, s->out_active);
+ }
+ if (s->voice_in) {
+ audio_be_set_active_in(s->audio_be, s->voice_in, s->in_active);
+ }
+ harmony_update_active(s);
+}
+
+static const char *harmony_reg_name(hwaddr addr)
+{
+ switch (addr) {
+ case HARMONY_ID: return "ID";
+ case HARMONY_RESET: return "RESET";
+ case HARMONY_CNTL: return "CNTL";
+ case HARMONY_GAINCTL: return "GAINCTL";
+ case HARMONY_PNXTADD: return "PNXTADD";
+ case HARMONY_PCURADD: return "PCURADD";
+ case HARMONY_RNXTADD: return "RNXTADD";
+ case HARMONY_RCURADD: return "RCURADD";
+ case HARMONY_DSTATUS: return "DSTATUS";
+ case HARMONY_OV: return "OV";
+ case HARMONY_PIO: return "PIO";
+ case HARMONY_DIAG: return "DIAG";
+ default: return "?";
+ }
+}
+
+static uint64_t harmony_read(void *opaque, hwaddr addr, unsigned size)
+{
+ HarmonyState *s = opaque;
+ uint32_t val = 0;
+
+ switch (addr) {
+ case HARMONY_ID:
+ val = s->id;
+ break;
+ case HARMONY_RESET:
+ val = s->reset;
+ break;
+ case HARMONY_CNTL:
+ /* control writes complete at once, so C never reads back set */
+ val = (s->cntl & ~(CNTL_C | CNTL_CODEC_REV_MASK)) |
+ (s->codec_rev << CNTL_CODEC_REV_SHIFT);
+ break;
+ case HARMONY_GAINCTL:
+ val = s->gainctl;
+ break;
+ case HARMONY_PNXTADD:
+ val = s->pnxtadd;
+ break;
+ case HARMONY_PCURADD:
+ val = s->pcuradd + s->poff;
+ break;
+ case HARMONY_RNXTADD:
+ val = s->rnxtadd;
+ break;
+ case HARMONY_RCURADD:
+ val = s->rcuradd + s->roff;
+ break;
+ case HARMONY_DSTATUS:
+ val = s->dstatus & DSTATUS_IE;
+ val |= s->pnxt_valid ? 0 : DSTATUS_PN;
+ val |= s->rnxt_valid ? 0 : DSTATUS_RN;
+ if (harmony_running(s)) {
+ val |= s->pcur_valid ? DSTATUS_PC : 0;
+ val |= s->rcur_valid ? DSTATUS_RC : 0;
+ }
+ break;
+ case HARMONY_OV:
+ val = s->ov;
+ break;
+ case HARMONY_PIO:
+ val = s->pio & PIO_PO;
+ break;
+ case HARMONY_DIAG:
+ val = (s->reset & RESET_RST) ? 0 : DIAG_CO;
+ break;
+ default:
+ qemu_log_mask(LOG_UNIMP, "harmony: read from unknown register "
+ "0x%" HWADDR_PRIx "\n", addr);
+ break;
+ }
+
+ trace_harmony_reg_read(addr, harmony_reg_name(addr), val);
+ return val;
+}
+
+static void harmony_write(void *opaque, hwaddr addr, uint64_t val64,
+ unsigned size)
+{
+ HarmonyState *s = opaque;
+ uint32_t val = val64;
+
+ trace_harmony_reg_write(addr, harmony_reg_name(addr), val);
+
+ switch (addr) {
+ case HARMONY_RESET:
+ s->reset = val & RESET_RST;
+ harmony_load(s);
+ harmony_update_active(s);
+ break;
+ case HARMONY_CNTL:
+ s->cntl = val & ~CNTL_C;
+ harmony_open_voices(s);
+ break;
+ case HARMONY_GAINCTL:
+ s->gainctl = val;
+ break;
+ case HARMONY_PNXTADD:
+ s->pnxtadd = val;
+ s->pnxt_valid = true;
+ harmony_load(s);
+ harmony_update_active(s);
+ harmony_resume(s);
+ break;
+ case HARMONY_RNXTADD:
+ s->rnxtadd = val;
+ s->rnxt_valid = true;
+ harmony_load(s);
+ harmony_update_active(s);
+ harmony_resume(s);
+ break;
+ case HARMONY_DSTATUS:
+ s->dstatus = val & DSTATUS_IE;
+ harmony_load(s);
+ harmony_update_irq(s);
+ harmony_update_active(s);
+ break;
+ case HARMONY_OV:
+ s->ov = val & OV_OV;
+ break;
+ case HARMONY_PIO:
+ s->pio = val & PIO_PO;
+ break;
+ case HARMONY_ID:
+ case HARMONY_PCURADD:
+ case HARMONY_RCURADD:
+ case HARMONY_DIAG:
+ qemu_log_mask(LOG_GUEST_ERROR, "harmony: write to read-only register "
+ "%s\n", harmony_reg_name(addr));
+ break;
+ default:
+ qemu_log_mask(LOG_UNIMP, "harmony: write to unknown register "
+ "0x%" HWADDR_PRIx "\n", addr);
+ break;
+ }
+}
+
+static const MemoryRegionOps harmony_ops = {
+ .read = harmony_read,
+ .write = harmony_write,
+ .endianness = DEVICE_BIG_ENDIAN,
+ .valid = {
+ .min_access_size = 1,
+ .max_access_size = 4,
+ },
+ .impl = {
+ .min_access_size = 4,
+ .max_access_size = 4,
+ },
+};
+
+static void harmony_reset_hold(Object *obj, ResetType type)
+{
+ HarmonyState *s = HARMONY(obj);
+
+ s->reset = 0;
+ s->cntl = 0x08; /* 8 kHz, S16BE, mono */
+ s->gainctl = 0;
+ s->pnxtadd = 0;
+ s->rnxtadd = 0;
+ s->dstatus = 0;
+ s->ov = 0;
+ s->pio = 0;
+ s->pcuradd = 0;
+ s->poff = 0;
+ s->rcuradd = 0;
+ s->roff = 0;
+ s->pcur_valid = false;
+ s->pnxt_valid = false;
+ s->rcur_valid = false;
+ s->rnxt_valid = false;
+
+ harmony_update_irq(s);
+ harmony_open_voices(s);
+ harmony_set_voices(s, false, false);
+ s->rec_seen = false;
+ s->starved = false;
+ s->idle_ticks = 0;
+}
+
+static int harmony_post_load(void *opaque, int version_id)
+{
+ HarmonyState *s = opaque;
+
+ s->freq = 0;
+ harmony_open_voices(s);
+ if (s->voice_out) {
+ audio_be_set_active_out(s->audio_be, s->voice_out, false);
+ }
+ if (s->voice_in) {
+ audio_be_set_active_in(s->audio_be, s->voice_in, false);
+ }
+ s->out_active = false;
+ s->in_active = false;
+ s->rec_seen = false;
+ s->starved = false;
+ s->idle_ticks = 0;
+ harmony_update_active(s);
+ qemu_set_irq(s->irq, s->irq_level);
+ return 0;
+}
+
+static const VMStateDescription vmstate_harmony = {
+ .name = "harmony",
+ .version_id = 1,
+ .minimum_version_id = 1,
+ .post_load = harmony_post_load,
+ .fields = (const VMStateField[]) {
+ VMSTATE_UINT32(reset, HarmonyState),
+ VMSTATE_UINT32(cntl, HarmonyState),
+ VMSTATE_UINT32(gainctl, HarmonyState),
+ VMSTATE_UINT32(pnxtadd, HarmonyState),
+ VMSTATE_UINT32(rnxtadd, HarmonyState),
+ VMSTATE_UINT32(dstatus, HarmonyState),
+ VMSTATE_UINT32(ov, HarmonyState),
+ VMSTATE_UINT32(pio, HarmonyState),
+ VMSTATE_UINT32(pcuradd, HarmonyState),
+ VMSTATE_UINT32(poff, HarmonyState),
+ VMSTATE_UINT32(rcuradd, HarmonyState),
+ VMSTATE_UINT32(roff, HarmonyState),
+ VMSTATE_BOOL(pcur_valid, HarmonyState),
+ VMSTATE_BOOL(pnxt_valid, HarmonyState),
+ VMSTATE_BOOL(rcur_valid, HarmonyState),
+ VMSTATE_BOOL(rnxt_valid, HarmonyState),
+ VMSTATE_INT32(irq_level, HarmonyState),
+ VMSTATE_END_OF_LIST()
+ }
+};
+
+static void harmony_realize(DeviceState *dev, Error **errp)
+{
+ HarmonyState *s = HARMONY(dev);
+ int i;
+
+ /*
+ * Output attenuation is 1.5 dB per step. HP-UX puts its 0 dB play gain
+ * at attenuation 6 (range -84..+9 dB), so that step is unity here and
+ * the steps below it amplify, saturating at full scale.
+ */
+ for (i = 0; i < 64; i++) {
+ s->out_att[i] = lrint(32768.0 *
+ pow(10.0, -1.5 * (i - s->out_unity_att) / 20.0));
+ }
+
+ if (!audio_be_check(&s->audio_be, errp)) {
+ return;
+ }
+
+ harmony_open_voices(s);
+ if (!s->voice_out) {
+ error_setg(errp, "harmony: initializing audio output failed");
+ return;
+ }
+}
+
+static void harmony_unrealize(DeviceState *dev)
+{
+ HarmonyState *s = HARMONY(dev);
+
+ if (s->voice_in) {
+ audio_be_close_in(s->audio_be, s->voice_in);
+ s->voice_in = NULL;
+ }
+ if (s->voice_out) {
+ audio_be_close_out(s->audio_be, s->voice_out);
+ s->voice_out = NULL;
+ }
+}
+
+static void harmony_init(Object *obj)
+{
+ HarmonyState *s = HARMONY(obj);
+ SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
+
+ memory_region_init_io(&s->mmio, obj, &harmony_ops, s, "harmony",
+ HARMONY_MMIO_SIZE);
+ sysbus_init_mmio(sbd, &s->mmio);
+ sysbus_init_irq(sbd, &s->irq);
+}
+
+static const Property harmony_properties[] = {
+ DEFINE_AUDIO_PROPERTIES(HarmonyState, audio_be),
+ /* revision 0x14 = no teleshare, 0x15 = teleshare */
+ DEFINE_PROP_UINT32("id", HarmonyState, id, 0x00140000),
+ DEFINE_PROP_UINT8("codec-rev", HarmonyState, codec_rev, 0x02),
+ DEFINE_PROP_UINT8("out-unity-att", HarmonyState, out_unity_att, 6),
+};
+
+static void harmony_class_init(ObjectClass *oc, const void *data)
+{
+ DeviceClass *dc = DEVICE_CLASS(oc);
+ ResettableClass *rc = RESETTABLE_CLASS(oc);
+
+ harmony_init_tables();
+
+ dc->realize = harmony_realize;
+ dc->unrealize = harmony_unrealize;
+ dc->vmsd = &vmstate_harmony;
+ dc->desc = "HP Harmony audio";
+ set_bit(DEVICE_CATEGORY_SOUND, dc->categories);
+ device_class_set_props(dc, harmony_properties);
+ rc->phases.hold = harmony_reset_hold;
+}
+
+static const TypeInfo harmony_info = {
+ .name = TYPE_HARMONY,
+ .parent = TYPE_SYS_BUS_DEVICE,
+ .instance_size = sizeof(HarmonyState),
+ .instance_init = harmony_init,
+ .class_init = harmony_class_init,
+};
+
+static void harmony_register_types(void)
+{
+ type_register_static(&harmony_info);
+}
+
+type_init(harmony_register_types)
diff --git a/hw/audio/trace-events b/hw/audio/trace-events
index 30f5921545..99d39075c2 100644
--- a/hw/audio/trace-events
+++ b/hw/audio/trace-events
@@ -61,3 +61,13 @@ virtio_snd_handle_event(void) "event queue callback called"
virtio_snd_pcm_stream_flush(uint32_t stream) "flushing stream %"PRIu32
virtio_snd_handle_tx_xfer(void) "tx queue callback called"
virtio_snd_handle_rx_xfer(void) "rx queue callback called"
+
+# harmony.c
+harmony_reg_read(uint64_t addr, const char *name, uint32_t val) "0x%02"PRIx64" %s -> 0x%08x"
+harmony_reg_write(uint64_t addr, const char *name, uint32_t val) "0x%02"PRIx64" %s <- 0x%08x"
+harmony_irq(int level) "irq %d"
+harmony_page(const char *dir, uint32_t addr) "%s page 0x%08x"
+harmony_starved(int play, int rec) "no next page queued (play %d rec %d)"
+harmony_rec_read(int want, int got, int backlog) "want %d got %d frames, backlog %d"
+harmony_in_cb(int avail) "input voice has %d bytes"
+harmony_config(int freq, int fmt, int channels) "%d Hz format %d channels %d"
diff --git a/include/hw/audio/harmony.h b/include/hw/audio/harmony.h
new file mode 100644
index 0000000000..0a0f6c4dfa
--- /dev/null
+++ b/include/hw/audio/harmony.h
@@ -0,0 +1,76 @@
+/*
+ * HP Harmony audio (LASI audio function, CS4215/AD1849 codec)
+ *
+ * Copyright (c) 2026 Keith Monahan <keith@techtravels.org>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef HW_AUDIO_HARMONY_H
+#define HW_AUDIO_HARMONY_H
+
+#include "hw/core/sysbus.h"
+#include "qemu/audio.h"
+#include "qom/object.h"
+
+#define TYPE_HARMONY "harmony"
+OBJECT_DECLARE_SIMPLE_TYPE(HarmonyState, HARMONY)
+
+#define HARMONY_MMIO_SIZE 0x1000
+#define HARMONY_PAGE_SIZE 4096
+#define HARMONY_MIX_FRAMES 1024
+#define HARMONY_IN_RING 8192 /* frames, power of 2 */
+
+struct HarmonyState {
+ SysBusDevice parent_obj;
+
+ MemoryRegion mmio;
+ qemu_irq irq;
+ AudioBackend *audio_be;
+ SWVoiceOut *voice_out;
+ SWVoiceIn *voice_in;
+
+ /* properties */
+ uint32_t id;
+ uint8_t codec_rev;
+ uint8_t out_unity_att;
+
+ /* registers */
+ uint32_t reset;
+ uint32_t cntl;
+ uint32_t gainctl;
+ uint32_t pnxtadd;
+ uint32_t rnxtadd;
+ uint32_t dstatus; /* IE only, PN/RN are below */
+ uint32_t ov;
+ uint32_t pio;
+
+ /* DMA engine, playback and record share the codec sample clock */
+ uint32_t pcuradd;
+ uint32_t poff;
+ uint32_t rcuradd;
+ uint32_t roff;
+ bool pcur_valid;
+ bool pnxt_valid; /* PN = !pnxt_valid */
+ bool rcur_valid;
+ bool rnxt_valid; /* RN = !rnxt_valid */
+
+ int irq_level;
+ int freq;
+ int32_t out_att[64]; /* Q15 per attenuation step */
+
+ int16_t out_buf[HARMONY_MIX_FRAMES * 2];
+ int16_t in_buf[HARMONY_MIX_FRAMES * 2];
+ /* host input captured by the input voice, consumed at the codec clock */
+ int16_t in_ring[HARMONY_IN_RING * 2];
+ uint32_t in_rd;
+ uint32_t in_wr;
+ bool out_active;
+ bool in_active;
+ bool rec_seen;
+ bool starved;
+ int idle_ticks;
+ uint8_t dma_buf[HARMONY_MIX_FRAMES * 4];
+};
+
+#endif
--
2.54.0
next prev parent reply other threads:[~2026-10-28 22:49 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-28 22:48 [PATCH 0/4] Harmony sound card driver for 32-bit PA-RISC machines Helge Deller
2026-10-28 22:48 ` Helge Deller [this message]
2026-10-28 22:48 ` [PATCH 2/4] hw/audio: Wire up the harmony driver in meson and Kconfig Helge Deller
2026-10-28 22:48 ` [PATCH 3/4] hw/hppa: Add harmony sound card to HP 715 and B160L machines Helge Deller
2026-10-28 22:48 ` [PATCH 4/4] hw/misc: Harmony sound registers now handled by harmony driver Helge Deller
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261028224815.32502-2-deller@kernel.org \
--to=deller@kernel.org \
--cc=deller@gmx.de \
--cc=keith@techtravels.org \
--cc=kraxel@redhat.com \
--cc=qemu-devel@nongnu.org \
--cc=richard.henderson@linaro.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.