Victoria
Victoria is a basic Raspberry Pi audio sampler that can play back audio samples from a USB thumb drive. It is named after the 2015 movie by Sebastian Schipper.
It uses Pimoronis Piano HAT, Drum HAT, pHAT Stack and Adafruits I2S Audio Bonnet (Pimoronis pHAT DAC also works; for direct output to speakers the HiFiBerry MiniAmp is a good option). A Raspberry Pi Zero W was soldered directly onto the pHAT Stack with enough spacing to put the Audio Bonnet on top. Only one micro USB cable is needed to power the sampler, so a power bank can be used to play on the go. The Transcend JetFlash 880 is the perfect thumb drive for this project because it plugs directly into the Pi Zeros micro USB interface, while also providing a USB Type-A connector.
On the USB thumb drive are two directories: drums and piano. The drums folder can hold up to 8 samples which can be played via the Drum HAT, the piano folder can hold hundreds of samples which can be played via the Piano HAT; with the Octave Up / Down buttons one can cycle through the samples in batches of 13.
When holding the Instrument button and pressing either Octave Up or Down one can change the output volume. Holding Instrument and pressing the Drum HAT pad #8 two times will shut down the Pi.
The code is written in Python and based on the Piano HATs simple-piano.py example and the Drum HATs drums.py example.
#!/usr/bin/python
#########################
# Victoria
# by maxhaesslein, 2020
# https://maxhaesslein.de
#
# v.1.1.0
#########################
import glob
import stat
import os
import re
import signal
import time
from sys import exit
import pygame
import pianohat
import drumhat
BANK_PIANO = os.path.join(os.path.dirname(__file__), "sounds/piano")
BANK_DRUMS = os.path.join(os.path.dirname(__file__), "sounds/drums2")
MOUNT_PATH = '/mnt/victoria_usb'
MOUNT_VOLUME = '/dev/sda1'
FILETYPES = ['*.wav', '*.WAV', '*.ogg', '*.OGG']
use_drive = False # gets set to true if the volume is mounted
volume_step = 1.0/13.0
global_volume = 10.0/13.0
print("Press CTRL+C to exit.")
NOTE_OFFSET = 3
samples_piano = []
files_piano = []
octave_piano = 0
octaves_piano = 0
instrument_button_down = False
shutdown_button_counter = 0
options = {
'samplerate': 44100,
'folder_piano': '/piano',
'folder_drums': '/drums'
}
def disk_exists(path):
try:
return stat.S_ISBLK(os.stat(path).st_mode)
except:
return False
# check if usb thumb drive exists
print( 'check if '+str(MOUNT_VOLUME)+' exists')
if( disk_exists(MOUNT_VOLUME) ):
print(' yes')
# try to mount
os.system( 'sudo mount -o ro '+str(MOUNT_VOLUME)+' '+str(MOUNT_PATH) )
use_drive = True
BANK_PIANO = str(MOUNT_PATH)+options['folder_piano']
BANK_DRUMS = str(MOUNT_PATH)+options['folder_drums']
else:
print(' no')
if use_drive and os.path.isfile(str(MOUNT_PATH)+'/config.txt'):
import ConfigParser
configParser = ConfigParser.RawConfigParser()
configParser.read( str(MOUNT_PATH)+'/config.txt' )
for option in options:
if configParser.has_option('Victoria', option):
options[option] = configParser.get('Victoria', option)
if options[option] == 'False':
options[option] = False
if options[option] == 'True':
options[option] = True
if option == 'samplerate':
options[option] = int(options[option])
print( 'options:', options )
pygame.mixer.pre_init(options['samplerate'], -16, 1, 512)
pygame.mixer.init()
pygame.mixer.set_num_channels(32)
def set_volume( direction ):
global global_volume
if direction > 0:
global_volume += volume_step
elif direction < 0:
global_volume -= volume_step
if global_volume >= 1:
global_volume = 1.0
elif global_volume <= 0:
global_volume = 0.0
max_led = int(round(global_volume * 13))
for i in range(0, 13):
pianohat.set_led(i, False)
for i in range(0, max_led):
pianohat.set_led(i, True)
set_all_volume()
def set_all_volume():
print("set volume to "+str(global_volume))
for sound in samples_piano:
sound.set_volume( global_volume )
for sound in samples_drums:
sound.set_volume( global_volume )
def handle_note(channel, pressed):
channel = channel + (12 * octave_piano)
if len(samples_piano) > 13:
channel += NOTE_OFFSET
if channel < len(samples_piano) and pressed:
print('Playing Sound: {}'.format(files_piano[channel]))
samples_piano[channel].play(loops=0)
def handle_instrument(channel, pressed):
global instrument_button_down
instrument_button_down = pressed
pianohat.set_led(13, False)
pianohat.set_led(14, False)
pianohat.set_led(15, False)
if pressed:
pianohat.auto_leds(False)
pianohat.set_led(channel, True)
max_led = int(round(global_volume * 13))
for i in range(0, 13):
pianohat.set_led(i, False)
for i in range(0, max_led):
pianohat.set_led(i, True)
else:
pianohat.auto_leds(True)
global shutdown_button_counter
shutdown_button_counter = 0
for i in range(0, 16):
pianohat.set_led(i, False)
def handle_octave_up(channel, pressed):
if instrument_button_down:
pianohat.set_led(channel, pressed)
if pressed:
set_volume( +1 )
else:
global octave_piano
if pressed and octave_piano < octaves_piano:
octave_piano += 1
print('Selected Octave: {}'.format(octave_piano))
def handle_octave_down(channel, pressed):
if instrument_button_down:
pianohat.set_led(channel, pressed)
if pressed:
set_volume( -1 )
else:
global octave_piano
if pressed and octave_piano > 0:
octave_piano -= 1
print('Selected Octave: {}'.format(octave_piano))
# drums
def handle_drums_hit(event):
# event.channel is a zero based channel index for each pad
# event.pad is the pad number from 1 to 8
if event.pad == 8 and instrument_button_down:
global shutdown_button_counter
shutdown_button_counter += 1
if shutdown_button_counter > 0:
pianohat.auto_leds(False)
for i in range(0, 16):
pianohat.set_led(i, True)
if shutdown_button_counter > 1:
drumhat.all_on()
for i in range(0, 16):
pianohat.set_led(i, False)
time.sleep(0.02)
drumhat.all_off()
shutdown_action( False )
return
try:
samples_drums[event.channel].play(loops=0)
print("You hit pad {}, playing: {}".format(event.pad,files_drums[event.channel]))
except IndexError:
print("Pad {} has no sound".format(event.pad))
def handle_drums_release():
pass
def shutdown_action( skip_shutdown ):
print( 'starting shut down ...')
pianohat.auto_leds(False)
for i in range(0, 16):
pianohat.set_led(i, False)
drumhat.all_off()
if use_drive:
print( 'unmounting '+str(MOUNT_PATH) )
os.system( 'sudo umount '+str(MOUNT_PATH) )
if not skip_shutdown:
print("shut down ...")
os.system('sudo shutdown now')
files_piano = []
for filetype in FILETYPES:
files_piano.extend(glob.glob(os.path.join(BANK_PIANO, filetype)))
files_piano.sort()
octaves_piano = len(files_piano) / 12
samples_piano = [pygame.mixer.Sound(sample) for sample in files_piano]
octave_piano = int(octaves_piano / 2)
files_drums = []
for filetype in FILETYPES:
files_drums.extend(glob.glob(os.path.join(BANK_DRUMS, filetype)))
files_drums.sort()
samples_drums = [pygame.mixer.Sound(f) for f in files_drums]
pianohat.auto_leds(False)
for i in range(0, 16):
pianohat.set_led(i, False)
# ready-animation
for i in range(0, 13):
pianohat.set_led(i, True)
time.sleep(0.05)
time.sleep(0.2)
for i in range(0, 16):
pianohat.set_led(i, False)
pianohat.auto_leds(True)
set_all_volume()
drumhat.on_hit(drumhat.PADS, handle_drums_hit)
drumhat.on_release(drumhat.PADS, handle_drums_release)
pianohat.on_note(handle_note)
pianohat.on_octave_up(handle_octave_up)
pianohat.on_octave_down(handle_octave_down)
pianohat.on_instrument(handle_instrument)
def sigint_handler(signal_received, frame):
print('SIGINT or CTRL-C detected. Exiting gracefully')
shutdown_action( True )
exit(0)
signal.signal(signal.SIGINT, sigint_handler) # capture ctrl+c
signal.pause()
If you want to use this script make sure that:
-
you have the Adafruit I2S Audio Bonnet script or the Pimoroni pHAT DAC script installed
-
the directory /mnt/victoria_usb exists so the USB thumb drive can be mounted (you can change this with the MOUNT_PATH variable)
the script assumes the path to thumb drive is /dev/sda1 (if not, change the MOUNT_VOLUME variable) -
you can create a config.txt file in the root directory of the thumb drive to overwrite some options; it must look like this:
[Victoria] samplerate = 48000 folder_piano = /piano folder_drums = /drums
-
the samples need to be .wav or .ogg files with stereo channel and the correct samplerate (see options)
-
on the thumb drive there must be a folder called drums with up to 8 samples and a folder called piano with 13 or more samples; the folder names can be changed in the options variable or via the config.txt file
-
the default sample rate is 44100 but can be changed in the options variable or via the config.txt file; all samples need to be in this sample rate
-
there a two folders (piano and drums2) with default samples if the USB thumb drive is not present, in the same folder as the script in a subfolder called sounds; change the variables BANK_PIANO and BANK_DRUMS if you want to move them
-
set the Pi to Autologin (Console) and auto start the script on log in
-
you can activate the Overlay FS option in raspi-config to protect the SD-card from data loss
-
the thumb drive gets mounted as read only (-o ro argument)
-
the I2S Audio Bonnet or pHAT DAC have a line out, not a headphone jack plug; if you use headphones the output volume will be too loud
-
to find out which HATs work together you can use the pinout.xyz pHAT Stack Configurator
Thanks to Simona for sound, performance in the video and additional photos.