Created
December 8, 2019 20:19
-
-
Save argarak/e8c72aface47a18bc5aaed18a6579e13 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
# | |
# code for the botb entry "an extra cup of tea" | |
# F MAJOR | |
# http://battleofthebits.org/arena/Entry/an+extra+cup+of+tea/33279/ | |
# | |
import struct | |
import wave | |
import math | |
from random import random, randint | |
# "custom" version of the sine function to prevent it from returning 0 | |
# and causing sudden dc jumps in the audio | |
def b_sin(v): | |
if math.sin(v) == 0: | |
return 0.000001 | |
else: | |
return math.sin(v) | |
# js bytebeat converted into python | |
# the bytebeat is simply enclosed in a function with parameter t, which is | |
# the bytebeat counter | |
def bytebeat(t): | |
sin = b_sin | |
C = 0.10265; C_ = 0.1088; D = 0.1153; D_ = 0.122; E = 0.1293; F = 0.1373 | |
F_ = 0.1453; G = 0.154; G_ = 0.163; A = 0.173; A_ = 0.183; B = 0.194 | |
tunes = [0.10265, 0.1088, 0.1153, 0.122, 0.1293, 0.1373, 0.1453, 0.154, 0.163, 0.173, 0.183, 0.194] | |
chord_vol = 15 | |
c1_seq = [C,G,F,G] | |
c2_seq = [D_,A_,G_,A_] | |
c3_seq = [G,D,C,C] | |
c4_seq = [A_,D_,D_,D_] | |
sq_seq = [C,D_*2,A_,G_,G*2,A_,C*2,A_,G_,F] | |
rate_div = 3.3 | |
def chord(s): | |
a = sin(s * c1_seq[round(int((s/24000)%4))] + sin(s * c1_seq[round(int((s/24000)%4))]) * 1 ) * [chord_vol,0,0][round(int((s/8000)%3))] + sin(s * c2_seq[round(int((s/24000)%4))] + sin(s * c2_seq[round(int((s/24000)%4))]) * 1) * [chord_vol,0,0][round(int((s/8000)%3))] + sin(s * c3_seq[round(int((s/24000)%4))] + sin(s * c3_seq[round(int((s/24000)%4))]) * 1) * [chord_vol,0,0][round(int((s/8000)%3))] + sin(s * (c4_seq[round(int((s/24000)%4))]) + sin(s * c4_seq[round(int((s/24000)%4))]) * 1) * [chord_vol,0,0][round(int((s/8000)%3))] | |
if a != 0 or (s > 200000 and s < 800000): | |
return 128 + a | |
else: | |
return 0 | |
def bass(s): | |
if s < 200000: | |
return 0 | |
if s > 800000: | |
return 0 | |
return sin(s * [C,D_,F,G,A_,A_/2,D][round(int((s/8000)%7))] + sin(s * [C,D_,F,G,A_,A_/2,D][round(int((s/8000)%7))]/2)*(2-(s/8000)%2))*(15-(s/800)%15) | |
def pian(s): | |
if s < 300000: | |
return 0 | |
if s > 600000: | |
return 0 | |
return sin(s * [C,D_,F,G,A_,A_/2,D][round(int((s/4200)%7))] + sin(s * [C,D_,F,G,A_,A_/2,D][round(int((s/4200)%7))]*2)*(2-(s/8000)%2))*(15-(s/800)%15) | |
def lead(s): | |
if s < 400000: | |
return 0 | |
if s > 600000: | |
return 0 | |
l = 10; | |
a = sin(s * 1 * [A_,C*2,D_*2,F*2,C*3,A_*2,G*2][round(int((s/[4000,8000,2000][round(int((s/4000)%3))])%7))]) * 20; | |
if a > l: | |
return l | |
elif a < -l: | |
return -l | |
else: | |
return a | |
try: | |
return chord(t/rate_div)*.8 + (bass(t/rate_div))*.9 + pian(t/rate_div)*.75 + lead(t/rate_div) + random()*(sin((t/rate_div)*0.00001)*2) | |
except ZeroDivisionError: | |
return 0 | |
# storing 2,600,000 samples in a normal python array | |
# this is good programming lol | |
bytebeat_pack = [] | |
for i in range(1, 2600000): | |
bytebeat_pack.append(bytebeat(i)) | |
# print(bytebeat_pack) | |
# with wave.open('music.wav', 'rb') as wr: | |
# frames = wr.readframes(wr.getnframes()) | |
# with some settings it may be needed to change the pitch of the original file | |
# to balance compression with correct pitch | |
# note which is used when representing pcm | |
# in my testing this doesn't change the output wav at all | |
NOTE = "c1" | |
# the length of each note which represents a sample | |
# also doesn't seem to affect the wav file | |
NOTE_LENGTH = "f" | |
# any fsound data to be added at the end of each note | |
APPEND_DATA = "\n" | |
# try changing this value, some values may offer more compression at the expense | |
# of more silent gaps between some parts of the audio | |
EMPTY_SAMPLES_LIMIT = 100 | |
# loops each note write by this number | |
# affects quality and pitch | |
NOTE_LOOP = 1 | |
# base 2 number, used to determine how many samples are taken from | |
# the original pcm data | |
ITER_SAMPLE = 1 | |
non_zero_counter = 0 | |
file = open('musictest.fss', 'w') | |
file.write("0\n") | |
offset = 0 | |
# changed this to a while loop as I wanted to modify the offset value outside | |
# of this loop | |
while offset < len(bytebeat_pack): | |
#data_new = abs(struct.unpack_from('<h', bytebeat_pack, offset)[0]) | |
data_new = (bytebeat_pack[offset] ** 2)/2 | |
if data_new > 258: | |
non_zero_counter = 1 | |
if data_new <= 258 and non_zero_counter > 0: | |
ignore_end = False | |
if (offset + ITER_SAMPLE) >= len(bytebeat_pack): | |
continue | |
#_data_new = abs(struct.unpack_from('<h', frames, offset + 1)[0]) | |
_data_new = bytebeat_pack[offset] ** 2 | |
empty_samples = 1 | |
# get number of empty samples while iterating offset | |
while _data_new <= 258: | |
if (offset + ITER_SAMPLE) >= len(bytebeat_pack): | |
ignore_end = True | |
break | |
offset += ITER_SAMPLE | |
#_data_new = abs(struct.unpack_from('<h', frames, offset)[0]) | |
_data_new = bytebeat_pack[offset] ** 2 | |
empty_samples += 1 | |
# complete ignore any end rests + end rests which are less than 10 | |
# in length | |
if not ignore_end and empty_samples > 10: | |
# write normal rests if there is a low amount of rests | |
if empty_samples < EMPTY_SAMPLES_LIMIT: | |
for _ in range(0, round(empty_samples / 2)): | |
file.write("r\n") | |
else: | |
# slow down the tempo by a lot and write a lot less rests | |
file.write("t16\n"); | |
for _ in range(0, round(empty_samples / EMPTY_SAMPLES_LIMIT / 2)): | |
file.write("r\n") | |
file.write("t0\n"); | |
write_note = "" | |
if 258 < data_new <= 1027: | |
write_note = (NOTE + NOTE_LENGTH + "1" + APPEND_DATA) | |
elif 1027 < data_new <= 1538: | |
write_note = (NOTE + NOTE_LENGTH + "2" + APPEND_DATA) | |
elif 1538 < data_new <= 2305: | |
write_note = (NOTE + NOTE_LENGTH + "3" + APPEND_DATA) | |
elif 2305 < data_new <= 3071: | |
write_note = (NOTE + NOTE_LENGTH + "4" + APPEND_DATA) | |
elif 3071 < data_new <= 3836: | |
write_note = (NOTE + NOTE_LENGTH + "5" + APPEND_DATA) | |
elif 3836 < data_new <= 5372: | |
write_note = (NOTE + NOTE_LENGTH + "6" + APPEND_DATA) | |
elif 5372 < data_new <= 6908: | |
write_note = (NOTE + NOTE_LENGTH + "7" + APPEND_DATA) | |
elif 6908 < data_new <= 8445: | |
write_note = (NOTE + NOTE_LENGTH + "8" + APPEND_DATA) | |
elif 8445 < data_new <= 9214: | |
write_note = (NOTE + NOTE_LENGTH + "9" + APPEND_DATA) | |
elif 9214 < data_new <= 9471: | |
write_note = (NOTE + NOTE_LENGTH + "a" + APPEND_DATA) | |
elif 9471 < data_new <= 10240: | |
write_note = (NOTE + NOTE_LENGTH + "b" + APPEND_DATA) | |
elif 10240 < data_new <= 11776: | |
write_note = (NOTE + NOTE_LENGTH + "c" + APPEND_DATA) | |
elif 11776 < data_new <= 16640: | |
write_note = (NOTE + NOTE_LENGTH + "d" + APPEND_DATA) | |
elif 16640 < data_new <= 20736: | |
write_note = (NOTE + NOTE_LENGTH + "e" + APPEND_DATA) | |
elif 20736 < data_new: | |
write_note = (NOTE + NOTE_LENGTH + APPEND_DATA) | |
for _ in range(0, NOTE_LOOP): | |
# randomly throw out notes for a more classic lo-fi crappy 2003 mp3 sound | |
if randint(0, 100) % 30 != 0: | |
file.write(write_note) | |
offset += ITER_SAMPLE | |
file.close() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
actually in retrospect, I probably should have just directly called
bytebeat(t)
instead of storing it in the array but that was the most naive way to do it and I guess computers have a lot of memory :poh and I also tested out the global volume and yeah it works! setting vx volume does actually change the volumes of those notes, so that would be a pretty cool way of compressing the code even more when there is >4 notes of the same volume at the same time resulting in a savings of 1 byte for 4 consecutive notes, 2 bytes for 5 notes and so on - might try implementing that at some point!