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.

Victoria

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:

Thanks to Simona for sound, performance in the video and additional photos.