-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcedargrove_punkconsole.py
More file actions
executable file
·189 lines (150 loc) · 7.59 KB
/
cedargrove_punkconsole.py
File metadata and controls
executable file
·189 lines (150 loc) · 7.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# SPDX-FileCopyrightText: Copyright (c) 2022 JG for Cedar Grove Maker Studios
#
# SPDX-License-Identifier: MIT
"""
`cedargrove_punkconsole`
================================================================================
A CircuitPython helper class to emulate the Atari Punk Console.
* Author(s): JG
Implementation Notes
--------------------
**Hardware:**
**Software and Dependencies:**
* Adafruit CircuitPython firmware for the supported boards:
https://circuitpython.org/downloads
"""
import pwmio
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/CedarGroveStudios/CircuitPython_PunkConsole.git"
class PunkConsole:
"""A CircuitPython-based Atari Punk Console emulation helper class based on
the "Stepped Tone Generator" circuit, "Engineer's Mini-Notebook: 555
Circuits", Forrest M. Mims III (1984).
The CedarGrove Punk Console emulates an astable square-wave oscillator and
synchronized non-retriggerable one-shot monostable multivibrator to create
the classic stepped-tone generator sound of the Atari Punk Console. As with
the original circuit, the oscillator frequency and one-shot pulse width are
the input parameters. Instantiation of the Punk Console class will start the
output waveform based on the input parameters and enable the output signal
if `mute=False`. If no input parameters are provided, the output signal
will be disabled regardless of the mute value. Once instantiated, the class
is controlled by the `frequency`, `pulse_width_ms`, and `mute` properties.
This version of the emulator works only with PWM-capable output pins.
Depending on the timer and PWM capabilities of the host MPU board, the
emulator can easily outperform the original analog circuit. Oscillator
frequency is only limited by the MPU's PWM duty cycle and frequency
parameters, which may create output signals well above the practical audio
hearing range. Therefore, it is recommended that one-shot pulse width input
be limited to the range of 0.5ms and 5ms and that the oscillator frequency
input range be between 3Hz and 3kHz -- although experimentation is
encouraged!
The repo contains three examples, a simple single-channel console, an
annoying stereo noisemaker, and a note table driven sequencer. For the first
two examples, input is provided by potentiometers attached to
two analog input pins. The sequencer is controlled by an internal list of
notes that select the oscillator frequency; pulse width is potentiometer
controlled.
Minimum and maximum input ranges (may be further limited by the MPU):
pulse_width: 0.05ms to 5000ms
frequency: 1Hz to >4MHz
Practical input ranges for audio (empirically determined):
pulse_width: 0.5ms to 5ms
frequency: 3Hz to 3kHz
The CedarGrove Punk Console algorithm uses PWM frequency and duty cycle
parameters to build the output waveform. The PWM output frequency is an
integer multiple of the oscillator frequency input compared to the one-shot
pulse width input:
`pwm_freq = freq_in / (int((pulse_width) * freq_in) + 1)`
The PWM output duty cycle is calculated after the PWM output frequency is
determined. The PWM output duty cycle is the ratio of the one-shot pulse
width and the wavelength of the PWM output frequency:
`pwm_duty_cycle = pulse_width * pwm_freq`
Notes:
Future update: For non-PWM analog output, the plans are to use audiocore
with a waveform sample in the `RawSample` binary array, similar to the
`simpleio.tone()` helper. The output waveform's duty cycle will be adjusted
by altering the contents of the array, perhaps with `ulab` to improve code
execution time. The `audiocore.RawSample.sample_rate` frequency is expected
to be directly proportional to the original algorithm's PWM frequency output
value, calculated from the `sample_rate` divided by the length of the
`audiocore.RawSample` array (number of samples).
MIDI control: A version that uses USB and/or UART MIDI is in the queue. Note
that the property `PunkConsole.mute` could be used for note-on and note-off.
`note_in_example.py` shows how muting can be used for individual notes.
CV control: A Eurorack version was discussed, it's just a bit lower on the
to-do list, that's all."""
def __init__(self, pin, frequency=1, pulse_width_ms=0, mute=True):
"""Intantiate and start the oscillator. Initial output is muted by
default.
:param ~board pin: The PWM-capable output pin.
:param int frequency: The oscillator frequency setting in Hertz.
Defaults to 1 Hertz to squelch output noise.
:param float pulse_width_ms: The non-retriggerable one-shot monostable
multivibrator delay (pulse_width) setting in milliseconds. Defaults to
zero milliseconds to squelch output noise.
:param bool mute: The output signal state. `True` to mute; `False` to
unmute. Defaults to muted.
"""
self._pin = pin
self._freq_in = frequency
self._pulse_width_ms = pulse_width_ms
self._mute = mute
# Set the maximum PWM frequency and duty cycle values (PWMOut limits)
# Frequency maximum: 4,294,967,295Hz (32-bits)
# Duty cycle maximum: 65,535 = 1.0 duty cycle (16-bits)
self._pwm_freq_range = (2**32) - 1
self._pwm_duty_cycle_range = (2**16) - 1
try:
# Instantiate PWM output with some initial low-noise values
self._pwm_out = pwmio.PWMOut(self._pin, variable_frequency=True)
self._pwm_out.frequency = 1
self._pwm_out.duty_cycle = 0x0000
self._update()
except ValueError:
# The output pin is not PWM capable
print("Specified output pin is not PWM capable.")
@property
def frequency(self):
"""The oscillator frequency integer value setting in Hertz. Defaults to
1 Hz to squelch output noise."""
return self._freq_in
@frequency.setter
def frequency(self, new_frequency):
self._freq_in = min(max(new_frequency, 1), self._pwm_freq_range)
self._lambda_in = 1 / self._freq_in
self._update()
@property
def pulse_width_ms(self):
"""The non-retriggerable one-shot monostable multivibrator delay
(pulse_width) floating value setting in milliseconds. Defaults to zero
milliseconds to squelch output noise."""
return self._pulse_width_ms
@pulse_width_ms.setter
def pulse_width_ms(self, new_width):
self._pulse_width_ms = min(max(new_width, 0.050), 5000)
self._update()
@property
def mute(self):
"""The boolean output signal state: `True` to mute; `False` to unmute
Defaults to muted."""
return self._mute
@mute.setter
def mute(self, new_mute):
self._mute = new_mute
self._update()
def _update(self):
"""Calculate and set PWM frequency and duty cycle using current
frequency and pulse width input values."""
# Set the PWM output frequency based on freq_in and pulse_width_ms
self._pwm_freq = self._freq_in / (
int((self._pulse_width_ms / 1000) * self._freq_in) + 1
)
self._pwm_out.frequency = int(round(self._pwm_freq, 0))
# Set the PWM output duty cycle based on pulse_width_ms and pwm_freq
self._pwm_duty_cycle = (self._pulse_width_ms / 1000) * self._pwm_freq
if not self._mute:
self._pwm_out.duty_cycle = int(
self._pwm_duty_cycle * self._pwm_duty_cycle_range
)
else:
self._pwm_out.duty_cycle = 0