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

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