|
| 1 | +# SPDX-FileCopyrightText: 2021 Phil Burgess for Adafruit Industries |
| 2 | +# |
| 3 | +# SPDX-License-Identifier: MIT |
| 4 | + |
| 5 | +""" |
| 6 | +KZINTI COSPLAY PROPS for Feather M4 Express. Same code can be used for |
| 7 | +the "talking computer" prop or the simpler "total conversion beam." |
| 8 | +It's essentially just a sound board and relies on a bit of acting flair |
| 9 | +from its operator: understand that the "mode selector" slider really |
| 10 | +just makes clicky noises (doesn't select actual modes), the keypad (if |
| 11 | +building the talking computer) plays or selects one of nine sounds, and |
| 12 | +the trigger either plays a zap-gun noise or the last-selected sound. |
| 13 | +
|
| 14 | +'pew.wav' derived from freesound.org/people/newlocknew/sounds/520056 |
| 15 | +CC BY 3.0 creativecommons.org/licenses/by/3.0 |
| 16 | +Other sounds via Adafruit, MIT license. |
| 17 | +""" |
| 18 | + |
| 19 | +import board # For pin names |
| 20 | +import keypad # For talking computer buttons |
| 21 | +import pwmio # For LED flicker |
| 22 | +from analogio import AnalogIn # For slider potentiometer |
| 23 | +from audiocore import WaveFile # For WAV file handling |
| 24 | + |
| 25 | +# audioio is present on boards w/DAC out. If not available, fall back on |
| 26 | +# audiopwmio. If neither is supported, code stops w/ImportError exception. |
| 27 | +try: |
| 28 | + from audioio import AudioOut |
| 29 | +except ImportError: |
| 30 | + from audiopwmio import PWMAudioOut as AudioOut |
| 31 | + |
| 32 | + |
| 33 | +# CONFIGURABLES ----- |
| 34 | + |
| 35 | +# If building the talking computer: setting this True makes the keypad |
| 36 | +# buttons play sounds when pressed. If False, keypad buttons select but |
| 37 | +# do not play sounds -- that's done with the trigger button. |
| 38 | +buttons_play = True |
| 39 | + |
| 40 | +sound_folder = "/sounds" # Location of WAV files |
| 41 | +num_modes = 5 # Number of clicks from slider, plus one |
| 42 | +pin_to_wave = ( # This table maps input pins to corresponding WAV files: |
| 43 | + (board.A1, "pew.wav"), # Trigger on handle |
| 44 | + (board.D4, "1.wav"), # 9 buttons on top (if building |
| 45 | + (board.D12, "2.wav"), # talking computer, else ignored) |
| 46 | + (board.D11, "3.wav"), |
| 47 | + (board.D10, "4.wav"), |
| 48 | + (board.D9, "5.wav"), |
| 49 | + (board.D6, "6.wav"), |
| 50 | + (board.D5, "7.wav"), |
| 51 | + (board.SCL, "8.wav"), |
| 52 | + (board.SDA, "9.wav"), |
| 53 | +) # Tip: avoid pin D13 for keypad input; LED sometimes interferes. |
| 54 | + |
| 55 | +# HARDWARE SETUP ---- mode selector, LED, speaker, keypad ---- |
| 56 | + |
| 57 | +analog_in = AnalogIn(board.A2) # Slider for "mode selector" |
| 58 | +mode = (analog_in.value * (num_modes - 1) + 32768) // 65536 # Initial mode |
| 59 | +bounds = ( # Lower, upper limit to detect change from current mode |
| 60 | + (mode * 65535 - 32768) // (num_modes - 1) - 512, |
| 61 | + (mode * 65535 + 32768) // (num_modes - 1) + 512, |
| 62 | +) |
| 63 | + |
| 64 | +led = pwmio.PWMOut(board.A3) |
| 65 | +led.duty_cycle = 0 # Start w/LED off |
| 66 | +led_sync = 0 # LED behavior for different sounds, see comments later |
| 67 | + |
| 68 | +# AudioOut MUST be invoked AFTER PWMOut, for correct WAV playback timing. |
| 69 | +# Maybe sharing a timer or IRQ. Unsure if bug or just overlooked docs. |
| 70 | +audio = AudioOut(board.A0) # A0 is DAC pin on M0/M4 boards |
| 71 | + |
| 72 | +# To simplify the build, each key is wired to a separate input pin rather |
| 73 | +# than making an X/Y matrix. CircuitPython's keypad module is still used |
| 74 | +# (treating the buttons as a 1x10 matrix) as this gives us niceties such |
| 75 | +# as background processing, debouncing and an event queue! |
| 76 | +keys = keypad.Keys([x[0] for x in pin_to_wave], value_when_pressed=False, pull=True) |
| 77 | +event = keypad.Event() # Single key event for re-use |
| 78 | +keys.events.clear() |
| 79 | + |
| 80 | +# Load all the WAV files from the pin_to_wave list, and one more for the |
| 81 | +# mode selector, sharing a common buffer since only one is used at a time. |
| 82 | +# Also, play a startup sound. |
| 83 | +audio_buf = bytearray(1024) |
| 84 | +waves = [ |
| 85 | + WaveFile(open(sound_folder + "/" + x[1], "rb"), audio_buf) for x in pin_to_wave |
| 86 | +] |
| 87 | +active_sound = 0 # Index of waves[] to play when trigger is pressed |
| 88 | +selector_wave = WaveFile(open(sound_folder + "/" + "click.wav", "rb"), audio_buf) |
| 89 | +audio.play(WaveFile(open(sound_folder + "/" + "startup.wav", "rb"), audio_buf)) |
| 90 | + |
| 91 | +# MAIN LOOP --------- repeat forever ---- |
| 92 | + |
| 93 | +while True: |
| 94 | + |
| 95 | + # Process the mode selector slider, check if moved into a new position. |
| 96 | + # This is currently just used to make click noises, it doesn't actually |
| 97 | + # change any "mode" in the operation of the prop, but it could if we |
| 98 | + # really wanted, with additional code (e.g. different sound sets). |
| 99 | + selector_pos = analog_in.value |
| 100 | + if not bounds[0] < selector_pos < bounds[1]: # Moved out of mode range? |
| 101 | + # New mode, new bounds. +/-512 adds a little hysteresis to selection. |
| 102 | + mode = (selector_pos * (num_modes - 1) + 32768) // 65536 |
| 103 | + bounds = ( |
| 104 | + (mode * 65535 - 32768) // (num_modes - 1) - 512, |
| 105 | + (mode * 65535 + 32768) // (num_modes - 1) + 512, |
| 106 | + ) |
| 107 | + led_sync = 0 # LED stays off for selector sound |
| 108 | + audio.play(selector_wave) # Make click sound |
| 109 | + |
| 110 | + # Process keypad input. If building the "total conversion beam," |
| 111 | + # only the trigger button is wired up, the rest simply ignored. |
| 112 | + if keys.events.get_into(event) and event.pressed: |
| 113 | + if event.key_number == 0: # Trigger button |
| 114 | + # LED is steady for zap gun (index 0), flickers for other sounds |
| 115 | + led_sync = 1 if active_sound else 2 |
| 116 | + audio.play(waves[active_sound]) |
| 117 | + elif buttons_play: # Other buttons, play immediately |
| 118 | + led_sync = 1 |
| 119 | + audio.play(waves[event.key_number]) |
| 120 | + else: # Other buttons, select but don't play |
| 121 | + # Once another sound is selected, no going back to the zap. |
| 122 | + active_sound = event.key_number |
| 123 | + led_sync = 0 # Don't blink during selector sound |
| 124 | + audio.play(selector_wave) |
| 125 | + |
| 126 | + # LED is continually updated. If sound playing, and led_sync set above... |
| 127 | + if audio.playing and led_sync > 0: |
| 128 | + # Trigger button sound is steady on. For others, peek inside the |
| 129 | + # WAV audio buffer, this provides a passable voice-to-LED flicker. |
| 130 | + if led_sync == 2: |
| 131 | + led.duty_cycle = 65535 |
| 132 | + else: |
| 133 | + led.duty_cycle = 65535 - abs(audio_buf[1] - 128) * 65535 // 128 |
| 134 | + else: # No sound, or is just selector clicks (no LED) |
| 135 | + led.duty_cycle = 0 |
0 commit comments