adafruit-circuitpython-ay8912 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- adafruit_ay8912/ay8912_emulator.py +459 -0
- adafruit_ay8912/vgm_player.py +416 -0
- adafruit_circuitpython_ay8912-1.0.0.dist-info/METADATA +151 -0
- adafruit_circuitpython_ay8912-1.0.0.dist-info/RECORD +7 -0
- adafruit_circuitpython_ay8912-1.0.0.dist-info/WHEEL +5 -0
- adafruit_circuitpython_ay8912-1.0.0.dist-info/licenses/LICENSE +21 -0
- adafruit_circuitpython_ay8912-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026 Liz Clark for Adafruit Industries
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
"""
|
|
5
|
+
:py:class:`~adafruit_ay8912.ay8912_emulator.AY8912`
|
|
6
|
+
================================================================================
|
|
7
|
+
|
|
8
|
+
AY-3-8910 / AY-8912 emulator for CircuitPython with :py:mod:`synthio`.
|
|
9
|
+
|
|
10
|
+
Translates AY-3-8910 register writes into ``synthio.Note`` property updates. Three
|
|
11
|
+
synthesizers (one per channel) feed an :py:class:`audiomixer.Mixer` for
|
|
12
|
+
per-channel volume and stereo panning.
|
|
13
|
+
|
|
14
|
+
* Author(s): Liz Clark
|
|
15
|
+
|
|
16
|
+
Implementation Notes
|
|
17
|
+
--------------------
|
|
18
|
+
|
|
19
|
+
**Software and Dependencies:**
|
|
20
|
+
|
|
21
|
+
* Adafruit CircuitPython firmware for the supported boards:
|
|
22
|
+
https://circuitpython.org/downloads
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import array
|
|
26
|
+
from random import randint
|
|
27
|
+
|
|
28
|
+
import audiomixer
|
|
29
|
+
import synthio
|
|
30
|
+
import ulab.numpy as np
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
from typing import Tuple
|
|
34
|
+
except ImportError:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
__version__ = "1.0.0"
|
|
38
|
+
__repo__ = "https://github.com/your-org/Adafruit_CircuitPython_AY8912_Emulator.git"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AY8912:
|
|
42
|
+
"""AY-3-8910 / AY-8912 emulator backed by :py:mod:`synthio`.
|
|
43
|
+
|
|
44
|
+
:param int sample_rate: Output sample rate in Hz.
|
|
45
|
+
:param int clock_rate: AY chip clock frequency in Hz. Defaults to the ZX
|
|
46
|
+
Spectrum 128K clock (1773400 Hz).
|
|
47
|
+
:param int waveform_size: Number of samples in the square waveform.
|
|
48
|
+
:param int noise_size: Number of samples in the noise waveform.
|
|
49
|
+
:param int volume: Peak sample amplitude used when building the waveforms.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
# AY DAC volume table -- 16 levels
|
|
53
|
+
DAC = (
|
|
54
|
+
0.0,
|
|
55
|
+
0.00999,
|
|
56
|
+
0.01445,
|
|
57
|
+
0.02106,
|
|
58
|
+
0.03070,
|
|
59
|
+
0.04555,
|
|
60
|
+
0.06450,
|
|
61
|
+
0.10736,
|
|
62
|
+
0.12659,
|
|
63
|
+
0.20499,
|
|
64
|
+
0.29221,
|
|
65
|
+
0.37284,
|
|
66
|
+
0.49253,
|
|
67
|
+
0.63532,
|
|
68
|
+
0.80558,
|
|
69
|
+
1.0,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Envelope segment functions per shape: (segment0, segment1)
|
|
73
|
+
# 0=decay (start high), 1=attack (start low), 2=hold, 3=hold-alt
|
|
74
|
+
_ENV_SEG = (
|
|
75
|
+
(0, 2),
|
|
76
|
+
(0, 2),
|
|
77
|
+
(0, 2),
|
|
78
|
+
(0, 2), # shapes 0-3: \___
|
|
79
|
+
(1, 2),
|
|
80
|
+
(1, 2),
|
|
81
|
+
(1, 2),
|
|
82
|
+
(1, 2), # shapes 4-7: /|__
|
|
83
|
+
(0, 0),
|
|
84
|
+
(0, 2),
|
|
85
|
+
(0, 1),
|
|
86
|
+
(0, 3), # shapes 8-11: \\\\ \___ \/\/ \^^^
|
|
87
|
+
(1, 1),
|
|
88
|
+
(1, 3),
|
|
89
|
+
(1, 0),
|
|
90
|
+
(1, 2), # shapes 12-15: //// /^^^ /\/\ /|__
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
sample_rate: int = 22050,
|
|
96
|
+
clock_rate: int = 1773400,
|
|
97
|
+
waveform_size: int = 256,
|
|
98
|
+
noise_size: int = 256,
|
|
99
|
+
volume: int = 32000,
|
|
100
|
+
) -> None:
|
|
101
|
+
self._clock = clock_rate
|
|
102
|
+
self._sample_rate = sample_rate
|
|
103
|
+
|
|
104
|
+
self._regs = bytearray(16)
|
|
105
|
+
|
|
106
|
+
self._square = array.array(
|
|
107
|
+
"h",
|
|
108
|
+
[volume] * (waveform_size // 2) + [-volume] * (waveform_size // 2),
|
|
109
|
+
)
|
|
110
|
+
self._noise = np.array(
|
|
111
|
+
[randint(-volume, volume) for _ in range(noise_size)],
|
|
112
|
+
dtype=np.int16,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
self._synths = []
|
|
116
|
+
self._notes = []
|
|
117
|
+
for _ in range(3):
|
|
118
|
+
s = synthio.Synthesizer(sample_rate=sample_rate, channel_count=2)
|
|
119
|
+
n = synthio.Note(
|
|
120
|
+
frequency=440.0,
|
|
121
|
+
waveform=self._square,
|
|
122
|
+
amplitude=1.0,
|
|
123
|
+
)
|
|
124
|
+
s.press(n)
|
|
125
|
+
self._synths.append(s)
|
|
126
|
+
self._notes.append(n)
|
|
127
|
+
|
|
128
|
+
self._mixer = audiomixer.Mixer(
|
|
129
|
+
voice_count=3,
|
|
130
|
+
sample_rate=sample_rate,
|
|
131
|
+
channel_count=2,
|
|
132
|
+
buffer_size=2048,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
self._started = False
|
|
136
|
+
|
|
137
|
+
self._tone_on = [False, False, False]
|
|
138
|
+
self._noise_on = [False, False, False]
|
|
139
|
+
self._env_enabled = [False, False, False]
|
|
140
|
+
|
|
141
|
+
self._env_shape = 0
|
|
142
|
+
self._env_period = 1
|
|
143
|
+
self._env_level = 0 # 0..31
|
|
144
|
+
self._env_segment = 0 # 0 or 1
|
|
145
|
+
self._env_accumulator = 0.0
|
|
146
|
+
self._env_steps_per_tick = 0.0
|
|
147
|
+
self._compute_env_rate()
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def mixer(self) -> "audiomixer.Mixer":
|
|
151
|
+
"""The :py:class:`audiomixer.Mixer`. Connect to audio output
|
|
152
|
+
with ``audio.play(ay.mixer)`` (or just call :meth:`begin`)."""
|
|
153
|
+
return self._mixer
|
|
154
|
+
|
|
155
|
+
def begin(self, audio_out) -> None:
|
|
156
|
+
"""Begin audio signal chain.
|
|
157
|
+
|
|
158
|
+
Handles ``audio_out.play(mixer)`` first, then connects the synth voices::
|
|
159
|
+
|
|
160
|
+
ay = AY8912(sample_rate=22050)
|
|
161
|
+
ay.begin(audio)
|
|
162
|
+
|
|
163
|
+
:param audio_out: The audio output object
|
|
164
|
+
"""
|
|
165
|
+
audio_out.play(self._mixer)
|
|
166
|
+
|
|
167
|
+
for i in range(3):
|
|
168
|
+
self._mixer.voice[i].play(self._synths[i], loop=True)
|
|
169
|
+
self._mixer.voice[i].level = 0.0
|
|
170
|
+
|
|
171
|
+
# Default ACB stereo panning
|
|
172
|
+
self._mixer.voice[0].panning = -0.4 # A -> left
|
|
173
|
+
self._mixer.voice[1].panning = 0.0 # B -> center
|
|
174
|
+
self._mixer.voice[2].panning = 0.4 # C -> right
|
|
175
|
+
|
|
176
|
+
self._started = True
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def notes(self) -> "Tuple[synthio.Note, ...]":
|
|
180
|
+
"""Direct access to the three ``synthio.Note`` objects (read-only
|
|
181
|
+
tuple)."""
|
|
182
|
+
return tuple(self._notes)
|
|
183
|
+
|
|
184
|
+
def read_register(self, reg: int) -> int:
|
|
185
|
+
"""Read an AY register.
|
|
186
|
+
|
|
187
|
+
:param int reg: Register index (0-15).
|
|
188
|
+
:return: The stored register value, or 0 if ``reg`` is out of range.
|
|
189
|
+
"""
|
|
190
|
+
if reg > 15:
|
|
191
|
+
return 0
|
|
192
|
+
return self._regs[reg]
|
|
193
|
+
|
|
194
|
+
def write_register(self, reg: int, value: int) -> None:
|
|
195
|
+
"""Write an AY register (0-13). R14/R15 (I/O ports) are stored but
|
|
196
|
+
otherwise ignored.
|
|
197
|
+
|
|
198
|
+
:param int reg: Register index (0-15). Out-of-range writes are ignored.
|
|
199
|
+
:param int value: Byte value to write (masked to 0-255).
|
|
200
|
+
"""
|
|
201
|
+
if reg > 15:
|
|
202
|
+
return
|
|
203
|
+
value &= 0xFF
|
|
204
|
+
self._regs[reg] = value
|
|
205
|
+
self._apply_reg(reg)
|
|
206
|
+
|
|
207
|
+
def set_pan(self, channel: int, pan: float) -> None:
|
|
208
|
+
"""Stereo panning for a channel.
|
|
209
|
+
|
|
210
|
+
:param int channel: Channel index (0-2).
|
|
211
|
+
:param float pan: Pan position: ``-1.0`` = hard left, ``0.0`` = center,
|
|
212
|
+
``1.0`` = hard right. Values are clamped to that range.
|
|
213
|
+
"""
|
|
214
|
+
if 0 <= channel <= 2:
|
|
215
|
+
self._mixer.voice[channel].panning = max(-1.0, min(1.0, pan))
|
|
216
|
+
|
|
217
|
+
def set_tone_period(self, channel: int, period: int) -> None:
|
|
218
|
+
"""The 12-bit tone period directly (R0/R1, R2/R3, R4/R5).
|
|
219
|
+
|
|
220
|
+
:param int channel: Channel index (0-2).
|
|
221
|
+
:param int period: Tone period, clamped to 1-4095.
|
|
222
|
+
"""
|
|
223
|
+
if channel > 2:
|
|
224
|
+
return
|
|
225
|
+
period = max(1, min(4095, period))
|
|
226
|
+
r = channel * 2
|
|
227
|
+
self.write_register(r, period & 0xFF)
|
|
228
|
+
self.write_register(r + 1, (period >> 8) & 0x0F)
|
|
229
|
+
|
|
230
|
+
def set_noise_period(self, period: int) -> None:
|
|
231
|
+
"""The 5-bit noise period (R6).
|
|
232
|
+
|
|
233
|
+
:param int period: Noise period (only the low 5 bits are used).
|
|
234
|
+
"""
|
|
235
|
+
self.write_register(6, period & 0x1F)
|
|
236
|
+
|
|
237
|
+
def set_volume(self, channel: int, volume: int, envelope: bool = False) -> None:
|
|
238
|
+
"""Set a channel's volume (R8/R9/R10).
|
|
239
|
+
|
|
240
|
+
:param int channel: Channel index (0-2).
|
|
241
|
+
:param int volume: Fixed volume level (0-15).
|
|
242
|
+
:param bool envelope: If ``True``, the hardware envelope controls the
|
|
243
|
+
channel volume instead of the fixed level.
|
|
244
|
+
"""
|
|
245
|
+
if channel > 2:
|
|
246
|
+
return
|
|
247
|
+
val = min(15, volume) & 0x0F
|
|
248
|
+
if envelope:
|
|
249
|
+
val |= 0x10
|
|
250
|
+
self.write_register(8 + channel, val)
|
|
251
|
+
|
|
252
|
+
def enable_tone(self, channel: int, enable: bool = True) -> None:
|
|
253
|
+
"""Enable or disable tone output for a channel via the mixer (R7).
|
|
254
|
+
|
|
255
|
+
:param int channel: Channel index (0-2).
|
|
256
|
+
:param bool enable: ``True`` to enable tone, ``False`` to disable.
|
|
257
|
+
"""
|
|
258
|
+
if channel > 2:
|
|
259
|
+
return
|
|
260
|
+
bit = 1 << channel
|
|
261
|
+
mixer = self._regs[7]
|
|
262
|
+
if enable:
|
|
263
|
+
mixer &= ~bit # bit=0 means ON in the AY mixer
|
|
264
|
+
else:
|
|
265
|
+
mixer |= bit
|
|
266
|
+
self.write_register(7, mixer)
|
|
267
|
+
|
|
268
|
+
def enable_noise(self, channel: int, enable: bool = True) -> None:
|
|
269
|
+
"""Enable or disable noise output for a channel via the mixer (R7).
|
|
270
|
+
|
|
271
|
+
:param int channel: Channel index (0-2).
|
|
272
|
+
:param bool enable: ``True`` to enable noise, ``False`` to disable.
|
|
273
|
+
"""
|
|
274
|
+
if channel > 2:
|
|
275
|
+
return
|
|
276
|
+
bit = 1 << (channel + 3)
|
|
277
|
+
mixer = self._regs[7]
|
|
278
|
+
if enable:
|
|
279
|
+
mixer &= ~bit
|
|
280
|
+
else:
|
|
281
|
+
mixer |= bit
|
|
282
|
+
self.write_register(7, mixer)
|
|
283
|
+
|
|
284
|
+
def set_envelope(self, period: int, shape: int) -> None:
|
|
285
|
+
"""Set the envelope period (R11/R12) and shape (R13).
|
|
286
|
+
|
|
287
|
+
Writing R13 resets the envelope generator.
|
|
288
|
+
|
|
289
|
+
:param int period: 16-bit envelope period.
|
|
290
|
+
:param int shape: Envelope shape (0-15).
|
|
291
|
+
"""
|
|
292
|
+
self.write_register(11, period & 0xFF)
|
|
293
|
+
self.write_register(12, (period >> 8) & 0xFF)
|
|
294
|
+
self.write_register(13, shape & 0x0F)
|
|
295
|
+
|
|
296
|
+
def reset(self) -> None:
|
|
297
|
+
"""Reset all registers and state to power-on defaults."""
|
|
298
|
+
self._regs = bytearray(16)
|
|
299
|
+
self._env_level = 0
|
|
300
|
+
self._env_segment = 0
|
|
301
|
+
self._env_shape = 0
|
|
302
|
+
self._env_period = 1
|
|
303
|
+
self._env_accumulator = 0.0
|
|
304
|
+
self._compute_env_rate()
|
|
305
|
+
|
|
306
|
+
for ch in range(3):
|
|
307
|
+
self._notes[ch].frequency = 440.0
|
|
308
|
+
self._notes[ch].amplitude = 1.0
|
|
309
|
+
self._notes[ch].waveform = self._square
|
|
310
|
+
self._notes[ch].ring_waveform = None
|
|
311
|
+
self._mixer.voice[ch].level = 0.0
|
|
312
|
+
self._tone_on[ch] = False
|
|
313
|
+
self._noise_on[ch] = False
|
|
314
|
+
self._env_enabled[ch] = False
|
|
315
|
+
|
|
316
|
+
def tick(self) -> None:
|
|
317
|
+
"""Advance the envelope generator.
|
|
318
|
+
|
|
319
|
+
Call this at ~50 Hz from your main loop or a timer interrupt.
|
|
320
|
+
"""
|
|
321
|
+
self._env_accumulator += self._env_steps_per_tick
|
|
322
|
+
while self._env_accumulator >= 1.0:
|
|
323
|
+
self._env_accumulator -= 1.0
|
|
324
|
+
self._step_envelope()
|
|
325
|
+
|
|
326
|
+
for ch in range(3):
|
|
327
|
+
if self._env_enabled[ch]:
|
|
328
|
+
self._apply_volume(ch)
|
|
329
|
+
|
|
330
|
+
def _apply_reg(self, reg: int) -> None:
|
|
331
|
+
"""React to a register write."""
|
|
332
|
+
if reg <= 5:
|
|
333
|
+
# Tone period (R0/R1 -> ch0, R2/R3 -> ch1, R4/R5 -> ch2)
|
|
334
|
+
ch = reg // 2
|
|
335
|
+
self._update_channel_freq(ch)
|
|
336
|
+
|
|
337
|
+
elif reg == 6:
|
|
338
|
+
# Noise period -- update noise frequency on relevant channels
|
|
339
|
+
self._update_mixer_state()
|
|
340
|
+
|
|
341
|
+
elif reg == 7:
|
|
342
|
+
# Mixer enable bits
|
|
343
|
+
self._update_mixer_state()
|
|
344
|
+
|
|
345
|
+
elif 8 <= reg <= 10:
|
|
346
|
+
# Volume / envelope-enable
|
|
347
|
+
ch = reg - 8
|
|
348
|
+
vreg = self._regs[reg]
|
|
349
|
+
self._env_enabled[ch] = bool(vreg & 0x10)
|
|
350
|
+
self._apply_volume(ch)
|
|
351
|
+
|
|
352
|
+
elif reg in {11, 12}:
|
|
353
|
+
# Envelope period
|
|
354
|
+
self._env_period = self._regs[11] | (self._regs[12] << 8)
|
|
355
|
+
self._env_period = max(self._env_period, 1)
|
|
356
|
+
self._compute_env_rate()
|
|
357
|
+
|
|
358
|
+
elif reg == 13:
|
|
359
|
+
# Envelope shape -- reset envelope generator
|
|
360
|
+
self._env_shape = self._regs[13] & 0x0F
|
|
361
|
+
self._env_segment = 0
|
|
362
|
+
self._env_accumulator = 0.0
|
|
363
|
+
self._env_reset_segment()
|
|
364
|
+
self._compute_env_rate()
|
|
365
|
+
|
|
366
|
+
def _period_to_hz(self, period: int) -> float:
|
|
367
|
+
"""Convert an AY tone/noise period to Hz, clamped to synthio's valid
|
|
368
|
+
range of 0-32767 Hz."""
|
|
369
|
+
period = max(period, 1)
|
|
370
|
+
freq = self._clock / (16.0 * period)
|
|
371
|
+
freq = min(freq, 32767.0)
|
|
372
|
+
freq = max(freq, 1.0)
|
|
373
|
+
return freq
|
|
374
|
+
|
|
375
|
+
def _update_channel_freq(self, ch: int) -> None:
|
|
376
|
+
"""Set a note's frequency from the current tone registers, but only if
|
|
377
|
+
tone is active on that channel."""
|
|
378
|
+
if self._tone_on[ch]:
|
|
379
|
+
lo = self._regs[ch * 2]
|
|
380
|
+
hi = self._regs[ch * 2 + 1] & 0x0F
|
|
381
|
+
period = lo | (hi << 8)
|
|
382
|
+
period = max(period, 1)
|
|
383
|
+
self._notes[ch].frequency = self._period_to_hz(period)
|
|
384
|
+
|
|
385
|
+
def _update_mixer_state(self) -> None:
|
|
386
|
+
"""Read the mixer register (R7) and update waveforms & frequencies."""
|
|
387
|
+
mixer_reg = self._regs[7]
|
|
388
|
+
noise_period = self._regs[6] & 0x1F
|
|
389
|
+
noise_period = max(noise_period, 1)
|
|
390
|
+
noise_hz = self._period_to_hz(noise_period)
|
|
391
|
+
|
|
392
|
+
for ch in range(3):
|
|
393
|
+
tone_on = not bool(mixer_reg & (1 << ch))
|
|
394
|
+
noise_on = not bool(mixer_reg & (1 << (ch + 3)))
|
|
395
|
+
|
|
396
|
+
self._tone_on[ch] = tone_on
|
|
397
|
+
self._noise_on[ch] = noise_on
|
|
398
|
+
|
|
399
|
+
if tone_on and noise_on:
|
|
400
|
+
self._notes[ch].waveform = self._square
|
|
401
|
+
self._notes[ch].ring_waveform = self._noise
|
|
402
|
+
self._notes[ch].ring_frequency = noise_hz
|
|
403
|
+
self._update_channel_freq(ch)
|
|
404
|
+
|
|
405
|
+
elif tone_on:
|
|
406
|
+
self._notes[ch].waveform = self._square
|
|
407
|
+
self._notes[ch].ring_waveform = None
|
|
408
|
+
self._update_channel_freq(ch)
|
|
409
|
+
|
|
410
|
+
elif noise_on:
|
|
411
|
+
self._notes[ch].waveform = self._noise
|
|
412
|
+
self._notes[ch].ring_waveform = None
|
|
413
|
+
self._notes[ch].frequency = noise_hz
|
|
414
|
+
|
|
415
|
+
self._apply_volume(ch)
|
|
416
|
+
|
|
417
|
+
def _apply_volume(self, ch: int) -> None:
|
|
418
|
+
"""Set the mixer voice level for a channel based on current state."""
|
|
419
|
+
# If the channel is fully off (no tone, no noise), silence it
|
|
420
|
+
if not self._tone_on[ch] and not self._noise_on[ch]:
|
|
421
|
+
self._mixer.voice[ch].level = 0.0
|
|
422
|
+
return
|
|
423
|
+
|
|
424
|
+
if self._env_enabled[ch]:
|
|
425
|
+
# Envelope: 0-31 mapped to the 16-entry DAC (pairs)
|
|
426
|
+
self._mixer.voice[ch].level = self.DAC[self._env_level >> 1]
|
|
427
|
+
else:
|
|
428
|
+
vol = self._regs[8 + ch] & 0x0F
|
|
429
|
+
self._mixer.voice[ch].level = self.DAC[vol]
|
|
430
|
+
|
|
431
|
+
def _compute_env_rate(self) -> None:
|
|
432
|
+
"""Compute how many envelope steps to advance per 50 Hz tick."""
|
|
433
|
+
# The AY envelope steps once per (256 * period) clock cycles:
|
|
434
|
+
# steps_per_second = clock / (256 * period)
|
|
435
|
+
# steps_per_tick = steps_per_second / 50
|
|
436
|
+
self._env_steps_per_tick = self._clock / (256.0 * self._env_period * 50.0)
|
|
437
|
+
|
|
438
|
+
def _env_reset_segment(self) -> None:
|
|
439
|
+
"""Set the envelope level to the starting value for the current
|
|
440
|
+
segment."""
|
|
441
|
+
func = self._ENV_SEG[self._env_shape][self._env_segment & 1]
|
|
442
|
+
# Decay (0) and hold-alt (3) start at max; attack (1) and hold (2)
|
|
443
|
+
# start at 0.
|
|
444
|
+
self._env_level = 31 if func in {0, 3} else 0
|
|
445
|
+
|
|
446
|
+
def _step_envelope(self) -> None:
|
|
447
|
+
"""Advance the envelope by one step."""
|
|
448
|
+
func = self._ENV_SEG[self._env_shape][self._env_segment & 1]
|
|
449
|
+
if func == 0: # Decay
|
|
450
|
+
self._env_level -= 1
|
|
451
|
+
if self._env_level < 0:
|
|
452
|
+
self._env_segment ^= 1
|
|
453
|
+
self._env_reset_segment()
|
|
454
|
+
elif func == 1: # Attack
|
|
455
|
+
self._env_level += 1
|
|
456
|
+
if self._env_level > 31:
|
|
457
|
+
self._env_segment ^= 1
|
|
458
|
+
self._env_reset_segment()
|
|
459
|
+
# func 2 & 3 = hold -- do nothing
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026 Liz Clark for Adafruit Industries
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
"""
|
|
5
|
+
:py:class:`~adafruit_ay8912.vgm_player.VGMFile`
|
|
6
|
+
================================================================================
|
|
7
|
+
|
|
8
|
+
VGM chiptune file parser and player for CircuitPython.
|
|
9
|
+
|
|
10
|
+
Plays VGM (and gzip-compressed VGZ) files through an
|
|
11
|
+
:py:class:`~adafruit_ay8912.ay8912_emulator.AY8912` instance.
|
|
12
|
+
|
|
13
|
+
Only files with a non-zero AY-3-8910 clock (header offset ``0x74``) are
|
|
14
|
+
supported. Files targeting other chips (SN76489, YM2612, etc.) are rejected.
|
|
15
|
+
|
|
16
|
+
* Author(s): Liz Clark
|
|
17
|
+
|
|
18
|
+
Implementation Notes
|
|
19
|
+
--------------------
|
|
20
|
+
|
|
21
|
+
**Software and Dependencies:**
|
|
22
|
+
|
|
23
|
+
* Adafruit CircuitPython firmware for the supported boards:
|
|
24
|
+
https://circuitpython.org/downloads
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import struct
|
|
28
|
+
import time
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
from typing import TYPE_CHECKING, Optional, Tuple
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from .ay8912_emulator import AY8912
|
|
35
|
+
except ImportError:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
import zlib
|
|
40
|
+
except ImportError:
|
|
41
|
+
zlib = None
|
|
42
|
+
|
|
43
|
+
__version__ = "1.0.0"
|
|
44
|
+
__repo__ = "https://github.com/your-org/Adafruit_CircuitPython_AY8912.git"
|
|
45
|
+
|
|
46
|
+
# VGM runs on a 44100 Hz sample clock for all wait commands
|
|
47
|
+
_VGM_RATE = 44100
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class VGMFile:
|
|
51
|
+
"""Parse and play an AY8912 VGM/VGZ file.
|
|
52
|
+
|
|
53
|
+
:param str filename: Path to a ``.vgm`` or ``.vgz`` file to load
|
|
54
|
+
immediately. Pass ``None`` to create an empty object and call
|
|
55
|
+
:meth:`load` later.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, filename: Optional[str] = None) -> None:
|
|
59
|
+
self._data = None
|
|
60
|
+
self._ay = None
|
|
61
|
+
|
|
62
|
+
# Metadata
|
|
63
|
+
self._title = ""
|
|
64
|
+
self._author = ""
|
|
65
|
+
self._game = ""
|
|
66
|
+
|
|
67
|
+
# Header info
|
|
68
|
+
self._clock_hz = 1773400
|
|
69
|
+
self._total_samples = 0
|
|
70
|
+
self._loop_offset = 0
|
|
71
|
+
self._loop_samples = 0
|
|
72
|
+
self._data_offset = 0
|
|
73
|
+
self._eof_offset = 0
|
|
74
|
+
self._version = 0
|
|
75
|
+
|
|
76
|
+
# Playback state
|
|
77
|
+
self._pos = 0 # current byte position in command stream
|
|
78
|
+
self._playing = False
|
|
79
|
+
self._sample_clock = 0 # cumulative samples played
|
|
80
|
+
self._wait_remaining = 0 # samples still to wait
|
|
81
|
+
self._next_time = 0.0 # monotonic time of next command batch
|
|
82
|
+
self._loop_count = 0 # how many times we've looped
|
|
83
|
+
|
|
84
|
+
if filename:
|
|
85
|
+
self.load(filename)
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def title(self) -> str:
|
|
89
|
+
"""Track title from the GD3 tag, or ``""`` if absent."""
|
|
90
|
+
return self._title
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def author(self) -> str:
|
|
94
|
+
"""Track author from the GD3 tag, or ``""`` if absent."""
|
|
95
|
+
return self._author
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def game(self) -> str:
|
|
99
|
+
"""Game/source name from the GD3 tag, or ``""`` if absent."""
|
|
100
|
+
return self._game
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def clock_hz(self) -> int:
|
|
104
|
+
"""AY chip clock in Hz."""
|
|
105
|
+
return self._clock_hz
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def version(self) -> int:
|
|
109
|
+
"""VGM format version as a packed BCD integer"""
|
|
110
|
+
return self._version
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def total_samples(self) -> int:
|
|
114
|
+
"""Total length of the song in 44100 Hz samples."""
|
|
115
|
+
return self._total_samples
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def duration(self) -> float:
|
|
119
|
+
"""Song duration in seconds."""
|
|
120
|
+
if self._total_samples:
|
|
121
|
+
return self._total_samples / _VGM_RATE
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def elapsed(self) -> float:
|
|
126
|
+
"""Elapsed playback time in seconds."""
|
|
127
|
+
return self._sample_clock / _VGM_RATE
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def progress(self) -> float:
|
|
131
|
+
"""Playback progress as a fraction from ``0.0`` to ``1.0``."""
|
|
132
|
+
if self._total_samples:
|
|
133
|
+
p = self._sample_clock / self._total_samples
|
|
134
|
+
return p if p < 1.0 else 1.0
|
|
135
|
+
return 0
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def loop_count(self) -> int:
|
|
139
|
+
"""How many times the song has looped back."""
|
|
140
|
+
return self._loop_count
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def loops(self) -> bool:
|
|
144
|
+
"""``True`` if this song defines a loop point."""
|
|
145
|
+
return bool(self._loop_offset and self._loop_samples)
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def playing(self) -> bool:
|
|
149
|
+
"""``True`` while playback is active."""
|
|
150
|
+
return self._playing
|
|
151
|
+
|
|
152
|
+
def load(self, filename: str) -> None:
|
|
153
|
+
"""Load a ``.vgm`` or ``.vgz`` file, decompressing gzip natively.
|
|
154
|
+
|
|
155
|
+
:param str filename: Path to the file to load.
|
|
156
|
+
:raises RuntimeError: If the file is not a VGM, cannot be decompressed,
|
|
157
|
+
or does not target the AY-3-8910.
|
|
158
|
+
"""
|
|
159
|
+
with open(filename, "rb") as f:
|
|
160
|
+
raw = f.read()
|
|
161
|
+
|
|
162
|
+
# Detect gzip (VGZ) -- magic bytes 0x1F 0x8B
|
|
163
|
+
if raw[0] == 0x1F and raw[1] == 0x8B:
|
|
164
|
+
raw = self._gunzip(raw)
|
|
165
|
+
|
|
166
|
+
# Check the VGM magic
|
|
167
|
+
if bytes(raw[0:4]) != b"Vgm ":
|
|
168
|
+
raise RuntimeError("Not a VGM file (bad magic)")
|
|
169
|
+
|
|
170
|
+
self._data = raw
|
|
171
|
+
self._parse_header()
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def _gunzip(data: bytes) -> bytes:
|
|
175
|
+
"""Decompress gzip (VGZ) data"""
|
|
176
|
+
if zlib is None:
|
|
177
|
+
raise RuntimeError(
|
|
178
|
+
"Playing compressed .vgz files needs the 'zlib' module, which is "
|
|
179
|
+
"not present in this build. Use an uncompressed .vgm, or a build "
|
|
180
|
+
"that includes zlib."
|
|
181
|
+
)
|
|
182
|
+
# wbits=31 tells zlib to expect a gzip header/trailer
|
|
183
|
+
return zlib.decompress(data, 31)
|
|
184
|
+
|
|
185
|
+
def _parse_header(self) -> None:
|
|
186
|
+
"""Parse the VGM header and locate the command stream."""
|
|
187
|
+
data = self._data
|
|
188
|
+
|
|
189
|
+
eof_rel = struct.unpack("<I", data[0x04:0x08])[0]
|
|
190
|
+
self._eof_offset = 0x04 + eof_rel if eof_rel else len(data)
|
|
191
|
+
self._eof_offset = min(self._eof_offset, len(data))
|
|
192
|
+
|
|
193
|
+
self._version = struct.unpack("<I", data[0x08:0x0C])[0]
|
|
194
|
+
self._total_samples = struct.unpack("<I", data[0x18:0x1C])[0]
|
|
195
|
+
|
|
196
|
+
loop_rel = struct.unpack("<I", data[0x1C:0x20])[0]
|
|
197
|
+
self._loop_offset = (0x1C + loop_rel) if loop_rel else 0
|
|
198
|
+
self._loop_samples = struct.unpack("<I", data[0x20:0x24])[0]
|
|
199
|
+
|
|
200
|
+
# AY-3-8910 clock at offset 0x74 (VGM 1.51+)
|
|
201
|
+
ay_clock = 0
|
|
202
|
+
if len(data) >= 0x78:
|
|
203
|
+
ay_clock = struct.unpack("<I", data[0x74:0x78])[0]
|
|
204
|
+
|
|
205
|
+
# Mask off the top bits (flags), keep the clock value
|
|
206
|
+
ay_clock &= 0x3FFFFFFF
|
|
207
|
+
|
|
208
|
+
if ay_clock == 0:
|
|
209
|
+
raise RuntimeError(
|
|
210
|
+
"No AY-3-8910 clock in this VGM (offset 0x74 is 0). "
|
|
211
|
+
"This file targets a different chip."
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
self._clock_hz = ay_clock
|
|
215
|
+
|
|
216
|
+
if self._version >= 0x150:
|
|
217
|
+
data_rel = struct.unpack("<I", data[0x34:0x38])[0]
|
|
218
|
+
self._data_offset = 0x34 + data_rel if data_rel else 0x40
|
|
219
|
+
else:
|
|
220
|
+
self._data_offset = 0x40
|
|
221
|
+
|
|
222
|
+
gd3_rel = struct.unpack("<I", data[0x14:0x18])[0]
|
|
223
|
+
if gd3_rel:
|
|
224
|
+
self._parse_gd3(0x14 + gd3_rel)
|
|
225
|
+
|
|
226
|
+
self._pos = self._data_offset
|
|
227
|
+
|
|
228
|
+
def _parse_gd3(self, offset: int) -> None:
|
|
229
|
+
"""Parse the GD3 metadata tag (UTF-16LE null-terminated strings)."""
|
|
230
|
+
data = self._data
|
|
231
|
+
if bytes(data[offset : offset + 4]) != b"Gd3 ":
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
ptr = offset + 12
|
|
235
|
+
|
|
236
|
+
fields = []
|
|
237
|
+
for _ in range(11):
|
|
238
|
+
s, ptr = self._read_utf16(data, ptr)
|
|
239
|
+
fields.append(s)
|
|
240
|
+
|
|
241
|
+
if len(fields) >= 7:
|
|
242
|
+
self._title = fields[0]
|
|
243
|
+
self._game = fields[2]
|
|
244
|
+
self._author = fields[6]
|
|
245
|
+
|
|
246
|
+
@staticmethod
|
|
247
|
+
def _read_utf16(data: bytes, offset: int) -> "Tuple[str, int]":
|
|
248
|
+
"""Read a UTF-16LE null-terminated string.
|
|
249
|
+
|
|
250
|
+
:param bytes data: Buffer to read from.
|
|
251
|
+
:param int offset: Byte offset to start reading at.
|
|
252
|
+
:return: A ``(string, next_offset)`` tuple.
|
|
253
|
+
"""
|
|
254
|
+
chars = []
|
|
255
|
+
ptr = offset
|
|
256
|
+
while ptr + 1 < len(data):
|
|
257
|
+
lo = data[ptr]
|
|
258
|
+
hi = data[ptr + 1]
|
|
259
|
+
ptr += 2
|
|
260
|
+
if lo == 0 and hi == 0:
|
|
261
|
+
break
|
|
262
|
+
code = lo | (hi << 8)
|
|
263
|
+
if 32 <= code < 127:
|
|
264
|
+
chars.append(chr(code))
|
|
265
|
+
elif code >= 127:
|
|
266
|
+
chars.append("?")
|
|
267
|
+
return "".join(chars), ptr
|
|
268
|
+
|
|
269
|
+
def play(self, ay: "AY8912") -> None:
|
|
270
|
+
"""Start playback through the AY8912 instance.
|
|
271
|
+
|
|
272
|
+
:param ~adafruit_ay8912.ay8912_emulator.AY8912 ay: The emulator that
|
|
273
|
+
register writes will be sent to. It is reset before playback.
|
|
274
|
+
"""
|
|
275
|
+
self._ay = ay
|
|
276
|
+
self._pos = self._data_offset
|
|
277
|
+
self._sample_clock = 0
|
|
278
|
+
self._wait_remaining = 0
|
|
279
|
+
self._loop_count = 0
|
|
280
|
+
self._playing = True
|
|
281
|
+
self._next_time = time.monotonic()
|
|
282
|
+
self._ay.reset()
|
|
283
|
+
|
|
284
|
+
def stop(self) -> None:
|
|
285
|
+
"""Stop playback and reset the attached AY8912"""
|
|
286
|
+
self._playing = False
|
|
287
|
+
if self._ay:
|
|
288
|
+
self._ay.reset()
|
|
289
|
+
|
|
290
|
+
def update(self) -> None:
|
|
291
|
+
"""Process the command stream with correct timing.
|
|
292
|
+
|
|
293
|
+
Call repeatedly in the playback loop. It tracks real time internally
|
|
294
|
+
and only advances the command stream when the accumulated wait has
|
|
295
|
+
elapsed.
|
|
296
|
+
"""
|
|
297
|
+
if not self._playing or self._data is None:
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
now = time.monotonic()
|
|
301
|
+
if now < self._next_time:
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
wait_samples = self._process_until_wait()
|
|
305
|
+
|
|
306
|
+
if wait_samples < 0:
|
|
307
|
+
if self._loop_offset and self._loop_samples:
|
|
308
|
+
self._pos = self._loop_offset
|
|
309
|
+
self._loop_count += 1
|
|
310
|
+
self._sample_clock = self._total_samples - self._loop_samples
|
|
311
|
+
wait_samples = 0
|
|
312
|
+
else:
|
|
313
|
+
self._playing = False
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
self._sample_clock += wait_samples
|
|
317
|
+
self._next_time += wait_samples / _VGM_RATE
|
|
318
|
+
|
|
319
|
+
self._next_time = max(self._next_time, now)
|
|
320
|
+
|
|
321
|
+
def _process_until_wait(self) -> int: # noqa: PLR0912
|
|
322
|
+
"""Execute commands until a wait.
|
|
323
|
+
|
|
324
|
+
:return: The number of samples to wait, or ``-1`` at end of stream.
|
|
325
|
+
"""
|
|
326
|
+
data = self._data
|
|
327
|
+
ay = self._ay
|
|
328
|
+
|
|
329
|
+
end = self._eof_offset if self._eof_offset else len(data)
|
|
330
|
+
while self._pos < end:
|
|
331
|
+
cmd = data[self._pos]
|
|
332
|
+
self._pos += 1
|
|
333
|
+
|
|
334
|
+
# AY-3-8910 register write: 0xA0 aa dd
|
|
335
|
+
if cmd == 0xA0:
|
|
336
|
+
reg = data[self._pos]
|
|
337
|
+
val = data[self._pos + 1]
|
|
338
|
+
self._pos += 2
|
|
339
|
+
reg &= 0x0F if reg < 0x10 else 0xFF
|
|
340
|
+
if reg <= 13:
|
|
341
|
+
ay.write_register(reg, val)
|
|
342
|
+
|
|
343
|
+
# Wait n samples: 0x61 nn nn
|
|
344
|
+
elif cmd == 0x61:
|
|
345
|
+
n = data[self._pos] | (data[self._pos + 1] << 8)
|
|
346
|
+
self._pos += 2
|
|
347
|
+
return n
|
|
348
|
+
|
|
349
|
+
# Wait 1/60 second (735 samples)
|
|
350
|
+
elif cmd == 0x62:
|
|
351
|
+
return 735
|
|
352
|
+
|
|
353
|
+
# Wait 1/50 second (882 samples)
|
|
354
|
+
elif cmd == 0x63:
|
|
355
|
+
return 882
|
|
356
|
+
|
|
357
|
+
# End of sound data
|
|
358
|
+
elif cmd == 0x66:
|
|
359
|
+
return -1
|
|
360
|
+
|
|
361
|
+
# Wait n+1 samples (0x70-0x7F)
|
|
362
|
+
elif 0x70 <= cmd <= 0x7F:
|
|
363
|
+
return (cmd & 0x0F) + 1
|
|
364
|
+
|
|
365
|
+
# --- Commands to skip (other chips / unsupported) ---
|
|
366
|
+
|
|
367
|
+
# 0x4F dd / 0x50 dd -- SN76489 (Game Gear / SMS): 1 data byte
|
|
368
|
+
elif cmd in {0x4F, 0x50}:
|
|
369
|
+
self._pos += 1
|
|
370
|
+
|
|
371
|
+
# 0x51-0x5F -- YM chips: 2 data bytes
|
|
372
|
+
elif 0x51 <= cmd <= 0x5F:
|
|
373
|
+
self._pos += 2
|
|
374
|
+
|
|
375
|
+
# 0xA1-0xBF -- various chip writes: 2 data bytes
|
|
376
|
+
elif 0xA1 <= cmd <= 0xBF:
|
|
377
|
+
self._pos += 2
|
|
378
|
+
|
|
379
|
+
# 0xC0-0xDF -- various: 3 data bytes
|
|
380
|
+
elif 0xC0 <= cmd <= 0xDF:
|
|
381
|
+
self._pos += 3
|
|
382
|
+
|
|
383
|
+
# 0xE0-0xFF -- various: 4 data bytes
|
|
384
|
+
elif 0xE0 <= cmd <= 0xFF:
|
|
385
|
+
self._pos += 4
|
|
386
|
+
|
|
387
|
+
# 0x90-0x95 -- DAC stream control (various lengths)
|
|
388
|
+
elif cmd == 0x90:
|
|
389
|
+
self._pos += 4
|
|
390
|
+
elif cmd == 0x91:
|
|
391
|
+
self._pos += 4
|
|
392
|
+
elif cmd == 0x92:
|
|
393
|
+
self._pos += 5
|
|
394
|
+
elif cmd == 0x93:
|
|
395
|
+
self._pos += 10
|
|
396
|
+
elif cmd == 0x94:
|
|
397
|
+
self._pos += 1
|
|
398
|
+
elif cmd == 0x95:
|
|
399
|
+
self._pos += 4
|
|
400
|
+
|
|
401
|
+
# 0x67 -- data block: 0x67 0x66 tt ss ss ss ss [data]
|
|
402
|
+
elif cmd == 0x67:
|
|
403
|
+
self._pos += 1 # skip 0x66
|
|
404
|
+
self._pos += 1 # skip the type byte
|
|
405
|
+
size = struct.unpack("<I", data[self._pos : self._pos + 4])[0]
|
|
406
|
+
self._pos += 4 + size
|
|
407
|
+
|
|
408
|
+
else:
|
|
409
|
+
# Unknown command
|
|
410
|
+
pass
|
|
411
|
+
return -1
|
|
412
|
+
|
|
413
|
+
def __str__(self) -> str:
|
|
414
|
+
title = self._title or "Untitled"
|
|
415
|
+
author = self._author or "Unknown"
|
|
416
|
+
return f"VGM v{self._version:X} | {title} -- {author} ({self.duration:.1f}s)"
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: adafruit-circuitpython-ay8912
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: CircuitPython synthio helper library for AY8912/AY-3-8910 emulation
|
|
5
|
+
Author-email: Adafruit Industries <circuitpython@adafruit.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/adafruit/Adafruit_CircuitPython_AY8912
|
|
8
|
+
Keywords: adafruit,blinka,circuitpython,micropython,ay8912_emulator,ay8912,,ay-3-8910,,synthio,,chiptune
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
11
|
+
Classifier: Topic :: Software Development :: Embedded Systems
|
|
12
|
+
Classifier: Topic :: System :: Hardware
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Description-Content-Type: text/x-rst
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: Adafruit-Blinka
|
|
17
|
+
Provides-Extra: optional
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
Introduction
|
|
21
|
+
============
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
.. image:: https://readthedocs.org/projects/adafruit-circuitpython-ay8912/badge/?version=latest
|
|
25
|
+
:target: https://docs.circuitpython.org/projects/ay8912/en/latest/
|
|
26
|
+
:alt: Documentation Status
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
.. image:: https://raw.githubusercontent.com/adafruit/Adafruit_CircuitPython_Bundle/main/badges/adafruit_discord.svg
|
|
30
|
+
:target: https://adafru.it/discord
|
|
31
|
+
:alt: Discord
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
.. image:: https://github.com/adafruit/Adafruit_CircuitPython_AY8912/workflows/Build%20CI/badge.svg
|
|
35
|
+
:target: https://github.com/adafruit/Adafruit_CircuitPython_AY8912/actions
|
|
36
|
+
:alt: Build Status
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
|
|
40
|
+
:target: https://github.com/astral-sh/ruff
|
|
41
|
+
:alt: Code Style: Ruff
|
|
42
|
+
|
|
43
|
+
CircuitPython synthio helper library for AY8912/AY-3-8910 emulation
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
Dependencies
|
|
47
|
+
=============
|
|
48
|
+
This driver depends on:
|
|
49
|
+
|
|
50
|
+
* `Adafruit CircuitPython <https://github.com/adafruit/circuitpython>`_
|
|
51
|
+
|
|
52
|
+
Please ensure all dependencies are available on the CircuitPython filesystem.
|
|
53
|
+
This is easily achieved by downloading
|
|
54
|
+
`the Adafruit library and driver bundle <https://circuitpython.org/libraries>`_
|
|
55
|
+
or individual libraries can be installed using
|
|
56
|
+
`circup <https://github.com/adafruit/circup>`_.
|
|
57
|
+
|
|
58
|
+
Installing from PyPI
|
|
59
|
+
=====================
|
|
60
|
+
|
|
61
|
+
On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally `from
|
|
62
|
+
PyPI <https://pypi.org/project/adafruit-circuitpython-ay8912/>`_.
|
|
63
|
+
To install for current user:
|
|
64
|
+
|
|
65
|
+
.. code-block:: shell
|
|
66
|
+
|
|
67
|
+
pip3 install adafruit-circuitpython-ay8912
|
|
68
|
+
|
|
69
|
+
To install system-wide (this may be required in some cases):
|
|
70
|
+
|
|
71
|
+
.. code-block:: shell
|
|
72
|
+
|
|
73
|
+
sudo pip3 install adafruit-circuitpython-ay8912
|
|
74
|
+
|
|
75
|
+
To install in a virtual environment in your current project:
|
|
76
|
+
|
|
77
|
+
.. code-block:: shell
|
|
78
|
+
|
|
79
|
+
mkdir project-name && cd project-name
|
|
80
|
+
python3 -m venv .venv
|
|
81
|
+
source .env/bin/activate
|
|
82
|
+
pip3 install adafruit-circuitpython-ay8912
|
|
83
|
+
|
|
84
|
+
Installing to a Connected CircuitPython Device with Circup
|
|
85
|
+
==========================================================
|
|
86
|
+
|
|
87
|
+
Make sure that you have ``circup`` installed in your Python environment.
|
|
88
|
+
Install it with the following command if necessary:
|
|
89
|
+
|
|
90
|
+
.. code-block:: shell
|
|
91
|
+
|
|
92
|
+
pip3 install circup
|
|
93
|
+
|
|
94
|
+
With ``circup`` installed and your CircuitPython device connected use the
|
|
95
|
+
following command to install:
|
|
96
|
+
|
|
97
|
+
.. code-block:: shell
|
|
98
|
+
|
|
99
|
+
circup install adafruit_ay8912
|
|
100
|
+
|
|
101
|
+
Or the following command to update an existing version:
|
|
102
|
+
|
|
103
|
+
.. code-block:: shell
|
|
104
|
+
|
|
105
|
+
circup update
|
|
106
|
+
|
|
107
|
+
Usage Example
|
|
108
|
+
=============
|
|
109
|
+
|
|
110
|
+
.. code-block:: python
|
|
111
|
+
|
|
112
|
+
import time
|
|
113
|
+
import board
|
|
114
|
+
import audiobusio
|
|
115
|
+
from adafruit_ay8912.ay8912_emulator import AY8912
|
|
116
|
+
from adafruit_ay8912.vgm_player import VGMFile
|
|
117
|
+
|
|
118
|
+
audio = audiobusio.I2SOut(board.D9, board.D10, board.D11)
|
|
119
|
+
|
|
120
|
+
VGM_FILE = "song.vgz" # .vgm or .vgz both work
|
|
121
|
+
vgm = VGMFile(VGM_FILE)
|
|
122
|
+
|
|
123
|
+
ay = AY8912(sample_rate=22050, clock_rate=vgm.clock_hz)
|
|
124
|
+
ay.begin(audio)
|
|
125
|
+
vgm.play(ay)
|
|
126
|
+
|
|
127
|
+
last_status = time.monotonic()
|
|
128
|
+
|
|
129
|
+
while vgm.playing:
|
|
130
|
+
vgm.update()
|
|
131
|
+
now = time.monotonic()
|
|
132
|
+
if now - last_status >= 5.0:
|
|
133
|
+
last_status = now
|
|
134
|
+
print(" %.0fs / %.0fs (%d%%)" % (
|
|
135
|
+
vgm.elapsed, vgm.duration, int(vgm.progress * 100)))
|
|
136
|
+
|
|
137
|
+
ay.reset()
|
|
138
|
+
|
|
139
|
+
Documentation
|
|
140
|
+
=============
|
|
141
|
+
API documentation for this library can be found on `Read the Docs <https://docs.circuitpython.org/projects/ay8912/en/latest/>`_.
|
|
142
|
+
|
|
143
|
+
For information on building library documentation, please check out
|
|
144
|
+
`this guide <https://learn.adafruit.com/creating-and-sharing-a-circuitpython-library/sharing-our-docs-on-readthedocs#sphinx-5-1>`_.
|
|
145
|
+
|
|
146
|
+
Contributing
|
|
147
|
+
============
|
|
148
|
+
|
|
149
|
+
Contributions are welcome! Please read our `Code of Conduct
|
|
150
|
+
<https://github.com/adafruit/Adafruit_CircuitPython_AY8912/blob/HEAD/CODE_OF_CONDUCT.md>`_
|
|
151
|
+
before contributing to help this project stay welcoming.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
adafruit_ay8912/ay8912_emulator.py,sha256=ah437jIiNVHjmqmd6ABcqn8GlyN4H7ffU4w6ZD8BYYs,15107
|
|
2
|
+
adafruit_ay8912/vgm_player.py,sha256=nzc9HUrEEmyEO0u63U2moxl1cxHV9XZ9RLoh73bf8zw,12792
|
|
3
|
+
adafruit_circuitpython_ay8912-1.0.0.dist-info/licenses/LICENSE,sha256=0JsO0yQOspYeKuhk2Y_s1wan9NO0DYeWSRJBrYJdeko,1100
|
|
4
|
+
adafruit_circuitpython_ay8912-1.0.0.dist-info/METADATA,sha256=V2dS0jaW1EQzsvXOElK6ZMg8-2WY2FKrnRmKAocv0Sk,4696
|
|
5
|
+
adafruit_circuitpython_ay8912-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
6
|
+
adafruit_circuitpython_ay8912-1.0.0.dist-info/top_level.txt,sha256=5hB9mo7MFFlBGB-11CcR1ZIe6tcFtBfN4WHeJ_yTF7E,16
|
|
7
|
+
adafruit_circuitpython_ay8912-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Liz Clark for Adafruit Industries
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
adafruit_ay8912
|