Unfortunately, the timing seems to be off; playback is definitely wrong in the converted version. I was careful to add silent notes to the patterns in order to fill in points where notes are not playing, and when I analyzed this it seems like I'm adding pauses of the correct length. The fact remains that the playback just sounds bad and is not right.
Here is how the song is saved in EEPROM. There is a "magic number" of 'A' at the start:
- Code: Select all
A[16 instruments of 8 bytes each][128 notes of 5 bytes each]
Here is the python script:
- Code: Select all
import sys
INST_SIZE = 8
INSTBUF_SIZE = 16
WAV = 0
ARD = 1
ARS = 2
VOL = 3
SLD = 4
SLS = 5
TRD = 6
TRS = 7
NOTE_SIZE = 5
NOTEBUF_SIZE = 128
CHA = 0
TIM = 1
PIT = 2
INS = 3
DUR = 4
SILENCE = 63
CMD_VOLUME = 0
CMD_INSTRUMENT = 1
CMD_SLIDE = 2
CMD_ARPEGGIO = 3
CMD_TREMOLO = 4
#For now just keep this fixed here
FPR = 2
def print_bytes(bytes):
out = ''
for b in bytes:
out += '%02x '%ord(b)
print out
def print_note(ins):
print 'Channel: %d'%ord(ins[CHA])
print 'Time: %d'%ord(ins[TIM])
print 'Pitch: %d'%ord(ins[PIT])
print 'Instrument: %d'%ord(ins[INS])
print 'Duration: %d'%ord(ins[DUR])
def convert_instruments(ibuf):
instruments = []
for i in range(0,INSTBUF_SIZE):
inst = []
for j in range(0,INST_SIZE):
inst.append(ord(ibuf[i*INST_SIZE + j]))
instruments.append(inst)
return instruments
def convert_notes(nbuf):
notes = []
for i in range(0,NOTEBUF_SIZE):
note = []
for j in range(0,NOTE_SIZE):
note.append(ord(nbuf[i*NOTE_SIZE + j]))
notes.append(note)
return notes
#Used by gen_instrument
def gen_command(ID, X, Y):
''' This code was adapted from RoDoT's tracker code '''
Y += 16
word = Y
word <<= 5
word += X
word <<= 4
word += ID
word <<= 2
word +=1 #set LSB to 1 to indicate it's a command
return '0x%02x,'%word
#Generate actual bytes to define instrument
def gen_instrument(inst):
out = ''
out += gen_command(CMD_INSTRUMENT,inst[WAV],0)
out += gen_command(CMD_ARPEGGIO,inst[ARD],inst[ARS])
out += gen_command(CMD_VOLUME,inst[VOL],0)
out += gen_command(CMD_SLIDE,inst[SLD],inst[SLS])
out += gen_command(CMD_TREMOLO,inst[TRD],inst[TRS])
return out
#Generate actual bytes we need for a note in a pattern
def gen_note(note):
''' This code was adapted from RoDoT's tracker code '''
word = note[DUR]
word <<= 6
word += note[PIT]
word <<= 2
return '0x%02x,'%word
#For simplicity convert only one channel at a time
def convert_pattern(instruments,notes,channel):
''' Some of the boilerplate code here was adapted from RoDoT's tracker code '''
out = 'const unsigned int pat0cha%d[] PROGMEM = {'%channel
lastnote = None
for time in range(0,255*FPR): #Go through each time slot (inefficient, but I don't care)
for note in notes:
if note[CHA] == channel and note[TIM]*FPR == time:
if lastnote is not None:
#print_note(lastnote)
#If we have a time gap between the two notes
if lastnote[TIM]*FPR + lastnote[DUR] < note[TIM]*FPR:
#We must insert a gap of silence to fill the space
silent = [channel,lastnote[TIM]*FPR + lastnote[DUR] + 1, SILENCE, 0, note[TIM]*FPR - (lastnote[TIM]*FPR + lastnote[DUR])]
print 'adding silence of %d frames'%silent[DUR]
out += gen_note(silent)
#If the note length is too long to fit in the gap
if lastnote[TIM]*FPR + lastnote[DUR] > note[TIM]*FPR:
print 'shortening note from %d frames to %d frames'%(lastnote[DUR], note[TIM]*FPR - lastnote[TIM]*FPR)
lastnote[DUR] = note[TIM]*FPR - lastnote[TIM]*FPR#We must shorten the note to fit the gap perfectly
out += gen_note(lastnote)
#Check for instrument change, and insert commands to change it if so
if( lastnote[INS] != note[INS] ):
out += gen_instrument(instruments[note[INS]])
lastnote = note
else:
#insert commands to set the instrument
out += gen_instrument(instruments[note[INS]])
lastnote = note
if lastnote is not None:
#print_note(lastnote)
out += gen_note(lastnote)
out += '0x000};\n'
return out
def convert_track(file):
bytes = file.read() #Read all 1k of EEPROM
if bytes[0] == 'A':
instruments = bytes[1:INSTBUF_SIZE*INST_SIZE+1]
#print_bytes(instruments)
instruments = convert_instruments(instruments)
notes = bytes[INSTBUF_SIZE*INST_SIZE+1:INSTBUF_SIZE*INST_SIZE+1 + NOTEBUF_SIZE*NOTE_SIZE]
#print_bytes(notes)
notes = convert_notes(notes)
#print_note(notes[0])
result = convert_pattern(instruments,notes,0)
result += convert_pattern(instruments,notes,1)
result += convert_pattern(instruments,notes,2)
result += '''
void playTrack(){
gb.sound.playPattern(pat0cha0, 0);
gb.sound.playPattern(pat0cha1, 1);
gb.sound.playPattern(pat0cha2, 2);
}
'''
return result
else:
print 'Invalid or corrupted EEPROM file.'
return ''
if __name__ == '__main__':
if len(sys.argv) > 1:
f = open(sys.argv[len(sys.argv)-1], 'rb')
converted = convert_track(f)
f.close()
if converted != '':
if sys.argv[1] == '-o' and len(sys.argv) == 4:
f = open(sys.argv[2], 'wb')
f.write(converted)
else:
print converted
else:
print 'Wrong number of arguments. Usage:\n\t%s [-o <output file name>] <EEPROM save file>'%sys.argv[0]
The script can be run like this:
- Code: Select all
python convert_track.py ATTOTRAK.SAV
I figure it might be an issue with how I calculate note lengths, which is a little complicated. I use a value I call FPR, which stands for frames per row, and which is 2 by default. The note duration, as specified in the sound library, is in frames, meaning that if a note in a row has duration 1, and there is a note in the following row, then a silent note should be inserted of duration 1, giving a total duration of 2 and accounting for all the two frames that should occur before the next note.
I have test output that confirms that the silent notes I expect with the durations I expect are being inserted. In addition, if a note is too long (i.e. it would last longer than before the next note starts), I shorten its duration, and that also seems to be working. Therefore, I don't understand why the result sounds...well, bad. I figure I'm making a wrong assumption, but I don't know where.
Attached is an example ATTOTRAK.SAV file with a few bars from the Mario theme. If you run the script on it, you will get output that can be pasted into a game, but here it is in a small test game, with only the first channel playing:
- Code: Select all
#include <Gamebuino.h>
Gamebuino gb;
const unsigned int pat0cha0[] PROGMEM = {0x8005,0x800d,0x8281,0x8009,0x8011,0x1fc,0x178,0x2fc,0x278,0x3fc,0x178,0x1fc,0x168,0x2fc,0x278,0x6fc,0x284,0x6fc,0x254,0x4fc,0x268,0x5fc,0x154,0x4fc,0x248,0x3fc,0x15c,0x3fc,0x164,0x1fc,0x160,0x2fc,0x25c,0x2fc,0x254,0x278,0x1fc,0x184,0x2fc,0x28c,0x1fc,0x17c,0x2fc,0x284,0x3fc,0x178,0x1fc,0x168,0x1fc,0x170,0x164,0x000};
const unsigned int pat0cha1[] PROGMEM = {0x000};
const unsigned int pat0cha2[] PROGMEM = {0x000};
void playTrack(){
gb.sound.playPattern(pat0cha0, 0);
//gb.sound.playPattern(pat0cha1, 1);
//gb.sound.playPattern(pat0cha2, 2);
}
void setup() {
// put your setup code here, to run once:
gb.begin();
gb.titleScreen(F("Song Test"));
playTrack();
}
void loop() {
if( gb.update() ){
gb.display.cursorX = 0;
gb.display.cursorY = 0;
gb.display.print(F("Song Test \16\n\n\25 to restart playback"));
if( gb.buttons.pressed(BTN_A) ){
playTrack();
}
}
}
If you load that into a Gamebuino, you'll immediately be able to tell what I'm talking about. If you don't want to do all the work of making new projects and compiling, I'm attaching the compiled test hex as well. It's all in the zip.
So, I'm kind of stuck here. If this can be fixed, then it will be immediately possible to create songs on the gamebuino that can then be easily put into other games with minimal effort and program space. If anybody has any ideas, I would appreciate it.