Skip to content

Commit 00017e8

Browse files
initial repo load
1 parent d7260a7 commit 00017e8

19 files changed

+421
-52
lines changed

README.rst

Lines changed: 110 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,18 @@ Introduction
99
:alt: Discord
1010

1111

12-
.. image:: https://github.com/CedarGroveStudios/Cedargrove_CircuitPython_PunkConsole/workflows/Build%20CI/badge.svg
13-
:target: https://github.com/CedarGroveStudios/Cedargrove_CircuitPython_PunkConsole/actions
12+
.. image:: https://github.com/CedarGroveStudios/CircuitPython_PunkConsole/workflows/Build%20CI/badge.svg
13+
:target: https://github.com/CedarGroveStudios/CircuitPython_PunkConsole/actions
1414
:alt: Build Status
1515

1616

1717
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
1818
:target: https://github.com/psf/black
1919
:alt: Code Style: Black
2020

21-
A CircuitPython-based Atari Punk Console emulation helper class.
21+
A CircuitPython-based Atari Punk Console emulation helper class based on the
22+
"Stepped Tone Generator" circuit, "Engineer's Mini-Notebook: 555 Circuits",
23+
Forrest M. Mims III (1984).
2224

2325

2426
Dependencies
@@ -31,35 +33,7 @@ Please ensure all dependencies are available on the CircuitPython filesystem.
3133
This is easily achieved by downloading
3234
`the Adafruit library and driver bundle <https://circuitpython.org/libraries>`_
3335
or individual libraries can be installed using
34-
`circup <https://github.com/adafruit/circup>`_.Installing from PyPI
35-
=====================
36-
.. note:: This library is not available on PyPI yet. Install documentation is included
37-
as a standard element. Stay tuned for PyPI availability!
38-
39-
.. todo:: Remove the above note if PyPI version is/will be available at time of release.
40-
41-
On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally `from
42-
PyPI <https://pypi.org/project/Cedargrove-circuitpython-punkconsole/>`_.
43-
To install for current user:
44-
45-
.. code-block:: shell
46-
47-
pip3 install Cedargrove-circuitpython-punkconsole
48-
49-
To install system-wide (this may be required in some cases):
50-
51-
.. code-block:: shell
52-
53-
sudo pip3 install Cedargrove-circuitpython-punkconsole
54-
55-
To install in a virtual environment in your current project:
56-
57-
.. code-block:: shell
58-
59-
mkdir project-name && cd project-name
60-
python3 -m venv .venv
61-
source .env/bin/activate
62-
pip3 install Cedargrove-circuitpython-punkconsole
36+
`circup <https://github.com/adafruit/circup>`_.
6337

6438
Installing to a Connected CircuitPython Device with Circup
6539
==========================================================
@@ -76,7 +50,7 @@ following command to install:
7650

7751
.. code-block:: shell
7852
79-
circup install punkconsole
53+
circup install cedargrove_punkconsole
8054
8155
Or the following command to update an existing version:
8256

@@ -87,12 +61,112 @@ Or the following command to update an existing version:
8761
Usage Example
8862
=============
8963

90-
.. todo:: Add a quick, simple example. It and other examples should live in the
91-
examples folder and be included in docs/examples.rst.
64+
.. code-block:: python
65+
66+
import board
67+
import analogio
68+
import pwmio
69+
from simpleio import map_range
70+
from cedargrove_punkconsole import PunkConsole
71+
72+
# instantiate a PunkConsole output on pin A1 (PWM-capable)
73+
punk_console = PunkConsole(board.A1, mute=False)
74+
75+
# define the two potentiometer inputs
76+
f_in = analogio.AnalogIn(board.A2) # Oscillator Frequency
77+
pw_in = analogio.AnalogIn(board.A3) # One-Shot Pulse Width
78+
79+
while True:
80+
# read the inputs, map to practical audio ranges, send to PunkConsole instance
81+
# oscillator frequency range: 3Hz to 3kHz
82+
# one-shot pulse width range: 0.5ms to 5ms
83+
punk_console.frequency = map_range(f_in.value, 0, 65535, 3, 3000)
84+
punk_console.pulse_width_ms = map_range(pw_in.value, 0, 65535, 0.5, 5.0)
9285
9386
Documentation
9487
=============
95-
API documentation for this library can be found on `Read the Docs <https://circuitpython-punkconsole.readthedocs.io/>`_.
88+
API documentation for this library can be found in `PunkConsole_API <https://github.com/CedarGroveStudios/CircuitPython_PunkConsole/blob/main/media/pseudo_readthedocs_punkconsole.pdf>`_.
89+
90+
.. image:: https://github.com/CedarGroveStudios/CircuitPython_PunkConsole/blob/main/docs/Stereo_Punk_Console_test.png
91+
92+
The CedarGrove PunkConsole emulates an astable square-wave oscillator and
93+
synchronized non-retriggerable one-shot monostable multivibrator to create
94+
the classic stepped-tone generator sound of the Atari Punk Console. As with
95+
the original circuit, the oscillator frequency and one-shot pulse width are
96+
the input parameters. Instantiation of the Punk Console class will start the
97+
output waveform based on the input parameters and enable the output signal
98+
if `mute=False`. If no input parameters are provided, the output signal
99+
will be disabled regardless of the mute value. Once instantiated, the class
100+
is controlled by the `frequency`, `pulse_width_ms`, and `mute` properties.
101+
102+
This version of the emulator works only with PWM-capable output pins.
103+
104+
Depending on the timer and PWM capabilities of the host MPU board, the
105+
emulator can easily outperform the original analog circuit. Oscillator
106+
frequency is only limited by the MPU's PWM duty cycle and frequency
107+
parameters, which may create output signals well above the practical audio
108+
hearing range. Therefore, it is recommended that one-shot pulse width input
109+
be limited to the range of 0.5ms and 5ms and that the oscillator frequency
110+
input range be between 3Hz and 3kHz -- although experimentation is
111+
encouraged!
112+
113+
The repo contains three examples, a simple single-channel console, an
114+
annoying stereo noisemaker, and a note table driven sequencer. For the first
115+
two examples, input is provided by potentiometers attached to
116+
two analog input pins. The sequencer is controlled by an internal list of
117+
notes that select the oscillator frequency; pulse width is potentiometer
118+
controlled.
119+
120+
Minimum and maximum input ranges (may be further limited by the MPU):
121+
pulse_width: 0.05ms to 5000ms
122+
frequency: 1Hz to >4MHz
123+
124+
Practical input ranges for audio (empirically determined):
125+
pulse_width: 0.5ms to 5ms
126+
frequency: 3Hz to 3kHz
127+
128+
The CedarGrove Punk Console algorithm uses PWM frequency and duty cycle
129+
parameters to build the output waveform. The PWM output frequency is an
130+
integer multiple of the oscillator frequency input compared to the one-shot
131+
pulse width input:
132+
133+
`pwm_freq = freq_in / (int((pulse_width) * freq_in) + 1)`
134+
135+
The PWM output duty cycle is calculated after the PWM output frequency is
136+
determined. The PWM output duty cycle is the ratio of the one-shot pulse
137+
width and the wavelength of the PWM output frequency:
138+
139+
`pwm_duty_cycle = pulse_width * pwm_freq`
140+
141+
Notes:
142+
Planned updates:
143+
144+
For non-PWM analog output, use `audiocore` with a waveform sample in the
145+
`RawSample` binary array, similar to the `simpleio.tone()` helper. The output
146+
waveform's duty cycle will be adjusted by altering the contents of the array,
147+
perhaps with `ulab` to improve code execution time. The
148+
`audiocore.RawSample.sample_rate` frequency is expected to be directly
149+
proportional to the original algorithm's PWM frequency output value, calculated
150+
from the `sample_rate` divided by the length of the `audiocore.RawSample` array
151+
(number of samples).
152+
153+
MIDI control: A version that uses USB and/or UART MIDI is in the queue. Note
154+
that the `PunkConsole.mute` property could be used for note-on and note-off.
155+
`note_in_example.py` shows how muting can be used for individual notes.
156+
157+
CV control: A Eurorack version was discussed, it's just a bit lower on the
158+
to-do list, that's all. But you know, the first two examples use analog inputs
159+
(0 to +3.3 volts) for frequency and pulse width control. Just sayin'.
160+
161+
162+
.. image:: https://github.com/CedarGroveStudios/CircuitPython_PunkConsole/blob/main/docs/CG_PunkConsole_04.jpeg
163+
164+
.. image:: https://github.com/CedarGroveStudios/CircuitPython_PunkConsole/blob/main/docs/CG_PunkConsole_01.jpeg
165+
166+
.. image:: https://github.com/CedarGroveStudios/CircuitPython_PunkConsole/blob/main/docs/CG_PunkConsole_02.jpeg
167+
168+
.. image:: (https://github.com/CedarGroveStudios/CircuitPython_PunkConsole/blob/main/docs/CG_PunkConsole_03.jpeg
169+
96170

97171
For information on building library documentation, please check out
98172
`this guide <https://learn.adafruit.com/creating-and-sharing-a-circuitpython-library/sharing-our-docs-on-readthedocs#sphinx-5-1>`_.

cedargrove_punkconsole.py

100644100755
Lines changed: 164 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
21
# SPDX-FileCopyrightText: Copyright (c) 2022 JG for Cedar Grove Maker Studios
32
#
43
# SPDX-License-Identifier: MIT
54
"""
65
`cedargrove_punkconsole`
76
================================================================================
87
9-
A CircuitPython-based Atari Punk Console emulation helper class.
8+
A CircuitPython helper class to emulate the Atari Punk Console.
109
1110
1211
* Author(s): JG
@@ -16,22 +15,175 @@
1615
1716
**Hardware:**
1817
19-
.. todo:: Add links to any specific hardware product page(s), or category page(s).
20-
Use unordered list & hyperlink rST inline format: "* `Link Text <url>`_"
21-
2218
**Software and Dependencies:**
2319
2420
* Adafruit CircuitPython firmware for the supported boards:
2521
https://circuitpython.org/downloads
2622
27-
.. todo:: Uncomment or remove the Bus Device and/or the Register library dependencies
28-
based on the library's use of either.
29-
30-
# * Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice
31-
# * Adafruit's Register library: https://github.com/adafruit/Adafruit_CircuitPython_Register
3223
"""
3324

34-
# imports
25+
import pwmio
3526

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

0 commit comments

Comments
 (0)