Created
April 26, 2023 08:06
-
-
Save knot126/8dc9584def702f67752d21ec6870b7da to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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