* [PATCH 1/4] hw/audio: Add HP Harmony audio (LASI audio function, CS4215/AD1849 codec)
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
2026-10-28 22:48 ` [PATCH 2/4] hw/audio: Wire up the harmony driver in meson and Kconfig Helge Deller
` (2 subsequent siblings)
3 siblings, 0 replies; 5+ messages in thread
From: Helge Deller @ 2026-10-28 22:48 UTC (permalink / raw)
To: qemu-devel; +Cc: Richard Henderson, Keith Monahan, Gerd Hoffmann, Helge Deller
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
^ permalink raw reply related [flat|nested] 5+ messages in thread