2 Sep 2005 16:09
reverse-engineering the .NOT format
Bob the Hamster <Bob <at> HamsterRepublic.com>
2005-09-02 14:09:04 GMT
2005-09-02 14:09:04 GMT
> I should've asked this question a long time ago, however... > > What's the format of the .NOT files that NOTATE produces. My idea is: > Notate is the second most useless utility on the planet (roughly), > because it can't edit pre-existing BAMs (i.e anything that MIDI2BAM > produces). So, there's two ways to fix it: > > 1. Modify NOTATE to read BAM files. This would essentially take a > re-write of NOTATE. > 2. Write a converter to convert between BAM and NOT formats. Much > easier, AFAIK > > However, I'm too lazy... er, that is, pressed for time to go through > the code for NOTATE and figure it out. So, I want someone else to do > it for me :) > > Or, if there's any existing docs on it, that'd be nice. The BAM SDK > has nothing... The only thing I remember about the NOT format is that it is very simple, lazy, and wasteful. Here is the code from notate.bas that does the evil deed. --------- OPEN file$ FOR BINARY AS #1 FOR i = 0 TO 8 a$ = CHR$(cinst(i)) PUT #1, 32770 + i, a$ NEXT i PUT #1, 32770 + 9, last FOR i = 1 TO 15 PUT #1, 32770 + 11 + i * 2, tag(i) NEXT i FOR i = 0 TO 99 PUT #1, 32770 + 45 + (i * 6 + 0), jump(i) PUT #1, 32770 + 45 + (i * 6 + 2), jid(i) PUT #1, 32770 + 45 + (i * 6 + 4), j2tag(i) NEXT i CLOSE #1 copyfile file$ + CHR$(0), f$ + ".not" + CHR$(0), buffer() --------- Lets go through it a bit at a time. > FOR i = 0 TO 8 > a$ = CHR$(cinst(i)) > PUT #1, 32770 + i, a$ > NEXT i starting at offset 32770 are nine chars which I believe are the instrument ID numbers. Unlike a .BAM file, a .NOT file does not embed instrument data in the file. Instead it just keeps track of which instrument in IBANK.IBK each voice uses. Also, NOT files do not support mid-song instrument changes. > PUT #1, 32770 + 9, last at offset 32779 is the size of the song. This, I think, is the total number of 1/32 notes.. (or is it 1/64 notes? I forget) ..*checks the BAM docs* Yeah, 1/32 notes. > FOR i = 1 TO 15 > PUT #1, 32770 + 11 + i * 2, tag(i) > NEXT i at offset 32781 there are 15 16-bit integers, each containing tag locations (offset in 1/32nd notes from the beginning of the song) > FOR i = 0 TO 99 > PUT #1, 32770 + 45 + (i * 6 + 0), jump(i) > PUT #1, 32770 + 45 + (i * 6 + 2), jid(i) > PUT #1, 32770 + 45 + (i * 6 + 4), j2tag(i) > NEXT i So staring at offset 32815 we have 100 records for jump information. Each record is 6 bytes long. A 16-bit int for jump position "jump", a 16-bit int for jump type "jid" and a 16-bit int for jump destination tag "j2tag" jid = 0 is unuised jid = 1 thru 253 is a finite loop jid = 254 is an infitie loop jid = 255 is a gosub (chorus) jid = -1 is a return (end-of-chorus) Now, what that save routine /doesn't/ tell us anything about is the rest of the song data, in offsets 0 to 32769 ... (actually, I would guess that 0 - 32767 is what we care about, and that 32768-32769 are two bytes that I just wasted for no obvious reason) Lets look at the rather ugly "setnote" block of code... > setnote: > fmkeyon ins, vptr + sharp: noff(ins) = nlen(nl) This just makes the note play when you place it... > OPEN file$ FOR BINARY AS #1 > a$ = " " Notate writes directly to a temporary working .NOT file to avoid having to keep the song in memory. > GET #1, 1 + (ptr * 9) + ins, a$ ptr is the current position time-wize in the song. So if ptr was 0 we would be at the beginning. If ptr was 32 we would be one whole note into the song. if ptr was 111 we would be 3 whole notes + 15 1/32 notes into the song. The data appears to be in blocks of nine bytes per each 1/32 note. One byte for each instrument. So if you song is silent for 500 1/32 notes, you have 500 * 9 bytes of empty space to show for it. > b = ASC(a$) > n(ptr - left, ins) = vptr + sharp > l(ptr - left, ins) = nlen(nl) * dot! > IF (b AND 128) = 128 THEN n(ptr - left, ins) = (n(ptr - left, ins) OR 128) > a$ = CHR$(n(ptr - left, ins)) > PUT #1, 1 + (ptr * 9) + ins, a$ > FOR i = ptr + 1 TO (ptr + (nlen(nl) * dot!)) - 1 > a$ = CHR$(0) > PUT #1, 1 + (i * 9) + ins, a$ > NEXT i > a$ = " " > GET #1, 1 + ((ptr + (nlen(nl) * dot!)) * 9) + ins, a$ > b = (ASC(a$) OR 128) > a$ = CHR$(b) > PUT #1, 1 + ((ptr + (nlen(nl) * dot!)) * 9) + ins, a$ ouch. this makes my head hurt. Okay, here is what I think is happening. For each 1/32 note in the song we have nine bytes of data, right? one for each of the 9 voices. The low 7 bits contains a frequency value 0-127. This is the same number to use in the "Start-Note" command in a BAM stream. This marks the beginning of a note. The eight bit of each byte is set to mark a stop-note. (that's why this ugly code has stuff like "x AND 128" and "x OR 128") > CLOSE #1 > IF ptr + (nlen(nl) * dot!) > last THEN last = ptr + (nlen(nl) * dot!) + 2 > GOSUB visnotes > RETURN I think that is enough information for somebody to write a BAM2NOT converter. Building BAM import capabilities into notate would naturally be better... except then you would have to touch that awful ugly code :P --- Bob the Hamster
RSS Feed