* [PATCH 0/4] Harmony sound card driver for 32-bit PA-RISC machines
@ 2026-10-28 22:48 Helge Deller
2026-10-28 22:48 ` [PATCH 1/4] hw/audio: Add HP Harmony audio (LASI audio function, CS4215/AD1849 codec) Helge Deller
` (3 more replies)
0 siblings, 4 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: Helge Deller <deller@gmx.de>
Implement the Harmony audio sound chip which is part of the LASI
controller. Used on 715 and B160L machines.
Patch set is on top of the previous series for which I just sent
a pull request.
Helge Deller (1):
hw/misc: Harmony sound registers now handled by harmony driver
Keith Monahan (3):
hw/audio: Add HP Harmony audio (LASI audio function, CS4215/AD1849
codec)
hw/audio: Wire up the harmony driver in meson and Kconfig
hw/hppa: Add harmony sound card to HP 715 and B160L machines
hw/audio/Kconfig | 3 +
hw/audio/harmony.c | 892 +++++++++++++++++++++++++++++++++++++
hw/audio/meson.build | 1 +
hw/audio/trace-events | 10 +
hw/hppa/Kconfig | 1 +
hw/hppa/machine.c | 24 +
hw/misc/lasi.c | 12 +-
include/hw/audio/harmony.h | 76 ++++
8 files changed, 1010 insertions(+), 9 deletions(-)
create mode 100644 hw/audio/harmony.c
create mode 100644 include/hw/audio/harmony.h
--
2.54.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* [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
* [PATCH 2/4] hw/audio: Wire up the harmony driver in meson and Kconfig
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 ` [PATCH 1/4] hw/audio: Add HP Harmony audio (LASI audio function, CS4215/AD1849 codec) Helge Deller
@ 2026-10-28 22:48 ` 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
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>
Signed-off-by: Keith Monahan <keith@techtravels.org
Signed-off-by: Helge Deller <deller@gmx.de>
---
hw/audio/Kconfig | 3 +++
hw/audio/meson.build | 1 +
2 files changed, 4 insertions(+)
diff --git a/hw/audio/Kconfig b/hw/audio/Kconfig
index ed93fa2698..ba6f66342e 100644
--- a/hw/audio/Kconfig
+++ b/hw/audio/Kconfig
@@ -51,3 +51,6 @@ config VIRTIO_SND
bool
default y
depends on VIRTIO
+
+config HARMONY
+ bool
diff --git a/hw/audio/meson.build b/hw/audio/meson.build
index 3312bcec10..f440324cdf 100644
--- a/hw/audio/meson.build
+++ b/hw/audio/meson.build
@@ -6,6 +6,7 @@ system_ss.add(when: 'CONFIG_CS4231', if_true: files('cs4231.c'))
system_ss.add(when: 'CONFIG_CS4231A', if_true: files('cs4231a.c'))
system_ss.add(when: 'CONFIG_ES1370', if_true: files('es1370.c'))
system_ss.add(when: 'CONFIG_GUS', if_true: files('gus.c', 'gusemu_hal.c', 'gusemu_mixer.c'))
+system_ss.add(when: 'CONFIG_HARMONY', if_true: files('harmony.c'))
system_ss.add(when: 'CONFIG_HDA', if_true: files('intel-hda.c', 'hda-codec.c'))
system_ss.add(when: 'CONFIG_PCSPK', if_true: files('pcspk.c'))
system_ss.add(when: 'CONFIG_PL041', if_true: files('pl041.c', 'lm4549.c'))
--
2.54.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH 3/4] hw/hppa: Add harmony sound card to HP 715 and B160L machines
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 ` [PATCH 1/4] hw/audio: Add HP Harmony audio (LASI audio function, CS4215/AD1849 codec) Helge Deller
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 ` Helge Deller
2026-10-28 22:48 ` [PATCH 4/4] hw/misc: Harmony sound registers now handled by harmony driver Helge Deller
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>
Signed-off-by: Keith Monahan <keith@techtravels.org
Signed-off-by: Helge Deller <deller@gmx.de>
---
hw/hppa/Kconfig | 1 +
hw/hppa/machine.c | 24 ++++++++++++++++++++++++
2 files changed, 25 insertions(+)
diff --git a/hw/hppa/Kconfig b/hw/hppa/Kconfig
index 9ede4d1140..d00d7ca040 100644
--- a/hw/hppa/Kconfig
+++ b/hw/hppa/Kconfig
@@ -19,3 +19,4 @@ config HPPA_B160L
select LSI_SCSI_PCI
select PARALLEL
select ARTIST
+ select HARMONY
diff --git a/hw/hppa/machine.c b/hw/hppa/machine.c
index f23936fc5b..940bf2c72e 100644
--- a/hw/hppa/machine.c
+++ b/hw/hppa/machine.c
@@ -32,6 +32,7 @@
#include "hw/misc/lasi.h"
#include "hw/scsi/ncr53c710.h"
#include "hw/scsi/lasi_ncr710.h"
+#include "hw/audio/harmony.h"
#include "hppa_hardware.h"
#include "qemu/units.h"
#include "qapi/error.h"
@@ -271,6 +272,21 @@ static LasiState *lasi_init(void)
return LASI_CHIP(dev);
}
+/* Harmony audio, inside the LASI register block at LASI + 0x4000 */
+static void harmony_create(MachineState *machine, hwaddr addr, qemu_irq irq)
+{
+ DeviceState *dev = qdev_new(TYPE_HARMONY);
+
+ if (machine->audiodev) {
+ qdev_prop_set_string(dev, "audiodev", machine->audiodev);
+ }
+ sysbus_realize_and_unref(SYS_BUS_DEVICE(dev), &error_fatal);
+ sysbus_connect_irq(SYS_BUS_DEVICE(dev), 0, irq);
+ memory_region_add_subregion_overlap(get_system_memory(), addr,
+ sysbus_mmio_get_region(
+ SYS_BUS_DEVICE(dev), 0), 1);
+}
+
static DinoState *dino_init(MemoryRegion *addr_space)
{
DeviceState *dev;
@@ -699,6 +715,9 @@ static void machine_HP_715_init(MachineState *machine)
sysbus_mmio_get_region(SYS_BUS_DEVICE(dev), 0));
}
+ harmony_create(machine, translate(NULL, LASI_HPA_715 + LASI_AUDIO),
+ qdev_get_gpio_in(lasi_dev, LASI_IRQ_AUDIO_HPA));
+
/* Add NICs, graphics & load firmware */
machine_HP_common_init_tail(machine, NULL, translate, false);
}
@@ -778,6 +797,9 @@ static void machine_HP_B160L_init(MachineState *machine)
lsi53c8xx_handle_legacy_cmdline(dev);
}
+ harmony_create(machine, translate(NULL, LASI_HPA + LASI_AUDIO),
+ qdev_get_gpio_in(lasi_dev, LASI_IRQ_AUDIO_HPA));
+
/* Add NICs, graphics & load firmware */
machine_HP_common_init_tail(machine, pci_bus, translate, false);
}
@@ -985,6 +1007,7 @@ static void HP_B160L_machine_init_class_init(ObjectClass *oc, const void *data)
mc->init = machine_HP_B160L_init;
mc->is_default = true;
mc->default_ram_size = 512 * MiB;
+ machine_add_audiodev_property(mc);
}
static void HP_C3700_machine_init_class_init(ObjectClass *oc, const void *data)
@@ -1036,6 +1059,7 @@ static void HP_715_machine_init_class_init(ObjectClass *oc, const void *data)
mc->default_cpu_type = TYPE_HPPA_CPU_PA_7300LC;
mc->valid_cpu_types = valid_cpu_types;
mc->init = machine_HP_715_init;
+ machine_add_audiodev_property(mc);
/* can only support up to max. 8 CPUs due inventory major numbers */
mc->max_cpus = MIN_CONST(HPPA_MAX_CPUS, 8);
mc->default_ram_size = 256 * MiB;
--
2.54.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH 4/4] hw/misc: Harmony sound registers now handled by harmony driver
2026-10-28 22:48 [PATCH 0/4] Harmony sound card driver for 32-bit PA-RISC machines Helge Deller
` (2 preceding siblings ...)
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 ` Helge Deller
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: Helge Deller <deller@gmx.de>
No need to handle the LASI AUDIO registers here.
Signed-off-by: Helge Deller <deller@gmx.de>
---
hw/misc/lasi.c | 12 +++---------
1 file changed, 3 insertions(+), 9 deletions(-)
diff --git a/hw/misc/lasi.c b/hw/misc/lasi.c
index 76029804e1..d4ef832af1 100644
--- a/hw/misc/lasi.c
+++ b/hw/misc/lasi.c
@@ -86,13 +86,7 @@ static MemTxResult lasi_chip_read_with_attrs(void *opaque, hwaddr addr,
val = s->iar;
break;
- case LASI_AUDIO ... LASI_AUDIO + 0xFF:
- /*
- * Harmony audio not yet emulated. DIAG (0x3c) reports a running
- * sample clock, which HP-UX 9 checks before using the beeper.
- */
- val = (addr == LASI_AUDIO + 0x3c);
- break;
+ case LASI_AUDIO: /* handled by harmony audio driver */
case LASI_LPT:
case LASI_UART:
case LASI_LAN:
@@ -164,8 +158,8 @@ static MemTxResult lasi_chip_write_with_attrs(void *opaque, hwaddr addr,
case LASI_LPT:
/* XXX: reset parallel port */
break;
- case LASI_AUDIO ... LASI_AUDIO + 0xFF:
- /* XXX: Harmony audio is not emulated */
+ case LASI_AUDIO:
+ /* handled by harmony audio driver */
break;
case LASI_UART:
/* XXX: reset serial port */
--
2.54.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-10-28 22:49 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [PATCH 1/4] hw/audio: Add HP Harmony audio (LASI audio function, CS4215/AD1849 codec) Helge Deller
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
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.