-
-
Save argarak/e8c72aface47a18bc5aaed18a6579e13 to your computer and use it in GitHub Desktop.
# | |
# 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() |
damn, your entry is even more impressive now. so much work went into this!
"storing 2,600,000 samples in a normal python array, this is good programming lol"
thats how you do it xD
the fact about c1ff->c1f is interesting and good to know to save space, though i wonder if it depends on the global volume v? if vf is the global volume, c1f is at volume f by default, but what if its va or v6 ?
im very excited about your work here. you found a lot of cool new compression techniques and im pretty sure, there are even more hidden inside fsound, we just have to find them.
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 :p
oh 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!
there's quite a few modifications to the original fspeed by kleeder (https://github.com/kleeder/fSpeed) including some interesting compression:
when a note is of value
c1ff
, the script drops the lastf
and hasc1f
instead. fsound magically assumes that the note is at max volumethe rest value doesn't have to written as
r-f
when working with 0 tempo, so the script simply writesr
instead and fsound seems to assign some kind of length value to the rest (which doesn't really matter in 0 tempo anyway)the script also counts the number of rests which happen at a single time and if the number of rests exceeds
EMPTY_SAMPLES_LIMIT
, it will addoriginal number of rests / EMPTY_SAMPLES_LIMIT / 2
amounts of rests in tempo 16 instead (though this isn't fully accurate as I think it's slightly slower, maybe tempo 15 might work better)rests under 10 in length are completely ignored, this also counts for any rests which may exist at the end of the file
the code was also modified so that it works for an array of values taken from a bytebeat (which is any program that outputs pcm data in the range of 0-255), though since the original code expected 16-bit audio, the bytebeat values were squared so that they would be in the range of 0-65535
additionally bytebeat has the advantage of not having to enclose the input pcm data with
abs()
as you can very easily add a dc offset to bytebeat programs, however to save extra space I had to change the dc offset to 0 for the bytebeat program every time there was any silent section, so that the script could compress the rest datathe final fsound file also contains a loop inside to save even more space, however I added that manually afterwards, unfortunately this script doesn't have that feature built-in :p
original js bytebeat: here