Skip to content

Instantly share code, notes, and snippets.

@SuperSonicHub1
Created October 12, 2023 01:56
Show Gist options
  • Save SuperSonicHub1/0a434c7102e74eb0cfca3d3cf1de74cf to your computer and use it in GitHub Desktop.
Save SuperSonicHub1/0a434c7102e74eb0cfca3d3cf1de74cf to your computer and use it in GitHub Desktop.
Synth-sampler hybrid for Winterbloom's Big Honking Button
# We had to patch select_from_list_using_cv LOL
# The MIT License (MIT)
#
# Copyright (c) 2020 Alethea Flowers for Winterbloom
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import audiocore
import audioio
import board
import digitalio
import winterbloom_voltageio
try:
import _bhb
except ImportError:
raise RuntimeError("This BHB library requires CircuitPython >= 6.0.0")
def _detect_board_revision():
v5pin = digitalio.DigitalInOut(board.V5)
v5pin.switch_to_input(pull=digitalio.Pull.UP)
# Pulled low on v5+, pull-up will make it true
# on <=v4.
if not v5pin.value:
return 5
else:
return 4
class _AnalogIn:
def __init__(self):
_bhb.init_adc()
@property
def value(self):
return _bhb.read_adc()
class _InputState:
def __init__(self, pin):
self._in = digitalio.DigitalInOut(pin)
self._in.switch_to_input(pull=digitalio.Pull.UP)
self.state = False
self.last_state = False
def update(self):
self.last_state = self.state
self.state = not self._in.value
@property
def rising_edge(self):
return self.state and not self.last_state
@property
def falling_edge(self):
return not self.state and self.last_state
@property
def value(self):
return self.state
pressed = rising_edge
triggered = rising_edge
released = falling_edge
held = value
__bool__ = value
class BigHonkingButton:
def __init__(self):
self.board_revision = _detect_board_revision()
self._button = _InputState(board.BUTTON)
self._gate_in = _InputState(board.GATE_IN)
self._gate_out = digitalio.DigitalInOut(board.GATE_OUT)
self._gate_out.switch_to_output()
self._pitch_in = winterbloom_voltageio.VoltageIn(_AnalogIn())
if self.board_revision >= 5:
self._pitch_in.direct_calibration(
{4068: -5.0, 3049: -2.5, 2025: 0, 1001: 2.5, 8: 5.0}
)
self.min_cv = -5.0
self.max_cv = 5.0
else:
self._pitch_in.direct_calibration(
{4068: -2.0, 3049: -1.0, 2025: 0, 1001: 1.0, 8: 2.0}
)
self.min_cv = -2.0
self.max_cv = 2.0
self.audio_out = audioio.AudioOut(board.HONK_OUT)
def update(self):
self._gate_in.update()
self._button.update()
return True
@property
def button(self):
return self._button
@property
def gate_in(self):
return self._gate_in
@property
def pitch_in(self):
return self._pitch_in.voltage
@property
def gate_out(self):
return self._gate_out.value
@gate_out.setter
def gate_out(self, value):
self._gate_out.value = value
@property
def triggered(self):
return self._button.triggered or self._gate_in.triggered
@property
def released(self):
return self._button.released or self._gate_in.released
def load_sample(self, path):
return audiocore.WaveFile(open(path, "rb"))
def play(self, sample, pitch_cv=None, loop=False):
if pitch_cv is not None:
sample_rate = min(int(44100 * pow(2, pitch_cv)), (350000 - 1))
sample.sample_rate = sample_rate
self.audio_out.stop()
self.audio_out.play(sample, loop=loop)
def stop(self):
self.audio_out.stop()
def select_from_list_using_cv(self, list, cv, low=None, high=None):
if low is None:
low = self.min_cv
if high is None:
high = self.max_cv
cv = min(high, max(low, cv))
count = len(list)
span = high - low
value = cv - low
index = int((value / span) * count)
if index >= count: index = count - 1
return list[index]
# This advanced example shows how to create a custom waveform
# for the button to output. In this case, it generates a
# sine wave. You can use this example as a basis for very
# basic oscillators.
# See also noise.py
import array
import math
import random
import audiocore
import winterbloom_bhb
import time
bhb = winterbloom_bhb.BigHonkingButton()
# This generates a raw set of samples that represents one full
# cycle of a sine wave. If you wanted different waveforms, you
# could change the formula here to generate that instead.
def generate_sine_wave(volume=1.0, wave = 0):
volume = volume * (2 ** 15 - 1) # Increase this to increase the volume of the tone.
length = 100
L = 25
samples = array.array("H", [0] * length)
L = L
G = L / 2**(7/12.) # 8
for i in range(length):
samples[i] = (
int(
(1 - (i%G<G/2) + random.random() * 0.5)
* (volume / (1+4*(wave==0)))
)
+ int((1 - (i%L<L/2)) * (volume / (1+4*(wave==1))))
)
return samples
# print(len(bhb.load_sample(f"samples/amen_crash.wav")))
drum_samples = [
bhb.load_sample(f"samples/kick.wav"),
bhb.load_sample(f"samples/snare.wav"),
bhb.load_sample(f"samples/clap.wav"),
bhb.load_sample(f"samples/hihat.wav"),
bhb.load_sample(f"samples/amen_crash.wav"),
]
last_released = None
muted = False
wave = 0
# time to press the button the
PRESS_DELAY = 0.2
while bhb.update():
# Holding down the button after striking it quickly mutes
if bhb.button.pressed:
pressed = time.monotonic()
if last_released is not None and pressed - last_released < PRESS_DELAY:
muted = True
if bhb.button.released:
last_released = time.monotonic()
muted = False
wave = (wave + 1) % 2
if not muted and bhb.gate_in.triggered:
bhb.gate_out = random.choice([True] + [False] * 9)
# Our Korg SQ-1 sequencer's CV output ranges from 0 to 5V.
# The lower 2.5V plays drums.
# The upper half plays a synthesizer.
if bhb.pitch_in <= 2.5:
sample = bhb.select_from_list_using_cv(
drum_samples,
bhb.pitch_in,
low=0,
high=2.5
)
bhb.play(
sample,
pitch_cv=bhb.pitch_in
)
else:
raw_sine = generate_sine_wave(0.8, wave)
sine_wave = audiocore.RawSample(raw_sine)
# Change this to play different notes. You can also
# check the CV input using `bhb.pitch_in` and re-adjust
# the sample rate.
frequency = 440
sine_wave.sample_rate = frequency * len(raw_sine)
bhb.play(sine_wave, pitch_cv=bhb.pitch_in - 4.5, loop=True)
# Print out every other sample to prevent a data flood
# for x in raw_sine:
# print((x,))
if bhb.released:
bhb.gate_out = False
# bhb.stop()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment