Generating Patterns for Sound Library

Understanding the language, error messages, etc.

Generating Patterns for Sound Library

Postby wuuff » Mon Jan 30, 2017 9:20 am

In another topic, I showed that I was developing a tracker. However, the way that the tracker actually plays back sounds is not efficient or the "right" way to play sounds, but it was easier to organize that way for the tracker. Internally, the data in the tracker is saved to EEPROM. I then intend to allow a user to export the EEPROM save file contents to C source, which can be placed into any game to play the music. I do this with a python script that you can run on the ATTOTRAK.SAV file. It produces C source that can be copied into a project. I'm happy to say that it processes the notes and already outputs source that works and can indeed be played in another game with the addition of only a few lines!

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.
Attachments
broken_song_export.zip
Contains ATTOTRAK.HEX, ATTOTRAK.SAV, TESTSONG.HEX, and convert_track.py
(33.46 KiB) Downloaded 462 times
wuuff
 
Posts: 61
Joined: Sun Aug 28, 2016 6:05 am

Re: Generating Patterns for Sound Library

Postby wuuff » Thu Feb 23, 2017 10:06 am

I finally found some time to dig deeper into the problem, and it turns out it was actually a really basic mistake. I had mixed the order that I inserted silent notes into the output array of notes. I accidentally put every silent note before the note it was supposed to go after, so it was actually quite a simple fix in the end.

Now the playback sounds mostly like it should. The only remaining issue is that there is a difference in the tempo between when it plays in the tracker and when the exported song is played. I'm not sure whether the tracker or exported song is playing at the right tempo. I will eventually do some experiments with a metronome to test this. At least the playback sounds good now.

Attached is the fixed conversion script. Any songs made with the tracker prototype I linked in the first post can be exported. Take any ATTOTRAK.SAV file, which is generated by the loader when it saves the contents of EEPROM, run this script ("python convert_track.py path/to/ATTOTRAK.SAV"), and the resulting output can be put in any Gamebuino game with just a couple of lines of code.
Attachments
convert_track.zip
(1.91 KiB) Downloaded 494 times
wuuff
 
Posts: 61
Joined: Sun Aug 28, 2016 6:05 am


Return to Programming Questions

Who is online

Users browsing this forum: No registered users and 15 guests

cron