Skip to main content

Procedural music with PyAudio and NumPy

Combining two of my favorite pastimes, programming and music... This is the hacky "reduced to it's basic components" version of a library I've been working on for generating music and dealing with music theory.

Tweaking the harmonics by changing the shape of the harmonic components and ratios can produce some interesting sounds. This one only uses sine waveforms, but a square / saw generator is trivial with numpy.

It takes a second to generate, so don't turn your volume up too loud in anticipation (it may be loud).

import math
import numpy
import pyaudio
import itertools
from scipy import interpolate
from operator import itemgetter


class Note:

NOTES = ['c','c#','d','d#','e','f','f#','g','g#','a','a#','b']

def __init__(self, note, octave=4):
self.octave = octave
if isinstance(note, int):
self.index = note
self.note = Note.NOTES[note]
elif isinstance(note, str):
self.note = note.strip().lower()
self.index = Note.NOTES.index(self.note)

def transpose(self, halfsteps):
octave_delta, note = divmod(self.index + halfsteps, 12)
return Note(note, self.octave + octave_delta)

def frequency(self):
base_frequency = 16.35159783128741 * 2.0 ** (float(self.index) / 12.0)
return base_frequency * (2.0 ** self.octave)

def __float__(self):
return self.frequency()


class Scale:

def __init__(self, root, intervals):
self.root = Note(root.index, 0)
self.intervals = intervals

def get(self, index):
intervals = self.intervals
if index < 0:
index = abs(index)
intervals = reversed(self.intervals)
intervals = itertools.cycle(self.intervals)
note = self.root
for i in xrange(index):
note = note.transpose(intervals.next())
return note

def index(self, note):
intervals = itertools.cycle(self.intervals)
index = 0
x = self.root
while x.octave != note.octave or x.note != note.note:
x = x.transpose(intervals.next())
index += 1
return index

def transpose(self, note, interval):
return self.get(self.index(note) + interval)


def sine(frequency, length, rate):
length = int(length * rate)
factor = float(frequency) * (math.pi * 2) / rate
return numpy.sin(numpy.arange(length) * factor)

def shape(data, points, kind='slinear'):
items = points.items()
items.sort(key=itemgetter(0))
keys = map(itemgetter(0), items)
vals = map(itemgetter(1), items)
interp = interpolate.interp1d(keys, vals, kind=kind)
factor = 1.0 / len(data)
shape = interp(numpy.arange(len(data)) * factor)
return data * shape

def harmonics1(freq, length):
a = sine(freq * 1.00, length, 44100)
b = sine(freq * 2.00, length, 44100) * 0.5
c = sine(freq * 4.00, length, 44100) * 0.125
return (a + b + c) * 0.2

def harmonics2(freq, length):
a = sine(freq * 1.00, length, 44100)
b = sine(freq * 2.00, length, 44100) * 0.5
return (a + b) * 0.2

def pluck1(note):
chunk = harmonics1(note.frequency(), 2)
return shape(chunk, {0.0: 0.0, 0.005: 1.0, 0.25: 0.5, 0.9: 0.1, 1.0:0.0})

def pluck2(note):
chunk = harmonics2(note.frequency(), 2)
return shape(chunk, {0.0: 0.0, 0.5:0.75, 0.8:0.4, 1.0:0.1})

def chord(n, scale):
root = scale.get(n)
third = scale.transpose(root, 2)
fifth = scale.transpose(root, 4)
return pluck1(root) + pluck1(third) + pluck1(fifth)

root = Note('A', 3)
scale = Scale(root, [2, 1, 2, 2, 1, 3, 1])

chunks = []
chunks.append(chord(21, scale))
chunks.append(chord(19, scale))
chunks.append(chord(18, scale))
chunks.append(chord(20, scale))
chunks.append(chord(21, scale))
chunks.append(chord(22, scale))
chunks.append(chord(20, scale))
chunks.append(chord(21, scale))

chunks.append(chord(21, scale) + pluck2(scale.get(38)))
chunks.append(chord(19, scale) + pluck2(scale.get(37)))
chunks.append(chord(18, scale) + pluck2(scale.get(33)))
chunks.append(chord(20, scale) + pluck2(scale.get(32)))
chunks.append(chord(21, scale) + pluck2(scale.get(31)))
chunks.append(chord(22, scale) + pluck2(scale.get(32)))
chunks.append(chord(20, scale) + pluck2(scale.get(29)))
chunks.append(chord(21, scale) + pluck2(scale.get(28)))

chunk = numpy.concatenate(chunks) * 0.25

p = pyaudio.PyAudio()
stream = p.open(format=pyaudio.paFloat32, channels=1, rate=44100, output=1)
stream.write(chunk.astype(numpy.float32).tostring())
stream.close()
p.terminate()


The scale in use is Harmonic Minor.
It's encoded via halfsteps as: "[2, 1, 2, 2, 1, 3, 1]"
Try changing it to natural minor: "[2, 1, 2, 2, 1, 2, 2]"

I'd be interested in teaming up with anyone willing to write some actual input methods (matrix sequencer, tab reader, however else it could be done) as well as an interface for tweaking synthesized sounds. My present code base is a bit much for a blog post, so contact me if interested.

Popular posts from this blog

Exploring Sub-GHz Radio With RTL-SDR and GQRX

Today I took a look at some hardware for working with sub-GHz radio transmissions, especially FM. All of which came in a previous HackerBoxes box. My favorite item was definitely the RTL-SDR dongle! I could play around with this thing for hours surfing the radio waves and listening to weird broadcasts. There's even a Python library named pyrtlsdr for working with these devices so you can scan/record radio transmissions programmatically:  https://github.com/roger-/pyrtlsdr Next I'm going to solder together the FM transceivers included in the box and play around with writing Arduino firmware for them. Those things should work similar to the LoRa modules I've been using but they trade in the long range for increased bandwidth. Should be good for streaming data locally and still have much better range than most home WiFi or Bluetooth. Here's a video overview of the hardware included in HackerBox 0034 as well as a demo of the RTL-SDR dongle using GQRX to visually and...

Always Secure Your localhost Servers

Recently I was surprised to learn that web browsers allow any site you visit to make requests to resources on localhost (and that they will happily allow unreported mixed-content ). If you'd like to test this out, run an HTTP server on port 8080 (for instance with python -m http.server 8080 ) and then visit this page. You should see "Found: HTTP (8080)" listed and that's because the Javascript on that page made an HTTP GET request to your local server to determine that it was running. Chances are it detected other services as well (for instance if you run Tor or Keybase locally). There are two implications from this that follow: Website owners could potentially use this to collect information about what popular services are running on your local network. Malicious actors could use this to exploit vulnerabilities in those services. Requests made this way are limited in certain ways since they're considered opaque , meaning that the web page isn't able...

DIY Solar Powered LoRa Repeater (with Arduino)

In today's video I be built a solar powered LoRa signal repeater to extend the range of my LoRa network. This can easily be used as the basis for a LoRa mesh network with a bit of extra code and additional repeaters. Even if you're not into LoRa networks all of the solar power hardware in this video can be used for any off-the-grid electronics projects or IoT nodes!