Skip to content

Instantly share code, notes, and snippets.

@argarak
Created December 8, 2019 20:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save argarak/e8c72aface47a18bc5aaed18a6579e13 to your computer and use it in GitHub Desktop.
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()
@argarak
Copy link
Author

argarak commented Dec 8, 2019

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 last f and has c1f instead. fsound magically assumes that the note is at max volume

  • the rest value doesn't have to written as r-f when working with 0 tempo, so the script simply writes r 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 add original 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 data

the 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

@kleeder
Copy link

kleeder commented Dec 8, 2019

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.

@argarak
Copy link
Author

argarak commented Dec 8, 2019

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment