From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from lists1p.gnu.org (lists1p.gnu.org [209.51.188.17]) (using TLSv1.2 with cipher ECDHE-ECDSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 463DF606A31A for ; Wed, 28 Oct 2026 22:49:16 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key, unprotected) header.d=kernel.org header.i=@kernel.org header.a=rsa-sha256 header.s=k20260515 header.b=WznaIcKi Received: from localhost ([::1] helo=lists1p.gnu.org) by lists1p.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1xMCRX-0001uI-Ig; Wed, 28 Oct 2026 18:48:27 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists1p.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1xMCRV-0001tk-Hf for qemu-devel@nongnu.org; Wed, 28 Oct 2026 18:48:25 -0400 Received: from tor.source.kernel.org ([172.105.4.254]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1xMCRS-0007cb-40 for qemu-devel@nongnu.org; Wed, 28 Oct 2026 18:48:25 -0400 Received: from smtp.kernel.org (quasi.space.kernel.org [100.103.45.18]) by tor.source.kernel.org (Postfix) with ESMTP id CC38A600DA; Wed, 28 Oct 2026 22:48:20 +0000 (UTC) Received: by smtp.kernel.org (Postfix) with ESMTPSA id 2CFC51F01825; Wed, 28 Oct 2026 22:48:19 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1793227700; bh=5PwoP5eFK/VgNiqkCXL3FsO/x8WWjBSqellI8Ng4YYc=; h=From:To:Cc:Subject:Date:In-Reply-To:References; b=WznaIcKi6exD7uIbK+RhwX1o9b3fFTGEvUs6YguazCP/rGvso5flHD4sKWPLcJlA5 Vl/U2NfKosTDbHA2zA98/6rBHRGtkuPof6Cv5q13O85q3VytyDpIXs2jrKoOzTtLf+ 2o41E5iBqL1OMbOaKQvFAsOvsLF04bHHFNwHLR/cALJiHfuQi9FNDjabrhz/gHitQ5 nvmewpLgOVTTEVu2Lrr3hu+6kyONnB+crLhkxfoh5NMmc7ZjmUsD650RPPFKtD8DNQ x+sqbvLH8JiAO+ZforsAbkURKEYlgCFflqUAE5tZbFtQSOv4OmayR9ZbOUbE5uLbCA tF7iEACXRAuIw== From: Helge Deller To: qemu-devel@nongnu.org Cc: Richard Henderson , Keith Monahan , Gerd Hoffmann , Helge Deller 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 Message-ID: <20261028224815.32502-2-deller@kernel.org> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20261028224815.32502-1-deller@kernel.org> References: <20261028224815.32502-1-deller@kernel.org> X-Archived-By: MIME-Version: 1.0 Content-Transfer-Encoding: 8bit Received-SPF: pass client-ip=172.105.4.254; envelope-from=deller@kernel.org; helo=tor.source.kernel.org X-Spam_score_int: -43 X-Spam_score: -4.4 X-Spam_bar: ---- X-Spam_report: (-4.4 / 5.0 requ) BAYES_00=-1.9, DKIMWL_WL_HIGH=-0.001, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, RCVD_IN_DNSWL_MED=-2.3, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: qemu development List-Archive: List-Post: List-Help: List-Subscribe: , Sender: qemu-devel-bounces+qemu-devel=archiver.kernel.org@nongnu.org X-Spam-Status: No, score=-18.8 required=3.0 tests=BAYES_00,DKIMWL_WL_HIGH, DKIM_SIGNED,DKIM_VALID,DKIM_VALID_AU,HEADER_FROM_DIFFERENT_DOMAINS, INCLUDES_CR_TRAILER,INCLUDES_PATCH,MAILING_LIST_MULTI,SPF_HELO_PASS, SPF_PASS,USER_AGENT_GIT autolearn=ham autolearn_force=no version=4.0.1 X-Spam-Checker-Version: SpamAssassin 4.0.1 (2024-03-26) on aws-us-west-2-korg-listserv-alma10-1 From: Keith Monahan 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 --- 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 + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "qemu/osdep.h" +#include +#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 + * + * 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