shithub: neindaw

Download patch

ref: 72f3156830b1458e856c0a5171a4042e62d3b8ac
parent: d110794e66d44f71191c74a10a079a1951a893a6
author: Sigrid Haflínudóttir <[email protected]>
date: Wed Feb 26 14:48:11 EST 2020

add AY-3-8190 WIP

diff: cannot open b/ay38910//null: file does not exist: 'b/ay38910//null'
--- /dev/null
+++ b/ay38910/README.md
@@ -1,0 +1,12 @@
+# ay38910
+
+AY-3-8910 as a filesystem, part of neindaw. WIP.
+
+This is a prototype built to achieve the following:
+
+ * figure out ways to deal with multi-voice/polyphony and mixing
+ * create a UDP-to-neindaw translation layer, a companion
+   app that allows using neindaw with [Orca](https://github.com/hundredrabbits/Orca).
+ * visualizing waveforms
+
+AY-3-8910 is emulated using (floooh/chips)[https://github.com/floooh/chips].
--- /dev/null
+++ b/ay38910/ay38910.c
@@ -1,0 +1,193 @@
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include <libsec.h>
+#include <draw.h>
+#include <keyboard.h>
+#define CHIPS_IMPL
+#define CHIPS_ASSERT assert
+#include "ay38910.h"
+
+#define MIN(a,b) ((a)<=(b)?(a):(b))
+#define MAX(a,b) ((a)>=(b)?(a):(b))
+
+enum {
+	Tickhz = 2000000,
+
+	Levelenv = 1<<4,
+
+	Hold = 1<<0,
+	Alternate = 1<<1,
+	Attack = 1<<2,
+	Continue = 1<<3,
+};
+
+static void
+regw(ay38910_t *ay, int reg, int v)
+{
+	u64int p;
+
+	/* latch address */
+	p = AY38910_BDIR | AY38910_BC1;
+	AY38910_SET_DATA(p, reg);
+	ay38910_iorq(ay, p);
+
+	/* write to psg */
+	p = AY38910_BDIR;
+	AY38910_SET_DATA(p, v);
+	ay38910_iorq(ay, p);
+
+	/* inactive */
+	ay38910_iorq(ay, 0);
+}
+
+static int
+regr(ay38910_t *ay, int reg)
+{
+	u64int p;
+	int v;
+
+	/* latch address */
+	p = AY38910_BDIR | AY38910_BC1;
+	AY38910_SET_DATA(p, reg);
+	ay38910_iorq(ay, p);
+
+	/* read from psg */
+	v = AY38910_GET_DATA(ay38910_iorq(ay, AY38910_BC1));
+
+	/* inactive */
+	ay38910_iorq(ay, 0);
+
+	return v;
+}
+
+static int
+hz2tp(int hz)
+{
+	return MAX(1, MIN(4095, Tickhz / (MAX(1, hz) * 16)));
+}
+
+static int
+hz2ep(int hz)
+{
+	return MAX(1, MIN(65535, Tickhz / (MAX(1, hz) * 256)));
+}
+
+static int
+ms2ep(int ms)
+{
+	return MAX(1, MIN(65535, (Tickhz / 1000) * ms / 256));
+}
+
+static void
+tone(ay38910_t *ay, int chan, int hz)
+{
+	int tp;
+
+	tp = hz2tp(hz);
+	regw(ay, chan*2+0, tp & 0xff); /* fine */
+	regw(ay, chan*2+1, (tp>>8) & 0x0f); /* coarse */
+}
+
+static void
+toneon(ay38910_t *ay, int chan)
+{
+	regw(ay, AY38910_REG_ENABLE, regr(ay, AY38910_REG_ENABLE) & ~(1<<chan));
+}
+
+static void
+toneoff(ay38910_t *ay, int chan)
+{
+	regw(ay, AY38910_REG_ENABLE, regr(ay, AY38910_REG_ENABLE) | 1<<chan);
+}
+
+static void
+envp(ay38910_t *ay, int p)
+{
+	regw(ay, AY38910_REG_ENV_PERIOD_FINE, p & 0xff);
+	regw(ay, AY38910_REG_ENV_PERIOD_COARSE, p>>8);
+}
+
+static void
+envsc(ay38910_t *ay, int v)
+{
+	regw(ay, AY38910_REG_ENV_SHAPE_CYCLE, v & 0xf);
+}
+
+static void
+amp(ay38910_t *ay, int chan, int level)
+{
+	regw(ay, AY38910_REG_AMP_A+chan, level);
+}
+
+static ay38910_t *
+aynew(float mag)
+{
+	ay38910_desc_t d = {
+		.type = AY38910_TYPE_8910,
+		.tick_hz = Tickhz,
+		.sound_hz = 44100,
+		.magnitude = mag,
+	};
+	ay38910_t *ay;
+
+	ay = malloc(sizeof(*ay));
+	ay38910_init(ay, &d);
+	regw(ay, AY38910_REG_ENABLE, 0xff); /* disable everything */
+
+	return ay;
+}
+
+void
+threadmain(int argc, char **argv)
+{
+	u64int tick;
+	ay38910_t *ay[2];
+	int i, base, diff[] = {20, 200, 40, 220, 60};
+	float f[2], x;
+	bool havesample[2];
+
+	USED(argc); USED(argv);
+
+	ay[0] = aynew(1.0);
+	amp(ay[0], 0, Levelenv | 4);
+	envp(ay[0], ms2ep(200));
+	envsc(ay[0], Continue|Alternate);
+	toneon(ay[0], 0);
+
+	ay[1] = aynew(1.0);
+	amp(ay[1], 0, Levelenv | 4);
+	envp(ay[1], ms2ep(500));
+	envsc(ay[1], Continue|Attack);
+	toneon(ay[1], 0);
+	tone(ay[1], 0, 500);
+
+	base = 200;
+	for (i = 0, tick = 0;; tick++) {
+		/* 100Hz */
+		if ((tick % (Tickhz / 100)) == 0) {
+			if (i >= nelem(diff))
+				i = 0;
+			tone(ay[0], 0, base+diff[i++]);
+		}
+
+		if (!havesample[0] && ay38910_tick(ay[0])) {
+			f[0] = ay[0]->sample;
+			havesample[0] = true;
+		}
+		if (!havesample[1] && ay38910_tick(ay[1])) {
+			f[1] = ay[1]->sample;
+			havesample[1] = true;
+		}
+		if (havesample[0] && havesample[1]) {
+			havesample[0] = havesample[1] = false;
+			x = f[0] + f[1];
+			if (write(1, &x, sizeof(x)) < 0) 
+				break;
+		}
+	}
+
+	free(ay[0]);
+	free(ay[1]);
+	threadexitsall(nil);
+}
--- /dev/null
+++ b/ay38910/ay38910.h
@@ -1,0 +1,598 @@
+#pragma once
+/*
+    ay38910.h   -- AY-3-8910/2/3 sound chip emulator
+
+    Do this:
+        #define CHIPS_IMPL
+    before you include this file in *one* C or C++ file to create the 
+    implementation.
+
+    Optionally provde the following macros with your own implementation
+    
+    CHIPS_ASSERT(c)     -- your own assert macro (default: assert(c))
+
+    EMULATED PINS:
+
+             +-----------+
+      BC1 -->|           |<-> DA0
+     BDIR -->|           |...
+             |           |<-> DA7
+             |           |
+             |           |<-> (IOA0)
+             |           |...
+             |           |<-> (IOA7)
+             |           |
+             |           |<-> (IOB0)
+             |           |...
+             |           |<-> (IOB7)
+             +-----------+
+
+    NOT EMULATED:
+
+    - the BC2 pin is ignored since it makes only sense when connected to
+      a CP1610 CPU
+    - the RESET pin state is ignored, instead call ay38910_reset()
+
+    ## zlib/libpng license
+
+    Copyright (c) 2018 Andre Weissflog
+    This software is provided 'as-is', without any express or implied warranty.
+    In no event will the authors be held liable for any damages arising from the
+    use of this software.
+    Permission is granted to anyone to use this software for any purpose,
+    including commercial applications, and to alter it and redistribute it
+    freely, subject to the following restrictions:
+        1. The origin of this software must not be misrepresented; you must not
+        claim that you wrote the original software. If you use this software in a
+        product, an acknowledgment in the product documentation would be
+        appreciated but is not required.
+        2. Altered source versions must be plainly marked as such, and must not
+        be misrepresented as being the original software.
+        3. This notice may not be removed or altered from any source
+        distribution. 
+*/
+#include <stdint.h>
+#include <stdbool.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+    Pin definitions.
+
+    Note that the BC2 is not emulated since it is usually always
+    set to active when not connected to a CP1610 processor. The
+    remaining BDIR and BC1 pins are interpreted as follows:
+
+    |BDIR|BC1|
+    +----+---+
+    |  0 | 0 |  INACTIVE
+    |  0 | 1 |  READ FROM PSG
+    |  1 | 0 |  WRITE TO PSG
+    |  1 | 1 |  LATCH ADDRESS
+*/
+
+/* 8 bits data/address bus shared with CPU data bus */
+#define AY38910_DA0 (1ULL<<16)
+#define AY38910_DA1 (1ULL<<17)
+#define AY38910_DA2 (1ULL<<18)
+#define AY38910_DA3 (1ULL<<19)
+#define AY38910_DA4 (1ULL<<20)
+#define AY38910_DA5 (1ULL<<21)
+#define AY38910_DA6 (1ULL<<22)
+#define AY38910_DA7 (1ULL<<23)
+
+/* reset pin shared with CPU */
+#define AY38910_RESET   (1ULL<<34)
+
+/* chip-specific pins start at position 44 */
+#define AY38910_BDIR    (1ULL<<40)
+#define AY38910_BC1     (1ULL<<41)
+
+/* IO port pins */
+#define AY38910_IOA0    (1ULL<<48)
+#define AY38910_IOA1    (1ULL<<49)
+#define AY38910_IOA2    (1ULL<<50)
+#define AY38910_IOA3    (1ULL<<51)
+#define AY38910_IOA4    (1ULL<<52)
+#define AY38910_IOA5    (1ULL<<53)
+#define AY38910_IOA6    (1ULL<<54)
+#define AY38910_IOA7    (1ULL<<55)
+
+#define AY38910_IOB0    (1ULL<<56)
+#define AY38910_IOB1    (1ULL<<57)
+#define AY38910_IOB2    (1ULL<<58)
+#define AY38910_IOB3    (1ULL<<59)
+#define AY38910_IOB4    (1ULL<<60)
+#define AY38910_IOB5    (1ULL<<61)
+#define AY38910_IOB6    (1ULL<<62)
+#define AY38910_IOB7    (1ULL<<63)
+
+/* AY-3-8910 registers */
+#define AY38910_REG_PERIOD_A_FINE       (0)
+#define AY38910_REG_PERIOD_A_COARSE     (1)
+#define AY38910_REG_PERIOD_B_FINE       (2)
+#define AY38910_REG_PERIOD_B_COARSE     (3)
+#define AY38910_REG_PERIOD_C_FINE       (4)
+#define AY38910_REG_PERIOD_C_COARSE     (5)
+#define AY38910_REG_PERIOD_NOISE        (6)
+#define AY38910_REG_ENABLE              (7)
+#define AY38910_REG_AMP_A               (8)
+#define AY38910_REG_AMP_B               (9)
+#define AY38910_REG_AMP_C               (10)
+#define AY38910_REG_ENV_PERIOD_FINE     (11)
+#define AY38910_REG_ENV_PERIOD_COARSE   (12)
+#define AY38910_REG_ENV_SHAPE_CYCLE     (13)
+#define AY38910_REG_IO_PORT_A           (14)    /* not on AY-3-8913 */
+#define AY38910_REG_IO_PORT_B           (15)    /* not on AY-3-8912/3 */
+/* number of registers */
+#define AY38910_NUM_REGISTERS (16)
+/* error-accumulation precision boost */
+#define AY38910_FIXEDPOINT_SCALE (16)
+/* number of channels */
+#define AY38910_NUM_CHANNELS (3)
+/* DC adjustment buffer length */
+#define AY38910_DCADJ_BUFLEN (512)
+
+/* IO port names */
+#define AY38910_PORT_A (0)
+#define AY38910_PORT_B (1)
+
+/* envelope shape bits */
+#define AY38910_ENV_HOLD        (1<<0)
+#define AY38910_ENV_ALTERNATE   (1<<1)
+#define AY38910_ENV_ATTACK      (1<<2)
+#define AY38910_ENV_CONTINUE    (1<<3)
+
+/* callbacks for input/output on I/O ports */
+typedef uint8_t (*ay38910_in_t)(int port_id, void* user_data);
+typedef void (*ay38910_out_t)(int port_id, uint8_t data, void* user_data);
+
+/* chip subtypes */
+typedef enum {
+    AY38910_TYPE_8910 = 0,
+    AY38910_TYPE_8912,
+    AY38910_TYPE_8913
+} ay38910_type_t;
+
+/* setup parameters for ay38910_init() call */
+typedef struct {
+    ay38910_type_t type;    /* the subtype (default 0 is AY-3-8910) */
+    int tick_hz;            /* frequency at which ay38910_tick() will be called in Hz */
+    int sound_hz;           /* number of samples that will be produced per second */
+    float magnitude;        /* output sample magnitude, from 0.0 (silence) to 1.0 (max volume) */ 
+    ay38910_in_t in_cb;     /* I/O port input callback */
+    ay38910_out_t out_cb;   /* I/O port output callback */
+    void* user_data;        /* optional user-data for callbacks */
+} ay38910_desc_t;
+
+/* a tone channel */
+typedef struct {
+    uint16_t period;
+    uint16_t counter;
+    uint32_t bit;
+    uint32_t tone_disable;
+    uint32_t noise_disable;
+} ay38910_tone_t;
+
+/* the noise channel state */
+typedef struct {
+    uint16_t period;
+    uint16_t counter;
+    uint32_t rng;
+    uint32_t bit;
+} ay38910_noise_t;
+
+/* the envelope generator */
+typedef struct {
+    uint16_t period;
+    uint16_t counter;
+    bool shape_holding;
+    bool shape_hold;
+    uint8_t shape_counter;
+    uint8_t shape_state;
+} ay38910_env_t;
+
+/* AY-3-8910 state */
+typedef struct {
+    ay38910_type_t type;        /* the chip flavour */
+    ay38910_in_t in_cb;         /* the port-input callback */
+    ay38910_out_t out_cb;       /* the port-output callback */
+    void* user_data;            /* optional user-data for callbacks */
+    uint32_t tick;              /* a tick counter for internal clock division */
+    uint8_t addr;               /* 4-bit address latch */
+    union {                     /* the register bank */
+        uint8_t reg[AY38910_NUM_REGISTERS];
+        struct {
+            uint8_t period_a_fine;
+            uint8_t period_a_coarse;
+            uint8_t period_b_fine;
+            uint8_t period_b_coarse;
+            uint8_t period_c_fine;
+            uint8_t period_c_coarse;
+            uint8_t period_noise;
+            uint8_t enable;
+            uint8_t amp_a;
+            uint8_t amp_b;
+            uint8_t amp_c;
+            uint8_t period_env_fine;
+            uint8_t period_env_coarse;
+            uint8_t env_shape_cycle;
+            uint8_t port_a;
+            uint8_t port_b;
+        };
+    };
+    ay38910_tone_t tone[AY38910_NUM_CHANNELS];  /* the 3 tone channels */
+    ay38910_noise_t noise;                      /* the noise generator state */
+    ay38910_env_t env;                          /* the envelope generator state */
+    uint64_t pins;          /* last pin state for debug inspection */
+
+    /* sample generation state */
+    int sample_period;
+    int sample_counter;
+    float mag;
+    float sample;
+    float dcadj_sum;
+    uint32_t dcadj_pos;
+    float dcadj_buf[AY38910_DCADJ_BUFLEN];
+} ay38910_t;
+
+/* extract 8-bit data bus from 64-bit pins */
+#define AY38910_GET_DATA(p) ((uint8_t)(p>>16))
+/* merge 8-bit data bus value into 64-bit pins */
+#define AY38910_SET_DATA(p,d) {p=((p&~0xFF0000)|((d&0xFF)<<16));}
+/* set 8-bit port A data on 64-bit pin mask */
+#define AY38910_SET_PA(p,d) {p=((p&~0x00FF000000000000ULL)|((((uint64_t)d)&0xFFULL)<<48));}
+/* set 8-bit port B data on 64-bit pin mask */
+#define AY38910_SET_PB(p,d) {p=((p&~0xFF00000000000000ULL)|((((uint64_t)d)&0xFFULL)<<56));}
+
+/* initialize a AY-3-8910 instance */
+void ay38910_init(ay38910_t* ay, const ay38910_desc_t* desc);
+/* reset an existing AY-3-8910 instance */
+void ay38910_reset(ay38910_t* ay);
+/* perform an IO request machine cycle */
+uint64_t ay38910_iorq(ay38910_t* ay, uint64_t pins);
+/* tick the AY-3-8910, return true if a new sample is ready */
+bool ay38910_tick(ay38910_t* ay);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+/*-- IMPLEMENTATION ----------------------------------------------------------*/
+#ifdef CHIPS_IMPL
+#include <string.h>
+#ifndef CHIPS_ASSERT
+    #include <assert.h>
+    #define CHIPS_ASSERT(c) assert(c)
+#endif
+
+/* extract 8-bit data bus from 64-bit pins */
+#define AY38910_DATA(p) ((uint8_t)(p>>16))
+/* merge 8-bit data bus value into 64-bit pins */
+#define AY38910_SET_DATA(p,d) {p=((p&~0xFF0000)|((d&0xFF)<<16));}
+
+/* valid register content bitmasks */
+static const uint8_t _ay38910_reg_mask[AY38910_NUM_REGISTERS] = {
+    0xFF,       /* AY38910_REG_PERIOD_A_FINE */
+    0x0F,       /* AY38910_REG_PERIOD_A_COARSE */
+    0xFF,       /* AY38910_REG_PERIOD_B_FINE */
+    0x0F,       /* AY38910_REG_PERIOD_B_COARSE */
+    0xFF,       /* AY38910_REG_PERIOD_C_FINE */
+    0x0F,       /* AY38910_REG_PERIOD_C_COARSE */
+    0x1F,       /* AY38910_REG_PERIOD_NOISE */
+    0xFF,       /* AY38910_REG_ENABLE */
+    0x1F,       /* AY38910_REG_AMP_A (0..3: 4-bit volume, 4: use envelope) */
+    0x1F,       /* AY38910_REG_AMP_B (0..3: 4-bit volume, 4: use envelope) */
+    0x1F,       /* AY38910_REG_AMP_C (0..3: 4-bit volume, 4: use envelope) */
+    0xFF,       /* AY38910_REG_ENV_PERIOD_FINE */
+    0xFF,       /* AY38910_REG_ENV_PERIOD_COARSE */
+    0x0F,       /* AY38910_REG_ENV_SHAPE_CYCLE */
+    0xFF,       /* AY38910_REG_IO_PORT_A */
+    0xFF,       /* AY38910_REG_IO_PORT_B */
+};
+
+/* volume table from: https://github.com/true-grue/ayumi/blob/master/ayumi.c */
+static const float _ay38910_volumes[16] = {
+  0.0f,
+  0.00999465934234f,
+  0.0144502937362f,
+  0.0210574502174f,
+  0.0307011520562f,
+  0.0455481803616f,
+  0.0644998855573f,
+  0.107362478065f,
+  0.126588845655f,
+  0.20498970016f,
+  0.292210269322f,
+  0.372838941024f,
+  0.492530708782f,
+  0.635324635691f,
+  0.805584802014f,
+  1.0f
+};
+
+/* canned envelope generator shapes */
+static const uint8_t _ay38910_shapes[16][32] = {
+    /* CONTINUE ATTACK ALTERNATE HOLD */
+    /* 0 0 X X */
+    { 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
+    { 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
+    { 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
+    { 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
+    /* 0 1 X X */
+    { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
+    { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
+    { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
+    { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
+    /* 1 0 0 0 */
+    { 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 },
+    /* 1 0 0 1 */
+    { 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
+    /* 1 0 1 0 */
+    { 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 },
+    /* 1 0 1 1 */
+    { 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15 },
+    /* 1 1 0 0 */
+    { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 },
+    /* 1 1 0 1 */
+    { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15 },
+    /* 1 1 1 0 */
+    { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 },
+    /* 1 1 1 1 */
+    { 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
+};
+
+/* DC adjustment filter from StSound, this moves an "offcenter"
+   signal back to the zero-line (e.g. the volume-level output
+   from the chip simulation which is >0.0 gets converted to
+   a +/- sample value)
+*/
+static float _ay38910_dcadjust(ay38910_t* ay, float s) {
+    ay->dcadj_sum -= ay->dcadj_buf[ay->dcadj_pos];
+    ay->dcadj_sum += s;
+    ay->dcadj_buf[ay->dcadj_pos] = s;
+    ay->dcadj_pos = (ay->dcadj_pos + 1) & (AY38910_DCADJ_BUFLEN-1);
+    return s - (ay->dcadj_sum / AY38910_DCADJ_BUFLEN);
+}
+
+/* update computed values after registers have been reprogrammed */
+static void _ay38910_update_values(ay38910_t* ay) {
+    for (int i = 0; i < AY38910_NUM_CHANNELS; i++) {
+        ay38910_tone_t* chn = &ay->tone[i];
+        /* "...Note also that due to the design technique used in the Tone Period
+           count-down, the lowest period value is 000000000001 (divide by 1)
+           and the highest period value is 111111111111 (divide by 4095)
+        */
+        chn->period = (ay->reg[2*i+1]<<8)|(ay->reg[2*i]);
+        if (0 == chn->period) {
+            chn->period = 1;
+        }
+        /* a set 'enable bit' actually means 'disabled' */
+        chn->tone_disable = (ay->enable>>i) & 1;
+        chn->noise_disable = (ay->enable>>(3+i)) & 1;
+    }
+    /* noise generator values */
+    ay->noise.period = ay->period_noise;
+    if (ay->noise.period == 0) {
+        ay->noise.period = 1;
+    }
+    /* envelope generator values */
+    ay->env.period = (ay->period_env_coarse<<8)|ay->period_env_fine;
+    if (ay->env.period == 0) {
+        ay->env.period = 1;
+    }
+}
+
+/* reset the env shape generator, only called when env-shape register is updated */
+static void _ay38910_restart_env_shape(ay38910_t* ay) {
+    ay->env.shape_holding = false;
+    ay->env.shape_counter = 0;
+    if (!(ay->env_shape_cycle & AY38910_ENV_CONTINUE) || (ay->env_shape_cycle & AY38910_ENV_HOLD)) {
+        ay->env.shape_hold = true;
+    }
+    else {
+        ay->env.shape_hold = false;
+    }
+}
+
+void ay38910_init(ay38910_t* ay, const ay38910_desc_t* desc) {
+    CHIPS_ASSERT(ay && desc);
+    CHIPS_ASSERT(desc->tick_hz > 0);
+    CHIPS_ASSERT(desc->sound_hz > 0);
+    memset(ay, 0, sizeof(*ay));
+    /* note: input and output callbacks are optional */
+    ay->in_cb = desc->in_cb;
+    ay->out_cb = desc->out_cb;
+    ay->user_data = desc->user_data;
+    ay->type = desc->type;
+    ay->noise.rng = 1;
+    ay->sample_period = (desc->tick_hz * AY38910_FIXEDPOINT_SCALE) / desc->sound_hz;
+    ay->sample_counter = ay->sample_period;
+    ay->mag = desc->magnitude;
+    _ay38910_update_values(ay);
+    _ay38910_restart_env_shape(ay);
+}
+
+void ay38910_reset(ay38910_t* ay) {
+    CHIPS_ASSERT(ay);
+    ay->addr = 0;
+    ay->tick = 0;
+    for (int i = 0; i < AY38910_NUM_REGISTERS; i++) {
+        ay->reg[i] = 0;
+    }
+    _ay38910_update_values(ay);
+    _ay38910_restart_env_shape(ay);
+}
+
+bool ay38910_tick(ay38910_t* ay) {
+    ay->tick++;
+    if ((ay->tick & 7) == 0) {
+        /* tick the tone channels */
+        for (int i = 0; i < AY38910_NUM_CHANNELS; i++) {
+            ay38910_tone_t* chn = &ay->tone[i];
+            if (++chn->counter >= chn->period) {
+                chn->counter = 0;
+                chn->bit ^= 1;
+            }
+        }
+
+        /* tick the noise channel */
+        if (++ay->noise.counter >= ay->noise.period) {
+            ay->noise.counter = 0;
+            ay->noise.bit ^= 1;
+            if (ay->noise.bit) {
+                // random number generator from MAME:
+                // https://github.com/mamedev/mame/blob/master/src/devices/sound/ay8910.cpp
+                // The Random Number Generator of the 8910 is a 17-bit shift
+                // register. The input to the shift register is bit0 XOR bit3
+                // (bit0 is the output). This was verified on AY-3-8910 and YM2149 chips.
+                ay->noise.rng ^= (((ay->noise.rng & 1) ^ ((ay->noise.rng >> 3) & 1)) << 17);
+                ay->noise.rng >>= 1;
+            }
+        }
+    }
+
+    /* tick the envelope generator */
+    if ((ay->tick & 15) == 0) {
+        if (++ay->env.counter >= ay->env.period) {
+            ay->env.counter = 0;
+            if (!ay->env.shape_holding) {
+                ay->env.shape_counter = (ay->env.shape_counter + 1) & 0x1F;
+                if (ay->env.shape_hold && (0x1F == ay->env.shape_counter)) {
+                    ay->env.shape_holding = true;
+                }
+            }
+            ay->env.shape_state = _ay38910_shapes[ay->env_shape_cycle][ay->env.shape_counter];
+        }
+    }
+
+    /* generate new sample? */
+    ay->sample_counter -= AY38910_FIXEDPOINT_SCALE;
+    if (ay->sample_counter <= 0) {
+        ay->sample_counter += ay->sample_period;
+        float sm = 0.0f;
+        for (int i = 0; i < AY38910_NUM_CHANNELS; i++) {
+            const ay38910_tone_t* chn = &ay->tone[i];
+            float vol;
+            if (0 == (ay->reg[AY38910_REG_AMP_A+i] & (1<<4))) {
+                /* fixed amplitude */
+                vol = _ay38910_volumes[ay->reg[AY38910_REG_AMP_A+i] & 0x0F];
+            }
+            else {
+                /* envelope control */
+                vol = _ay38910_volumes[ay->env.shape_state];
+            }
+            int vol_enable = (chn->bit|chn->tone_disable) & ((ay->noise.rng&1)|(chn->noise_disable));
+            if (vol_enable) {
+                sm += vol;
+            }
+        }
+        ay->sample = _ay38910_dcadjust(ay, sm) * ay->mag;
+        return true;    /* new sample is ready */
+    }
+    /* fallthrough: no new sample ready yet */
+    return false;
+}
+
+uint64_t ay38910_iorq(ay38910_t* ay, uint64_t pins) {
+    if (pins & (AY38910_BDIR|AY38910_BC1)) {
+        if (pins & AY38910_BDIR) {
+            const uint8_t data = AY38910_DATA(pins);
+            if (pins & AY38910_BC1) {
+                /* latch address */
+                ay->addr = data;
+            }
+            else {
+                /* Write to register using the currently latched address.
+                   The whole 8-bit address is considered, the low 4 bits
+                   are the register index, and the upper bits are burned
+                   into the chip as a 'chip select' and are usually 0
+                   (this emulator assumes they are 0, so addresses greater
+                   are ignored for reading and writing)
+                */
+                if (ay->addr < AY38910_NUM_REGISTERS) {
+                    /* write register content, and update dependent values */
+                    ay->reg[ay->addr] = data & _ay38910_reg_mask[ay->addr];
+                    _ay38910_update_values(ay);
+                    if (ay->addr == AY38910_REG_ENV_SHAPE_CYCLE) {
+                        _ay38910_restart_env_shape(ay);
+                    }
+                    /* Handle port output:
+
+                        If port A or B is in output mode, call the
+                        port output callback to notify the outer world
+                        about the new register value.
+
+                        input/output mode is defined by bits 6 and 7 of
+                        the 'enable' register
+                            bit6 = 1: port A in output mode
+                            bit7 = 1: port B in output mode
+                    */
+                    else if (ay->addr == AY38910_REG_IO_PORT_A) {
+                        if (ay->enable & (1<<6)) {
+                            if (ay->out_cb) {
+                                ay->out_cb(AY38910_PORT_A, ay->port_a, ay->user_data);
+                            }
+                        }
+                    }
+                    else if (ay->addr == AY38910_REG_IO_PORT_B) {
+                        if (ay->enable & (1<<7)) {
+                            if (ay->out_cb) {
+                                ay->out_cb(AY38910_PORT_B, ay->port_b, ay->user_data);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        else {
+            /* Read from register using the currently latched address.
+               See 'write' for why the latched address must be in the
+               valid register range to have an effect.
+            */
+            if (ay->addr < AY38910_NUM_REGISTERS) {
+                /* Handle port input:
+
+                    If port A or B is in input mode, first call the port
+                    input callback to update the port register content.
+
+                    input/output mode is defined by bits 6 and 7 of
+                    the 'enable' register:
+                        bit6 = 0: port A in input mode
+                        bit7 = 0: port B in input mode
+                */
+                if (ay->addr == AY38910_REG_IO_PORT_A) {
+                    if ((ay->enable & (1<<6)) == 0) {
+                        if (ay->in_cb) {
+                            ay->port_a = ay->in_cb(AY38910_PORT_A, ay->user_data);
+                        }
+                        else {
+                            ay->port_a = 0xFF;
+                        }
+                    }
+                }
+                else if (ay->addr == AY38910_REG_IO_PORT_B) {
+                    if ((ay->enable & (1<<7)) == 0) {
+                        if (ay->in_cb) {
+                            ay->port_b = ay->in_cb(AY38910_PORT_B, ay->user_data);
+                        }
+                        else {
+                            ay->port_b = 0xFF;
+                        }
+                    }
+                }
+                /* read register content into data pins */
+                const uint8_t data = ay->reg[ay->addr];
+                AY38910_SET_DATA(pins, data);
+            }
+        }
+        AY38910_SET_PA(pins, ay->port_a);
+        AY38910_SET_PB(pins, ay->port_b);
+        ay->pins = pins;
+    }
+    return pins;
+}
+
+#endif /* CHIPS_IMPL */
--- /dev/null
+++ b/ay38910/mkfile
@@ -1,0 +1,18 @@
+</$objtype/mkfile
+
+TARG=ay38910
+CFLAGS=$CFLAGS -p
+BIN=/$objtype/bin/audio
+MAN=/sys/man/1
+
+HFILES=\
+	ay38910.h\
+
+OFILES=\
+	ay38910.$O\
+
+default:V:	all
+
+</sys/src/cmd/mkone
+
+install:V: $MAN/$TARG
--- /dev/null
+++ b/ay38910/stdbool.h
@@ -1,0 +1,1 @@
+typedef enum { false, true } bool;
--- /dev/null
+++ b/ay38910/stdint.h
@@ -1,0 +1,4 @@
+typedef u8int uint8_t;
+typedef u16int uint16_t;
+typedef u32int uint32_t;
+typedef u64int uint64_t;