Skip to content

Instantly share code, notes, and snippets.

@knot126
Created April 26, 2023 08:06
Show Gist options
  • Save knot126/8dc9584def702f67752d21ec6870b7da to your computer and use it in GitHub Desktop.
Save knot126/8dc9584def702f67752d21ec6870b7da to your computer and use it in GitHub Desktop.
import simpleaudio
import math
import struct
import random
def mix(a, b):
return (a + b) - (a * b)
def sample_sine(freq, i, volume = 0.3, rate = 44100):
"""
Sample sine wave (currently with a tiny bit of randomness)
"""
sample = math.sin((freq * 2 * math.pi * i) / rate) * volume
return sample
def sample_sawtooth(freq, i, volume = 0.5, rate = 44100):
z = ((freq * i) / rate)
sample = volume * (z - math.floor(z)) - 0.5
return sample
def sample_square(freq, i, volume = 0.5, rate = 44100):
z = ((freq * i) / rate)
sample = volume * (0.0 if (z % 2) < 1 else 1.0) - 0.5
return sample
def sample_fade(length, i, power = 1.0, rate = 44100):
"""
Calculate the fade of the instrument
"""
return 1.0 - (min(max((length * (i / rate)), 0.0), 1.0) ** power)
def randfreq():
basenum = 20
baserand = random.random()
baserand = (0.3 * baserand) + (0.7 * (baserand ** 18))
final = basenum + (math.floor(120 * baserand) * 100)
return final
class Instrument():
"""
An instrument that can be sampled at lower or higher pitches with effects.
At the moment, I use the fact that any periodic signal can be made of sines and cosines.
And then I apply effects like linear/nonlinear fading to those so they sound
a bit more like instruments.
This currently isn't my feild of expertise, but I intend to change that someday!
"""
def __init__(self, rate = 44100):
# Audio rate for sampling
self.rate = rate
# Decide on the important frequencies that will be sampled
self.freq_count = 7 #random.randint(8, 15)
self.freq = [randfreq() for x in range(self.freq_count)]
# Decide on the volume of each frequency
self.freq_volume = [random.random() for x in range(self.freq_count)]
sum = 0.0
for n in range(self.freq_count):
sum += self.freq_volume[n]
for n in range(self.freq_count):
self.freq_volume[n] = self.freq_volume[n] / sum
# Length of the instrument
self.length = 0.3 + (0.25 * random.random())
# Power to use when fading
self.fade_exp = [2.5 * random.random() + 0.5 for x in range(self.freq_count)]
# Wave type
self.wave = random.randint(0, 2)
def sample(self, i, pitch = 1.0):
"""
Sample audio at this point
"""
sample = 0.0
for n in range(self.freq_count):
sample += self.freq_volume[n] * sample_fade(self.length, i, self.fade_exp[n], rate = self.rate)
if (self.wave == 0):
sample *= sample_sine(pitch * self.freq[n], i, rate = self.rate)
elif (self.wave == 1):
sample *= sample_sawtooth(pitch * self.freq[n], i, rate = self.rate)
elif (self.wave == 2):
sample *= sample_square(pitch * self.freq[n], i, rate = self.rate)
return sample
def getLength(self):
"""
Get how long the instrument lasts
"""
return self.length
class Piece():
"""
Holds the layout for a music track
"""
def __init__(self, rate = 44100):
self.playlist = []
self.rate = rate
def addNote(self, time, instrument, pitch):
"""
Add an instrument to be played at a certian time
"""
self.playlist.append({
"which": instrument,
"begin": time,
"end": time + instrument.getLength(),
"pitch": pitch,
})
def sample(self, i):
"""
Get the audio sample for a point in time
"""
play_count = 0
time = i / self.rate
sample = 0.0
# TODO: Should really not have to be a lienar search
for play in self.playlist:
# If this is playing
if (play["begin"] < time < play["end"]):
# Play the note :)
location = i - int(play["begin"] * self.rate)
sample += play["which"].sample(location, pitch = play["pitch"])
# And increment number of playing instruments
play_count += 1
if (play_count == 0): play_count = 1
return sample * (1 / play_count)
def getLength(self):
"""
Get length in seconds of the piece
"""
longest = 0.0
for play in self.playlist:
if (play["end"] > longest):
longest = play["end"]
return longest
def randpitch():
return 1.0 + random.randint(-5, 5) / 10
def generate_pattern(length = 4):
"""
Generates a nice pitch pattern for beat notes to use
"""
pattern = []
for i in range(length):
pattern.append(randpitch())
return pattern
PREVGLOBALNUM = False
def random_boolean_long():
"""
Generates booleans in longer seqences
"""
global PREVGLOBALNUM
if (not random.randint(0, 6)):
PREVGLOBALNUM = not PREVGLOBALNUM
return PREVGLOBALNUM
class Track():
"""
A single track of the song! This samples all data upon creation so it's faster just
to play it.
"""
def __init__(self, rate = 44100):
# Save deatils
self.rate = rate
# Make the instruments
self.instrument_count = random.randint(3, 5)
self.instruments = [Instrument() for x in range(self.instrument_count)]
# Beats per minute
self.bpm = random.choice([120, 130, 140, 150, 160])
print(self.bpm)
# Make the beat instrument
self.beat = Instrument()
self.beat.length = 60 / self.bpm
# Possible sections (pieces) of the track
self.pieces = []
self.create_piece()
self.create_piece()
self.create_piece()
def create_piece(self):
"""
Create a small section of music
"""
samples = bytearray()
piece = Piece()
# Construct the piece of music
# The beat or baseline
beat_len = (60 / self.bpm)
beat_pattern = generate_pattern()
# Number of beats in a piece
PIECELEN = 16
for b in range(PIECELEN):
i = b % len(beat_pattern)
piece.addNote(b * beat_len, self.beat, beat_pattern[i])
# The instruments
for t in self.instruments:
pattern = generate_pattern()
use_table = [random.randint(0, 1) for n in range(6)]
for b in range(PIECELEN):
i = b % len(pattern)
if (use_table[b // 4]):
print("1", end='')
piece.addNote(b * beat_len, t, pattern[i])
if (random.randint(1, 5) == 1):
pattern = generate_pattern()
else:
print("0", end='')
print()
# Sample the pieces into audio data
print("Beginning to sample...")
for i in range(int(self.rate * piece.getLength())):
sample = piece.sample(i)
samples += struct.pack('h', int(sample * (2 ** 15)))
# Add the sampled track to valid pieces
self.pieces.append(samples)
def next_piece(self):
"""
Return the data for a random piece of the track
"""
return self.pieces[random.randint(0, len(self.pieces) - 1)]
def save_wave_file(data):
import wave
filename = f"Track {random.randint(0, 100000)}.wav"
w = wave.open(filename, "wb")
w.setnchannels(1)
w.setsampwidth(2)
w.setframerate(44100)
w.writeframes(data)
w.close()
print(f"Saved as {filename} !")
def main():
rate = 44100
track = Track()
data = bytearray()
for n in range(6):
data += track.next_piece()
print("Writing wave file...")
save_wave_file(data)
print("Now playing")
play_info = simpleaudio.play_buffer(data, 1, 2, rate)
play_info.wait_done()
if (__name__ == "__main__"):
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment