├── LICENSE ├── README.md ├── build your own ├── README.md ├── display_scrolling-text.py ├── display_shapes.py └── read_gpio_pins.py ├── clip-recorder ├── README.md ├── background.png ├── cliprecord.py ├── controls.png ├── fft.py ├── install.sh └── uninstall.sh ├── examples ├── README.md ├── backlight-pwm.py ├── buttons.py ├── rainbow.py └── shairport-sync-control.py ├── mopidy ├── README.md └── install.sh ├── raspotify └── README.md ├── shairport-sync └── README.md └── speaker-and-mic ├── .gitignore ├── Makefile ├── README.md ├── adau7002-pcm5102a-io-overlay.dts └── asound.conf /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Pimoroni Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pirate Audio 2 | 3 | Pirate Audio is a range of audio output boards for the Raspberry Pi. 4 | 5 | Each board includes an ST7789 240x240 pixel LCD display, four buttons and some form of audio output (except for the Pirate Audio: Dual Mic which offers two microphones instead of audio output). 6 | 7 | 8 | ## Hardware 9 | 10 | * st7789 display (see [Python library](https://github.com/pimoroni/st7789-python)) 11 | * four buttons, active low connected to BCM 5, 6, 16, and 24 (A, B, X, Y respectively) 12 | 13 | ## Installation 14 | 15 | You'll need to add the following lines to `/boot/config.txt` to get audio up and running: 16 | 17 | ``` 18 | dtoverlay=hifiberry-dac 19 | gpio=25=op,dh 20 | ``` 21 | 22 | You can also disable onboard audio if you're not going to use it, this sometimes helps applications find the right audio device without extra prompting: 23 | 24 | ``` 25 | dtparam=audio=off 26 | ``` 27 | 28 | And for Dual Mic, you'll need: 29 | ``` 30 | dtoverlay=adau7002-simple 31 | ``` 32 | (See [Clip Recorder](./clip-recorder) for further example of use.) 33 | 34 | ## Using with Spotify Connect 35 | 36 | If you want to display album art and track information on your Pirate Audio LCD then check out [PiDi Spotify](https://github.com/pimoroni/pidi-spotify). 37 | 38 | Note: PiDi Spotify is currently in beta, and does not work alongside [Mopidy](https://mopidy.com/). If you want to smush track information into Mopidy from [Raspotify](https://dtcooper.github.io/raspotify/) see: https://github.com/pimoroni/pirate-audio/issues/17 39 | 40 | ## Using With Mopidy 41 | 42 | We've created plugins to get you up and running with Pirate Audio and Mopidy. 43 | 44 | These will give you album art display, volume, play/pause and skip control. 45 | 46 | ## Build Your Own 47 | 48 | If you're planning to build your own application you'll find some inspiration in examples. 49 | 50 | But first you'll need some dependencies: 51 | 52 | ``` 53 | sudo apt update 54 | sudo apt install python-rpi.gpio python-spidev python-pip python-pil python-numpy 55 | ``` 56 | 57 | And then you'll need the st7789 library: 58 | 59 | ``` 60 | sudo pip install st7789 61 | ``` 62 | 63 | For more display examples see the [st7789 Python library examples](https://github.com/pimoroni/st7789-python/tree/master/examples). 64 | 65 | For more help with using the Pirate Audio Headphone Amp, see [build your own](build%20your%20own/README.md). 66 | -------------------------------------------------------------------------------- /build your own/README.md: -------------------------------------------------------------------------------- 1 | Here are some instructions to use the Pirate Audio Headphone Amp (https://shop.pimoroni.com/products/pirate-audio-headphone-amp, PIM482) without any other software. 2 | 3 | # Sound 4 | 5 | ## /boot/config.txt 6 | 7 | Edit /boot/config.txt 8 | ``` 9 | sudo nano /boot/config.txt 10 | ``` 11 | ### Set up audio 12 | Add this to the end of of the file: 13 | ``` 14 | # Pirate Audio 15 | dtoverlay=hifiberry-dac 16 | gpio=25=op,dh 17 | dtparam=audio=off 18 | ``` 19 | (Note: The Pirate Audio Headphone Amp is not made by [hifiberry](https://hifiberry.com), but the overlay works.) 20 | 21 | If you are on Raspberry Pi Zero, depending on how you use the display, you may want to disable the Raspberry Pi Zero led: 22 | ``` 23 | # disable led 24 | dtparam=act_led_trigger=none 25 | dtparam=act_led_activelow=on 26 | ``` 27 | For other Raspberry Pi, please see: https://mlagerberg.gitbooks.io/raspberry-pi/content/5.2-leds.html. 28 | 29 | ### Enable i2c and spi 30 | You need to enable i2c and spi. You can do this via `config.txt` or using `raspi-config`. If you want to set this up while you're editing `config.txt`, look for the lines below and remove the `#` from i2c and spi: 31 | 32 | ``` 33 | # Uncomment some or all of these to enable the optional hardware interfaces 34 | dtparam=i2c_arm=on 35 | #dtparam=i2s=on 36 | dtparam=spi=on 37 | ``` 38 | 39 | If you prefer to use `raspi-config`, boot your Raspberry Pi and type 40 | ``` 41 | sudo raspi-config 42 | ``` 43 | 44 | ## Edit asound.conf 45 | 46 | Add this to asound.conf 47 | ``` 48 | pcm.!default { 49 | type hw card 0 50 | } 51 | ctl.!default { 52 | type hw card 0 53 | } 54 | pcm.!default { 55 | type plug 56 | slave.pcm “softvol” 57 | } 58 | pcm.softvol { 59 | type softvol 60 | slave { 61 | pcm “dmix” 62 | } 63 | control { 64 | name “Amp” 65 | card 0 66 | } 67 | min_dB -5.0 68 | max_dB 20.0 69 | resolution 6 70 | } 71 | ``` 72 | (Source: https://forums.pimoroni.com/t/volume-for-pirate-audio-headphone-amp-for-raspberry-pi/22058) 73 | 74 | # Screen and GPIO 75 | 76 | ## Install required packages 77 | 78 | As per usual, make sure you have the latest Raspian: 79 | ``` 80 | sudo apt update 81 | sudo apt upgrade 82 | ``` 83 | Then: 84 | ``` 85 | sudo apt install python3-rpi.gpio python3-spidev python3-pip python3-pil python3-numpy 86 | sudo pip3 install st7789 87 | ``` 88 | 89 | ## Using the GPIO 90 | If you've not used the GPIO much, this script may be helpful: 91 | * [read_gpio_pins.py](read_gpio_pins.py). 92 | 93 | ## Using the square display (240x240): 94 | The get started with the square display, see [st7789 Python library examples](https://github.com/pimoroni/st7789-python/tree/master/examples), such as: 95 | * [display_scrolling-text.py](display_scrolling-text.py) 96 | * [display_shapes.py](display_shapes.py) 97 | 98 | ## Things to note 99 | 100 | ### Turning the backlight off while the Pi is running 101 | It's possible to turn the backlight of the display on/off. For example, with the above code, and `disp` initialised accordingly (`disp = ST7789.ST7789(...)`), the following command will turn off the backlight: 102 | ``` 103 | disp.set_backlight(0) 104 | ``` 105 | With the above libraries, it is not possible to dim the backlight. Hwoever, there are notes here on how this may be possible: https://github.com/pimoroni/st7789-python/issues/8/ 106 | 107 | ### Turning the backlight off when the Pi is powered down. 108 | 109 | Note that when Pi is powered down, the backlight comes back on. The backlight is turned off by a low signal on the backlight pin. When the Pi shuts down that pin is likely going to a floating state (not a low), thus the backlight goes back on, see [forum](https://forums.pimoroni.com/t/pirate-audio-headphone-amp-why-does-the-backlight-stay-on-when-the-raspberry-pi-is-powered-down/22126). 110 | 111 | With the display configuration including `BACKLIGHT=13`, add the following text to `/boot/config.txt`: 112 | ``` 113 | dtoverlay=gpio-poweroff,gpiopin=13,active_low=1 114 | ``` 115 | This pulls the pin low for poweroff. It has the side effect that the Pi will no longer boot by pulling pins high. The documentation states that "Note that this will require the support of a custom dt-blob.bin to prevent a power-down during the boot process, and that a reboot will also cause the pin to go low." However, just making the above modification, the Pi does boot after power dis/re-connection. 116 | 117 | 118 | (See [overlays here](https://github.com/raspberrypi/firmware/blob/9f4983548584d4f70e6eec5270125de93a081483/boot/overlays/README#L758-L807).) 119 | -------------------------------------------------------------------------------- /build your own/display_scrolling-text.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import time 4 | 5 | from PIL import Image 6 | from PIL import ImageDraw 7 | from PIL import ImageFont 8 | 9 | import ST7789 10 | 11 | 12 | MESSAGE = "Hello World! How are you today?" 13 | 14 | print(""" 15 | scrolling-test.py - Display scrolling text. 16 | 17 | If you're using Breakout Garden, plug the 1.3" LCD (SPI) 18 | breakout into the front slot. 19 | 20 | Usage: {} "" 21 | 22 | Where is one of: 23 | 24 | * square - 240x240 1.3" Square LCD 25 | * round - 240x240 1.3" Round LCD (applies an offset) 26 | * rect - 240x135 1.14" Rectangular LCD (applies an offset) 27 | * dhmini - 320x240 2.0" Display HAT Mini 28 | """.format(sys.argv[0])) 29 | 30 | try: 31 | MESSAGE = sys.argv[1] 32 | except IndexError: 33 | pass 34 | 35 | try: 36 | display_type = sys.argv[2] 37 | except IndexError: 38 | display_type = "square" 39 | 40 | 41 | # Create ST7789 LCD display class. 42 | 43 | if display_type in ("square", "rect", "round"): 44 | disp = ST7789.ST7789( 45 | height=135 if display_type == "rect" else 240, 46 | rotation=0 if display_type == "rect" else 90, 47 | port=0, 48 | cs=ST7789.BG_SPI_CS_FRONT, # BG_SPI_CS_BACK or BG_SPI_CS_FRONT 49 | dc=9, 50 | backlight=19, # 18 for back BG slot, 19 for front BG slot. 51 | spi_speed_hz=80 * 1000 * 1000, 52 | offset_left=0 if display_type == "square" else 40, 53 | offset_top=53 if display_type == "rect" else 0 54 | ) 55 | 56 | elif display_type == "dhmini": 57 | disp = ST7789.ST7789( 58 | height=240, 59 | width=320, 60 | rotation=180, 61 | port=0, 62 | cs=1, 63 | dc=9, 64 | backlight=13, 65 | spi_speed_hz=60 * 1000 * 1000, 66 | offset_left=0, 67 | offset_top=0 68 | ) 69 | 70 | else: 71 | print ("Invalid display type!") 72 | 73 | # Initialize display. 74 | disp.begin() 75 | 76 | WIDTH = disp.width 77 | HEIGHT = disp.height 78 | 79 | 80 | img = Image.new('RGB', (WIDTH, HEIGHT), color=(0, 0, 0)) 81 | 82 | draw = ImageDraw.Draw(img) 83 | 84 | font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 30) 85 | 86 | size_x, size_y = draw.textsize(MESSAGE, font) 87 | 88 | text_x = disp.width 89 | text_y = (disp.height - size_y) // 2 90 | 91 | t_start = time.time() 92 | 93 | while True: 94 | x = (time.time() - t_start) * 100 95 | x %= (size_x + disp.width) 96 | draw.rectangle((0, 0, disp.width, disp.height), (0, 0, 0)) 97 | draw.text((int(text_x - x), text_y), MESSAGE, font=font, fill=(255, 255, 255)) 98 | disp.display(img) 99 | -------------------------------------------------------------------------------- /build your own/display_shapes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | 4 | from PIL import Image 5 | from PIL import ImageDraw 6 | from PIL import ImageFont 7 | 8 | import ST7789 9 | 10 | print(""" 11 | shapes.py - Display test shapes on the LCD using PIL. 12 | 13 | If you're using Breakout Garden, plug the 1.3" LCD (SPI) 14 | breakout into the front slot. 15 | 16 | Usage: {} 17 | 18 | Where is one of: 19 | 20 | * square - 240x240 1.3" Square LCD 21 | * round - 240x240 1.3" Round LCD (applies an offset) 22 | * rect - 240x135 1.14" Rectangular LCD (applies an offset) 23 | * dhmini - 320x240 2.0" Display HAT Mini 24 | """.format(sys.argv[0])) 25 | 26 | try: 27 | display_type = sys.argv[1] 28 | except IndexError: 29 | display_type = "square" 30 | 31 | # Create ST7789 LCD display class. 32 | 33 | if display_type in ("square", "rect", "round"): 34 | disp = ST7789.ST7789( 35 | height=135 if display_type == "rect" else 240, 36 | rotation=0 if display_type == "rect" else 90, 37 | port=0, 38 | cs=ST7789.BG_SPI_CS_FRONT, # BG_SPI_CS_BACK or BG_SPI_CS_FRONT 39 | dc=9, 40 | backlight=19, # 18 for back BG slot, 19 for front BG slot. 41 | spi_speed_hz=80 * 1000 * 1000, 42 | offset_left=0 if display_type == "square" else 40, 43 | offset_top=53 if display_type == "rect" else 0 44 | ) 45 | 46 | elif display_type == "dhmini": 47 | disp = ST7789.ST7789( 48 | height=240, 49 | width=320, 50 | rotation=180, 51 | port=0, 52 | cs=1, 53 | dc=9, 54 | backlight=13, 55 | spi_speed_hz=60 * 1000 * 1000, 56 | offset_left=0, 57 | offset_top=0 58 | ) 59 | 60 | else: 61 | print ("Invalid display type!") 62 | 63 | # Initialize display. 64 | disp.begin() 65 | 66 | WIDTH = disp.width 67 | HEIGHT = disp.height 68 | 69 | 70 | # Clear the display to a red background. 71 | # Can pass any tuple of red, green, blue values (from 0 to 255 each). 72 | # Get a PIL Draw object to start drawing on the display buffer. 73 | img = Image.new('RGB', (WIDTH, HEIGHT), color=(255, 0, 0)) 74 | 75 | draw = ImageDraw.Draw(img) 76 | 77 | # Draw a purple rectangle with yellow outline. 78 | draw.rectangle((10, 10, WIDTH - 10, HEIGHT - 10), outline=(255, 255, 0), fill=(255, 0, 255)) 79 | 80 | # Draw some shapes. 81 | # Draw a blue ellipse with a green outline. 82 | draw.ellipse((10, 10, WIDTH - 10, HEIGHT - 10), outline=(0, 255, 0), fill=(0, 0, 255)) 83 | 84 | # Draw a white X. 85 | draw.line((10, 10, WIDTH - 10, HEIGHT - 10), fill=(255, 255, 255)) 86 | draw.line((10, HEIGHT - 10, WIDTH - 10, 10), fill=(255, 255, 255)) 87 | 88 | # Draw a cyan triangle with a black outline. 89 | draw.polygon([(WIDTH / 2, 10), (WIDTH - 10, HEIGHT - 10), (10, HEIGHT - 10)], outline=(0, 0, 0), fill=(0, 255, 255)) 90 | 91 | # Load default font. 92 | font = ImageFont.load_default() 93 | 94 | # Alternatively load a TTF font. 95 | # Some other nice fonts to try: http://www.dafont.com/bitmap.php 96 | # font = ImageFont.truetype('Minecraftia.ttf', 16) 97 | 98 | 99 | # Define a function to create rotated text. Unfortunately PIL doesn't have good 100 | # native support for rotated fonts, but this function can be used to make a 101 | # text image and rotate it so it's easy to paste in the buffer. 102 | def draw_rotated_text(image, text, position, angle, font, fill=(255, 255, 255)): 103 | # Get rendered font width and height. 104 | draw = ImageDraw.Draw(image) 105 | width, height = draw.textsize(text, font=font) 106 | # Create a new image with transparent background to store the text. 107 | textimage = Image.new('RGBA', (width, height), (0, 0, 0, 0)) 108 | # Render the text. 109 | textdraw = ImageDraw.Draw(textimage) 110 | textdraw.text((0, 0), text, font=font, fill=fill) 111 | # Rotate the text image. 112 | rotated = textimage.rotate(angle, expand=1) 113 | # Paste the text into the image, using it as a mask for transparency. 114 | image.paste(rotated, position, rotated) 115 | 116 | 117 | # Write two lines of white text on the buffer, rotated 90 degrees counter clockwise. 118 | draw_rotated_text(img, 'Hello World!', (0, 0), 90, font, fill=(255, 255, 255)) 119 | draw_rotated_text(img, 'This is a line of text.', (10, HEIGHT - 10), 0, font, fill=(255, 255, 255)) 120 | 121 | # Write buffer to display hardware, must be called to make things visible on the 122 | # display! 123 | disp.display(img) 124 | -------------------------------------------------------------------------------- /build your own/read_gpio_pins.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Script from: https://community.volumio.org/t/for-pimoroni-pirate-audio-device-users-python-code-to-unleash-the-power-of-your-hat/43702/28 3 | # Tested with recent version of Pirate Audio Headphone Amp, https://github.com/bjohas, 2023-05-07 4 | 5 | import sys 6 | import logging 7 | # logging.getLogger('socketIO-client').setLevel(logging.DEBUG) 8 | # logging.basicConfig() 9 | 10 | import signal 11 | import RPi.GPIO as GPIO 12 | 13 | print("""buttons.py - Detect which button has been pressed 14 | This example should demonstrate how to: 15 | 1. set up RPi.GPIO to read buttons, 16 | 2. determine which button has been pressed 17 | Press Ctrl+C to exit! 18 | """) 19 | 20 | # The buttons on Pirate Audio are connected to pins 5, 6, 16 and 24 21 | # Boards prior to 23 January 2020 used 5, 6, 16 and 20 22 | # try changing 24 to 20 if your Y button doesn't work. 23 | BUTTONS = [5, 6, 16, 24] 24 | 25 | # These correspond to buttons A, B, X and Y respectively 26 | LABELS = ['A', 'B', 'X', 'Y'] 27 | 28 | # Set up RPi.GPIO with the "BCM" numbering scheme 29 | GPIO.setmode(GPIO.BCM) 30 | 31 | # Buttons connect to ground when pressed, so we should set them up 32 | # with a "PULL UP", which weakly pulls the input signal to 3.3V. 33 | GPIO.setup(BUTTONS, GPIO.IN, pull_up_down=GPIO.PUD_UP) 34 | 35 | 36 | # "handle_button" will be called every time a button is pressed 37 | # It receives one argument: the associated input pin. 38 | def handle_button(pin): 39 | label = LABELS[BUTTONS.index(pin)] 40 | print("Button press detected on pin={} label={}".format(pin, label)) 41 | #if pin == 16: 42 | # print('initalisiere Shutdown') 43 | sys.stdout.flush() 44 | 45 | # Loop through out buttons and attach the "handle_button" function to each 46 | # We're watching the "FALLING" edge (transition from 3.3V to Ground) and 47 | # picking a generous bouncetime of 100ms to smooth out button presses. 48 | for pin in BUTTONS: 49 | GPIO.add_event_detect(pin, GPIO.FALLING, handle_button, bouncetime=250) 50 | 51 | # Finally, since button handlers don't require a "while True" loop, 52 | # we pause the script to prevent it exiting immediately. 53 | signal.pause() 54 | -------------------------------------------------------------------------------- /clip-recorder/README.md: -------------------------------------------------------------------------------- 1 | # Pirate Audio Dual Mic Recording Utility 2 | 3 | ## Installing 4 | 5 | ### Pre-requisites 6 | 7 | Install the LADSPA plugins: 8 | 9 | ``` 10 | sudo apt install ladspa-sdk invada-studio-plugins-ladspa 11 | ``` 12 | 13 | ### Basic Audio Config 14 | 15 | Dual Mic needs some config to enable the microphone and boost the input gain. 16 | 17 | Add the following to `/boot/config.txt` to enable Dual Mic as an audio input: 18 | 19 | ``` 20 | dtoverlay=adau7002-simple 21 | ``` 22 | 23 | The following config uses a LADSPA plugin (Invada High-Pass Stero Filter) to remove DC bias and amplify the input from the microphone. 24 | 25 | Add it to `~/.asoundrc` or `/etc/asound.conf`: 26 | 27 | ``` 28 | pcm.mic_hw{ 29 | type hw 30 | card adau7002 31 | format S32_LE 32 | rate 48000 33 | channels 2 34 | } 35 | pcm.mic_rt{ 36 | type route 37 | slave.pcm mic_hw 38 | ttable.0.0 1 39 | ttable.0.1 0 40 | ttable.1.0 0 41 | ttable.1.1 1 42 | } 43 | pcm.mic_plug { 44 | type plug 45 | slave.pcm mic_rt 46 | } 47 | pcm.mic_filter { 48 | type ladspa 49 | slave.pcm mic_plug 50 | path "/usr/lib/ladspa"; 51 | plugins [ 52 | { 53 | label invada_hp_stereo_filter_module_0_1 54 | input { 55 | controls [ 56 | 50 # Cut off frequency (Hz) 57 | 30 # Gain (dB) 58 | 1 # Soft Clip (on/off) 59 | ] 60 | } 61 | } 62 | ] 63 | } 64 | pcm.mic_out { 65 | type plug 66 | slave.pcm mic_filter 67 | } 68 | ``` 69 | 70 | The device for recording is `mic_out`, and `mic_hw` is the raw, unfiltered input. 71 | 72 | To test the microphone setup, use `arecord` like so: 73 | 74 | ``` 75 | arecord -Dmic_out -c2 -r48000 -fS32_LE -twav -d5 -R10000 -Vstereo test.wav 76 | ``` 77 | 78 | The ASCII VU meter should correspond to what the microphone is picking up. 79 | 80 | ## Preparing to run these examples 81 | 82 | ### Install Pulseaudio 83 | 84 | Pulse audio supplies an "upmix" output device which allows `cliprecord.py` to play back lower samplerate (smaller) recording without handling resampling. 85 | 86 | ``` 87 | sudo apt install pulseaudio 88 | ``` 89 | 90 | ### Enable SPI 91 | 92 | ``` 93 | sudo raspi-config nonint do_spi 0 94 | ``` 95 | 96 | ### Install for Python 3 97 | 98 | ``` 99 | sudo apt install python3-pip python3-rpi.gpio python3-spidev python3-numpy python3-pil python3-pil.imagetk libportaudio2 100 | sudo python3 -m pip install fonts font-roboto ST7789 sounddevice 101 | ``` 102 | 103 | ### Run 104 | 105 | ``` 106 | python3 cliprecord.py 107 | ``` 108 | -------------------------------------------------------------------------------- /clip-recorder/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimoroni/pirate-audio/b7d60bcacedff52cf99a7e35fbd0132446d944ff/clip-recorder/background.png -------------------------------------------------------------------------------- /clip-recorder/cliprecord.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import math 3 | import time 4 | # import tkinter 5 | import pathlib 6 | import numpy 7 | from PIL import Image, ImageTk, ImageDraw, ImageFont 8 | 9 | from fonts.ttf import RobotoMedium 10 | import RPi.GPIO as GPIO 11 | from ST7789 import ST7789 12 | import sounddevice 13 | import wave 14 | 15 | WIDTH = 480 16 | HEIGHT = 480 17 | 18 | COLOR_WHITE = (255, 255, 255) 19 | COLOR_RED = (232, 56, 58) 20 | COLOR_GREEN = (47, 173, 102) 21 | COLOR_YELLOW = (242, 146, 0) 22 | 23 | BUTTON_RECORD = (0, 0, 50, 50) 24 | BUTTON_PLAY = (390, 0, 460, 50) 25 | BUTTON_DELETE = (390, 230, 460, 280) 26 | BUTTON_NEXT = (0, 230, 50, 280) 27 | 28 | BUTTONS = [5, 6, 16, 24] 29 | LABELS = ["A", "B", "X", "Y"] 30 | 31 | 32 | def transparent(color, opacity=0.2): 33 | opacity = int(255 * opacity) 34 | r, g, b = color 35 | return r, g, b, opacity 36 | 37 | class Recordamajig: 38 | def __init__(self, device="mic_out", output_device="default", samplerate=16000): 39 | self._state = "initial" 40 | self._clip = 1 41 | 42 | self._vu_left = 0 43 | self._vu_right = 0 44 | 45 | self._graph = [0 for _ in range(44)] 46 | 47 | self._device = device 48 | self._out_device = output_device 49 | self._samplerate = samplerate 50 | 51 | self._image = Image.new("RGBA", (480, 480), (0, 0, 0, 0)) 52 | self._draw = ImageDraw.Draw(self._image) 53 | 54 | self._controls = Image.new("RGBA", (440, 280), (255, 255, 255, 50)) 55 | self._draw_controls = ImageDraw.Draw(self._controls) 56 | 57 | self._background = Image.open(pathlib.Path("background.png")) 58 | self._controls_mask = Image.open(pathlib.Path("controls.png")) 59 | 60 | self._font = ImageFont.truetype(RobotoMedium, size=62) 61 | self._font_small = ImageFont.truetype(RobotoMedium, size=47) 62 | self._font_tiny = ImageFont.truetype(RobotoMedium, size=28) 63 | 64 | self._wave = None 65 | 66 | self._recording = False 67 | self._confirm_delete = False 68 | 69 | self._written = 0 70 | self.running = True 71 | 72 | self._clip_exists = False 73 | self._update_clip() 74 | 75 | self._stream = sounddevice.InputStream( 76 | device=self._device, # adau7002", 77 | dtype="int16", 78 | channels=2, 79 | samplerate=self._samplerate, 80 | callback=self.audio_callback 81 | ) 82 | self._out_stream = sounddevice.OutputStream( 83 | device=self._out_device, 84 | dtype="int16", 85 | channels=2, 86 | samplerate=self._samplerate, 87 | callback=self.audio_playback_callback 88 | ) 89 | 90 | def next(self): 91 | if not self._clip_exists: 92 | return 93 | self._clip += 1 94 | self._update_clip() 95 | 96 | def _update_clip(self): 97 | self._clip_exists = self.clipfile.is_file() 98 | if self._clip_exists and not self._recording: 99 | self._wave_read = wave.open(str(self.clipfile), "r") 100 | self._written = self._wave_read.getnframes() 101 | if not (self._samplerate == self._wave_read.getframerate()): 102 | raise RuntimeError(f"Invalid samplerate in {self.clipfile}") 103 | 104 | def _playback_stopped(self): 105 | self._vu_left = 0 106 | self._vu_right = 0 107 | self._graph = [0 for _ in range(44)] 108 | 109 | def play(self): 110 | if self._confirm_delete: 111 | self._confirm_delete = False 112 | return False 113 | if self._recording: 114 | return False 115 | if self._out_stream.stopped or not self._out_stream.active: 116 | self._playback_stopped() 117 | self._out_stream.stop() 118 | self._update_clip() 119 | self._out_stream.start() 120 | return True 121 | else: 122 | self._out_stream.stop() 123 | self._playback_stopped() 124 | return False 125 | 126 | def record(self): 127 | if self._confirm_delete: 128 | self._confirm_delete = False 129 | return 130 | 131 | if self._recording: 132 | self.stop_recording() 133 | else: 134 | self.start_recording() 135 | return self._recording 136 | 137 | def delete(self): 138 | if self._recording: 139 | return False 140 | if self._confirm_delete: 141 | self._confirm_delete = False 142 | if self._wave_read is not None: 143 | self._wave_read.close() 144 | self.clipfile.unlink() 145 | if self._clip > 1: 146 | self._clip -= 1 147 | self._update_clip() 148 | else: 149 | self._confirm_delete = True 150 | 151 | @property 152 | def recording(self): 153 | return self._recording 154 | 155 | @property 156 | def clipfile(self): 157 | return pathlib.Path(f"clip-{self._clip:02d}.wav") 158 | 159 | def start_recording(self): 160 | if self._clip_exists: 161 | return 162 | self._written = 0 163 | self._wave = wave.open(str(self.clipfile), "w") 164 | self._wave.setframerate(self._samplerate) 165 | self._wave.setsampwidth(2) # size of the input dtype 166 | self._wave.setnchannels(2) 167 | self._recording = True 168 | 169 | self._stream.start() 170 | self._update_clip() 171 | 172 | def stop_recording(self): 173 | self._recording = False 174 | if self._wave is not None: 175 | self._wave.close() 176 | self._wave = None 177 | self._stream.stop() 178 | self._update_clip() 179 | 180 | def get_duration(self): 181 | return self._written / self._samplerate 182 | 183 | def audio_callback(self, indata, frames, time, status): 184 | self._vu_left = numpy.average(numpy.abs(indata[:,0])) / 65535.0 * 10 185 | self._vu_right = numpy.average(numpy.abs(indata[:,1])) / 65535.0 * 10 186 | #print(self._vu_left, self._vu_right) 187 | 188 | self._graph.append(min(1.0, max(self._vu_left, self._vu_right))) 189 | self._graph = self._graph[-44:] 190 | 191 | if self._recording and self._wave is not None: 192 | self._written += frames 193 | self._wave.writeframes(indata.tobytes()) 194 | 195 | def audio_playback_callback(self, outdata, frames, time, status): 196 | raw_data = self._wave_read.readframes(frames) 197 | outframes = len(raw_data) // 4 198 | data = numpy.frombuffer(raw_data, dtype="int16") 199 | outdata[:][:outframes] = data.reshape((outframes, 2)) 200 | 201 | self._vu_left = numpy.average(numpy.abs(outdata[:,0])) / 65535.0 * 10 202 | self._vu_right = numpy.average(numpy.abs(outdata[:,1])) / 65535.0 * 10 203 | self._graph.append(min(1.0, max(self._vu_left, self._vu_right))) 204 | self._graph = self._graph[-44:] 205 | 206 | if outframes < frames: 207 | self._playback_stopped() 208 | raise sounddevice.CallbackStop 209 | 210 | def draw_text(self, x, y, text, font, w=480, h=None, alignment="left", vertical_alignment="top", color=COLOR_WHITE): 211 | tw, th = self._draw.textsize(text, font=font) 212 | if h is None: 213 | h = th 214 | if alignment == "center": 215 | x += w // 2 216 | x -= tw // 2 217 | if vertical_alignment == "center": 218 | y += h // 2 219 | y -= th // 2 220 | self._draw.text((x, y), text, color, font=font) 221 | 222 | def render_controls(self): 223 | self._draw_controls.rectangle((0, 0, 480, 480), transparent(COLOR_WHITE)) 224 | if self._recording: 225 | self._draw_controls.rectangle(BUTTON_RECORD, COLOR_RED) 226 | else: 227 | if self._confirm_delete: 228 | self.draw_text(0, 350 - 6, "Delete Clip 1?", font=self._font_small, alignment="center", color=COLOR_RED) 229 | self._draw_controls.rectangle(BUTTON_DELETE, COLOR_RED) 230 | else: 231 | if self._clip_exists: 232 | self._draw_controls.rectangle(BUTTON_RECORD, transparent(COLOR_WHITE)) 233 | self._draw_controls.rectangle(BUTTON_PLAY, COLOR_WHITE) 234 | self._draw_controls.rectangle(BUTTON_DELETE, COLOR_WHITE) 235 | self._draw_controls.rectangle(BUTTON_NEXT, COLOR_WHITE) 236 | else: 237 | self._draw_controls.rectangle(BUTTON_RECORD, COLOR_WHITE) 238 | 239 | # Using the controls image as an alpha mask, 240 | # paste the "controls" image, which gives our buttons colour 241 | self._image.paste(self._controls, (20, 115), self._controls_mask) 242 | 243 | def stop(self): 244 | self.stop_recording() 245 | 246 | self.running = False 247 | 248 | def render(self): 249 | # Clear the canvas 250 | self._draw.rectangle((0, 0, 480, 480), (0, 0, 0, 0)) 251 | 252 | self.render_controls() 253 | 254 | if self._clip_exists: 255 | # Clip length bar 256 | self._draw.rectangle((20, 175, 460, 335), transparent(COLOR_WHITE)) 257 | 258 | bar_x = 20 259 | bar_y = 0 260 | bar_color = COLOR_WHITE 261 | 262 | if self._confirm_delete: 263 | bar_color = transparent(COLOR_WHITE) 264 | 265 | for bar in range(44): 266 | scale = self._graph[bar] 267 | bar_w = 5 268 | bar_h = 160 * scale 269 | if bar_h % 1: # Odd height bars wont look gud! 270 | bar_h += 1 271 | bar_y = (175 + 80) # Middle of bar graph 272 | bar_y -= (bar_h // 2) 273 | self._draw.rectangle((bar_x, bar_y, bar_x + bar_w - 1, bar_y + bar_h - 1), bar_color) 274 | bar_x += 10 # 5px bar, 5px gap 275 | 276 | duration_seconds = self.get_duration() 277 | duration_minutes = duration_seconds // 60 278 | duration_seconds %= 60 279 | 280 | duration_minutes = int(duration_minutes) 281 | duration_seconds = int(duration_seconds) 282 | 283 | color = COLOR_WHITE 284 | if self._confirm_delete: 285 | color = transparent(COLOR_WHITE) 286 | self.draw_text(0, 35 - 12, f"Clip {self._clip}", font=self._font, alignment="center", color=color) 287 | self.draw_text(0, 115 - 12, f"{duration_minutes:02d}:{duration_seconds:02d}", font=self._font, alignment="center", color=color) 288 | 289 | else: 290 | if self._clip == 1: 291 | self.draw_text(0, 175, "Press A to record", h=160, font=self._font_small, alignment="center", vertical_alignment="center") 292 | else: 293 | self.draw_text(0, 35 - 12, f"Clip {self._clip}", font=self._font, alignment="center", color=transparent(COLOR_WHITE)) 294 | self.draw_text(0, 115 - 12, "00:00", font=self._font, alignment="center", color=transparent(COLOR_WHITE)) 295 | 296 | 297 | color = COLOR_WHITE 298 | if not self._clip_exists or self._confirm_delete: 299 | color = transparent(COLOR_WHITE) 300 | 301 | self.draw_text(20, 405, "L", font=self._font_tiny, color=color) 302 | self.draw_text(20, 435, "R", font=self._font_tiny, color=color) 303 | 304 | bar_x = 65 305 | vu_left = int(self._vu_left * 40) 306 | vu_right = int(self._vu_right * 40) 307 | for bar in range(40): 308 | bar_w = 5 309 | bar_h = 20 310 | bar_y = 410 311 | 312 | color = transparent(COLOR_WHITE) 313 | 314 | if vu_left > bar: 315 | if self._confirm_delete: 316 | color = transparent(COLOR_GREEN) 317 | else: 318 | color = COLOR_GREEN 319 | 320 | self._draw.rectangle((bar_x, bar_y, bar_x + bar_w - 1, bar_y + bar_h - 1), color) 321 | 322 | bar_y = 440 323 | 324 | color = transparent(COLOR_WHITE) 325 | 326 | if vu_right > bar: 327 | if self._confirm_delete: 328 | color = transparent(COLOR_GREEN) 329 | else: 330 | color = COLOR_GREEN 331 | 332 | 333 | self._draw.rectangle((bar_x, bar_y, bar_x + bar_w - 1, bar_y + bar_h - 1), color) 334 | bar_x += 10 335 | 336 | return Image.alpha_composite(self._background.convert("RGBA"), self._image).convert("RGB") 337 | 338 | # window = tkinter.Tk() 339 | # window.geometry("480x480") 340 | # window.resizable(False, False) 341 | # window.title("Recordamajig") 342 | 343 | SPI_SPEED_MHZ = 80 344 | 345 | display = ST7789( 346 | rotation=90, # Needed to display the right way up on Pirate Audio 347 | port=0, # SPI port 348 | cs=1, # SPI port Chip-select channel 349 | dc=9, # BCM pin used for data/command 350 | backlight=13, 351 | spi_speed_hz=SPI_SPEED_MHZ * 1000 * 1000 352 | ) 353 | 354 | recordamajig = Recordamajig() 355 | 356 | def handle_keydown(e): 357 | char = getattr(e, "char", e) 358 | print("Key {}".format(char)) 359 | if char in ("r", 5): # A button 360 | recordamajig.record() 361 | if recordamajig.recording: 362 | print("Start recording...") 363 | else: 364 | print("Stop recording...") 365 | if char in ("d", 24): # Y button 366 | recordamajig.delete() 367 | if char in ("n", 6): # B button 368 | recordamajig.next() 369 | if char in ("p", 16): # X button 370 | if recordamajig.play(): 371 | print("Start playing...") 372 | else: 373 | print("Stop playing...") 374 | 375 | 376 | GPIO.setmode(GPIO.BCM) 377 | GPIO.setup(BUTTONS, GPIO.IN, pull_up_down=GPIO.PUD_UP) 378 | for pin in BUTTONS: 379 | GPIO.add_event_detect(pin, GPIO.FALLING, handle_keydown, bouncetime=250) 380 | 381 | 382 | # window.protocol("WM_DELETE_WINDOW", recordamajig.stop) 383 | # window.bind("", handle_keydown) 384 | 385 | while recordamajig.running: 386 | # imagetk = ImageTk.PhotoImage(recordamajig.render()) 387 | display.display(recordamajig.render().resize((240, 240))) 388 | 389 | # label = tkinter.Label( 390 | # window, 391 | # image=imagetk 392 | # ) 393 | 394 | # label.place(x=0, y=0, width=480, height=480) 395 | 396 | # window.update() 397 | 398 | time.sleep(1.0 / 30) 399 | -------------------------------------------------------------------------------- /clip-recorder/controls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimoroni/pirate-audio/b7d60bcacedff52cf99a7e35fbd0132446d944ff/clip-recorder/controls.png -------------------------------------------------------------------------------- /clip-recorder/fft.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import math 3 | import time 4 | # import tkinter 5 | import pathlib 6 | import numpy 7 | from PIL import Image, ImageTk, ImageDraw, ImageFont 8 | 9 | from fonts.ttf import RobotoMedium 10 | import RPi.GPIO as GPIO 11 | from ST7789 import ST7789 12 | import sounddevice 13 | import wave 14 | 15 | WIDTH = 480 16 | HEIGHT = 480 17 | 18 | COLOR_WHITE = (255, 255, 255) 19 | COLOR_RED = (232, 56, 58) 20 | COLOR_GREEN = (47, 173, 102) 21 | COLOR_YELLOW = (242, 146, 0) 22 | 23 | BUTTON_RECORD = (0, 0, 50, 50) 24 | BUTTON_PLAY = (390, 0, 460, 50) 25 | BUTTON_DELETE = (390, 230, 460, 280) 26 | BUTTON_NEXT = (0, 230, 50, 280) 27 | 28 | BUTTONS = [5, 6, 16, 24] 29 | LABELS = ["A", "B", "X", "Y"] 30 | 31 | 32 | def transparent(color, opacity=0.2): 33 | opacity = int(255 * opacity) 34 | r, g, b = color 35 | return r, g, b, opacity 36 | 37 | class Recordamajig: 38 | def __init__(self, device="mic_out", output_device="upmix", samplerate=16000): 39 | self._state = "initial" 40 | self._clip = 1 41 | 42 | self._vu_left = 0 43 | self._vu_right = 0 44 | self._graph = [0 for _ in range(44)] 45 | self._fft = [0 for _ in range(10)] 46 | self._indata = numpy.empty((0, 2)) 47 | 48 | self._device = device 49 | self._samplerate = samplerate 50 | 51 | self._image = Image.new("RGBA", (480, 480), (0, 0, 0, 0)) 52 | self._draw = ImageDraw.Draw(self._image) 53 | 54 | self._background = Image.open(pathlib.Path("background.png")) 55 | 56 | self._font = ImageFont.truetype(RobotoMedium, size=62) 57 | self._font_small = ImageFont.truetype(RobotoMedium, size=47) 58 | self._font_tiny = ImageFont.truetype(RobotoMedium, size=28) 59 | 60 | self._stream = sounddevice.InputStream( 61 | device=self._device, # adau7002", 62 | dtype="int16", 63 | channels=2, 64 | samplerate=self._samplerate, 65 | callback=self.audio_callback 66 | ) 67 | 68 | self._stream.start() 69 | 70 | def audio_callback(self, indata, frames, time, status): 71 | self._vu_left = numpy.average(numpy.abs(indata[:,0])) / 65535.0 * 5 72 | self._vu_right = numpy.average(numpy.abs(indata[:,1])) / 65535.0 * 5 73 | 74 | self._graph.append(min(1.0, max(self._vu_left, self._vu_right))) 75 | self._graph = self._graph[-44:] 76 | 77 | self._indata = numpy.concatenate((self._indata, indata)) 78 | if len(self._indata) >= self._samplerate: 79 | self._indata = self._indata[-self._samplerate:] 80 | self.calculate_fft() 81 | 82 | def calculate_fft(self): 83 | fft = numpy.abs(numpy.fft.fft(self._indata[:,0])) / self._samplerate 84 | fft = fft[range(2000)] 85 | 86 | self._fft = numpy.mean(fft.reshape(-1, 2000 // 10), axis=1) 87 | #print(self._fft) 88 | 89 | def draw_text(self, x, y, text, font, w=480, h=None, alignment="left", vertical_alignment="top", color=COLOR_WHITE): 90 | tw, th = self._draw.textsize(text, font=font) 91 | if h is None: 92 | h = th 93 | if alignment == "center": 94 | x += w // 2 95 | x -= tw // 2 96 | if vertical_alignment == "center": 97 | y += h // 2 98 | y -= th // 2 99 | self._draw.text((x, y), text, color, font=font) 100 | 101 | @property 102 | def running(self): 103 | return not self._stream.stopped and self._stream.active 104 | 105 | def render(self): 106 | # Clear the canvas 107 | self._draw.rectangle((0, 0, 480, 480), (0, 0, 0, 0)) 108 | 109 | bar_x = 0 110 | bar_y = 0 111 | bar_color = COLOR_WHITE 112 | 113 | for bar in range(10): 114 | scale = min(1.0, self._fft[bar] / 100.0) 115 | bar_w = 24 116 | bar_h = 480 * scale 117 | bar_h = max(2, bar_h) 118 | if bar_h % 1: 119 | bar_h += 1 120 | bar_y = 240 121 | bar_y -= (bar_h // 2) 122 | self._draw.rectangle((bar_x, bar_y, bar_x + bar_w - 1, bar_y + bar_h - 1), bar_color) 123 | bar_x += 48 124 | 125 | """ 126 | bar_x = 20 127 | 128 | for bar in range(44): 129 | scale = self._graph[bar] 130 | bar_w = 5 131 | bar_h = 480 * scale 132 | bar_h = max(2, bar_h) 133 | if bar_h % 1: # Odd height bars wont look gud! 134 | bar_h += 1 135 | bar_y = (240) # Middle of bar graph 136 | bar_y -= (bar_h // 2) 137 | self._draw.rectangle((bar_x, bar_y, bar_x + bar_w - 1, bar_y + bar_h - 1), bar_color) 138 | bar_x += 10 # 5px bar, 5px gap 139 | """ 140 | 141 | return Image.alpha_composite(self._background.convert("RGBA"), self._image).convert("RGB") 142 | 143 | 144 | SPI_SPEED_MHZ = 80 145 | 146 | display = ST7789( 147 | rotation=90, # Needed to display the right way up on Pirate Audio 148 | port=0, # SPI port 149 | cs=1, # SPI port Chip-select channel 150 | dc=9, # BCM pin used for data/command 151 | backlight=13, 152 | spi_speed_hz=SPI_SPEED_MHZ * 1000 * 1000 153 | ) 154 | 155 | recordamajig = Recordamajig() 156 | 157 | while recordamajig.running: 158 | display.display(recordamajig.render().resize((240, 240))) 159 | 160 | time.sleep(1.0 / 30) 161 | -------------------------------------------------------------------------------- /clip-recorder/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | DATESTAMP=`date "+%Y-%m-%d-%H-%M-%S"` 3 | ASOUND_CONFIG=$HOME/.asoundrc 4 | 5 | 6 | 7 | function add_to_config_text { 8 | CONFIG_LINE="$1" 9 | CONFIG="$2" 10 | sudo sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG 11 | if ! grep -q "$CONFIG_LINE" $CONFIG; then 12 | printf "$CONFIG_LINE\n" | sudo tee -a $CONFIG 13 | fi 14 | } 15 | 16 | function remove_from_config_text { 17 | CONFIG_LINE="$1" 18 | CONFIG="$2" 19 | if grep -qq "^$CONFIG_LINE" $CONFIG; then 20 | warning "Commenting out $CONFIG_LINE in $CONFIG"; 21 | sudo sed -i "s/^$CONFIG_LINE/#$CONFIG_LINE/" $CONFIG 22 | fi 23 | } 24 | 25 | success() { 26 | echo -e "$(tput setaf 2)$1$(tput sgr0)" 27 | } 28 | 29 | inform() { 30 | echo -e "$(tput setaf 6)$1$(tput sgr0)" 31 | } 32 | 33 | warning() { 34 | echo -e "$(tput setaf 1)$1$(tput sgr0)" 35 | } 36 | 37 | if [ $(id -u) -eq 0 ]; then 38 | inform "This script should not be run as root!"; 39 | inform "Try: $0"; 40 | exit 1 41 | fi 42 | 43 | inform "Enabling SPI" 44 | sudo raspi-config nonint do_spi 0 45 | 46 | inform "Installing dependencies" 47 | sudo apt update 48 | sudo apt install -y ladspa-sdk invada-studio-plugins-ladspa 49 | sudo apt install -y pulseaudio python3-pip python3-rpi.gpio python3-spidev python3-numpy python3-pil python3-pil.imagetk libportaudio2 50 | sudo python3 -m pip install fonts font-roboto ST7789 sounddevice 51 | 52 | remove_from_config_text "dtoverlay=hifiberry-dac" /boot/config.txt 53 | 54 | inform "Adding dtoverlay=adau7002-simple to /boot/config.txt" 55 | add_to_config_text "dtoverlay=adau7002-simple" /boot/config.txt 56 | 57 | if [ -f "$ASOUND_CONFIG" ]; then 58 | warning "Backing up $ASOUND_CONFIG to $ASOUND_CONFIG-$DATESTAMP" 59 | cp $ASOUND_CONFIG "$ASOUND_CONFIG-$DATESTAMP" 60 | fi 61 | 62 | inform "Creating $ASOUND_CONFIG" 63 | cat > $ASOUND_CONFIG < /dev/null 2>&1 110 | RESULT=$? 111 | if [ "$RESULT" == "0" ]; then 112 | inform "Stopping Mopidy service..." 113 | systemctl --user stop mopidy 114 | echo 115 | fi 116 | 117 | # Enable SPI 118 | sudo raspi-config nonint do_spi 0 119 | 120 | # Add necessary lines to config.txt (if they don't exist) 121 | add_to_config_text "gpio=25=op,dh" "$CONFIG_TXT" 122 | add_to_config_text "dtoverlay=hifiberry-dac" "$CONFIG_TXT" 123 | 124 | if [ -f "$MOPIDY_CONFIG" ]; then 125 | inform "Backing up mopidy config to: $MOPIDY_CONFIG.backup-$DATESTAMP" 126 | cp "$MOPIDY_CONFIG" "$MOPIDY_CONFIG.backup-$DATESTAMP" 127 | EXISTING_CONFIG=true 128 | echo 129 | fi 130 | 131 | # Install Mopidy and Iris web UI 132 | inform "Installing Mopidy and Iris web UI" 133 | $PIP_BIN install --upgrade mopidy mopidy-iris 134 | 135 | echo 136 | 137 | # Allow Iris to run its system.sh script for https://github.com/pimoroni/pirate-audio/issues/3 138 | # This script backs Iris UI buttons for local scan and server restart. 139 | 140 | # Get location of Iris's system.sh 141 | MOPIDY_SYSTEM_SH=$(python$PYTHON_MAJOR_VERSION - < "$MOPIDY_DEFAULT_CONFIG" 176 | 177 | # Create a directory to hold local music 178 | mkdir -p "$MUSIC_DIR" 179 | 180 | # Add pirate audio customisations 181 | cat < "$MOPIDY_CONFIG" 182 | 183 | [raspberry-gpio] 184 | enabled = true 185 | bcm5 = play_pause,active_low,250 186 | bcm6 = volume_down,active_low,250 187 | bcm16 = next,active_low,250 188 | bcm20 = volume_up,active_low,250 189 | bcm24 = volume_up,active_low,250 190 | 191 | [file] 192 | enabled = true 193 | media_dirs = $MUSIC_DIR 194 | show_dotfiles = false 195 | excluded_file_extensions = 196 | .directory 197 | .html 198 | .jpeg 199 | .jpg 200 | .log 201 | .nfo 202 | .pdf 203 | .png 204 | .txt 205 | .zip 206 | follow_symlinks = false 207 | metadata_timeout = 1000 208 | 209 | [local] 210 | media_dir = $MUSIC_DIR 211 | 212 | [pidi] 213 | enabled = true 214 | display = st7789 215 | rotation = 90 216 | 217 | [mpd] 218 | hostname = 0.0.0.0 219 | 220 | [http] 221 | hostname = 0.0.0.0 222 | 223 | [audio] 224 | mixer_volume = 40 225 | 226 | [spotify] 227 | enabled = false 228 | client_id = 229 | client_secret = 230 | EOF 231 | echo 232 | 233 | # MAYBE?: Remove the sources.list to avoid any future issues with apt.mopidy.com failing 234 | # rm -f /etc/apt/sources.list.d/mopidy.list 235 | 236 | sudo usermod -a -G spi,i2c,gpio,video "$MOPIDY_USER" 237 | 238 | inform "Installing Mopdify VirtualEnv Service" 239 | 240 | sudo mkdir -p /var/cache/mopidy 241 | sudo chown "$MOPIDY_USER:audio" /var/cache/mopidy 242 | mkdir -p "$HOME/.config/systemd/user" 243 | 244 | MOPIDY_BIN=$(which mopidy) 245 | inform "Found bin at $MOPIDY_BIN" 246 | 247 | cat << EOF > "$HOME/.config/systemd/user/mopidy.service" 248 | [Unit] 249 | Description=Mopidy music server 250 | After=avahi-daemon.service 251 | After=dbus.service 252 | After=network-online.target 253 | Wants=network-online.target 254 | After=nss-lookup.target 255 | After=pulseaudio.service 256 | After=remote-fs.target 257 | After=sound.target 258 | 259 | [Service] 260 | WorkingDirectory=/home/$MOPIDY_USER 261 | ExecStart=$MOPIDY_BIN --config $MOPIDY_DEFAULT_CONFIG:$MOPIDY_CONFIG 262 | 263 | [Install] 264 | WantedBy=default.target 265 | EOF 266 | 267 | inform "Enabling and starting Mopidy" 268 | systemctl --user enable mopidy 269 | systemctl --user restart mopidy 270 | 271 | echo 272 | success "All done!" 273 | if [ $EXISTING_CONFIG ]; then 274 | diff "$MOPIDY_CONFIG" "$MOPIDY_CONFIG.backup-$DATESTAMP" > /dev/null 2>&1 275 | RESULT=$? 276 | if [ ! $RESULT == "0" ]; then 277 | warning "Mopidy configuration has changed, see summary below and make sure to update $MOPIDY_CONFIG!" 278 | inform "Your previous configuration was backed up to $MOPIDY_CONFIG.backup-$DATESTAMP" 279 | diff "$MOPIDY_CONFIG" "$MOPIDY_CONFIG.backup-$DATESTAMP" 280 | else 281 | echo "Don't forget to edit $MOPIDY_CONFIG with your preferences and/or Spotify config." 282 | fi 283 | else 284 | echo "Don't forget to edit $MOPIDY_CONFIG with you preferences and/or Spotify config." 285 | fi 286 | -------------------------------------------------------------------------------- /raspotify/README.md: -------------------------------------------------------------------------------- 1 | # Pirate Audio Raspotify/Spotify Connect Setup 2 | 3 | **Note**: This software is currently in beta and subject to change. 4 | Hopefully we'll have an easy installer ready soon, but in the mean time read-on if you want to be an early adopter. 5 | 6 | ## Installing 7 | 8 | Album art and track information display is supported on Pirate Audio boards using [Raspotify](https://github.com/dtcooper/raspotify) and [PiDi Spotify](https://github.com/pimoroni/pidi-spotify). 9 | 10 | Currently it's not possible to control Spotify. 11 | 12 | This is beta software that uses librespotify's `--onevent` hook to push information to the display. 13 | 14 | For more information, see the [PiDi Spotify README](https://github.com/pimoroni/pidi-spotify). 15 | -------------------------------------------------------------------------------- /shairport-sync/README.md: -------------------------------------------------------------------------------- 1 | # Pirate Audio Shairport Sync Setup 2 | 3 | Note: This software is currently in beta and subject to change. Hopefully we'll have an easy installer ready soon, but in the mean time read-on if you want to be an early adopter. 4 | 5 | ## Installing 6 | 7 | Shairport Sync support comes in to parts: 8 | 9 | * Button control using (shairport-sync-control.py)[../examples/shairport-sync-control.py] 10 | * Album art/track information usiong (Pirate Display)[https://github.com/pimoroni/pidi/pull/3] (BETA) 11 | 12 | You must run both of these applications for full Shairport control and display. 13 | 14 | Additionally Shairport-Sync must be configured `--with-metadata` and `--with-dbus-interface` like so: 15 | 16 | ``` 17 | ./configure --with-metadata --with-dbus-interface --with-ssl=openssl --with-alsa --with-avahi --with-systemd 18 | ``` -------------------------------------------------------------------------------- /speaker-and-mic/.gitignore: -------------------------------------------------------------------------------- 1 | adau7002-pcm5102a-io.dtbo 2 | -------------------------------------------------------------------------------- /speaker-and-mic/Makefile: -------------------------------------------------------------------------------- 1 | dtoverlay: 2 | dtc -I dts -O dtb -o adau7002-pcm5102a-io.dtbo adau7002-pcm5102a-io-overlay.dts 3 | 4 | install: dtoverlay 5 | cp adau7002-pcm5102a-io.dtbo /boot/overlays 6 | -------------------------------------------------------------------------------- /speaker-and-mic/README.md: -------------------------------------------------------------------------------- 1 | # Speaker & Dual Mic HAT Combined 2 | 3 | The following dtoverlay and instructions will help you use a speaker/audio output HAT (specifically one that uses pcm5012a) alongside Dual Mic HAT (adau7002). 4 | 5 | ## Installing 6 | 7 | Build the .dtbo file with `make` and then copy it to `/boot/overlays` or run `sudo make install`. 8 | 9 | Add the following to `/boot/config.txt`: 10 | 11 | ``` 12 | dtoverlay=adau7002-pcm5102a-io 13 | ``` 14 | 15 | Add the following into `~/.asound.conf` or `/etc/asound.conf` as per the clip recorder README: https://github.com/pimoroni/pirate-audio/tree/master/clip-recorder 16 | 17 | ``` 18 | pcm.mic_hw{ 19 | type hw 20 | card adau7002 21 | format S32_LE 22 | rate 48000 23 | channels 2 24 | } 25 | pcm.mic_rt{ 26 | type route 27 | slave.pcm mic_hw 28 | ttable.0.0 1 29 | ttable.0.1 0 30 | ttable.1.0 0 31 | ttable.1.1 1 32 | } 33 | pcm.mic_plug { 34 | type plug 35 | slave.pcm mic_rt 36 | } 37 | pcm.mic_filter { 38 | type ladspa 39 | slave.pcm mic_plug 40 | path "/usr/lib/ladspa"; 41 | plugins [ 42 | { 43 | label invada_hp_stereo_filter_module_0_1 44 | input { 45 | controls [ 46 | 50 # Cut off frequency (Hz) 47 | 30 # Gain (dB) 48 | 1 # Soft Clip (on/off) 49 | ] 50 | } 51 | } 52 | ] 53 | } 54 | pcm.mic_out { 55 | type plug 56 | slave.pcm mic_filter 57 | } 58 | ``` 59 | 60 | Test the speaker with: 61 | 62 | ``` 63 | speaker-test -c2 -twav -l2 64 | ``` 65 | 66 | Try a test recording: 67 | 68 | ``` 69 | arecord -D hw:adau7002,1 -c2 -r48000 -fS32_LE -twav -d5 -R10000 -Vstereo test.wav 70 | ``` 71 | 72 | 73 | Thanks to Don - https://forums.pimoroni.com/t/small-speaker-dual-mic-hats-at-the-same-time/18083/7 - for verifying this! 74 | -------------------------------------------------------------------------------- /speaker-and-mic/adau7002-pcm5102a-io-overlay.dts: -------------------------------------------------------------------------------- 1 | /dts-v1/; 2 | /plugin/; 3 | 4 | / { 5 | compatible = "brcm,bcm2835"; 6 | 7 | fragment@0 { 8 | target = <&i2s>; 9 | __overlay__ { 10 | status = "okay"; 11 | }; 12 | }; 13 | 14 | fragment@1 { 15 | target-path = "/"; 16 | __overlay__ { 17 | pcm5102a_codec: pcm5102a-codec { 18 | #sound-dai-cells = <0>; 19 | compatible = "ti,pcm5102a"; 20 | status = "okay"; 21 | }; 22 | }; 23 | }; 24 | 25 | fragment@2 { 26 | target-path = "/"; 27 | __overlay__ { 28 | adau7002_codec: adau7002-codec { 29 | #sound-dai-cells = <0>; 30 | compatible = "adi,adau7002"; 31 | /* IOVDD-supply = <&supply>;*/ 32 | status = "okay"; 33 | }; 34 | }; 35 | }; 36 | 37 | fragment@3 { 38 | target = <&sound>; 39 | sound_overlay: __overlay__ { 40 | compatible = "simple-audio-card"; 41 | simple-audio-card,format = "i2s"; 42 | simple-audio-card,name = "adau7002"; 43 | simple-audio-card,bitclock-slave = <&dailink1>; 44 | simple-audio-card,frame-slave = <&dailink1>; 45 | simple-audio-card,widgets = 46 | "Microphone", "Microphone Jack"; 47 | simple-audio-card,routing = 48 | "PDM_DAT", "Microphone Jack"; 49 | status = "okay"; 50 | dailink0: simple-audio-card,dai-link@0 { 51 | reg = <0>; 52 | format = "i2s"; 53 | cpu { 54 | sound-dai = <&i2s>; 55 | }; 56 | codec { 57 | sound-dai = <&adau7002_codec>; 58 | }; 59 | }; 60 | dailink1: simple-audio-card,dai-link@1 { 61 | reg = <0>; 62 | format = "i2s"; 63 | cpu { 64 | sound-dai = <&i2s>; 65 | }; 66 | codec { 67 | sound-dai = <&pcm5102a_codec>; 68 | }; 69 | }; 70 | }; 71 | }; 72 | 73 | 74 | __overrides__ { 75 | card-name = <&sound_overlay>,"simple-audio-card,name"; 76 | }; 77 | }; 78 | -------------------------------------------------------------------------------- /speaker-and-mic/asound.conf: -------------------------------------------------------------------------------- 1 | pcm.mic_hw{ 2 | type hw 3 | card adau7002 4 | format S32_LE 5 | rate 48000 6 | channels 2 7 | } 8 | pcm.mic_rt{ 9 | type route 10 | slave.pcm mic_hw 11 | ttable.0.0 1 12 | ttable.0.1 0 13 | ttable.1.0 0 14 | ttable.1.1 1 15 | } 16 | pcm.mic_plug { 17 | type plug 18 | slave.pcm mic_rt 19 | } 20 | pcm.mic_filter { 21 | type ladspa 22 | slave.pcm mic_plug 23 | path "/usr/lib/ladspa"; 24 | plugins [ 25 | { 26 | label invada_hp_stereo_filter_module_0_1 27 | input { 28 | controls [ 29 | 50 # Cut off frequency (Hz) 30 | 30 # Gain (dB) 31 | 1 # Soft Clip (on/off) 32 | ] 33 | } 34 | } 35 | ] 36 | } 37 | pcm.mic_out { 38 | type plug 39 | slave.pcm mic_filter 40 | } 41 | 42 | --------------------------------------------------------------------------------