[{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/160/original/IMG_2269.jpeg?1725344506","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n        \u003ch2\u003eDeadline: We need it tonight\u003c/h2\u003e\n\u003cp\u003eWe have just eight hours until our classic rock band rehearsal session tonight, and we need some pre-recorded sound effects for three songs. The required sound effects (SFX) playback machine should be super easy to use, powered by a battery, and have a stereo output for the band’s PA system. Sound quality is important, but since the sound effect recordings aren’t actual music, we can be flexible with the bit rate to save space on the storage.\u003c/p\u003e\n      \n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch3\u003eHistory: The Old FXM-8 Effects Mixer\u003c/h3\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/159/original/Image.jpg?1725311177","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n        \u003cp\u003eThe band retired six years ago and, at the time, it didn’t seem like there would be a reunion tour. So, I stripped the old sound effects machine (the FXM-8 shown above) for parts and gave it a new life. The old unit used a special SFX board (Adafruit #\u003ca href=\"https://www.adafruit.com/product/2220\"\u003e\u003cspan\u003e2220\u003c/span\u003e\u003c/a\u003e) with \u003cem\u003e.ogg \u003c/em\u003esound files stored on-board. It worked and sounded good, but it was a pain to update the files. The simple and obvious FXM-8 tactile interface was also pretty cool in hindsight.\u003c/p\u003e\n\u003cp\u003eAnd of course, the band’s plans changed. They asked us to come out of retirement and play one more gig.\u003c/p\u003e\n\u003cp\u003eSo, we’re going to need a new SFX machine.\u003c/p\u003e\n      \n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n        \u003ch3\u003eThe Requirements\u003c/h3\u003e\n\u003cp\u003eHere's the list of requirements for the new SFX machine:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eChoose, play, and stop up to four selectable sound effect files.\u003c/li\u003e\n\u003cli\u003eUtilize a simple one-click play/stop button.\u003c/li\u003e\n\u003cli\u003eAccommodate 16-bit 22.050kps stereo wave files stored on a removable micro SD card.\u003c/li\u003e\n\u003cli\u003eProvide an obvious flashing light when playing that can be seen from across the stage.\u003c/li\u003e\n\u003cli\u003eBattery powered continuous operation for at least 3 hours. USB power connector for charging/wall wart use.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eMost importantly, it needs to be coded and tested in fewer than eight hours.\u003c/p\u003e\n      \n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n        \u003cp\u003eGiven that several of my existing projects incorporate code snippets for interfacing with the display and playing sounds, it was a logical assumption that\u0026nbsp;\u003cem\u003e\u003cstrong\u003eCircuitPython\u003c/strong\u003e\u003c/em\u003e would be the preferred platform for the new device. Additionally, I have a spare \u003ca href=\"https://www.adafruit.com/product/4242\"\u003e\u003cspan\u003ePyGamer\u003c/span\u003e\u003c/a\u003e device available, which would be ideal for the stereo output due to its 3.5mm TRS earphone jack, vibrant display, gamepad buttons and joystick, SD card, and demonstrated ability to play high-quality sound from wave files.\u003c/p\u003e\n\u003cp\u003eTime’s ticking, so let’s gather those code snippets and make sure this project idea is a hit!\u003c/p\u003e\n      \n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n        \u003ch3\u003eThe CircuitPython Code\u003c/h3\u003e\n\u003cp\u003eThe implementation of the audio output, keypad decoding, and SD card file reader was relatively straightforward. The file selection interface was constructed using simple\u0026nbsp;\u003cem\u003edisplayio.Label\u003c/em\u003e objects, which were controlled by a list of wave file properties. After some initial troubleshooting and minor adjustments to the positioning of graphics objects, the primary functionality, including the joystick and button interface, was successfully implemented.\u003c/p\u003e\n\u003cp\u003eNext, the 44.100kps 24-bit stereo source wave files were converted to 22.050kps 16-bit using the free version of \u003ca href=\"https://www.audacityteam.org\" target=\"_blank\"\u003eAudacity\u003c/a\u003e, placed on a micro SD card and the code was retested. Everything was working nicely.\u003c/p\u003e\n\u003cp\u003eThe final modifications to the code included enabling the on-board speaker, which is deactivated when the earphone jack is utilized, and incorporating a moving worm effect onto the five Neopixels to indicate the playback status of a sound file.\u003c/p\u003e\n      \n","metadata":{}},{"element_type":"embed","content":"https://youtu.be/9x4vPZjhaDw"},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eHere's the final version of the CircuitPython code including a battery charge icon graphic that was recently added. The code and bitmap files can be found in the \u003cem\u003ecode\u003c/em\u003e folder of the \u003ca href=\"https://github.com/CedarGroveStudios/H12_SFX_Machine\" target=\"_blank\"\u003eH12 SFX Machine GitHub repository\u003c/a\u003e.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"code","content":"# SPDX-FileCopyrightText: 2024 JG for Cedar Grove Maker Studios\n#\n# SPDX-License-Identifier: MIT\n\n\"\"\"A custom sound effects player for The Highway 12 Band\"\"\"\n\nimport time\nimport board\nfrom analogio import AnalogIn\nfrom digitalio import DigitalInOut\nimport sdcardio\nimport storage\nimport audiocore\nimport audioio\nimport audiomixer\nimport neopixel\nfrom simpleio import map_range\nimport terminalio\nimport keypad\nimport displayio\nfrom adafruit_display_text.label import Label\nfrom adafruit_bitmap_font import bitmap_font\nimport adafruit_imageload\n\n# Instantiate the integral display and define its size\ndisplay = board.DISPLAY\ndisplay.brightness = 0.6\nWIDTH = display.width\nHEIGHT = display.height\n\n# Display splash graphics first before instantiating everything\ntrack_group = displayio.Group(scale=1)\nsplash_group = displayio.Group(scale=1)\nsign_bright = displayio.OnDiskBitmap(\"/HW12_sign_bw_2012-11-17_v00.bmp\")\nsign_faded = displayio.OnDiskBitmap(\"/HW12_sign_bw_2012-11-17_v01_faded.bmp\")\nsplash_group.append(\n    displayio.TileGrid(sign_faded, pixel_shader=sign_faded.pixel_shader)\n)\nsplash_group.append(\n    displayio.TileGrid(sign_bright, pixel_shader=sign_bright.pixel_shader)\n)\ndisplay.root_group = splash_group\n\n# Instantiate and clear the NeoPixels\npixels = neopixel.NeoPixel(board.NEOPIXEL, 5, pixel_order=neopixel.GRB)\npixels.brightness = 0.06\npixels.fill(0x202020)\n\n# Battery indicator\nbattery = AnalogIn(board.A6)\nsprite_sheet, battery_palette = adafruit_imageload.load(\n    \"/batt_sprite_sheet.bmp\", bitmap=displayio.Bitmap, palette=displayio.Palette\n)\nbattery_icon = displayio.TileGrid(\n    sprite_sheet,\n    pixel_shader=battery_palette,\n    width=1,\n    height=1,\n    tile_width=16,\n    tile_height=16,\n)\nbattery_icon.x = WIDTH - 16\nbattery_icon.y = 1\nbattery_palette.make_transparent(0)  # Set icon background to transparent\n\n# Define track titles and associated SD card file names\nTRACK_TITLE = [\n    [\"409 REV\", \"409_22.wav\"],\n    [\"Jet START\", \"jet_start_22.wav\"],\n    [\"Jet END\", \"jet_start_22.wav\"],\n    [\"Wipeout\", \"wipeout_22.wav\"],\n]\nCURRENT_TRACK = 0\n\n\n# Define the state machine states\nclass State:\n    INIT = \"INIT\"\n    PLAYING = \"PLAY\"\n    PAUSED = \"PAUSE\"\n    STOPPED = \"STOP\"\n\n\nSTATE = State.INIT\nprint(f\"STATE: {STATE}\")\n\n# Enable the speaker\nDigitalInOut(board.SPEAKER_ENABLE).switch_to_output(value=True)\n\n# Instantiate PyGamer joystick\njoystick_x = AnalogIn(board.JOYSTICK_X)\njoystick_y = AnalogIn(board.JOYSTICK_Y)\n\n# Initialize ShiftRegisterKeys to read PyGamer buttons\npanel = keypad.ShiftRegisterKeys(\n    clock=board.BUTTON_CLOCK,\n    data=board.BUTTON_OUT,\n    latch=board.BUTTON_LATCH,\n    key_count=8,\n    value_when_pressed=True,\n)\n\n# Define front panel button event values\nBUTTON_SELECT = 3  # Not used\nBUTTON_START = 2  # Not used\nBUTTON_A = 1  # Player START/STOP\nBUTTON_B = 0  # Not used\n\n# Instantiate the SD card storage system\nsd = sdcardio.SDCard(board.SPI(), board.SD_CS)\nvfs = storage.VfsFat(sd)\nstorage.mount(vfs, \"/sd\")\n# print(os.listdir('/sd/SFX'))\n\n# Instantiate the mixer and audio output stream\naudio_out = audioio.AudioOut(\n    left_channel=board.A0, right_channel=board.A1, quiescent_value=32_768\n)\nmixer = audiomixer.Mixer(\n    voice_count=1,\n    buffer_size=2048,\n    sample_rate=22050,\n    channel_count=2,\n    bits_per_sample=16,\n    samples_signed=True,\n)\naudio_out.play(mixer)\n\n# Create a list of opened wave files from TRACK_TITLE list\nwave_files = [open(\"sd/SFX/\" + t[1], \"rb\") for t in TRACK_TITLE]\n# Create a list of wave objects from the opened wave files list\nwaves = [audiocore.WaveFile(f) for f in wave_files]\n\n# Define track labels\nfont_0 = bitmap_font.load_font(\"/fonts/helvB14.bdf\")\nlabel_padding = 7\n\ntrack_0_label = Label(\n    font_0,\n    text=TRACK_TITLE[0][0],\n    color=0xFFFFFF,\n    padding_top=label_padding,\n    padding_bottom=label_padding,\n    padding_left=label_padding,\n    padding_right=label_padding,\n)\ntrack_0_label.anchor_point = (0.5, 0.5)\ntrack_0_label.anchored_position = ((WIDTH // 2), 1 * HEIGHT // 5)\ntrack_group.append(track_0_label)\n\ntrack_1_label = Label(\n    font_0,\n    text=TRACK_TITLE[1][0],\n    color=0xFFFFFF,\n    padding_top=label_padding,\n    padding_bottom=label_padding,\n    padding_left=label_padding,\n    padding_right=label_padding,\n)\ntrack_1_label.anchor_point = (0.5, 0.5)\ntrack_1_label.anchored_position = ((WIDTH // 2), 2 * HEIGHT // 5)\ntrack_group.append(track_1_label)\n\ntrack_2_label = Label(\n    font_0,\n    text=TRACK_TITLE[2][0],\n    color=0xFFFFFF,\n    padding_top=label_padding,\n    padding_bottom=label_padding,\n    padding_left=label_padding,\n    padding_right=label_padding,\n)\ntrack_2_label.anchor_point = (0.5, 0.5)\ntrack_2_label.anchored_position = ((WIDTH // 2), 3 * HEIGHT // 5)\ntrack_group.append(track_2_label)\n\ntrack_3_label = Label(\n    font_0,\n    text=TRACK_TITLE[3][0],\n    color=0xFFFFFF,\n    padding_top=label_padding,\n    padding_bottom=label_padding,\n    padding_left=label_padding,\n    padding_right=label_padding,\n)\ntrack_3_label.anchor_point = (0.5, 0.5)\ntrack_3_label.anchored_position = ((WIDTH // 2), 4 * HEIGHT // 5)\ntrack_group.append(track_3_label)\n\nstate_status = Label(font_0, text=\"    \", color=0x404040)\nstate_status.anchor_point = (0, 1.0)\nstate_status.anchored_position = (0, 128)\ntrack_group.append(state_status)\n\nbattery_status = Label(terminalio.FONT, text=\"   \", color=0x808080)\nbattery_status.anchor_point = (1.0, 0)\nbattery_status.anchored_position = (WIDTH, 12)\ntrack_group.append(battery_status)\n\n# Reveal faded sign bitmap image, battery icon and show tracks\ndisplay.root_group.pop()\ndisplay.root_group.append(track_group)\nsplash_group.append(battery_icon)\n\n\n# FUNCTIONS\ndef stop():\n    # Stop track playback\n    global STATE, CURRENT_TRACK\n    mixer.voice[0].stop()\n    mixer.voice[0].level = 0.0\n    STATE = State.STOPPED\n    track_group[CURRENT_TRACK].color = 0x000000\n    track_group[CURRENT_TRACK].background_color = 0xFFFF00\n    pixels.fill(0x000000)\n    print(f\"STATE: {STATE}\")\n\n\ndef play():\n    # Start track playback\n    global STATE, CURRENT_TRACK, pixel_lighted\n    STATE = State.PLAYING\n    mixer.voice[0].level = 1.0\n    mixer.voice[0].play(waves[CURRENT_TRACK])\n    track_group[CURRENT_TRACK].color = 0x000000\n    track_group[CURRENT_TRACK].background_color = 0x00FF00\n    pixels.fill(0x00FF00)\n    pixel_lighted = 0\n    print(f\"STATE: {STATE}\")\n\n\ndef get_joystick():\n    # Read the joystick and interpret as up/down buttons\n    if joystick_y.value \u003c 20000:\n        return 1  # UP\n    if joystick_y.value \u003e 44000:\n        return -1  # DOWN\n    return 0\n\n\ndef display_battery_level():\n    \"\"\"Display the battery level.\"\"\"\n    battery_volts = round(battery.value * 6.6 / 0xFFF0, 1)\n    # battery_status.text = f\"{battery_volts:3.1f}\"\n    battery_level = int(map_range(battery_volts, 3.1, 3.6, 0, 5))\n    battery_icon[0] = battery_level\n\n\n# Initialization completed\njoystick_hold = False\npixel_lighted = 0\nstop()  # Stop mixer/audio output\ndisplay_battery_level()\n\n# Main Code Loop\nwhile True:\n    if time.monotonic() % 10 == 0:\n        # Update the battery display every 10 seconds\n        display_battery_level()\n    state_status.text = STATE\n    if STATE == State.STOPPED:\n        # Select next track to play only when stopped\n        joystick_delta = get_joystick()\n\n        if joystick_delta != 0:\n            if not joystick_hold:\n                track_group[CURRENT_TRACK].color = 0xFFFFFF\n                track_group[CURRENT_TRACK].background_color = None\n                CURRENT_TRACK = CURRENT_TRACK - joystick_delta\n                CURRENT_TRACK = min(max(0, CURRENT_TRACK), 3)\n            joystick_hold = True\n        else:\n            joystick_hold = False\n\n        track_group[CURRENT_TRACK].color = 0x000000\n        track_group[CURRENT_TRACK].background_color = 0xFFFF00\n\n        buttons = panel.events.get()\n        if buttons and buttons.pressed:\n            if buttons.key_number == BUTTON_A:  # START/STOP button pressed\n                play()  # This is non-blocking\n\n    if STATE == State.PLAYING:\n        # Watch for STOP button press only when playing\n        buttons = panel.events.get()\n        if buttons and buttons.pressed:\n            if buttons.key_number == BUTTON_A:  # START/STOP button pressed\n                stop()\n\n        if mixer.playing:\n            if (\n                pixels[pixel_lighted][0] * 0x010000\n                + pixels[pixel_lighted][1] * 0x000100\n                + pixels[pixel_lighted][2]\n            ) == 0x00FF00:\n                pixels[pixel_lighted] = 0x002000\n            else:\n                pixels[pixel_lighted] = 0x00FF00\n            time.sleep(0.05)\n\n            pixel_lighted += 1\n            if pixel_lighted \u003e 4:\n                pixel_lighted = 0\n        else:\n            stop()","metadata":{"language":"python","linenums":false}},{"element_type":"text","content":"\n        \u003ch3\u003eThe Rehearsal\u003c/h3\u003e\n\u003cp\u003eThe SFX player was ready just in time for a lovely family dinner before our band rehearsal. We played two songs that needed the sounds of an automobile engine and a jet airplane. The rehearsal was a huge success!\u003c/p\u003e\n\u003cp\u003eIf you want to use the code for your own SFX player, you’ll need to update the TRACK_TITLE list for your specific effect titles and SD card .wav files. You can also change the background graphics to show your band’s logo.\u003c/p\u003e\n\u003cp\u003eCircuitPython was the perfect choice for getting the project up and running quickly because there were already functional examples of the code that would make this project work.\u003c/p\u003e\n\u003ch3\u003eNext Steps\u003c/h3\u003e\n\u003cp\u003ePerhaps in the future, the code will be altered to automatically scan the SD card and list any stored .\u003cem\u003ewav\u003c/em\u003e file. It would also be helpful if the playback length in seconds was also displayed next to the sound effect name. What else might you add?\u003c/p\u003e\n\u003cp\u003e\u0026nbsp;\u003c/p\u003e\n      ","metadata":{}}]