A python synthesizer for creating and manipulating audio signals.
To install: pip install hum
Note: Some functionalities depend on pyo, which itself requires some tools (namely, portaudio and Portmidi) that may have to be installed manually.
The Synth class is a powerful wrapper around Pyo's audio engine, providing:
- Real-time parameter control - Change audio parameters (knobs) during playback
- Event recording - Automatically record all parameter changes with timestamps
- Event playback - Replay recorded events exactly as they happened
- Event rendering - Convert recorded events into audio files
- Context manager - Clean resource management with
withstatements
from hum.pyo_util import SynthLet's start with a simple sine wave synthesizer that demonstrates the basic functionality:
import time
from pyo import Sine
import recode
from hum.pyo_util import Synth, DFLT_PYO_SR, round_event_times
from hum.extra_util import estimate_frequencies
base_freq = 220
# Make a synth that plays a simple sine wave
def simple_sine(freq=base_freq):
return Sine(freq=freq)
s = Synth(simple_sine)
# Define a sequence of frequencies to play
freq_sequence = [base_freq] + [base_freq * 3 / 2, base_freq * 2]
# Play the frequencies in sequence
with s:
time.sleep(1) # let base_freq play for a second
s(freq=freq_sequence[1]) # Change to 330 Hz
time.sleep(1) # play that for a second
s['freq'] = freq_sequence[2] # Change to 440 Hz (alternative syntax)
time.sleep(1) # play that for a second
# The context manager exits here, which stops the synthAfter playing the synth, you can retrieve the recorded events:
# Get the recorded events
events = s.get_recording()
# Round timestamps for better readability
events = list(round_event_times(events, round_to=0.1))
# These are the events we expect to see:
expected_events = [
(
0.0,
{
# First event contains the synth's initial parameters
'freq': {'value': 220, 'time': 0.025, 'mul': 1, 'add': 0},
},
),
(1.0, {'freq': 330.0}), # Second event at 1.0 seconds
(2.0, {'freq': 440}), # Third event at 2.0 seconds
(3.0, {}), # Final event marks the end of recording
]
# Verify that we got what we expected
assert events == expected_eventsYou can render these events to audio:
# Render events to WAV format bytes
wav_bytes = s.render_events(events)
# Decode and verify the WAV bytes
wf, sr = recode.decode_wav_bytes(wav_bytes)
assert sr == DFLT_PYO_SR
total_duration = len(wf) / sr
assert abs(total_duration - 3.0) < 0.1This simple example demonstrates the core functionality of the Synth class:
- Creating a synthesizer with a function that returns a Pyo object
- Playing the synth with real-time parameter changes
- Recording parameter changes as events
- Rendering events to audio
For more interactive work, you can use the Synth class in a REPL environment. This allows you to experiment with different parameters in real-time.
The Synth class distinguishes between two types of parameters:
- Dials - Real-time controllable parameters that use
pyo.SigTofor smooth transitions - Settings - Parameters that require rebuilding the synthesis graph when changed
By default, all parameters are treated as dials. You can specify which parameters should be dials or settings using the decorator syntax:
from pyo import LFO, Adsr, Sine
from hum.pyo_util import Synth
from time import sleep
# Note that here we explicitly tell Synth what arguments are "dials" and what are "settings"
@Synth(dials='freq', settings='waveform attack')
def simple_waveform_synth(freq=440, attack=0.01, waveform='sine'):
env = Adsr(attack=attack, decay=0.1, sustain=0.8, release=0.1, dur=0, mul=0.5).play()
wave = {
'sine': Sine,
'triangle': lambda freq, mul: LFO(freq=freq, type=3, mul=mul),
'square': lambda freq, mul: LFO(freq=freq, type=1, mul=mul),
}.get(waveform, Sine)
return wave(freq=freq, mul=env)In this example:
freqis a dial - It changes smoothly in real-timewaveformandattackare settings - Changing them rebuilds the synth
For interactive control in a REPL, you need to explicitly start and stop the synth:
# Start the synth (begins making sound)
simple_waveform_synth.start()
# Change the frequency
simple_waveform_synth(freq=440 * 3 / 2) # Change to 660 Hz
# The sound continues to play, but now at a different frequency
# Change multiple parameters at once
simple_waveform_synth(freq=440, waveform='triangle')
# Now playing a triangle wave at 440 Hz
# Change the waveform and attack time
simple_waveform_synth(waveform='square', attack=0.5)
# Note: You won't hear the change in attack time until the next note is played!
# Stop the synth when done
simple_waveform_synth.stop()Note that when you restart a synth, it retains its last state:
# Start the synth again - it will use the last parameter values
simple_waveform_synth.start() # Still has square waveform and attack=0.5
# Change back to sine wave
simple_waveform_synth(waveform='sine') # Note the attack is still 0.5
# Stop the synth when done
simple_waveform_synth.stop()Warning: Always remember to stop your synths when done to avoid resource issues. Using the context manager approach is recommended for automatic cleanup.
Instead of interactive control, you can also precompute a sequence of parameter changes:
from pyo import LFO, Adsr, Sine
from hum.pyo_util import Synth
from time import sleep
@Synth(dials='freq', settings='waveform attack')
def simple_waveform_synth(freq=440, attack=0.01, waveform='sine'):
env = Adsr(attack=attack, decay=0.1, sustain=0.8, release=0.1, dur=0, mul=0.5).play()
wave = {
'sine': Sine,
'triangle': lambda freq, mul: LFO(freq=freq, type=3, mul=mul),
'square': lambda freq, mul: LFO(freq=freq, type=1, mul=mul),
}.get(waveform, Sine)
return wave(freq=freq, mul=env)
with simple_waveform_synth as s:
sleep(1) # Play default settings for a second
s(freq=440 * 3 / 2) # Change to 660 Hz
sleep(1)
s(freq=440, waveform='triangle') # Change to triangle wave at 440 Hz
sleep(0.5) # Shorter wait this time
s(waveform='square', attack=0.5) # Change to square wave with longer attack
sleep(2) # Wait a bit longer
s(waveform='sine') # Change back to sine wave (attack still 0.5)
sleep(1) # Wait for the final change to be heardThe Synth class automatically records all parameter changes with timestamps:
# Get the recorded events after playing
events = simple_waveform_synth.get_recording()This gives you a list of timestamped parameter changes:
[
(0,
{'freq': {'value': 440, 'time': 0.025, 'mul': 1, 'add': 0},
'attack': 0.01,
'waveform': 'sine'}),
(1.0052499771118164, {'freq': 660.0}),
(2.007974863052368, {'freq': 440, 'waveform': 'triangle'}),
(2.0084967613220215, {'waveform': 'triangle'}),
(2.515920877456665, {'waveform': 'square', 'attack': 0.5}),
(2.518988609313965, {'waveform': 'square', 'attack': 0.5}),
(4.524500846862793, {'waveform': 'sine'}),
(4.525563716888428, {'waveform': 'sine'}),
(5.528840780258179, {})
]You can round the timestamps for better readability:
from hum.pyo_util import round_event_times
events = list(round_event_times(simple_waveform_synth.get_recording(), 0.1))This gives you a cleaner view:
[
(0.0,
{'freq': {'value': 440, 'time': 0.025, 'mul': 1, 'add': 0},
'attack': 0.01,
'waveform': 'sine'}),
(1.0, {'freq': 660.0}),
(2.0, {'freq': 440, 'waveform': 'triangle'}),
(2.0, {'waveform': 'triangle'}),
(2.5, {'waveform': 'square', 'attack': 0.5}),
(2.5, {'waveform': 'square', 'attack': 0.5}),
(4.5, {'waveform': 'sine'}),
(4.5, {'waveform': 'sine'}),
(5.5, {})
]You can create, edit, or manipulate event sequences directly:
# Manually define an event sequence
events = [
(
0.0,
{
'freq': {'value': 440, 'time': 0.025, 'mul': 1, 'add': 0},
'attack': 0.01,
'waveform': 'sine',
},
),
(1.0, {'freq': 660.0}),
(2.0, {'freq': 440, 'waveform': 'triangle'}),
(2.0, {'waveform': 'triangle'}),
(2.5, {'waveform': 'square', 'attack': 0.5}),
(2.5, {'waveform': 'square', 'attack': 0.5}),
(4.5, {'waveform': 'sine'}),
(4.5, {'waveform': 'sine'}),
(5.5, {}),
]You can replay any event sequence through a compatible synth:
@Synth(dials='freq', settings='waveform attack')
def simple_waveform_synth(freq=440, attack=0.01, waveform='sine'):
env = Adsr(attack=attack, decay=0.1, sustain=0.8, release=0.1, dur=0, mul=0.5).play()
wave = {
'sine': Sine,
'triangle': lambda freq, mul: LFO(freq=freq, type=3, mul=mul),
'square': lambda freq, mul: LFO(freq=freq, type=1, mul=mul),
}.get(waveform, Sine)
return wave(freq=freq, mul=env)
# Replay the event sequence
simple_waveform_synth.replay_events(events)You can render events to audio without real-time playback:
import recode
# Render events to WAV format bytes
wav_bytes = simple_waveform_synth.render_events(events)
# Decode the WAV bytes for analysis or saving
wf, sr = recode.decode_wav_bytes(wav_bytes)
# Visualize the waveform
from hum import disp_wf
disp_wf(wf, sr)The ReplayEvents class is a utility for replaying timestamped events with proper timing:
from hum.pyo_util import ReplayEvents
# Get your events, either from a recording or created manually
events = [
(0.0, {'freq': {'value': 440, 'time': 0.025, 'mul': 1, 'add': 0}}),
(1.0, {'freq': 330.0}),
(2.0, {'freq': 440}),
(3.0, {})
]
# Create a replay generator and iterate through it
for knob_update in ReplayEvents(events):
print(f"Update: {knob_update}")
# Do something with each updateThe ReplayEvents class supports options like:
emit_none: If True, yields None to simulate time passingtime_scale: Speed up or slow down playback (e.g., 2.0 for twice as fast)ensure_sorted: Sort events by timestamp before playback
You can manipulate event sequences programmatically:
# Create a modified version by changing timestamps
faster_events = [(t/2, params) for t, params in events]
# Create a reversed sequence
reversed_events = [(events[-1][0] - t, params) for t, params in events]
# Combine sequences by appending
melody = events + [(t + events[-1][0], params) for t, params in events]
# Transpose a sequence by modifying frequency values
def transpose(events, semitones):
factor = 2 ** (semitones/12)
result = []
for t, params in events:
new_params = params.copy()
if 'freq' in new_params:
if isinstance(new_params['freq'], dict):
new_params['freq'] = {
k: v * factor if k == 'value' else v
for k, v in new_params['freq'].items()
}
else:
new_params['freq'] = new_params['freq'] * factor
result.append((t, new_params))
return result
# Transpose up a perfect fifth (7 semitones)
fifth_up = transpose(events, 7)You can create more sophisticated synthesis graphs by combining Pyo objects:
from pyo import Sine, Delay, Chorus, Harmonizer, MoogLP
@Synth(dials='freq cutoff', settings='delay_time num_voices')
def complex_synth(freq=440, cutoff=2000, delay_time=0.25, num_voices=3):
# Create a sine oscillator
osc = Sine(freq=freq, mul=0.3)
# Add harmonizer for multiple voices
harm = Harmonizer(osc, transpo=[0, 7, 12][:num_voices], feedback=0.1)
# Add a filter
filt = MoogLP(harm, freq=cutoff, res=0.3)
# etc...
You can connect your synths to external event sources like keyboards, sensors, or algorithmic generators. This allows you to create interactive instruments or audio installations.
The keyboard_control.py module provides a way to connect keyboard events to your synths, creating an interactive musical instrument controlled by your computer keyboard.
The module works by:
- Detecting keyboard events using the
pynputlibrary - Mapping keys to synth parameters using a configurable dictionary
- Calling your synth function with the mapped parameters when keys are pressed
- Supporting multiple callback patterns and configuration options
The simplest way to try keyboard control is through the command line:
python -m hum.examples.keyboard_control \
--callback "hum.pyo_synths:sine" \
--arg-mapping '{"q":130.81,"w":146.83,"e":164.81,"r":174.61,"t":196.00,"y":220.00,"u":246.94,"i":261.63,"o":293.66,"p":329.63,"a":261.63,"s":293.66,"d":329.63,"f":349.23,"g":392.00,"h":440.00,"j":493.88,"k":523.25,"l":587.33,"z":523.25,"x":587.33,"c":659.25,"v":698.46,"b":783.99,"n":880.00,"m":987.77}' \
--debugThis creates a two-octave piano keyboard layout on your QWERTY keyboard, where:
- The top two rows (Q-P and A-L) form the white keys
- The bottom row (Z-M) forms the black keys
- Each key is mapped to a specific frequency in Hz
You can also use the keyboard control module in your own code:
from hum.examples.keyboard_control import keyboard_reader
from hum.pyo_synths import sine # A pre-defined sine wave synth
# Define a mapping from keys to frequencies
key_mapping = {
"a": 261.63, # C4
"s": 293.66, # D4
"d": 329.63, # E4
"f": 349.23, # F4
"g": 392.00, # G4
"h": 440.00, # A4
"j": 493.88, # B4
"k": 523.25 # C5
}
# Create a keyboard reader with the mapping
reader = keyboard_reader(
callback=sine,
arg_mapping=key_mapping,
exit_key="escape",
read_rate=0.1,
debug=True
)
# Process keyboard events
for event in reader:
if event:
print(f"Key pressed: {event['key']}")
if event and event['key'] == 'escape':
breakYou can map keys to complex parameter dictionaries for more control:
# Map keys to multiple parameters
advanced_mapping = {
"a": {"freq": 261.63, "waveform": "sine"},
"s": {"freq": 293.66, "waveform": "triangle"},
"d": {"freq": 329.63, "waveform": "square", "attack": 0.2}
}
# Use with a more complex synth
reader = keyboard_reader(
callback=simple_waveform_synth,
arg_mapping=advanced_mapping
)The keyboard_control.py module:
- Registers a callback function with the
pynputlibrary to capture key presses - Processes the raw key data into a standardized format
- Looks up the key in the mapping dictionary to find associated parameters
- Calls the synth callback in the appropriate way based on its type:
- If it's a
Synthinstance, uses theupdatemethod - If it's a regular function, calls it with the mapped parameters
- If it's a
- Yields events in a generator pattern for processing in your code
This flexible design allows you to use keyboard control with any type of synth function or parameter mapping, making it easy to create interactive audio experiences.
The Synth and keyboard control tools can be used for:
- Interactive Music Creation - Build synthesizers and play them with your keyboard
- Sound Design - Experiment with different parameters in real-time
- Education - Demonstrate audio concepts with interactive examples
- Algorithmic Composition - Create and manipulate event sequences programmatically
- Audio Prototyping - Quickly test audio ideas before implementing them in other systems
- Game Audio - Create dynamic sound effects that respond to game events
- Installation Art - Build interactive audio installations
- Live Performance - Control audio in real-time for live shows
- Use Context Managers - The
withstatement ensures proper cleanup - Round Timestamps - Use
round_event_timesfor readable event sequences - Resource Management - Always stop synths when done to free audio resources
- Separate Concerns - Use dials for real-time control and settings for structural changes
- Start Simple - Build up complexity gradually as you get comfortable with the system
- Use Generators - The functional approach to audio processing is powerful and flexible
- Test with Rendering - Use
render_eventsto test your sequences without real-time playback - Documentation - Document your synth functions with docstrings and examples
The Synth class works by:
- Converting parameters to Pyo's
SigToobjects for smooth transitions - Managing a Pyo server for audio processing
- Recording all parameter changes with timestamps
- Managing the underlying synthesis graph rebuilding when necessary
- Handling event serialization and deserialization
The keyboard_control module:
- Uses non-blocking event detection to capture keystrokes
- Processes key events into a consistent format
- Maps keys to synth parameters using configurable dictionaries
- Handles different types of callback functions adaptively