|
1 |
| -# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries |
2 | 1 | # SPDX-FileCopyrightText: Copyright (c) 2022 JG for Cedar Grove Maker Studios
|
3 | 2 | #
|
4 | 3 | # SPDX-License-Identifier: MIT
|
5 | 4 | """
|
6 | 5 | `cedargrove_punkconsole`
|
7 | 6 | ================================================================================
|
8 | 7 |
|
9 |
| -A CircuitPython-based Atari Punk Console emulation helper class. |
| 8 | +A CircuitPython helper class to emulate the Atari Punk Console. |
10 | 9 |
|
11 | 10 |
|
12 | 11 | * Author(s): JG
|
|
16 | 15 |
|
17 | 16 | **Hardware:**
|
18 | 17 |
|
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 |
| -
|
22 | 18 | **Software and Dependencies:**
|
23 | 19 |
|
24 | 20 | * Adafruit CircuitPython firmware for the supported boards:
|
25 | 21 | https://circuitpython.org/downloads
|
26 | 22 |
|
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 |
32 | 23 | """
|
33 | 24 |
|
34 |
| -# imports |
| 25 | +import pwmio |
35 | 26 |
|
36 | 27 | __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