├── experimental_ff5 ├── ff1_battle_inst.bin ├── ot_oasis_inst_nat.bin ├── ff1_battle_data.bin ├── ot_oasis_data_nat.bin ├── quick_insert.py ├── Slam Shuffle.mml ├── Devil's Lab.mml ├── cc_world2.mml ├── ff1_battle.mml ├── mmltbl_ff5.py ├── README.TXT ├── ot_oasis.mml └── mml2akao3.py ├── .gitignore ├── experimental ├── spc_aux_ram.bin ├── spc_work_ram.bin └── sxc2mml.py ├── LICENSE ├── hex_tools ├── mfvitbl.py ├── mfvimerge.py ├── mfvidrums.py ├── mfvioctave.py ├── mfviclean.py ├── README └── mfvisplit.py ├── mfvitbl.py ├── test.py ├── sfxtest.700 ├── spc2brrs.py ├── README.md ├── mmltbl.py ├── mfvi2mml_py2.py ├── mass_extract.py ├── mfvi2mml.py ├── mfvitrace.py └── brr2sf2.py /experimental_ff5/ff1_battle_inst.bin: -------------------------------------------------------------------------------- 1 |   -------------------------------------------------------------------------------- /experimental_ff5/ot_oasis_inst_nat.bin: -------------------------------------------------------------------------------- 1 |   -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | spcbrr/ 4 | *spec 5 | *.smc 6 | *.spc 7 | *.pyc 8 | *.bak -------------------------------------------------------------------------------- /experimental/spc_aux_ram.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emberling/mfvitools/HEAD/experimental/spc_aux_ram.bin -------------------------------------------------------------------------------- /experimental/spc_work_ram.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emberling/mfvitools/HEAD/experimental/spc_work_ram.bin -------------------------------------------------------------------------------- /experimental_ff5/ff1_battle_data.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emberling/mfvitools/HEAD/experimental_ff5/ff1_battle_data.bin -------------------------------------------------------------------------------- /experimental_ff5/ot_oasis_data_nat.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emberling/mfvitools/HEAD/experimental_ff5/ot_oasis_data_nat.bin -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /experimental_ff5/quick_insert.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from mml2akao3 import * 5 | 6 | ROMFILE = "ff5test.smc" 7 | 8 | ADDRESS = 0x200000 9 | BYTEADR = b"\x00\x00\xE0" 10 | 11 | INSTADR = 0x43DAA 12 | PTRADDR = 0x43B97 13 | 14 | if len(sys.argv) >= 2: 15 | MMLFILE = sys.argv[1] 16 | else: 17 | print("Enter mml file name") 18 | MMLFILE = input() 19 | if '.' not in MMLFILE: MMLFILE += ".mml" 20 | 21 | try: 22 | with open(MMLFILE, 'r') as f: 23 | mml = f.readlines() 24 | 25 | variants = mml_to_akao(mml) 26 | for k, v in variants.items(): 27 | variants[k] = (bytes(v[0],encoding="latin-1"), bytes(v[1],encoding="latin-1")) 28 | variant_to_use = "akao3" if "akao3" in variants else "_default_" 29 | data = variants[variant_to_use][0] 30 | inst = variants[variant_to_use][1] 31 | 32 | data_size = len(data) 33 | if data_size < 0x1000: 34 | data += b"\x00" * (0x1000 - len(data)) 35 | 36 | with open(ROMFILE, 'r+b') as f: 37 | f.seek(ADDRESS) 38 | f.write(data) 39 | f.seek(INSTADR) 40 | f.write(inst) 41 | f.seek(PTRADDR) 42 | f.write(BYTEADR) 43 | 44 | padinfo = "" 45 | if len(data) > data_size: padinfo = f" (padded to 0x{len(data):X})" 46 | print(f"Wrote 0x{data_size:X} bytes{padinfo}.") 47 | input() 48 | 49 | except Exception as e: 50 | print(e) 51 | input() 52 | -------------------------------------------------------------------------------- /hex_tools/mfvitbl.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | notes = { ## divide by 0x0E round down 4 | 0x0: "C", 5 | 0x1: "C+", 6 | 0x2: "D", 7 | 0x3: "D+", 8 | 0x4: "E", 9 | 0x5: "F", 10 | 0x6: "F+", 11 | 0x7: "G", 12 | 0x8: "G+", 13 | 0x9: "A", 14 | 0xA: "A+", 15 | 0xB: "B", 16 | 0xC: "^", 17 | 0xD: "r" } 18 | 19 | lengths = { 20 | 0x0: 0xC0, 21 | 0x1: 0x60, 22 | 0x2: 0x40, 23 | 0x3: 0x48, 24 | 0x4: 0x30, 25 | 0x5: 0x20, 26 | 0x6: 0x24, 27 | 0x7: 0x18, 28 | 0x8: 0x10, 29 | 0x9: 0x0C, 30 | 0xA: 0x08, 31 | 0xB: 0x06, 32 | 0xC: 0x04, 33 | 0xD: 0x03 } 34 | 35 | codes = { #arg bytes, readable name 36 | 0xC4: (1, "Vol"), 37 | 0xC5: (2, "VolEnv"), 38 | 0xC6: (1, "Pan"), 39 | 0xC7: (2, "PanEnv"), 40 | 0xC8: (2, "PitchEnv"), 41 | 0xC9: (3, "Vibrato"), 42 | 0xCA: (0, "VibOff"), 43 | 0xCB: (3, "Tremolo"), 44 | 0xCC: (0, "TremOff"), 45 | 0xCD: (2, "Pansweep"), 46 | 0xCE: (0, "PswOff"), 47 | 0xCF: (1, "NoiseClk"), 48 | 0xD0: (0, "NoiseOn"), 49 | 0xD1: (0, "NoiseOff"), 50 | 0xD2: (0, "PitchModOn"), 51 | 0xD3: (0, "PitchModOff"), 52 | 0xD4: (0, "EchoOn"), 53 | 0xD5: (0, "EchoOff"), 54 | 0xD6: (1, "Oct"), 55 | 0xD7: (0, "Oct+"), 56 | 0xD8: (0, "Oct-"), 57 | 0xD9: (1, "TranSet"), 58 | 0xDA: (1, "TranAdd"), 59 | 0xDB: (1, "Detune"), 60 | 0xDC: (1, "Inst"), 61 | 0xDD: (1, "Attack"), 62 | 0xDE: (1, "Decay"), 63 | 0xDF: (1, "Sustain"), 64 | 0xE0: (1, "Release"), 65 | 0xE1: (0, "ClearADSR"), 66 | 0xE2: (1, "Loop"), 67 | 0xE3: (0, "LoopEnd"), 68 | 0xE4: (0, "Slur"), 69 | 0xE5: (0, "SlurOff"), 70 | 0xE6: (0, "Roll"), 71 | 0xE7: (0, "RollOff"), 72 | 0xE8: (1, "Add"), 73 | 0xE9: (1, "SFXA"), 74 | 0xEA: (1, "SFXB"), 75 | 0xEB: (0, "STOP"), 76 | 0xEC: (0, "???EC???"), 77 | 0xED: (0, "???ED???"), 78 | 0xEE: (0, "???EE???"), 79 | 0xEF: (0, "???EF???"), 80 | 0xF0: (1, "Tempo"), 81 | 0xF1: (2, "TempoEnv"), 82 | 0xF2: (1, "Echo"), 83 | 0xF3: (2, "EchoEnv"), 84 | 0xF4: (1, "MasterVol"), 85 | 0xF5: (3, "JumpIfLoop"), 86 | 0xF6: (2, "Jump"), 87 | 0xF7: (2, "Feedbk"), 88 | 0xF8: (2, "Filter"), 89 | 0xF9: (0, "Output+"), 90 | 0xFA: (3, "CTJumpIf"), 91 | 0xFB: (0, "DrumMode"), 92 | 0xFC: (2, "JumpIf"), 93 | 0xFD: (1, "???FD???"), 94 | 0xFE: (2, "???FE???"), 95 | 0xFF: (0, "???FF???") } -------------------------------------------------------------------------------- /mfvitbl.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | notes = { ## divide by 0x0E round down 4 | 0x0: "C", 5 | 0x1: "C+", 6 | 0x2: "D", 7 | 0x3: "D+", 8 | 0x4: "E", 9 | 0x5: "F", 10 | 0x6: "F+", 11 | 0x7: "G", 12 | 0x8: "G+", 13 | 0x9: "A", 14 | 0xA: "A+", 15 | 0xB: "B", 16 | 0xC: "^", 17 | 0xD: "r" } 18 | 19 | lengths = { 20 | 0x0: 0xC0, 21 | 0x1: 0x60, 22 | 0x2: 0x40, 23 | 0x3: 0x48, 24 | 0x4: 0x30, 25 | 0x5: 0x20, 26 | 0x6: 0x24, 27 | 0x7: 0x18, 28 | 0x8: 0x10, 29 | 0x9: 0x0C, 30 | 0xA: 0x08, 31 | 0xB: 0x06, 32 | 0xC: 0x04, 33 | 0xD: 0x03 } 34 | 35 | codes = { #arg bytes, readable name 36 | 0xC4: (1, "Vol"), 37 | 0xC5: (2, "VolEnv"), 38 | 0xC6: (1, "Pan"), 39 | 0xC7: (2, "PanEnv"), 40 | 0xC8: (2, "PitchEnv"), 41 | 0xC9: (3, "Vibrato"), 42 | 0xCA: (0, "VibOff"), 43 | 0xCB: (3, "Tremolo"), 44 | 0xCC: (0, "TremOff"), 45 | 0xCD: (2, "Pansweep"), 46 | 0xCE: (0, "PswOff"), 47 | 0xCF: (1, "NoiseClk"), 48 | 0xD0: (0, "NoiseOn"), 49 | 0xD1: (0, "NoiseOff"), 50 | 0xD2: (0, "PitchModOn"), 51 | 0xD3: (0, "PitchModOff"), 52 | 0xD4: (0, "EchoOn"), 53 | 0xD5: (0, "EchoOff"), 54 | 0xD6: (1, "Oct"), 55 | 0xD7: (0, "Oct+"), 56 | 0xD8: (0, "Oct-"), 57 | 0xD9: (1, "TranSet"), 58 | 0xDA: (1, "TranAdd"), 59 | 0xDB: (1, "Detune"), 60 | 0xDC: (1, "Inst"), 61 | 0xDD: (1, "Attack"), 62 | 0xDE: (1, "Decay"), 63 | 0xDF: (1, "Sustain"), 64 | 0xE0: (1, "Release"), 65 | 0xE1: (0, "ClearADSR"), 66 | 0xE2: (1, "Loop"), 67 | 0xE3: (0, "LoopEnd"), 68 | 0xE4: (0, "Slur"), 69 | 0xE5: (0, "SlurOff"), 70 | 0xE6: (0, "Roll"), 71 | 0xE7: (0, "RollOff"), 72 | 0xE8: (1, "Add"), 73 | 0xE9: (1, "SFXA"), 74 | 0xEA: (1, "SFXB"), 75 | 0xEB: (0, "STOP"), 76 | 0xEC: (0, "???EC???"), 77 | 0xED: (0, "???ED???"), 78 | 0xEE: (0, "???EE???"), 79 | 0xEF: (0, "???EF???"), 80 | 0xF0: (1, "Tempo"), 81 | 0xF1: (2, "TempoEnv"), 82 | 0xF2: (1, "Echo"), 83 | 0xF3: (2, "EchoEnv"), 84 | 0xF4: (1, "MasterVol"), 85 | 0xF5: (3, "JumpIfLoop"), 86 | 0xF6: (2, "Jump"), 87 | 0xF7: (2, "Feedbk"), 88 | 0xF8: (2, "Filter"), 89 | 0xF9: (0, "Output+"), 90 | 0xFA: (3, "CTJumpIf"), 91 | 0xFB: (0, "IgnoreMvol"), 92 | 0xFC: (2, "JumpIf"), 93 | 0xFD: (1, "???FD???"), 94 | 0xFE: (2, "???FE???"), 95 | 0xFF: (0, "???FF???") } -------------------------------------------------------------------------------- /experimental_ff5/Slam Shuffle.mml: -------------------------------------------------------------------------------- 1 | # Converted from binary by mfvitools 2 | 3 | #scale pan 2 4 | 5 | {1} 6 | l16t115%b0,100%f0,0%z100,0%x165r1 r1 %v50v60@1p40%e1m48,18,239%r20o4[8e12r12e12r4] $1 v60o4[16e12r12e12r4 7 | ][8g12r12g12r4][8e12r12e12r4 ]v80[2b12r12b12]a12r6a12 a12r6a12r6a12 9 | e12r12e12a12r6a12 b12a+12 10 | a1 r1 11 | ;1 12 | 13 | l16 14 | {2} 15 | l16r1 r1 16 | v60@1p40%e1m48,18,239%r20[4o3a12r12a12b12a12r12a12b12a12r12a12a12r12a12b12a12r12a12e12r12e12a12r12a12a12r12a12b12a+12 a1 r1 21 | 22 | ;2 23 | 24 | l16 25 | {3} 26 | l16r1 r1 v40@3p90%e1m48,18,239r1 r1 27 | o4[8a8m8,+3^24^12] $3 o4[32a8m8,+3^24^12]<[16c8m8,+3^24^12]>[16a8m8,+3^24^12 28 | ][16b8m8,+3^24^12]<[12e8m8,+3^24^12]r1 r1 >[8a8m8,+3^24^12] 29 | ;3 30 | 31 | l16 32 | {4} 33 | l16r1 34 | r1 v120@6p64%e0m24,64,239r1 r1 o1a12r3%l1g+12a12r3%l1g+12 35 | a12r12%l1g+12a12r12%l1g+12a12r4.r24 $4 [4a12r3%l1g+12a12r3%l1g+12 36 | a12r12%l1g+12a12r12%l1g+12a12r4.r24 ]<[2c12r3>%l1b12%l1b12 37 | %l1b12%l1b12[2a12r3%l1g+12a12r3%l1g+12 38 | a12r12%l1g+12a12r12%l1g+12a12r4.r24 ]%r20e12r12e12e12r12e12e12r12e12e12r12e12 39 | e12r12e12e12r12e12e12r12e12d+12 e12r12e12e12r12e12e12r12e12e12r12e12 40 | e12r12e12e12r12e12e12r12e12e12f+12g+12 a12r12a12a12r12a12a12r12a12a12r12a12 41 | a12r12a12a12r12a12a12r12a12g+12 a12r12a12a12r12a12a12r12a12a12r12a12 42 | b12a+12 %r0a12r3%l1g+12a12r3%l1g+12 43 | a12r12%l1g+12a12r12%l1g+12a12r4.r24 44 | ;4 45 | 46 | l16 47 | {5} 48 | l16r1 r1 v80@2p64%e1m48,18,239r1 49 | r1 r1 r1 $5 o5a12r6b12 a12r3g+12 a12r12b12a12r6b12 a12r3b12g+12 52 | a12r12b12b12a12^4.r24 f+4^6g12 a6g12a6a+12a6g12f+12d+12d12 53 | c12r3f+12g12r3>b12 a12r6b12 a12r3b12g+12 a12r12b12b12a12^4.r24 55 | r1 r1 r1 r1 56 | r1 r1 r1 r1 57 | r1 r1 r1 58 | ;5 59 | 60 | l16 61 | {6} 62 | l16p64v30@7o10v20,255,255%a4%l1c64 $6 c64 63 | ;6 64 | 65 | l16 66 | {7} 67 | l16p64v25r64%e1@7v0,180,201o18p0,255,60%p1%a4%l1c64 $7 c64 68 | ;7 69 | 70 | l16 71 | {8} 72 | l16r1 73 | r1 p75r1 r1 r1 74 | r1 $8 @4[46v80o6%r30a12r12a12v120o3%r20a4]r2 [6v80o6%r30a12r12a12]%p1%e1@5o5v125%s0%r26[2r4m7,+32c4r4 75 | m6,+31g4]%e0%p0 76 | ;8 77 | 78 | l16 79 | -------------------------------------------------------------------------------- /experimental_ff5/Devil's Lab.mml: -------------------------------------------------------------------------------- 1 | # Converted from binary by mfvitools 2 | 3 | #scale pan 2 4 | 5 | #WAVE 0x20 0x11 trumpet 6 | #WAVE 0x21 0x0B strings 7 | #WAVE 0x22 0x15 contrabass 8 | #WAVE 0x23 0x14 syn bass 9 | #WAVE 0x24 0x02 crunch 10 | #WAVE 0x25 0x18 clank 11 | #WAVE 0x26 0x01 kick 12 | #WAVE 0x27 0x03 snare 13 | 14 | #def tr= o4 15 | #def st= o5 16 | #def cb= o4 17 | #def sb= o6 18 | #def cx= o4 19 | #def ck= o4 20 | 21 | {1} 22 | l16t85%x232%z100,0%b0,100%f0,0%v50v120|0p75%e1m48,18,223r1 r1 $1 r1 23 | r1 r1 r1 o6'tr+o1'c+32r32>g+32r32b32r32bra+rgr g+1 25 | g+32r32bg+ef+ef+er4ref+g+re8 36 | r4bra+8r4grg+8 g+4b4b4b2c+d+c+d+c+r4rc+d+erc8 r4ere8r4ere8 47 | e4g+4a+4g+2f+4a+4 arbbb8b8a8b8a2 ba+a g+rg+ra+ra+ra+ra+r 49 | g+2a2 b1 50 | ;3 51 | 52 | l16 53 | {4} 54 | l16v70|2p70%e0r1 r1 55 | $4 o6'cb+o1'c+rc+re8.cc+d+ef+g+rc8 c+rc+r4cc+rc+r4r ererg+8.ef+8f+8>a+8ba+8ba+8ba4a4 a4a8b8 59 | c8c8c8c8c8c8ba+a g+8g+8r4f+8f+8r4 e8e8r4e8e8r4 60 | e8e8r4.e8d+8d8 61 | ;4 62 | 63 | l16 64 | {5} 65 | l16v50|3p60%e0r1 r1 $5 o5'sb'c+rc+re8.cc+d+ef+g+rc8 66 | c+rc+r4cc+rc+r4r ererg+8.ef+8f+8>a+8ba+8ba+8ba4a4 a4a8b8 c8c8c8c8c8c8ba+a 70 | g+8g+8r4f+8f+8r4 e8e8r4e8e8r4 e8e8r4.e8d+8d8 71 | ;5 72 | 73 | l16 74 | {6} 75 | l16v120|0p64%e1 $6 o5'cx'|4c4'ck'|5%y0%s2g4'cx'|4c8c8'ck'|5%y0%s2g4 76 | 77 | ;6 78 | 79 | l16 80 | {7} 81 | l16v100|0p64%e0 $7 o5|6a4|7c4|6a8a8|7c4 82 | ;7 83 | 84 | l16 85 | {8} 86 | l16p64%e0r1 87 | r1 r1 r1 r1 88 | r1 $8 [8r4r8@7%e1p20p24,64v50o3c32m18,+24^32^o6rv30p100p84,20@2%e0c+>@1g@0c+>@1g+@2c+>@1g@0c 89 | ]r1 r1 r1 r1 90 | r1 r1 r1 r1 91 | r1 r1 r1 r1 92 | 93 | ;8 94 | 95 | l16 96 | -------------------------------------------------------------------------------- /experimental_ff5/cc_world2.mml: -------------------------------------------------------------------------------- 1 | #TITLE Shore of Dreams ~ Another World 2 | #ALBUM Chrono Cross 3 | #COMPOSER Yasunori Mitsuda 4 | #ARRANGED emberling 5 | 6 | #WAVE 0x20 0x14 7 | #WAVE 0x21 0x0f 8 | #WAVE 0x22 0x0c 9 | #WAVE 0x23 0x0d 10 | #WAVE 0x24 0x0d 11 | 12 | #def init= t81 %x240 %v40 %b0,85 %f0,0 %z85,0 13 | 14 | #scale pan 2 15 | 16 | #def bs= |0 o7 'b' p64 %r10 17 | #def vi= |1 o4 'v' p70 18 | #def ch= |2 o4 'c' 19 | #def gt= |3 o4 'g' p48 %r12 20 | #def pi= @3 o5 'p' p84 %y3%s3%r13 21 | 22 | #def b= v96 23 | #def v= v104 24 | #def c= v80 25 | #def g= v80 26 | #def p= v40 27 | 28 | #def vf= v56v48,104 29 | 30 | #cdef ( %l1 31 | #cdef ) %l0 32 | 33 | 34 | {1} 'init' 35 | 'bs-o2' l16 36 | r4. a8< 37 | $##2 38 | m12,2c2..c24m4,2^48^> 39 | a+2^.r32a+8 40 | f2^8^32r32g 44 | a+2^8.ra+8 45 | f2^8^32r32ga+2^8f4a+8< 50 | {&71}f3^48^64m6,2^8m2,-2{&37}^8^32^48^64c8.f8 51 | c2^8c8>g8a+2..a+8 54 | a4.a4< 55 | d4.a4.d4 56 | a32.r64rga8%r16^4%r10d4 57 | ##18 58 | >a+4.a+4 59 | a4.a4 60 | g2^8g8g8 61 | a2^8^32r32a+2.^ra+8 f2.^rf8< 65 | ; 66 | 67 | {2} %e1 68 | 'ch+o1*v.67' p96 69 | %a2 d2 70 | $##2 71 | 'gt-o1' l16 72 | d2d2 >a+2a+4.< f4.f2 c4.c4. 73 | ##6 74 | 'vi+o1' l8 %r8 m48,32,207 75 | ['vf'd2<'vf'd2> 'vf'f2.(df) 76 | 'vf'a2^a+4(a24a+48a16) 'vf'g2.(fe)] 77 | ##10 78 | #^^ 79 | ##14 80 | 'vf'd2'vf'(f4..e32f32) 'vf'e2'vf'c4.>a< 81 | cd16.%r20^32%r5'vf'd1.. 82 | ##18 83 | %r8r4d4'vf'f4..(e32f32 'vf'e2)'vf'g2 84 | ^%r17^%r8g4'vf'd4.(e16f16) 'vf'%r5e1^1 85 | ; 86 | 87 | {3} %e1 88 | 'ch*v.67' p32 89 | %a2 a2 90 | $##2 91 | 'gt-o1' l8 92 | ra.a^.a.a. rf.f^.f.f. rg.g^.g.g. 93 | ##6 94 | [[da.a.] [>a+] [cg.g.]] 95 | ##10 96 | #^^ 97 | ##14 98 | [>a+aa+agaabbbb r8.b8.ab4b8.ab b w7 21 | c w7 #16 ; 1 in 16 22 | bhi 40 ; play one of our scenarios 23 | bra 1 ; restart 24 | 25 | :40 ; scenario select 26 | bra 99 ; rand => w7 27 | c w7 #32 ; 1 in 8 (7/8 remains) 28 | r0 29 | bhi 10 ; scenario: nothing going on for a bit 30 | c w7 #96 ; 2 in 8 (5/8 remains) 31 | bhi 12 ; scenario: deliberate menuing 32 | c w7 #128 ; 1 in 8 (4/8 remains) 33 | bhi 13 ; scenario: chain random game sfx 34 | c w7 #192 ; 2 in 8 (2/8 remains) 35 | bhi 31 ; play single random game sfx 36 | bra 11 ; else: scenario: isolated menu sound 37 | :41 ; scenario return point 38 | m #0 w2 ; clear current scenario debug display 39 | w 2048000 ; 1 sec delay between scenarios 40 | r 41 | 42 | :20 ; play system sound $20 43 | m #$20 i0 44 | wi 45 | r 46 | 47 | :21 ; play system sound $21 48 | m #$21 i0 49 | wi 50 | r 51 | 52 | :22 ; play system sound $22 (buzzer) 53 | m #$22 i0 54 | wi 55 | r 56 | 57 | :23 ; play system sound $23 (confirm) 58 | m #$23 i0 59 | wi 60 | r 61 | 62 | :28 ; play system sound $28 (turn) 63 | m #$28 i0 64 | wi 65 | r 66 | 67 | :30 ; play random menu sound 68 | r1 69 | bra 99 ; rand => w7 70 | c w7 #128 ; 1 in 2 71 | bhi 20 ; play sound 72 | blo 21 ; else play sound 73 | r 74 | 75 | :31 ; play random game sound effect 76 | r1 77 | bra 99 ; rand => w7 78 | m #$18 i0 79 | m w7 i1 80 | m #$80 i2 81 | wi 82 | r 83 | 84 | :55 ; delay some randomish amount 85 | ; 1/64 sec. chunks 86 | ; 7 in 8 chance of repeating / playing an additional chunk 87 | m #0 w3 ; debug counter 88 | :551 89 | r1 90 | w 320000 ; 1/64sec 91 | a #1 w3 ; debug counter 92 | bra 99 93 | m w7 w4 94 | c w7 #224 ; 7 in 8 (repeat) 95 | r0 96 | bhi 551 97 | r 98 | 99 | :10 ; scenario: nothing going on for a bit 100 | m #10 w2 ; set current scenario display 101 | w 128000 ; 1/16sec 102 | r1 103 | bra 99 ; rand => w7 104 | c w7 #8 ; 1 in 32 105 | r0 106 | blo 10 ; if failed, loop 107 | bra 41 ; scenario return point 108 | 109 | :11 ; scenario: isolated menu sound 110 | ; 50% one beep, 50% two, with some delay variance 111 | m #11 w2 ; set current scenario display 112 | r1 113 | bra 30 ; play random menu sound 114 | w 192000 ; 3/32sec 115 | bra 99 ; rand => w7 116 | r0 117 | c w7 #128 ; 1 in 2 (chance to play second menu sound) 118 | blo 111 ; skip forward if failed 119 | r1 120 | bra 99 ; rand => w7 121 | r0 122 | c w7 #128 ; 1 in 2 (chance to delay 1/4sec instead of 3/32) 123 | blo 112 ; skip forward if failed 124 | w 64000 ; 1/32sec 125 | w 256000 ; 1/8sec 126 | :112 127 | r1 128 | bra 30 ; play random menu sound 129 | :111 130 | r0 131 | bra 41 ; scenario return point 132 | 133 | :12 ; scenario: deliberate menuing 134 | m #12 w2 ; set current scenario display 135 | r1 136 | bra 30 ; play random menu sound 137 | w 192000 ; 3/32sec 138 | :121 139 | r1 140 | bra 30 ; play random menu sound 141 | w 96000 ; 3/64sec 142 | r1 143 | bra 99 ; rand => w7 144 | r0 145 | blo 122 146 | w 32000 ; 1/64 sec 147 | :122 148 | r1 149 | bra 99 ; rand => w7 150 | c w7 #192 ; 7 in 8 (chance to keep menuing more) 151 | r0 152 | bhi 121 153 | w 96000 ; 3/64sec 154 | r1 155 | bra 99 ; rand => w7 156 | c w7 #64 ; 1 in 4 (chance for error instead of confirm) 157 | r1 158 | bhi 22 159 | blo 23 160 | r0 161 | bra 41 ; scenario return point 162 | 163 | :13 ; scenario: chain random game sound effects 164 | m #13 w2 ; set current scenario display 165 | r1 166 | bra 31 ; play random game sfx 167 | r1 168 | bra 55 ; wait random amount 169 | r1 170 | bra 99 ; rand => w7 171 | c w7 #128 ; 1 in 2 (chance to repeat) 172 | r0 173 | bhi 13 ; repeat if passed 174 | r0 175 | bra 41 ; scenario return point 176 | 177 | :99 ; xorshift PRNG 178 | ; record previous results 179 | n #8 < w6 180 | a w7 w6 181 | ; generate 182 | m w0 w1 183 | n #13 < w1 184 | n w1 ^ w0 185 | m w0 w1 186 | n #17 > w1 187 | n w1 ^ w0 188 | m w0 w1 189 | n #5 < w1 190 | n w1 ^ w0 191 | ; isolate lowest 8 bits in w7 for simpler comparisons 192 | m w0 w7 193 | n #255 & w7 194 | r 195 | -------------------------------------------------------------------------------- /spc2brrs.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import traceback 4 | 5 | DIR_BASE = "spcbrr" 6 | 7 | def clean_end(): 8 | print("Processing ended.") 9 | input("Press enter to close.") 10 | quit() 11 | 12 | class Sample: 13 | def __init__(self, id, spc, dirloc): 14 | dirloc += id * 4 15 | 16 | self.id = id 17 | self.addr = int.from_bytes(spc[dirloc:dirloc+2], "little") 18 | self.loop_pos = int.from_bytes(spc[dirloc+2:dirloc+4], "little") - self.addr 19 | self.loop_flag = False 20 | self.warning = None 21 | # check for invalid locations 22 | if self.addr < 0x200: 23 | self.warning = "Sample address in zero page memory" 24 | self.data = bytearray() 25 | self.blocks = 0 26 | return 27 | # grab BRR data 28 | brr = bytearray() 29 | loc = self.addr 30 | while True: 31 | brr += spc[loc:loc+9] 32 | # END bit 33 | if spc[loc] & 1: 34 | # LOOP bit 35 | if spc[loc] & 0b10: 36 | self.loop_flag = True 37 | if self.loop_pos % 9: 38 | self.warning = "Loop point misaligned with block boundaries" 39 | break 40 | 41 | loc += 9 42 | if loc > (len(spc) - 9): 43 | self.warning = "Unterminated BRR" 44 | break 45 | self.data = brr 46 | self.blocks = len(self.data) // 9 47 | 48 | #### main execution block 49 | 50 | try: 51 | if len(sys.argv) >= 2: 52 | infilename = sys.argv[1] 53 | else: 54 | print("SPC filename:") 55 | infilename = input() 56 | 57 | try: 58 | with open(infilename, "rb") as f: 59 | spc = f.read()[0x100:] 60 | except IOError: 61 | print(f"couldn't open file {infilename}, aborting") 62 | clean_end() 63 | 64 | out_dir = os.path.join(DIR_BASE, os.path.splitext(os.path.basename(infilename))[0]) 65 | 66 | while True: 67 | print("Maximum sample ID to rip (hex):") 68 | max_id = input() 69 | try: 70 | max_id = int(max_id, 16) 71 | break 72 | except ValueError: 73 | print("Invalid input, try again") 74 | 75 | spc_dir = spc[0x1005D] * 0x100 76 | samples = [] 77 | for i in range(max_id): 78 | samp = Sample(i, spc, spc_dir) 79 | print(f"SRCN {i:02X}: ", end="") 80 | if samp.warning: 81 | print(f"{samp.warning} (loc {samp.addr:04X}, size {samp.blocks})") 82 | else: 83 | print(f"Sample found: loc {samp.addr:04X}, loop {samp.loop_pos:X}, size {samp.blocks}") 84 | samples.append(samp) 85 | 86 | if not samples: 87 | print("No files to write.") 88 | clean_end() 89 | 90 | if not os.path.exists(out_dir): 91 | os.makedirs(out_dir) 92 | 93 | samplist = [] 94 | for samp in samples: 95 | samplist.append((f"{samp.id:02X}", f"BRR{samp.id:02X}", f"@0x{samp.loop_pos:X}", "0000", "F 7 7 0 ", f"{{{samp.blocks}}}", f"idx{samp.id:02X} @ {samp.addr:04X}")) 96 | 97 | id_len = max([len(s[0]) for s in samplist]) 98 | fn_len = max([len(s[1]) for s in samplist]) 99 | lp_len = max([len(s[2]) for s in samplist]) 100 | tn_len = max([len(s[3]) for s in samplist]) 101 | nv_len = max([len(s[4]) for s in samplist]) 102 | bk_len = max([len(s[5]) for s in samplist]) 103 | tx_len = max([len(s[6]) for s in samplist]) 104 | 105 | listfile = "" 106 | for i, samp in enumerate(samples): 107 | id, fn, loop, tune, env, block, text = samplist[i] 108 | listfile += f"{id:<{id_len}}: {fn:<{fn_len}}, {loop:<{lp_len}}, {tune:<{tn_len}}, {env:<{nv_len}}, {block:<{bk_len}} [ ] {text}\n" 109 | with open(os.path.join(out_dir, fn+".brr"), "wb") as f: 110 | f.write(samp.data) 111 | 112 | with open(os.path.join(out_dir, "spcbrr.txt"), "w") as f: 113 | f.write(listfile) 114 | print(f"Wrote listfile to {os.path.join(out_dir, 'spcbrr.txt')}") 115 | 116 | clean_end() 117 | except SystemExit: 118 | pass 119 | except: 120 | traceback.print_exc() 121 | input() -------------------------------------------------------------------------------- /experimental_ff5/ff1_battle.mml: -------------------------------------------------------------------------------- 1 | #TITLE Battle Scene 2 | #ALBUM Final Fantasy 3 | #COMPOSER Nobuo Uematsu 4 | #ARRANGED Gi Nattak 5 | # Converted from binary by mfvitools 6 | 7 | #WAVE 0x20 -- 0x0E 8 | #WAVE 0x21 -- 0x0B 9 | #WAVE 0x22 -- 0x01 10 | #WAVE 0x23 -- 0x06 11 | #WAVE 0x24 -- 0x04 12 | #WAVE 0x25 -- 0x14 13 | #WAVE 0x26 -- 0x02 14 | #WAVE 0x27 -- 0x07 15 | 16 | #scale pan 2 17 | 18 | {1} 19 | t165%v25%x192 %z70,0 20 | l16v104p80%e1@32o5^8d+32f+32a32r32d+32f+32rf+32a32f+32a32a+agf+d+dc>a+agf+d+d4 a+agf+d+d4 24 | r8o6f+gf+d+dc>a+agf+[2drdr a+8g8drg8dra+8 gra+8g8dra+8drara8 25 | f+8d8arf+8d8a+8arf+8 d8f+8dra8f+8a8]g4 f+4g4a4a+4 a4a+4a+agf+ d+d $2 r1 ^1 ^1 38 | ^1 ^1 ^1 ^1 39 | ^1 ^1 ^1 arararar8^ar2^4^8^ a+ra+ra+ra+r8^a+4a+r ar1 ^1 41 | ^1 ^1 ^1 ^1 42 | ^1 ^1 ^1 ^1 43 | ^1 ^1 ^4^a+agf+d+d 44 | ;2 45 | 46 | l16 47 | {3} 48 | l16v60p24%e1@33o5^8 49 | f8gr a+8a8g8 50 | r8g8a8a+8r8a8f8gr a8a+rf8gra+8a8g8r8g8a8a+8r8 51 | a8f8gra8 a+rg4^8g4^8 f+4r1 52 | g4^8g4^8f+4 r1 d4^8d+4^8f4 ^8d+4^8d4r4 53 | >a4^8a+4^8a+4^8a4r4 a4^8a+4^8a+4^8a4grd4a4a+aa+aga+agf+gra+8a8g8r8 g8a8a+8r8a8f8gra8a+rf8gra+8a8g8r8g8a8a+8r8 a8f8gra8 a+rf8 gra+8a8 g8r8g8a8a+8r8a8f8 gra8a+rf8 gra+8a8 g8r8g8a8a+8r8a8f8 gra8a+ra+4^8a+4r4 f+4^8g4^8a4 67 | ^8g4^8f+4r4 a+4^8a+4r4 f+4^8g4^8a4 68 | ^8g4^8f+4a+8r8 a8r8a+8r8grgr grgr8^g4grf+r8^ a+agf+agf+egf+ed>gra+8a8g8r8 70 | g8a8a+8r8a8f8gra8a+rf8gra+8a8g8r8g8a8a+8r8 71 | a8f8gra8 a+ra8a+8g8a8a+8a8g8a8a+8g8a8a+8a8d+4^8d4 f+rf+rarard+rd+r d+4^8d4 r2^4>grg8 80 | g8grg8g8g8 g8grg8g8drd8 d8drd8d8d8 81 | d8drd8d8grg8 g8grg8g8g8 g8grg8g8drd8 82 | d8drd8d8d8 d8drd8d8g2 ^4a+2 ^4g8a8a+8c+1 $8 r1 ^1 110 | ^1 c+1 r1 ^1 111 | ^1 ^4v80p80@39d+4^8d+4^8 d+1 r4d+4^8d+4^8 112 | d+4r2^4 [4d+1 r1 ]d+1 113 | d+1 d+2r8d+4^8 d+4r2^4 d+2r8d+4^8 114 | d+4r2^4 v124p80@36c+1 115 | ;8 116 | 117 | l16 118 | -------------------------------------------------------------------------------- /hex_tools/mfvimerge.py: -------------------------------------------------------------------------------- 1 | from mfvitbl import notes, lengths, codes 2 | 3 | skipone = {"\xC4", "\xC6", "\xCF", "\xD6", "\xD9", "\xDA", "\xDB", "\xDC", "\xDD", "\xDE", "\xDF", "\xE0", 4 | "\xE2", "\xE8", "\xE9", "\xEA", "\xF0", "\xF2", "\xF4", "\xFD", "\xFE"} 5 | skiptwo = {"\xC5", "\xC7", "\xC8", "\xCD", "\xF1", "\xF3", "\xF7", "\xF8"} 6 | skipthree = {"\xC9", "\xCB"} 7 | 8 | source = raw_input("Please input the prefix file name of your split " 9 | "ROM dump\n(before the 00, 01 etc.):\n> ").strip() 10 | 11 | def read_word(f): 12 | low, high = 0, 0 13 | low = ord(f.read(1)) 14 | high = ord(f.read(1)) 15 | return low + (high * 0x100) 16 | 17 | def write_word(i, f): 18 | low = i & 0xFF 19 | high = (i & 0xFF00) >> 8 20 | f.write(chr(low)) 21 | f.write(chr(high)) 22 | 23 | def strmod(s, p, c): #overwrites part of a string 24 | while len(s) < p: 25 | s = s + " " 26 | if len(s) == p: 27 | return s[:p] + c 28 | else: 29 | return s[:p] + c + s[p+len(c):] 30 | 31 | def bytes(n): # int-word to two char-bytes 32 | low = n & 0xFF 33 | high = (n & 0xFF00) >> 8 34 | return chr(high) + chr(low) 35 | 36 | channels = [] 37 | try: 38 | f = open(source + "00", 'rb') 39 | for i in xrange(0,16): 40 | c = (ord(f.read(1)) << 8) + ord(f.read(1)) 41 | print "{} -- {} {} -- {} {}".format(c, hex(c >> 8), hex(c & 0xFF), hex(c >> 12), hex(c & 0xFFF)) 42 | channels.append(c) 43 | f.close() 44 | except IOError: 45 | raw_input("Couldn't open {}00. Press enter to quit. ".format(source)) 46 | quit() 47 | 48 | segments = ["\x00"*0x26] 49 | index = 1 50 | while index: 51 | try: 52 | fn = source + "%02d" % (index) 53 | print "reading {}".format(fn) 54 | f = open(fn, "rb") 55 | segments.append(f.read()) 56 | f.close() 57 | index += 1 58 | except: 59 | print "found {} segment files".format(index-1) 60 | if (index-1) <= 0: 61 | raw_input("Press enter to quit. ") 62 | quit() 63 | index = 0 64 | 65 | origins = [] 66 | lastorigin = 0 67 | for s in segments: 68 | origins.append(lastorigin + len(s)) 69 | lastorigin += len(s) 70 | #length = origins[-1] + len(segments[-1]) 71 | #lengthb = chr(length & 0xFF) + chr(length >> 8) 72 | 73 | def convert_pointer(ch, po): 74 | global origins 75 | pointer = origins[ch] + po 76 | return chr(pointer & 0xFF) + chr(pointer >> 8) 77 | 78 | for i, s in enumerate(segments): 79 | if i == 0: continue 80 | pos = 0 81 | while pos < len(s): 82 | byte = s[pos] 83 | if byte in {"\xF5", "\xF6", "\xFA", "\xFC"}: 84 | if byte in {"\xF5", "\xFA"}: 85 | pos += 1 86 | target = (ord(s[pos+1]) << 8) + ord(s[pos+2]) 87 | tarc = target >> 12 88 | tarp = target & 0xFFF 89 | if tarc >= len(segments): 90 | print "WARNING: {} {} -- jump to channel {} which does not exist".format(hex(ord(s[pos+1])), hex(ord(s[pos+2])), tarc+1) 91 | print "using current channel ({}) instead".format(i) 92 | tarc = i - 1 93 | tarst = convert_pointer(tarc, tarp) 94 | segments[i] = strmod(segments[i], pos+1, tarst) 95 | print "converting jump to ch{} p{} --> p{} {}".format(tarc, tarp, hex(ord(tarst[0])), hex(ord(tarst[1]))) 96 | pos += 2 97 | elif byte in skipone: 98 | pos += 1 99 | elif byte in skiptwo: 100 | pos += 2 101 | elif byte in skipthree: 102 | pos += 3 103 | pos += 1 104 | 105 | length = 0 106 | for s in segments: 107 | length += len(s) 108 | lengthb = chr(length & 0xFF) + chr(length >> 8) 109 | 110 | segments[0] = strmod(segments[0], 0, lengthb) 111 | segments[0] = strmod(segments[0], 2, "\x26\x00") 112 | segments[0] = strmod(segments[0], 4, lengthb) 113 | 114 | for i, c in enumerate(channels): 115 | segments[0] = strmod(segments[0], 6 + 2*i, convert_pointer(c >> 12, c & 0xFFF)) 116 | 117 | try: 118 | fout = open(source + "_data.bin", "wb") 119 | except: 120 | raw_input("Couldn't open output file, press enter to close ") 121 | quit() 122 | 123 | for s in segments: 124 | fout.write(s) 125 | fout.close() 126 | 127 | raw_input("Merge OK. Press enter to close ") 128 | -------------------------------------------------------------------------------- /experimental_ff5/mmltbl_ff5.py: -------------------------------------------------------------------------------- 1 | 2 | note_tbl = { 3 | "c": 0x0, 4 | "d": 0x2, 5 | "e": 0x4, 6 | "f": 0x5, 7 | "g": 0x7, 8 | "a": 0x9, 9 | "b": 0xB, 10 | "^": 0xC, 11 | "r": 0xD } 12 | 13 | length_tbl = { 14 | 1 : (0, 0xC0), 15 | "2.": (1, 0x90), 16 | 2 : (2, 0x60), 17 | 3 : (3, 0x40), 18 | "4.": (4, 0x48), 19 | 4 : (5, 0x30), 20 | 6 : (6, 0x20), 21 | "8.": (7, 0x24), 22 | 8 : (8, 0x18), 23 | 12 : (9, 0x10), 24 | 16 : (10, 0x0C), 25 | 24 : (11, 0x08), 26 | 32 : (12, 0x06), 27 | 48 : (13, 0x04), 28 | 64 : (14, 0x03) } 29 | 30 | r_length_tbl = { 31 | 0: "1", 32 | 1: "2.", 33 | 2: "2", 34 | 3: "3", 35 | 4: "4.", 36 | 5: "4", 37 | 6: "6", 38 | 7: "8.", 39 | 8: "8", 40 | 9: "12", 41 | 10: "", #default 42 | 11: "24", 43 | 12: "32", 44 | 13: "48", 45 | 14: "64" } 46 | 47 | CMD_END_TRACK = "\xFE" 48 | CMD_END_LOOP = "\xFA" 49 | CMD_JUMP_IF_LOOP = "\xF9" 50 | CMD_CONDITIONAL_JUMP = "\xFB" 51 | 52 | command_tbl = { 53 | ("@", 1) : 0xEA, #program 54 | ("|", 1) : 0xEA, #program (hex param) 55 | ("%a", 0): 0xEF, #reset ADSR 56 | ("%a", 1): 0xEB, #set attack 57 | #("%b", 1): 0xF7, #echo feedback (rs3) 58 | #("%b", 2): 0xF7, #echo feedback (ff6) 59 | ("%c", 1): 0xDD, #noise clock 60 | #("%d0", 0): 0xFC,#drum mode off (rs3) 61 | #("%d1", 0): 0xFB,#drum mode on (rs3) 62 | ("%e0", 0): 0xE3,#disable echo 63 | ("%e1", 0): 0xE2,#enable echo 64 | #("%f", 1): 0xF8, #filter (rs3) 65 | #("%f", 2): 0xF8, #filter (ff6) 66 | #("%g0", 0): 0xE7,#disable roll (enable gaps between notes) 67 | #("%g1", 0): 0xE6,#enable roll (disable gaps between notes) 68 | #("%k", 1): 0xF6 - jump to marker, segment continues 69 | ("%k", 1): 0xE7, #set transpose 70 | #("%l0", 0): 0xE5,#disable legato 71 | #("%l1", 0): 0xE4,#enable legato 72 | ("%n0", 0): 0xDF,#disable noise 73 | ("%n1", 0): 0xDE,#enable noise 74 | ("%p0", 0): 0xE1,#disable pitch mod 75 | ("%p1", 0): 0xE0,#enable pitch mod 76 | ("%r", 0): 0xEF, #reset ADSR 77 | ("%r", 1): 0xEE, #set release 78 | ("%s", 0): 0xEF, #reset ADSR 79 | ("%s", 1): 0xED, #set sustain 80 | ("%v", 1): 0xF5, #set echo volume 81 | ("%v", 2): 0xF6, #echo volume envelope 82 | ("%x", 1): 0xF8, #set master volume 83 | ("%y", 0): 0xEF, #reset ADSR 84 | ("%y", 1): 0xEC, #set decay 85 | ("%z", 2): 0xF7, #echo feedback and filter (ff5) 86 | #("j", 1): 0xF5 - jump out of loop after n iterations 87 | #("j", 2): 0xF5 - jump to marker after n iterations 88 | ("k", 1): 0xE9, #set detune 89 | ("m", 0): 0xD8, #disable vibrato 90 | ("m", 1): 0xE8, #add to transpose 91 | ("m", 2): 0xD6, #pitch envelope (portamento) 92 | ("m", 3): 0xD7, #enable vibrato 93 | ("o", 1): 0xE4, #set octave 94 | ("p", 0): 0xDC, #disable pan sweep 95 | ("p", 1): 0xD4, #set pan 96 | ("p", 2): 0xD5, #pan envelope 97 | ("p", 3): 0xDB, #pansweep 98 | #("s0", 1): 0xE9, #play sound effect with voice A 99 | #("s1", 1): 0xEA, #play sound effect with voice B 100 | ("t", 1): 0xF3, #set tempo 101 | ("t", 2): 0xF4, #tempo envelope 102 | #("u0", 0): 0xFA, #clear output code 103 | #("u1", 0): 0xF9, #increment output code 104 | ("v", 0): 0xDA, #disable tremolo 105 | ("v", 1): 0xD2, #set volume 106 | ("v", 2): 0xD3, #volume envelope 107 | ("v", 3): 0xD9, #set tremolo 108 | #("&", 1): 0xE8, #add to note duration 109 | ("<", 0): 0xE5, #increment octave 110 | (">", 0): 0xE6, #decrement octave 111 | ("[", 0): 0xF0, #start loop 112 | ("[", 1): 0xF0, #start loop 113 | ("]", 0): 0xF1 #end loop 114 | #(":", 1): 0xFC - jump to marker if event signal is sent 115 | #(";", 1): 0xF6 - jump to marker, end segment 116 | } 117 | 118 | byte_tbl = { 119 | 0xC4: (1, "v"), 120 | 0xC5: (2, "v"), 121 | 0xC6: (1, "p"), 122 | 0xC7: (2, "p"), 123 | 0xC8: (2, "m"), 124 | 0xC9: (3, "m"), 125 | 0xCA: (0, "m"), 126 | 0xCB: (3, "v"), 127 | 0xCC: (0, "v"), 128 | 0xCD: (2, "p0,"), 129 | 0xCE: (0, "p"), 130 | 0xCF: (1, "%c"), 131 | 0xD0: (0, "%n1"), 132 | 0xD1: (0, "%n0"), 133 | 0xD2: (0, "%p1"), 134 | 0xD3: (0, "%p0"), 135 | 0xD4: (0, "%e1"), 136 | 0xD5: (0, "%e0"), 137 | 0xD6: (1, "o"), 138 | 0xD7: (0, "<"), 139 | 0xD8: (0, ">"), 140 | 0xD9: (1, "%k"), 141 | 0xDA: (1, "m"), 142 | 0xDB: (1, "k"), 143 | 0xDC: (1, "@"), 144 | 0xDD: (1, "%a"), 145 | 0xDE: (1, "%y"), 146 | 0xDF: (1, "%s"), 147 | 0xE0: (1, "%r"), 148 | 0xE1: (0, "%y"), 149 | 0xE2: (1, "["), 150 | 0xE3: (0, "]"), 151 | 0xE4: (0, "%l1"), 152 | 0xE5: (0, "%l0"), 153 | 0xE6: (0, "%g1"), 154 | 0xE7: (0, "%g0"), 155 | 0xE8: (1, "&"), 156 | 0xE9: (1, "s0"), 157 | 0xEA: (1, "s1"), 158 | 0xEB: (0, "\n;"), 159 | 0xF0: (1, "t"), 160 | 0xF1: (2, "t"), 161 | 0xF2: (1, "%v"), 162 | 0xF3: (2, "%v"), 163 | 0xF4: (1, "%x"), 164 | 0xF5: (3, "j"), 165 | 0xF6: (2, "\n;"), 166 | 0xF7: (2, "%b"), 167 | 0xF8: (2, "%f"), 168 | 0xF9: (0, "u1"), 169 | 0xFA: (0, "u0"), 170 | 0xFB: (0, '"'), 171 | 0xFC: (2, ":"), 172 | 0xFD: (1, "{FD}") 173 | } 174 | 175 | equiv_tbl = { #Commands that modify the same data, for state-aware modes (drums) 176 | #Treats k as the same command as v, though # params is not adjusted 177 | "v0,0": "v0", 178 | "p0,0": "p0", 179 | "v0,0,0": "v", 180 | "m0,0,0": "m", 181 | "|0": "@0", 182 | "%a": "%y", 183 | "%s": "%y", 184 | "%r": "%y", 185 | "|": "@0", 186 | "@": "@0", 187 | "o": "o0", 188 | } -------------------------------------------------------------------------------- /experimental_ff5/README.TXT: -------------------------------------------------------------------------------- 1 | experimental MML interpreter for FF5 -- emberling 2 | 3 | - This program will give you two files, a data (up to $1000 bytes, generally) and inst ($20 bytes). 4 | - For basic testing, insert the inst at $43DAA. This will replace the program selections for song id 00 (Main Theme) 5 | - Put the data.bin anywhere there's room, e.g. $200000. Then change a song data pointer to point to this location, e.g. changing $43B97 to 00 00 E0 will load the data at $200000 for id 00 (Main Theme) 6 | 7 | I'll work on better documenting the MML format if there's interest; the core of it is based on the RS3ExTool format documented on ff6hacking.com wiki, but there are a lot of extra features 8 | 9 | Beyond Chaos has a large repository of works in the FF6 version of this format: 10 | https://github.com/emberling/beyondchaos/tree/master/custom/music 11 | SPCs: https://www.dropbox.com/sh/jtjl6g75n8gwipo/AABiN0mZ0JNsK49XiduxqRzha?dl=0 12 | 13 | You can also use mfvi2mml from the root of this repository to attempt to auto convert AKAO4 to MML, in order to extract music from FF6 hacks or (with some hex editing preparation - refer to vgmtrans source for bytecode differences) other AKAO4 games like chrono trigger, radical dreamers, rs2/3, live-a-live, front missions. 14 | 15 | Here's the basics of getting these going in FF5: 16 | 17 | - Obviously there are different samples available, so the sample table needs to be changed. 18 | VGMTrans' SF2 export is helpful for figuring out the existing sample table in an SPC. 19 | Samples available in unmodified FF5: 20 | id block size octave instrument 21 | 01 300 - kick drum 22 | 02 337 - snare drum 23 | 03 500 - hard snare drum 24 | 04 407 - crash cymbal 25 | 05 625 +1 tom 26 | 06 125 - closed hi-hat 27 | 07 500 - open hi-hat 28 | 08 500 +2 timpani 29 | 09 297 +1 glockenspiel 30 | 0A 78 - marimba 31 | 0B 896 - strings 32 | 0C 394 +1 choir 33 | 0D 97 +1 harp 34 | 0E 215 - trumpet 35 | 0F 70 - oboe 36 | 10 165 +1 flute 37 | 11 316 - rock organ 38 | 12 485 +1 like a cross between a honky-tonk and a music box? 39 | 13 54 - bagpipe drone 40 | 14 158 -2? bass guitar 41 | 15 199 -2? low piano 42 | 16 332 +1 music box 43 | 17 500 shout 44 | 18 923 deep synth 45 | 19 646 pad chord A+maj7 46 | 1A 316 dist guitar power chord 47 | 1B 460 clavinet 48 | 1C 629 - horn 49 | 1D 97 mandolin 50 | 1E 217 steel drum 51 | 1F 334 conga 52 | 20 243 shaker 53 | 21 281 wood block 54 | 22 412 guiro 55 | 23 325 clap 56 | 57 | The sample table is built using these directives: 58 | -> #WAVE 0xXX 0xYY 59 | this loads sample id YY into program XX. 60 | AKAO uses programs 0x20 - 0x2F for dynamic samples; all others are either unused or set to fixed samples used by sound effects. 61 | Using all 16 slots seems to crash the game, however, and FF5 seems to have issues if there are gaps. 62 | There are also memory limits; I believe FF5 is a constant 3895 blocks for dynamic samples, though some AKAO games (chrono trigger, secret of mana) do have adjustable limits (this is a factor of echo delay length) 63 | 64 | You may also see something like this: 65 | -> #WAVE 0xXX ~0xYY~ /0xZZ/ 66 | This comes from a method of storing two variant arrangements in one file for different instrument sets. The easiest ways to deal with this are: 67 | - set both YY and ZZ to the desired instrument, or 68 | - use regex search and replace to eliminate everything between two / or two ~ in the entire file. 69 | 70 | - FF5 uses 8-bit numbers for panning, while FF6 uses 7-bit. I've added a directive to scale all pan values in an MML: 71 | -> #scale pan X 72 | 73 | So add -> #scale pan 2 <- to use an FF6 MML in FF5. 74 | 75 | - FF5 and FF6 use different styles of setting echo settings. I've just rendered these as different commands. 76 | FF6: %vXX %b0,YY %f0,Z 77 | FF5: %vXX %zYY,Z 78 | XX - echo volume, YY - echo feedback, Z - echo FIR filter mode (0-3). 79 | values of Z other than 0 can be extremely glitchy, so stick with 0. 80 | 81 | Due to different global SPC volume settings between the games, the same echo settings on FF5 and FF6 won't sound the same - not currently sure of a conversion rate. Experiment or whatever. 82 | 83 | - Tempo clocks aren't the same. FF6 is very close to real BPM, FF5 is a bit off. don't remember offhand in which direction. This can mostly be ignored for ok results 84 | 85 | - There are a good handful of commands available in AKAO4 (FF6, etc) that are not in AKAO3 (FF5). Some just make things sound a little nicer, but others could screw up the whole track. 86 | 87 | Mostly, the &xx command (set length of next note) is not present in FF5, has no equivalents, and is vital to track synchronization. Anything using this has to be rewritten. 88 | Also, the jxx / jxx,yy command (jump on certain number of loop iterations) works differently in AKAO3. I've tried to compensate for this but I haven't really done any testing, and jxx,yy needs manual correction 89 | 90 | Also not present are %l and %g (legato with and without slur). These won't break anything for missing them but some stuff will sound pretty odd. Legato with slur can be faked in FF5 using instant pitch bends (m0,xx) but at the cost of effort and a lot of data bytes. 91 | -------------------------------------------------------------------------------- /hex_tools/mfvidrums.py: -------------------------------------------------------------------------------- 1 | from mfvitbl import notes, lengths, codes 2 | skipone = {"\xC4", "\xC6", "\xCF", "\xD6", "\xD9", "\xDA", "\xDB", "\xDC", "\xDD", "\xDE", "\xDF", "\xE0", 3 | "\xE2", "\xE8", "\xE9", "\xEA", "\xF0", "\xF2", "\xF4", "\xFD", "\xFE"} 4 | skiptwo = {"\xC5", "\xC7", "\xC8", "\xCD", "\xF1", "\xF3", "\xF7", "\xF8"} 5 | skipthree = {"\xC9", "\xCB"} 6 | 7 | print "Drum Mode Unroller" 8 | print 9 | print "NOTE: All drums will be automatically set to octave 5, adjust as necessary." 10 | print "This program will attempt to set the octave back to its previous value" 11 | print "after drum mode ends, but it may get confused, especially if octave" 12 | print "changes during a loop." 13 | print 14 | print "As this is generally to be used with Chrono Trigger imports, 0xFA will be" 15 | print "interpreted as a conditional jump (CT) instead of 'Clear Output Code' (FF6)" 16 | source = raw_input("Enter filename prefix: ") 17 | nextdrum = raw_input("Lowest free instrument slot (default 30): ") 18 | try: 19 | nextdrum = int(nextdrum, 16) 20 | except ValueError: 21 | nextdrum = 0x30 22 | print "using slots beginning from {}".format(hex(nextdrum)) 23 | 24 | def read_word(f): 25 | low, high = 0, 0 26 | low = ord(f.read(1)) 27 | high = ord(f.read(1)) 28 | return low + (high * 0x100) 29 | 30 | def write_word(i, f): 31 | low = i & 0xFF 32 | high = (i & 0xFF00) >> 8 33 | f.write(chr(low)) 34 | f.write(chr(high)) 35 | 36 | def strmod(s, p, c): #overwrites part of a string 37 | while len(s) < p: 38 | s = s + " " 39 | if len(s) == p: 40 | return s[:p] + c 41 | else: 42 | return s[:p] + c + s[p+len(c):] 43 | 44 | def bytes(n): # int-word to two char-bytes 45 | low = n & 0xFF 46 | high = (n & 0xFF00) >> 8 47 | return chr(high) + chr(low) 48 | 49 | data = [] 50 | for index in xrange(1, 17): 51 | try: 52 | with open(source + "%02d" % (index), 'rb') as f: 53 | data.append(f.read()) 54 | except IOError: 55 | pass 56 | print "{} files found".format(len(data)) 57 | 58 | if not len(data): 59 | raw_input("Failed, exiting... ") 60 | 61 | drumdb = {} 62 | 63 | for i, d in enumerate(data): 64 | print "checking segment {}".format(i+1) 65 | loc = 0 66 | drum = False 67 | lastdrum = None 68 | targets = [] 69 | newdata = "" 70 | # first we need to know all the jumps 71 | while loc < len(d): 72 | byte = d[loc] 73 | if byte in {"\xF5", "\xF6", "\xFA", "\xFC"}: 74 | if byte in {"\xF5", "\xFA"}: 75 | loc += 1 76 | targets.append((ord(d[loc+1]) << 8) + ord(d[loc+2])) 77 | loc += 2 78 | elif byte in skipone: 79 | loc += 1 80 | elif byte in skiptwo: 81 | loc += 2 82 | elif byte in skipthree: 83 | loc += 3 84 | loc += 1 85 | # then we can change stuff 86 | loc = 0 87 | octave = None 88 | while loc < len(d): 89 | byte = d[loc] 90 | if byte in {"\xF5", "\xF6", "\xFA", "\xFC"}: 91 | if byte in {"\xF5", "\xFA"}: 92 | newdata += d[loc] 93 | loc += 1 94 | newdata += d[loc:loc+3] 95 | loc += 2 96 | elif byte == "\xFB": 97 | drum = True 98 | lastdrum = None 99 | newdata += "\xD6\x05" 100 | for j, t in enumerate(targets): 101 | if t >> 12 == i and t & 0xFFF > loc: 102 | targets[j] += 1 103 | elif byte == "\xFC": 104 | drum = False 105 | if octave: 106 | newdata += "\xD6" + chr(octave) 107 | n = 1 108 | else: n = -1 109 | for j, t in enumerate(targets): 110 | if t >> 12 == i and t & 0xFFF > loc: 111 | targets[j] += n 112 | elif byte == "\xD6": 113 | octave = ord(d[loc+1]) 114 | newdata += d[loc:loc+2] 115 | loc += 1 116 | elif byte == "\xD7" and octave: 117 | octave += 1 118 | newdata += d[loc] 119 | elif byte == "\xD8" and octave: 120 | octave -= 1 121 | newdata += d[loc] 122 | elif drum and ord(byte) <= 0xA7: 123 | thisdrum = int(ord(byte) / 14) 124 | if thisdrum not in drumdb: 125 | drumdb[thisdrum] = nextdrum 126 | nextdrum += 1 127 | if thisdrum != lastdrum: 128 | lastdrum = thisdrum 129 | newdata += "\xDC" + chr(drumdb[thisdrum]) 130 | for j, t in enumerate(targets): 131 | if t >> 12 == i and t & 0xFFF > loc: 132 | targets[j] += 2 133 | newdata += d[loc] 134 | elif byte in skipone: 135 | newdata += d[loc:loc+2] 136 | loc += 1 137 | elif byte in skiptwo: 138 | newdata += d[loc:loc+3] 139 | loc += 2 140 | elif byte in skipthree: 141 | newdata += d[loc:loc+4] 142 | loc += 3 143 | else: 144 | newdata += d[loc] 145 | loc += 1 146 | # then we can update the jumps 147 | loc = 0 148 | while loc < len(newdata): 149 | byte = newdata[loc] 150 | if byte in {"\xF5", "\xF6", "\xFA", "\xFC"}: 151 | if byte in {"\xF5", "\xFA"}: 152 | loc += 1 153 | newdata = strmod(newdata, loc+1, bytes(targets.pop(0))) 154 | loc += 2 155 | elif byte in skipone: 156 | loc += 1 157 | elif byte in skiptwo: 158 | loc += 2 159 | elif byte in skipthree: 160 | loc += 3 161 | loc += 1 162 | data[i] = newdata 163 | 164 | for i, d in enumerate(data): 165 | try: 166 | with open(source + "d%02d" % (i+1), 'wb') as f: 167 | f.write(d) 168 | except IOError: 169 | print "couldn't write file #{}".format(i) 170 | 171 | raw_input("finished ") -------------------------------------------------------------------------------- /hex_tools/mfvioctave.py: -------------------------------------------------------------------------------- 1 | from mfvitbl import notes, lengths, codes 2 | skipone = {"\xC4", "\xC6", "\xCF", "\xD6", "\xD9", "\xDA", "\xDB", "\xDC", "\xDD", "\xDE", "\xDF", "\xE0", 3 | "\xE2", "\xE8", "\xE9", "\xEA", "\xF0", "\xF2", "\xF4", "\xFD", "\xFE"} 4 | skiptwo = {"\xC5", "\xC7", "\xC8", "\xCD", "\xF1", "\xF3", "\xF7", "\xF8"} 5 | skipthree = {"\xC9", "\xCB"} 6 | 7 | print "Quick and Dirty Octave Adjuster" 8 | print 9 | source = raw_input("Enter filename prefix: ") 10 | print "INSTRUCTIONS: Enter an instrument number (in hex) and one or more + or - symbols" 11 | print "WARNING: May cause havoc with or around loops" 12 | print 13 | print "Basic functionality: program changes TO or FROM the selected instrument will" 14 | print "be bracketed by octave up/down commands. Explicit octave commands while the" 15 | print "instrument is active will be raised or lowered." 16 | print 17 | print "Be careful! This operation is not its own inverse. Running multiple times" 18 | print "will result in a morass of extraneous octave changes." 19 | print 20 | mode = raw_input("Change: ") 21 | 22 | plus = len([c for c in mode if c == '+']) 23 | minus = len([c for c in mode if c == '-']) 24 | mode = "".join([c for c in mode.lower() if c in "1234567890abcdef"]) 25 | try: 26 | inst = int(mode, 16) 27 | assert inst <= 0x30 28 | except: 29 | print "that input didn't work." 30 | raw_input("Exiting.. ") 31 | quit() 32 | 33 | delta = plus - minus 34 | print 35 | if not delta: 36 | print "there's not much point to that, is there?" 37 | raw_input("Exiting.. ") 38 | quit() 39 | if delta > 0: 40 | onstring = "\xD7" * delta 41 | offstring = "\xD8" * delta 42 | print "increasing {} by {}".format(hex(inst), delta) 43 | else: 44 | onstring = "\xD8" * abs(delta) 45 | offstring = "\xD7" * abs(delta) 46 | print "decreasing {} by {}".format(hex(inst), delta) 47 | 48 | print " ".join(map(hex, map(ord, onstring))) 49 | print " ".join(map(hex, map(ord, offstring))) 50 | 51 | 52 | def read_word(f): 53 | low, high = 0, 0 54 | low = ord(f.read(1)) 55 | high = ord(f.read(1)) 56 | return low + (high * 0x100) 57 | 58 | def write_word(i, f): 59 | low = i & 0xFF 60 | high = (i & 0xFF00) >> 8 61 | f.write(chr(low)) 62 | f.write(chr(high)) 63 | 64 | def strmod(s, p, c): #overwrites part of a string 65 | while len(s) < p: 66 | s = s + " " 67 | if len(s) == p: 68 | return s[:p] + c 69 | else: 70 | return s[:p] + c + s[p+len(c):] 71 | 72 | def bytes(n): # int-word to two char-bytes 73 | low = n & 0xFF 74 | high = (n & 0xFF00) >> 8 75 | return chr(high) + chr(low) 76 | 77 | data = [] 78 | for index in xrange(1, 17): 79 | try: 80 | with open(source + "%02d" % (index), 'rb') as f: 81 | data.append(f.read()) 82 | except IOError: 83 | pass 84 | print "{} files found".format(len(data)) 85 | 86 | if not len(data): 87 | raw_input("Failed, exiting... ") 88 | 89 | drumdb = {} 90 | 91 | for i, d in enumerate(data): 92 | print "checking segment {}".format(i+1) 93 | loc = 0 94 | drum = False 95 | lastdrum = None 96 | targets = [] 97 | newdata = "" 98 | # first we need to know all the jumps 99 | while loc < len(d): 100 | byte = d[loc] 101 | if byte in {"\xF5", "\xF6", "\xFC"}: 102 | if byte in {"\xF5", "\xFA"}: 103 | loc += 1 104 | targets.append((ord(d[loc+1]) << 8) + ord(d[loc+2])) 105 | loc += 2 106 | elif byte in skipone: 107 | loc += 1 108 | elif byte in skiptwo: 109 | loc += 2 110 | elif byte in skipthree: 111 | loc += 3 112 | loc += 1 113 | # then we can change stuff 114 | loc = 0 115 | active = False 116 | while loc < len(d): 117 | byte = d[loc] 118 | if byte in {"\xF5", "\xF6", "\xFC"}: 119 | if byte in {"\xF5", "\xFA"}: 120 | newdata += d[loc] 121 | loc += 1 122 | elif active: 123 | active = False 124 | newdata += offstring 125 | newdata += d[loc:loc+3] 126 | loc += 2 127 | elif byte == "\xDC": 128 | program = ord(d[loc+1]) 129 | if active and program != inst: 130 | active = False 131 | newdata += offstring + d[loc:loc+2] 132 | for j, t in enumerate(targets): 133 | if t >> 12 == i and t & 0xFFF > loc: 134 | targets[j] += len(offstring) 135 | elif not active and program == inst: 136 | active = True 137 | newdata += onstring + d[loc:loc+2] 138 | for j, t in enumerate(targets): 139 | if t >> 12 == i and t & 0xFFF > loc: 140 | targets[j] += len(onstring) 141 | else: 142 | newdata += d[loc:loc+2] 143 | loc += 1 144 | elif byte == "\xD6" and active: 145 | newoct = ord(d[loc+1]) + delta 146 | while newoct < 0: newoct += 0xFF 147 | newdata += d[loc] + chr(newoct) 148 | loc += 1 149 | elif byte in skipone: 150 | newdata += d[loc:loc+2] 151 | loc += 1 152 | elif byte in skiptwo: 153 | newdata += d[loc:loc+3] 154 | loc += 2 155 | elif byte in skipthree: 156 | newdata += d[loc:loc+4] 157 | loc += 3 158 | else: 159 | newdata += d[loc] 160 | loc += 1 161 | # then we can update the jumps 162 | loc = 0 163 | while loc < len(newdata): 164 | byte = newdata[loc] 165 | if byte in {"\xF5", "\xF6", "\xFC"}: 166 | if byte in {"\xF5", "\xFC"}: 167 | loc += 1 168 | newdata = strmod(newdata, loc+1, bytes(targets.pop(0))) 169 | loc += 2 170 | elif byte in skipone: 171 | loc += 1 172 | elif byte in skiptwo: 173 | loc += 2 174 | elif byte in skipthree: 175 | loc += 3 176 | loc += 1 177 | data[i] = newdata 178 | 179 | for i, d in enumerate(data): 180 | try: 181 | with open(source + "o%02d" % (i+1), 'wb') as f: 182 | f.write(d) 183 | except IOError: 184 | print "couldn't write file #{}".format(i) 185 | 186 | raw_input("finished ") -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mfvitools 2 | some things i wrote to make ff6 music importing/editing easier 3 | * [windows binaries](https://github.com/emberling/mfvitools/releases) 4 | 5 | Currently these are focused on manipulating song data through the MML (Music Macro Language) text format. Legacy hex based tools are provided in a subfolder, but these have little to no utility anymore. 6 | 7 | ## MFVI2MML 8 | 9 | Converts binary FF6 format music sequence data into mfvitools MML format. The sequence will be represented as accurately as possible -- every byte translated into its equivalent. This MML format is designed to represent any valid FF6 sequence without loss, except for some extreme edge cases involving pointers that point to the middle of a command. If you have a _data.bin and _inst.bin file, it will import from both. 10 | 11 | ## MML2MFVI 12 | 13 | Converts mfvitools MML format into binary FF6 format. Outputs a data file with 38-byte header (ready to insert into ROM) and a 32-byte inst file. 14 | 15 | Format is based on rs3extool MML but extended and altered. Documentation [on github wiki](https://github.com/emberling/mfvitools/wiki/). 16 | 17 | ## INSERTMFVI 18 | 19 | General music insertion tool for FF6. Supports raw data and MML import for sequences. Can also import custom BRR samples defined either in imported MMLs or in independent sample list files. Handles song, sample, and ROM expansion automatically. More detailed documentation [here](https://github.com/emberling/mfvitools/wiki/insertmfvi) 20 | 21 | ## BUILD_SPC 22 | 23 | Extracts a music sequence within an FF6 ROM into an independently playable SPC file without need to actually launch the game. Experimental. 24 | 25 | ## SQSPCMML 26 | 27 | Converts binary music sequence data from various Square SPC sequence formats into mfvitools MML format (i.e., into FF6 format). This tool does not prioritize wholly accurate representation; instead, its design is focused on convenience and utility. Program, volume, and octave commands are replaced with macros, allowing these to be tweaked globally. Features not supported by FF6 are either converted to a close equivalent or rendered as comments. Samples may optionally also be ripped. 28 | 29 | Supports: 30 | * Bahamut Lagoon (SUZUKI) 31 | * Chrono Trigger (AKAO4) 32 | * Final Fantasy IV (AKAO1) 33 | * Final Fantasy V (AKAO3) 34 | * Final Fantasy VI (AKAO4) 35 | * Final Fantasy Mystic Quest (AKAO3) 36 | * Front Mission (AKAO4) 37 | * Front Mission: Gun Hazard (AKAO4) 38 | * Live A Live (AKAO4) 39 | * Romancing SaGa (AKAO2) 40 | * Romancing SaGa 2 (AKAO4) 41 | * Romancing SaGa 3 (AKAO4) 42 | * Secret of Mana / Seiken Densetsu 2 (AKAO3) 43 | * Trials of Mana / Seiken Densetsu 3 (SUZUKI) 44 | * Super Mario RPG: Legend of the Seven Stars (SUZUKI) 45 | * Treasure of the Rudras / Rudra no Hihou 46 | * Bandai Satellaview games by Square (AKAO4): 47 | * Treasure Conflix 48 | * DynamiTracer 49 | * Radical Dreamers 50 | * Koi ha Balance 51 | 52 | ## BRR2SF2 53 | 54 | Converts multiple BRR files into a SoundFont2 file for auditioning and testing. Designed to imitate some of BRR's quirks somewhat better than the raw waveform output other programs provide. Semi-accurately imitates SPC700 ADSR envelopes and allows for non-identical loop iterations as occur in some BRRs. All samples receive a vibrato delay of 1 quarter note at 120bpm. Minimum and maximum ranges are defined for each sample based on SPC and engine limits, so you can see which notes are playable and which are not. 55 | 56 | Input format is the same as `insertmfvi` sample listfiles, with some additions (which `insertmfvi` will happily ignore): After the ADSR value, put another comma, and the subsequent text will be processed by `brr2sf2`. From here, any text in `{`braces`}` will be ignored; by convention, the size of the sample in blocks is placed in braces here first, though it has no effect on the resulting sf2 file. Any text in `[`brackets`]` will be stripped out and used as an inverse transpose value. In other words: if a sample with the given tuning sounds one octave higher than the actual note played, you may represent this in the listfile as `[+12]`. The sample in the resulting soundfont will then be transposed one octave *down*, so that the note you play now matches the note you hear. After all text in brackets and braces is trimmed out, the remaining text, if any (with leading and trailing whitespace removed), is used as the sample's display name. 57 | 58 | On the command line, append `sort` if you want the soundfont to be sorted by size within each bank. Append `id` to add the hex ID of the instrument to its display name. 59 | 60 | ## SPC2BRRS 61 | 62 | Extracts multiple BRR files from an arbitrary SPC file. Should work on more or less any game. Sets up an `insertmfvi` listfile for the samples, with accurate loop points, which can be fed immediately into `brr2sf2`. DOES NOT attempt to tune the samples or apply ADSR other than default; you may wish to tune manually by editing the listfile, re-running `brr2sf2`, and repeating until successful. 63 | 64 | ## SXC2MML (experimental folder) 65 | 66 | Converts Neverland SFC/S2C format to mfvitools MML format. The conversion is rudimentary and not designed to directly create good-sounding or listenable output. The intended purpose is to use this output - both the MML file itself and a MIDI conversion of it via [vgmtrans](https://github.com/vgmtrans/vgmtrans) - as a reference for manually building a rendition of the song. 67 | 68 | There may currently be some issues where notes retrigger where they should hold, or hold where they should retrigger. 69 | 70 | Percussion isn't simulated (no samples are set), but seems to follow General MIDI key layout, so it should work once converted to MIDI. 71 | 72 | ## "How do I get these data files into my ROM?" 73 | 74 | * INSERTMFVI should cover most use cases. 75 | * The [Beyond Chaos EX](https://github.com/subtractionsoup/beyondchaos/releases) randomizer and its cosmetic-only little sister [nascentorder](https://github.com/emberling/nascentorder) use the mfvitools MML parser for their randomized music mode. 76 | * [This tutorial](https://www.ff6hacking.com/forums/thread-2584.html) covers inserting data and inst files into FF6 with a hex editor. 77 | * The archive for [this tutorial](https://www.ff6hacking.com/forums/thread-3922.html), on translating music from binary FF6 hacks to Beyond Chaos mfvitools format MML files, contains a small python script that automatically inserts songs into a ROM with a fixed location and ID. It's not configurable enough for serious hacking, but it's convenient for testing. 78 | 79 | ## contact info 80 | * [FF6hacking Discord](https://discord.gg/FFAHavK) 81 | * [Twitter](https://twitter.com/jen_imago) 82 | -------------------------------------------------------------------------------- /hex_tools/mfviclean.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | from sys import argv 3 | from shutil import copyfile 4 | 5 | args = list(argv) 6 | if len(args) > 2: 7 | sourcefile = args[1].strip() 8 | else: 9 | sourcefile = raw_input("Please input the file name of your music data " 10 | "ROM dump:\n> ").strip() 11 | 12 | try: 13 | f = open(sourcefile, 'rb') 14 | data = f.read() 15 | f.close() 16 | except IOError: 17 | raw_input("Couldn't open input file. Press enter to quit. ") 18 | quit() 19 | 20 | outfile = sourcefile + "_data.bin" 21 | copyfile(sourcefile, outfile) 22 | 23 | try: 24 | fout = open(outfile, "r+b") 25 | except IOError: 26 | raw_input("Couldn't open output file. Press enter to quit. ") 27 | quit() 28 | 29 | def read_word(f): 30 | low, high = 0, 0 31 | try: 32 | low = ord(f.read(1)) 33 | high = ord(f.read(1)) 34 | except: 35 | print "warning: read_word failed, probably unexpected end of file" 36 | return low + (high * 0x100) 37 | 38 | def write_word(i): 39 | global fout 40 | low = i & 0xFF 41 | high = (i & 0xFF00) >> 8 42 | fout.write(chr(low)) 43 | fout.write(chr(high)) 44 | 45 | fout.seek(0,2) 46 | size = fout.tell() 47 | assert size <= 0xFFFF 48 | 49 | ichannels, ochannels = [], [] 50 | fout.seek(0x06) 51 | for c in xrange(0,16): 52 | ichannels.append(read_word(fout)) 53 | 54 | shift = ichannels[0] - 0x26 55 | for c in ichannels: 56 | ochannels.append((c if c >= shift else c + 0x10000) - shift) 57 | 58 | fout.seek(0x06) 59 | for c in ochannels: 60 | write_word(c) 61 | 62 | 63 | maxvalue = {"\xCF":0x1F, "\xD6":0x07, "\xDD":0x15, "\xDE":0x07, "\xDF":0x07, "\xE0":0x31} 64 | skipone = {"\xC4", "\xC6", "\xCF", "\xD6", "\xD9", "\xDA", "\xDB", "\xDC", "\xDD", "\xDE", "\xDF", "\xE0", 65 | "\xE2", "\xE8", "\xE9", "\xEA", "\xF0", "\xF2", "\xF4"} 66 | skiptwo = {"\xC5", "\xC7", "\xC8", "\xCD", "\xF1", "\xF3", "\xF7", "\xF8"} 67 | skipthree = {"\xC9", "\xCB"} 68 | purgeone = {"\xFD", "\xFE"} 69 | purgetwo = set() 70 | instruments = set() 71 | scaleparms = {} 72 | 73 | #traverse and gather information 74 | pos = 0x26 75 | fout.seek(pos) 76 | while pos < size: 77 | byte = fout.read(1) 78 | if not byte: break 79 | 80 | if byte == "\xDC": 81 | pos = fout.tell() 82 | instruments.add(ord(fout.read(1))) 83 | fout.seek(pos) 84 | elif byte in {"\xC4", "\xC6"}: #one parameter, max 7F 85 | parm = ord(fout.read(1)) 86 | if parm > 0x7F and byte not in scaleparms: 87 | print "at {} found command {} {} outside expected maximum 0x7f. scaling enabled".format(hex(pos), hex(ord(byte)), hex(parm)) 88 | scaleparms[byte] = 1 89 | elif byte in {"\xC5", "\xC7"}: #two parameters, second has max 7F 90 | parm = ord(fout.read(2)[1]) 91 | if parm > 0x7F and byte not in scaleparms: 92 | print "at {} found command {} nn {} outside expected maximum 0x7f. scaling enabled".format(hex(pos), hex(ord(byte)), hex(parm)) 93 | scaleparms[byte] = 2 94 | elif byte in maxvalue: 95 | parm = ord(fout.read(1)) 96 | if parm > maxvalue[byte]: 97 | print "WARNING: at {} command {} {} outside expected maximum {}".format(hex(pos), hex(ord(byte)), hex(parm), hex(maxvalue[byte])) 98 | elif byte in skipone: fout.seek(1,1) 99 | elif byte in skiptwo or byte in set("\xF6"): fout.seek(2,1) 100 | elif byte in skipthree or byte == "\xF5": fout.seek(3,1) 101 | pos = fout.tell() 102 | 103 | #traverse and edit 104 | pos = 0x26 105 | fout.seek(pos) 106 | while pos < size: 107 | byte = fout.read(1) 108 | if not byte: break 109 | 110 | if byte in purgetwo: 111 | pos = fout.tell() 112 | #print "at {} purging {} {} {}".format(hex(pos), hex(ord(byte)), hex(ord(fout.read(1))), hex(ord(fout.read(1)))) 113 | fout.seek(pos-1) 114 | #fout.write("\xCC"*3) 115 | fout.seek(pos+2) 116 | print "WARNING: at {} found bad {} {} {}".format(hex(pos), hex(ord(byte)), hex(ord(fout.read(1))), hex(ord(fout.read(1)))) 117 | 118 | elif byte in purgeone: 119 | pos = fout.tell() 120 | #print "at {} purging {} {}".format(hex(pos), hex(ord(byte)), hex(ord(fout.read(1)))) 121 | print "WARNING: at {} found bad {} {}".format(hex(pos), hex(ord(byte)), hex(ord(fout.read(1)))) 122 | fout.seek(pos-1) 123 | #fout.write("\xCC"*2) 124 | fout.seek(pos+1) 125 | elif byte in scaleparms: 126 | pos = fout.tell() 127 | fout.seek(pos + scaleparms[byte] - 1) 128 | parm = ord(fout.read(1)) 129 | fout.seek(pos + scaleparms[byte] - 1) 130 | print "at {} scaling {} {}".format(hex(pos), hex(ord(byte)), hex(parm)) 131 | fout.write(chr(parm/2)) 132 | elif byte in skipthree: 133 | fout.seek(3,1) 134 | elif byte in skiptwo: 135 | fout.seek(2,1) 136 | elif byte in skipone: 137 | fout.seek(1,1) 138 | elif byte in ["\xF5", "\xFA"]: 139 | fout.seek(1,1) 140 | pos = fout.tell() 141 | dest = read_word(fout) 142 | ndest = (dest if dest >= shift else dest + 0x10000) - shift 143 | if dest != ndest: print "at {} shifting {} nn {} to {}".format(hex(pos-1), hex(ord(byte)), hex(dest), hex(ndest)) 144 | if ndest > size: print "WARNING: at {} jump destination {} not in file".format(hex(pos-1), hex(ndest)) 145 | fout.seek(pos) 146 | write_word(ndest) 147 | fout.seek(pos+2) 148 | elif byte in ["\xF6", "\xFC"]: 149 | pos = fout.tell() 150 | dest = read_word(fout) 151 | ndest = (dest if dest >= shift else dest + 0x10000) - shift 152 | if dest != ndest: print "at {} shifting {} {} to {}".format(hex(pos-1), hex(ord(byte)), hex(dest), hex(ndest)) 153 | if ndest > size: print "WARNING: at {} jump destination {} not in file".format(hex(pos-1), hex(ndest)) 154 | fout.seek(pos) 155 | write_word(ndest) 156 | fout.seek(pos+2) 157 | pos = fout.tell() 158 | 159 | fout.seek(0,2) 160 | size = fout.tell() 161 | assert size <= 0xFFFF 162 | 163 | fout.seek(0) 164 | write_word(size-2) 165 | write_word(0x26) 166 | write_word(size) 167 | fout.close() 168 | 169 | print "Found use of instruments {}".format(map(hex, sorted(instruments))) 170 | print 171 | 172 | 173 | try: 174 | iout = open(sourcefile + "_inst.bin", "wb") 175 | iout.write("\x00" * 0x20) 176 | for i in instruments: 177 | if i < 0x20: 178 | continue 179 | iout.seek((i - 0x20) * 2) 180 | iout.write("\xFF") 181 | 182 | iout.close() 183 | except: 184 | print "warning: couldn't write instfile. this is probably fine" 185 | print 186 | 187 | raw_input("Press enter to quit. ") 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | -------------------------------------------------------------------------------- /mmltbl.py: -------------------------------------------------------------------------------- 1 | # MMLTBL - some common tables shared by multiple MML tools 2 | 3 | # *** READ THIS BEFORE EDITING THIS FILE *** 4 | 5 | # This file is part of the mfvitools project. 6 | # ( https://github.com/emberling/mfvitools ) 7 | # mfvitools is designed to be used inside larger projects, e.g. 8 | # johnnydmad, Beyond Chaos, Beyond Chaos Gaiden, or potentially 9 | # others in the future. 10 | # If you are editing this file as part of "johnnydmad," "Beyond Chaos," 11 | # or any other container project, please respect the independence 12 | # of these projects: 13 | # - Keep mfvitools project files in a subdirectory, and do not modify 14 | # the directory structure or mix in arbitrary code files specific to 15 | # your project. 16 | # - Keep changes to mfvitools files in this repository to a minimum. 17 | # Don't make style changes to code based on the standards of your 18 | # containing project. Don't remove functionality that you feel your 19 | # containing project won't need. Keep it simple so that code and 20 | # changes can be easily shared across projects. 21 | # - Major changes and improvements should be handled through, or at 22 | # minimum shared with, the mfvitools project, whether through 23 | # submitting changes or through creating a fork that other mfvitools 24 | # maintainers can easily see and pull from. 25 | 26 | note_tbl = { 27 | "c": 0x0, 28 | "d": 0x2, 29 | "e": 0x4, 30 | "f": 0x5, 31 | "g": 0x7, 32 | "a": 0x9, 33 | "b": 0xB, 34 | "^": 0xC, 35 | "r": 0xD } 36 | 37 | length_tbl = { 38 | 1 : (0, 0xC0), 39 | 2 : (1, 0x60), 40 | 3 : (2, 0x40), 41 | "4.": (3, 0x48), 42 | 4 : (4, 0x30), 43 | 6 : (5, 0x20), 44 | "8.": (6, 0x24), 45 | 8 : (7, 0x18), 46 | 12 : (8, 0x10), 47 | 16 : (9, 0x0C), 48 | 24 : (10, 0x08), 49 | 32 : (11, 0x06), 50 | 48 : (12, 0x04), 51 | 64 : (13, 0x03) } 52 | 53 | r_length_tbl = { 54 | 0: "1", 55 | 1: "2", 56 | 2: "3", 57 | 3: "4.", 58 | 4: "4", 59 | 5: "6", 60 | 6: "8.", 61 | 7: "8", 62 | 8: "12", 63 | 9: "", #default 64 | 10: "24", 65 | 11: "32", 66 | 12: "48", 67 | 13: "64" } 68 | 69 | command_tbl = { 70 | ("@", 1) : 0xDC, #program 71 | ("|", 1) : 0xDC, #program (hex param) 72 | ("%a", 0): 0xE1, #reset ADSR 73 | ("%a", 1): 0xDD, #set attack 74 | ("%b", 1): 0xF7, #echo feedback (rs3) 75 | ("%b", 2): 0xF7, #echo feedback (ff6) 76 | ("%c", 1): 0xCF, #noise clock 77 | ("%d0", 0): 0xFC,#drum mode off (rs3) 78 | ("%d1", 0): 0xFB,#drum mode on (rs3) 79 | ("%e0", 0): 0xD5,#disable echo 80 | ("%e1", 0): 0xD4,#enable echo 81 | ("%f", 1): 0xF8, #filter (rs3) 82 | ("%f", 2): 0xF8, #filter (ff6) 83 | ("%g0", 0): 0xE7,#disable roll (enable gaps between notes) 84 | ("%g1", 0): 0xE6,#enable roll (disable gaps between notes) 85 | ("%i", 0): 0xFB, #ignore master volume (ff6) 86 | #("%j", 1): 0xF6 - jump to marker, segment continues 87 | ("%k", 1): 0xD9, #set transpose 88 | ("%l0", 0): 0xE5,#disable legato 89 | ("%l1", 0): 0xE4,#enable legato 90 | ("%n0", 0): 0xD1,#disable noise 91 | ("%n1", 0): 0xD0,#enable noise 92 | ("%p0", 0): 0xD3,#disable pitch mod 93 | ("%p1", 0): 0xD2,#enable pitch mod 94 | ("%r", 0): 0xE1, #reset ADSR 95 | ("%r", 1): 0xE0, #set release 96 | ("%s", 0): 0xE1, #reset ADSR 97 | ("%s", 1): 0xDF, #set sustain 98 | ("%v", 1): 0xF2, #set echo volume 99 | ("%v", 2): 0xF3, #echo volume envelope 100 | ("%x", 1): 0xF4, #set master volume 101 | ("%y", 0): 0xE1, #reset ADSR 102 | ("%y", 1): 0xDE, #set decay 103 | #("j", 1): 0xF5 - jump out of loop after n iterations 104 | #("j", 2): 0xF5 - jump to marker after n iterations 105 | ("k", 1): 0xDB, #set detune 106 | ("m", 0): 0xCA, #disable vibrato 107 | ("m", 1): 0xDA, #add to transpose 108 | ("m", 2): 0xC8, #pitch envelope (portamento) 109 | ("m", 3): 0xC9, #enable vibrato 110 | ("o", 1): 0xD6, #set octave 111 | ("p", 0): 0xCE, #disable pan sweep 112 | ("p", 1): 0xC6, #set pan 113 | ("p", 2): 0xC7, #pan envelope 114 | ("p", 3): 0xCD, #pansweep 115 | ("s0", 1): 0xE9, #play sound effect with voice A 116 | ("s1", 1): 0xEA, #play sound effect with voice B 117 | ("t", 1): 0xF0, #set tempo 118 | ("t", 2): 0xF1, #tempo envelope 119 | ("u0", 0): 0xFA, #clear output code 120 | ("u1", 0): 0xF9, #increment output code 121 | ("v", 0): 0xCC, #disable tremolo 122 | ("v", 1): 0xC4, #set volume 123 | ("v", 2): 0xC5, #volume envelope 124 | ("v", 3): 0xCB, #set tremolo 125 | ("&", 1): 0xE8, #add to note duration 126 | ("<", 0): 0xD7, #increment octave 127 | (">", 0): 0xD8, #decrement octave 128 | ("[", 0): 0xE2, #start loop 129 | ("[", 1): 0xE2, #start loop 130 | ("]", 0): 0xE3 #end loop 131 | #(":", 1): 0xFC - jump to marker if event signal is sent 132 | #(";", 1): 0xF6 - jump to marker, end segment 133 | } 134 | 135 | byte_tbl = { 136 | 0xC4: (1, "v"), 137 | 0xC5: (2, "v"), 138 | 0xC6: (1, "p"), 139 | 0xC7: (2, "p"), 140 | 0xC8: (2, "m"), 141 | 0xC9: (3, "m"), 142 | 0xCA: (0, "m"), 143 | 0xCB: (3, "v"), 144 | 0xCC: (0, "v"), 145 | 0xCD: (2, "p0,"), 146 | 0xCE: (0, "p"), 147 | 0xCF: (1, "%c"), 148 | 0xD0: (0, "%n1"), 149 | 0xD1: (0, "%n0"), 150 | 0xD2: (0, "%p1"), 151 | 0xD3: (0, "%p0"), 152 | 0xD4: (0, "%e1"), 153 | 0xD5: (0, "%e0"), 154 | 0xD6: (1, "o"), 155 | 0xD7: (0, "<"), 156 | 0xD8: (0, ">"), 157 | 0xD9: (1, "%k"), 158 | 0xDA: (1, "m"), 159 | 0xDB: (1, "k"), 160 | 0xDC: (1, "@"), 161 | 0xDD: (1, "%a"), 162 | 0xDE: (1, "%y"), 163 | 0xDF: (1, "%s"), 164 | 0xE0: (1, "%r"), 165 | 0xE1: (0, "%y"), 166 | 0xE2: (1, "["), 167 | 0xE3: (0, "]"), 168 | 0xE4: (0, "%l1"), 169 | 0xE5: (0, "%l0"), 170 | 0xE6: (0, "%g1"), 171 | 0xE7: (0, "%g0"), 172 | 0xE8: (1, "&"), 173 | 0xE9: (1, "s0"), 174 | 0xEA: (1, "s1"), 175 | 0xEB: (0, "\n;"), 176 | 0xF0: (1, "t"), 177 | 0xF1: (2, "t"), 178 | 0xF2: (1, "%v"), 179 | 0xF3: (2, "%v"), 180 | 0xF4: (1, "%x"), 181 | 0xF5: (3, "j"), 182 | 0xF6: (2, "\n;"), 183 | 0xF7: (2, "%b"), 184 | 0xF8: (2, "%f"), 185 | 0xF9: (0, "u1"), 186 | 0xFA: (0, "u0"), 187 | 0xFB: (0, '%i'), 188 | 0xFC: (2, ":"), 189 | 0xFD: (1, "{FD}") 190 | } 191 | 192 | equiv_tbl = { #Commands that modify the same data, for state-aware modes (drums) 193 | #Treats k as the same command as v, though # params is not adjusted 194 | "v0,0": "v0", 195 | "p0,0": "p0", 196 | "v0,0,0": "v", 197 | "m0,0,0": "m", 198 | "|0": "@0", 199 | "%a": "%y", 200 | "%s": "%y", 201 | "%r": "%y", 202 | "|": "@0", 203 | "@": "@0", 204 | "o": "o0", 205 | } -------------------------------------------------------------------------------- /hex_tools/README: -------------------------------------------------------------------------------- 1 | mfvitools - some things i wrote to make ff6 music importing/editing easier 2 | r3b.testing emberling 3 | 4 | this is sort of thrown together and might crash or break your stuff 5 | and will definitely have a bunch of confusing irrelevant outputs 6 | 7 | CLEAN -- prepares an import so that it works better with the quirks of ff6's 8 | version of the sound engine specifically 9 | 10 | if volumes or pans seem to be on a 0~~FF scale, it halves them all 11 | to match ff6's 0~~7F scale 12 | 13 | adjusts pointers to be relative (so 00 00 is the file beginning and 14 | 26 00 is the end of the header) 15 | 16 | replaces a few guaranteed crash commands (FE, FD) with harmless CC's 17 | NOTE: this assumes that FE and FD use one parameter 18 | I have sometimes seen them use apparently zero or two 19 | Watch the output for "purging" FE or FD and if it happens, 20 | make sure to manually check against the original file to be sure 21 | it hasn't guessed wrong. 22 | 23 | 24 | SPLIT -- this does two things: divides the data file into separate files 25 | that generally map to channels (split after F6 jumps) 26 | and produces a visual representation of what those channels are 27 | doing and how they match up sequentially with others. 28 | 29 | All pointers are translated into a new format where the first 30 | digit is a reference to the data file and the other three digits 31 | are a location within that data file (in regular, human readable 32 | order). 33 | 34 | So if your main data file begins with this, after the header: 35 | DC 00 D6 05 B6 B6 00 F6 2E 00 DC 01 D6 04 B6 01 1D F6 34 00 36 | it will produce a filename.01 with: 37 | DC 00 D6 05 B6 B6 00 F6 00 05 38 | and a filename.02 with: 39 | DC 01 D6 04 B6 01 1D F6 10 04 40 | 41 | if there are more than $FFF bytes between F6's or more than 16 F6's 42 | ... i don't really know what happens. maybe it will mess up 43 | the pointers, maybe it will just crash! shouldn't come up often 44 | 45 | MERGE -- takes those individual files made by SPLIT and mashes them back 46 | together, converting the pointers back to the proper format 47 | for import 48 | 49 | 50 | new in r2: 51 | DRUMS -- a Chrono Trigger import tool, mainly, though it may also be useful 52 | for RS3, etc. Used after SPLIT. Detects use of "drum mode" and replaces 53 | it with the more standard program change calls FF6 can understand. 54 | Assigns each new drum it encounters to the next unused program. 55 | Sets octave to 5 when drum mode is enabled, and tries to set it back 56 | to where it was after it's disabled, though this algorithm is quite 57 | basic and capable of getting confused on e.g. loops that increment/ 58 | decrement octave each iteration. Does not adjust volume or pan, but 59 | changing those manually becomes easier since you now have a DC xx 60 | call to search for. 61 | 62 | OCTAVE -- This is just a minor edit to the Drums code that uses the same method 63 | to affect a different sort of area .. specifically, it looks for areas 64 | using a specified instrument/program and tries to blanket shift them 65 | up or down one or more octaves. It uses only the most basic processes: 66 | octave change commands are added, but never removed. As such, this 67 | script should not be thought of as a tool that performs a portion of 68 | the editing for you, but as a tool that allows quick and hassle-free 69 | octave shifting for the purposes of prototyping instrument selections. 70 | It can be very convenient for instruments used in multiple channels, 71 | or for instruments used in channels that switch often between several 72 | instruments. But it CAN mess up your sequence and/or your looping, so 73 | apply with caution, or with the intent to throw out the generated 74 | sequence and rebuild it by hand once you've used the generated version 75 | to decide which octaves you want to use. 76 | 77 | new in r3: 78 | MML2MFVI -- extended implementation of rs3extool2-compatible MML 79 | this SHOULD be feature-complete for FF6's sound engine; no known 80 | command is unimplemented. 81 | new commands: 82 | {X} -- all text in braces is ignored but any number 1-16 83 | will point the channel header to that location. 84 | Channels 9-16 will duplicate 1-8 unless otherwise set 85 | Unset channels point to EOF 86 | |X -- shortcut for @0x2X. program change, in hex. 87 | %aX -- set attack 88 | %yX -- set decay 89 | %rX -- set release 90 | %sX -- set sustain 91 | %a / %y / %r / %s -- reset ADSR to default 92 | %bX,Y -- set echo feedback (with proper # of parameters) 93 | for backwards compatibility, %bX works as before. 94 | %fX,Y -- set filter (with proper # of parameters) 95 | likewise, %fX works as before. 96 | %g0 / %g1 -- disable/enable drum roll 97 | mX -- add to transpose 98 | s0,X / s1,X -- play sfx X with voice a / b 99 | u0 / u1 -- clear output code / increment output code (?????) 100 | &X -- add to note duration 101 | $X -- jump marker with id 102 | this is independent from $ and will not work with ; 103 | these ids are not specific to any channel/track 104 | jX -- after X iterations, jump to after next ] 105 | jX,Y -- after X iterations, jump to $Y (in any track) 106 | If there is more than one $Y, the one that will be used is 107 | the most recent one set. If none are set, the last one 108 | in the file is used. (Don't duplicate jump markers tho) 109 | :X -- conditional jump (FC) to $X. 110 | ;X -- hard jump (F6) to $X, for e.g. doubling channels 111 | 112 | MFVI2MML -- convert FF6 music binaries into mfvitools-format MML. 113 | If everything is working properly, this should be a 114 | completely lossless conversion, with the exception of: 115 | -unknown bytes 116 | -jumps that go outside the song data, or into the middle 117 | of a command. 118 | 119 | USE EXAMPLE: 120 | Say we want to import a song from Mystic Quest. 121 | It can be converted to approximately the right format by FF5~FF6MCS2 122 | but it will be messed up, so we need to fix it. 123 | 124 | The first rule of Mystic Quest import is the pointers are all lies. 125 | Just ignore them entirely. 126 | First run clean, which will fix the pan. 127 | Then just split it, and overwrite the 00 file with this: 128 | 00 00 10 00 20 00 30 00 40 00 50 00 60 00 70 00 129 | 00 00 10 00 20 00 30 00 40 00 50 00 60 00 70 00 130 | This sets all the channels to begin at the beginning of each split 131 | file. 132 | 133 | Open the .txt and listen to the original spc to figure out where 134 | the correct loop point is. Most of the time this will be a single 135 | vertical line across all channels in the .txt 136 | Figure out where in the file this line represents (check the 137 | measure line above the notes for a hint) and set the final F6 138 | to point to it. The first digit after F6 should be one less than 139 | the filename's number, unless something fancy is going on. 140 | 141 | If there are any F5's, point them to just after the next D3. 142 | If this doesn't work, there might be a second D3 afterward 143 | that needs to be deleted (MQ seems fine with ending a loop 144 | that's already ended, but FF6 is not.) 145 | 146 | Once this is done run merge and copy the new data file into the rom. 147 | It should loop properly. If any adjustments to octave or dynamics 148 | are needed then editing split will help greatly since only one or 149 | two pointers max, usually none, will need to be adjusted as the 150 | filesize changes. -------------------------------------------------------------------------------- /mfvi2mml_py2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | import sys, traceback 3 | import mfvitbl 4 | from mmltbl import * 5 | 6 | jump_bytes = [0xF5, 0xF6, 0xFC] 7 | 8 | def byte_insert(data, position, newdata, maxlength=0, end=0): 9 | while position > len(data): 10 | data = data + "\x00" 11 | if end: 12 | maxlength = end - position + 1 13 | if maxlength and len(data) > maxlength: 14 | newdata = newdata[:maxlength] 15 | return data[:position] + newdata + data[position+len(newdata):] 16 | 17 | 18 | def int_insert(data, position, newdata, length, reversed=True): 19 | n = int(newdata) 20 | l = [] 21 | while len(l) < length: 22 | l.append(chr(n & 0xFF)) 23 | n = n >> 8 24 | if not reversed: l.reverse() 25 | return byte_insert(data, position, "".join(l), length) 26 | 27 | def warn(fileid, cmd, msg): 28 | print "{}: WARNING: in {:<10}: {}".format(fileid, cmd, msg) 29 | 30 | def clean_end(): 31 | print "Processing ended." 32 | raw_input("Press enter to close.") 33 | quit() 34 | 35 | def akao_to_mml(data, inst=None, fileid='akao'): 36 | 37 | def unskew(base): 38 | base -= unskew.addr 39 | if base < 0: base += 0x10000 40 | return base + 0x26 41 | 42 | mml = ["# Converted from binary by mfvitools", ""] 43 | 44 | ## process inst 45 | if inst is not None: 46 | for slot in xrange(0,0x10): 47 | if len(inst) <= slot*2: break 48 | byte = inst[slot*2] 49 | if byte == "\x00": continue 50 | line = "#WAVE 0x{:02X} -- 0x{:02X}".format(slot+0x20, ord(byte)) 51 | mml.append(line) 52 | mml.append("") 53 | 54 | ## process header 55 | #ROM storage needs an extra two-byte header (38 total), SPCrips do not have this 56 | #we can't reliably tell whether the header is included in all cases, but the 57 | #common custom song address base (26 00) is an impossible ROM header so if 58 | #this is the first two bytes, we know it's an SPCrip. 59 | if data[0:2] == "\x26\x00": 60 | print "detected no header (26 00 base)" 61 | data = int_insert(" ", 0, len(data), 2) + data 62 | #alternately, it is almost certainly an SPCrip if $4-5 == $14-15 != $24-25 63 | elif data[4] == data[0x14] and data[0x14] != data[0x24]: 64 | if data[5] == data[0x15] and data[0x15] != data[0x25]: 65 | print "detected no header ($4 == $14 != $24)" 66 | data = int_insert(" ", 0, len(data), 2) + data 67 | unskew.addr = ord(data[2]) + (ord(data[3]) << 8) 68 | print "unskew.addr {}".format(hex(unskew.addr)) 69 | 70 | channels, r_channels = {}, {} 71 | for c in xrange(0,16): 72 | caddr = unskew(ord(data[6 + c*2]) + (ord(data[7 + c*2]) << 8)) 73 | if c >= 8: 74 | if caddr == channels[c-8]: continue 75 | channels[c] = caddr 76 | for k, v in channels.items(): 77 | r_channels[v] = k 78 | 79 | #some padding so we don't read beyond end of data 80 | data += "\x00\x00\x00" 81 | 82 | loc = 0x26 83 | jumps = {} 84 | nextjump = 1 85 | while loc < len(data)-3: 86 | byte = data[loc] 87 | if byte in ["\xF5", "\xF6", "\xFC"]: 88 | jloc = loc+2 if byte == "\xF5" else loc+1 89 | target = unskew(ord(data[jloc]) + (ord(data[jloc+1]) << 8)) 90 | jumps[target] = nextjump 91 | nextjump += 1 92 | bytelen = 1 93 | if ord(byte) in byte_tbl: bytelen += byte_tbl[ord(byte)][0] 94 | loc += bytelen 95 | for j, i in jumps.items(): 96 | print "jump id {} is at {}".format(i, hex(j)) 97 | 98 | loc = 0x26 99 | measure = 0 100 | sinceline = 0 101 | line = "" 102 | foundjumps = [] 103 | while loc < len(data)-3: 104 | byte = ord(data[loc]) 105 | bytelen = 1 106 | # IF channel points here 107 | if loc in r_channels: 108 | line += "\n{%d}\nl16" % (r_channels[loc]+1) 109 | # IF jump points here 110 | if loc in jumps: 111 | line += " $%d " % jumps[loc] 112 | foundjumps.append(jumps[loc]) 113 | # IF this is a jump 114 | if byte in jump_bytes: 115 | paramlen = byte_tbl[byte][0] 116 | s = byte_tbl[byte][1] 117 | params = [] 118 | late_add_param = None 119 | if byte == 0xF5: 120 | params.append(ord(data[loc+1])) 121 | tloc = loc + 2 122 | else: tloc = loc + 1 123 | dest = unskew(ord(data[tloc]) + (ord(data[tloc+1]) << 8)) 124 | bytelen += paramlen 125 | if dest in jumps: 126 | params.append(jumps[dest]) 127 | else: 128 | params.append("{N/A}") 129 | warn(fileid, map(hex, map(ord, data[loc:loc+bytelen])), "Error parsing jump to {}".format(hex(dest))) 130 | while params: 131 | s += str(params.pop(0)) 132 | if params: 133 | s += "," 134 | line += s 135 | if byte in [0xEB, 0xF6]: #segment enders 136 | line += "\n\nl16" 137 | else: 138 | line += " " 139 | # 140 | elif byte in byte_tbl: 141 | paramlen = byte_tbl[byte][0] 142 | s = byte_tbl[byte][1] 143 | params = [] 144 | for p in xrange(1,paramlen+1): 145 | params.append(ord(data[loc+p])) 146 | while params: 147 | if byte == 0xDC: 148 | if params[0] >= 32 and params[0] < 48: 149 | s = "|{:X}".format(params[0] % 16) 150 | break 151 | if byte == 0xE2: #loop 152 | params[0] += 1 153 | s += str(params.pop(0)) 154 | if params: 155 | s += "," 156 | if byte == 0xC8: #portamento 157 | if params[0] >= 128: 158 | s += "-" 159 | params[0] = 256 - params[0] 160 | else: 161 | s += "+" 162 | line += s 163 | if byte in [0xEB, 0xF6]: #segment enders 164 | line += "\n\nl16" 165 | bytelen += paramlen 166 | elif byte <= 0xC3: 167 | note = mfvitbl.notes[int(byte/14)].lower() 168 | length = r_length_tbl[byte%14] 169 | line += note + length 170 | measure += mfvitbl.lengths[byte%14] 171 | if measure >= 0xC0: 172 | line += " " 173 | sinceline += measure 174 | measure = 0 175 | if sinceline >= 0xC0 * 4 or len(line) >= 64: 176 | mml.append(line) 177 | line = "" 178 | sinceline = 0 179 | loc += bytelen 180 | mml.append(line) 181 | 182 | for k, v in jumps.items(): 183 | if v not in foundjumps: 184 | warn(fileid, "{} {}".format(k,v), "Jump destination never found") 185 | return mml 186 | 187 | if __name__ == "__main__": 188 | 189 | print "mfvitools AKAO SNESv4 to MML converter" 190 | print 191 | 192 | if len(sys.argv) >= 2: 193 | fn = sys.argv[1] 194 | else: 195 | print "If you have both data and instrument set files, named *_data.bin" 196 | print "and *_inst.bin respectively, you can enter only the prefix" 197 | print 198 | print "Enter AKAO filename.." 199 | fn = raw_input(" > ").replace('"','').strip() 200 | 201 | if fn.endswith("_data.bin"): fn = fn[:-9] 202 | prefix = False 203 | try: 204 | with open(fn + "_data.bin", 'rb') as df: 205 | data = df.read() 206 | prefix = True 207 | except: 208 | try: 209 | with open(fn, 'rb') as df: 210 | data = df.read() 211 | except IOError: 212 | print "Couldn't open {}".format(fn) 213 | clean_end() 214 | inst = None 215 | if prefix: 216 | try: 217 | with open(fn + "_inst.bin", 'rb') as instf: 218 | inst = instf.read() 219 | except IOError: 220 | pass 221 | 222 | try: 223 | mml = akao_to_mml(data, inst) 224 | except Exception: 225 | traceback.print_exc() 226 | clean_end() 227 | 228 | try: 229 | with open(fn + ".mml", 'w') as mf: 230 | for line in mml: 231 | mf.write(line + "\n") 232 | except IOError: 233 | print "Error writing {}.mml".format(fn) 234 | clean_end() 235 | 236 | print "OK" 237 | clean_end() 238 | -------------------------------------------------------------------------------- /experimental_ff5/ot_oasis.mml: -------------------------------------------------------------------------------- 1 | #TITLE Oasis in the Sparkling Sands 2 | #ALBUM Octopath Traveler 3 | #COMPOSER Yasunori Nishiki 4 | #ARRANGED emberling 5 | 6 | #VARIANT ~ enh 7 | #VARIANT / nat 8 | 9 | #WAVE 0x20 ~0x0B~/0x0B/ 10 | #WAVE 0x21 ~0x1D~/0x1D/ 11 | #WAVE 0x22 ~0x0D~/0x0D/ 12 | #WAVE 0x23 ~0x12~/0x12/ 13 | #WAVE 0x24 ~0x0F~/0x0F/ 14 | #WAVE 0x25 ~0x14~/0x14/ 15 | #WAVE 0x26 ~0x1c~/0x1c/ 16 | #WAVE 0x27 ~0x1F~/0x1F/ 17 | #WAVE 0x29 ~0x00~/0x00/ 18 | 19 | #scale pan 2 20 | 21 | #def global= t133 %x255 %v40 %z85,0 #%b0,85 %f0,0 22 | 23 | #def str= |0 ~o5~/o5/ m48,18,207 24 | #def vio= |4 ~o5~/o5/ 'v' p80 m48,18,207 'vr' 25 | #def dlc= |3 ~o4~/o4/ 'd' p76 ~m12,18,107~/m12,18,207/ %y0%s1 26 | #def gt1= |1 ~o6~/o6/ 'g' p48 m24,18,199 ~%y3%s3~ /%r13/ 27 | #def gt2= |2 o4 'h' p16 m %y3%s1 28 | #def hrn= |6 o5 'n' p48 m %r10 29 | #def mba= |2 o5 'm' p96 m %a12 30 | #def bas= |5 o8 'b' p64 m48,18,199 %y0%s2%r6 31 | 32 | #def conga= ~o5~ p72 m /|7/ 33 | #def co= ~|7~/o5/ 34 | #def cs= ~|7~/o6/ 35 | #def ch= 'c*v1.25' 36 | #def cl= 'c*v.6' 37 | #def cm= 'c*v.8' 38 | 39 | #def c= v72 40 | #def g= v64 41 | #def h= v32 42 | #def v= ~v80~/v52/ 43 | #def b= v64 44 | #def s= v14 45 | #def d= ~v48~/v120/ 46 | #def m= v56 47 | #def n= v32 48 | 49 | #def i= v20v144,52 50 | #def vf= ~v48,80~/v48,64/ 51 | #def sf= ~v144,14~/v144,14/ 52 | 53 | #def vr= %r5 54 | 55 | #cdef ( %l1 56 | #cdef ) %l0 57 | 58 | {1} 59 | 'global' r1{&90}r4.^16. $ 60 | %e1 l4 'vio+o1' 61 | m6,1e32 62 | ## 3 63 | m6,-1^(dc) %r14m8,1em144,-1^'vr'e (d32e32d8.^)c >(k12a+32k0{&90}a+4.^16.)g+ g2.%r11^^1^1< 64 | 'vr'(f32e8..)dc ('vf*v2,.5'f2'v'e) (b32a+...g+8.m12,-1^16) 'v*v.5''vf*v.67,'g6%r15^3'vr'r48({&44}f6^16 'v*v.75''vf*v2,1.25'e2%r13^2%r9^1^1)'vr''v' 65 | ## 19 66 | (f12e6)(dc) m16,1e12%r14^24^m32,-1^8'vr'e (d24e24d24^4.m12,-2d) (>a+24a+24^%r17^8)'vr'g+ 'v*v.75''vf*2,1.25'g2%r12^1^1'v'cd 67 | ## 27 68 | 'vf*v.67,1.4'd+.'vr''v'f8g 'vf*v,1.33'%r16m8,-2g%r19a+8r.'v''vr' d+.m8,2d+8m8,2f (g24f24^6)d+m8,2c 69 | 'v*v.75''vf*v2,1.25'c2^8%r14[39^8] 'dlc+o1' cd 70 | ## 39 71 | [[d+d32d+32d8.c fd+d] j2,151 c1. 72 | [d+d32d+32d8.c fd+j2d]f g1.] 73 | ## 51 74 | #^ [d+d32d+32d8.c fd+d] 75 | $151 [c+c32c+32c8.>a+< d+c+j2c]d+ 76 | ## 59 77 | [fd+32f32d+8.c+ g+gd+] 78 | fd+32f32d+8.c+ a+g+d+ fd+32f32d+8.f a+g+d+ 79 | f1. g1 80 | ##70 81 | 'conga' l8 82 | 'cs''c'%e0c'co''cm'd'cs''cl'>a'co'b< 83 | 'cs''c'%e1a4 'co''cl'%e0g'c'g+4 'cs''cl'>a+< 'co'%e1'ch' 'cl'%e0f+'cm'g'c'c+ 84 | 'cs'%e1b4 >'cl'%e0b<'c''co'f+4 'cl'd+ 'c'%e1 'cm'['cs'>%e0a+<'co'c+] 85 | 'cs'%e1'c'a+4 'cl'c'cm'c+4 'co'e 'ch'%e1'cs'%e0'c'd16. 86 | ; 87 | 88 | {2} 89 | $ 'bas-o2' l8 %e1 90 | r1. 91 | ## 3 92 | [4c1^4.>g< c1.] 93 | ## 19 94 | c2.c16.r32c c1^c16.r32fg c1^frg c1^4>g.r16 95 | ## 27 96 | g+4.g+16.r32g+4 a+2^r g+4..r16g+4 a+2. 97 | c c1. 98 | c1^4.c16r16 c1^4fg 99 | ## 39 100 | [[g+2^d+ g+.r16g+2] j2 f2^cf2f16r16g16r16] c2^gc2. 101 | ## 51 102 | [g+2^d+ g+.r16g+2] [d+2^>a+a+< d+.r16d+2 105 | d2^>a> ^g+4>] 113 | ## 19 114 | [cg> cg+>]a+> 115 | [[cg> cg+>] 116 | ## 27 117 | j2,331 [>g+> >a+]]$331 118 | #^ [cg> cg+>] 119 | cg> cg+> cg> cg+> 120 | >## 39 121 | 'g*v1.33' 122 | [g+g+d+>]^c>d+> faafc>f 123 | [[g+g+d+>] j2 ceg+fc>g] 124 | ## 51 125 | #^ [g+g+d+>] 126 | ['gt1-o2*v1.33'd+a+a+a+a+]f+>a+'gt1-o2*v1.33' 127 | ## 59 128 | [a+a+a+>] 129 | f+> f+a+a+> 130 | faafd>a b> b32## 39 150 | p40 ['s*v1.5''sf*v,2'g+2.'sf*v,2.5'^2.'sf*v,3'^2.'sf*v,3.5'^2. j2 f2.'sf*v,1.5'^2.] 151 | p32 'sf*v,3'([3ge])'sf*v,1.5'([3g+c]) 152 | ## 51 153 | p40 's*v1.5''sf*v,2'g+2.'sf*v,2.5'^2.'sf*v,3'^2.'sf*v,3.5'^2. 154 | 'sf*v,4'f+2.'sf*v,3'^2.'sf*v,2.15'^2.'sf*v,1.5'^2. 155 | ## 59 156 | 'sf*v,3'f2.'sf*v,4'^2.'sf*v,2.67'^2.'sf*v,1.5'^2. 157 | 'sf*v,3'f1. 'sf*v,2'g2.'sf*v,1.5'^2. 158 | 'sf*v,2.5'f2.'sf*v,3.5'^2.'sf*v,5'g2.'sf*v,1.5'^2. 159 | ##71 160 | %r17 ^1. [10 161 | ; 162 | 163 | {5} 164 | $ 'conga' r1. 165 | ## 3 166 | 'c''co'%e1c4. 'cs'%e0c4. >a2.< 167 | 'co'%e1c4. 'cs'%e0c4. >a< 'cm''co'e'c'e4 'cs'g+'co'g 168 | %e1c4. 'cs'%e0c4. %e1g+2. 169 | 'co'c4 'cs'%e0'cl'c'c'c4 'cl'c 'c'%e1g+4 [%e0c'cl'>a<'c'] 170 | 'co'%e1c4 'cs'%e0'cl'c'c'd4. %e1g+4. >a4 'cl'%e0a< 171 | 'co''c'%e1c4 'cs''cl'%e0c'c'c4. g+4>a4 'vio+o1'%e1d32%r26^16.'conga''cs''cl'>%e0a< 172 | 'co'%e1'c'c4 'cl'%e0d'cs''c'>a<'cl''co'd4 'cs''c'>a4.< 'co'd'cs'>ab4< 'cl''co'c'cs'>b'c'b<'co'%e1g+4 'cs'%e0g+'co'a 174 | ## 19 175 | %e1c4 'cs''cl'%e0c'c'c'co''cm'f'cl'f 'cs''c'%e1g4 'co''cl'%e0f'cs''c'c'cm'c'cl'c 176 | 'co'%e1'c'c4 'cs'%e0'cl'c'c'c4. %e1g+4 %e0['cs''c'c'co''cm'f] 177 | 'c'%e1c4 %e0'cs''cl'c'c'c4. %e1a4. 'co''ch'e4. 178 | 'c'c4 %e0'cs''cl'c'c'c4. %e1'co''ch' %e0'cs''c'd4 'co'c4 179 | ## 27 180 | %e1c4. 'cs''cm'%e0>a+<'co'd+4 'c'e2. 181 | 'cs'%e1g+4 %e0[>'cl'a'c'a<'co'j2f4]e4 'cs'g+'co'f 182 | %e1c4 'cs'%e0'cl'>b'c'b4.< %e1a2^ 'co''cl'>%e0g+16'cm'g+16< 183 | 'c'c4 'cs'>'cl'a+'cm'a+4 'cl'a+< 'c'%e1a4 %e0'cm'['cs'>b<'co'j2f]'cl'f 184 | 'c'%e1c4 'cs'>%e0'cl'b'c'b4 'cl'b< 'co'%e1'c'd4. %e0d'cs'ca 185 | 'co'%e1c2. 'cs'%e0c2 >'cl'g'c'g< 186 | ## 39 187 | ['co''c'%e1%e0'cl'c'cm'c'c''cs'd+4j2 'cl'>a< 'co''c'f4 f4. 'cs''cl'>b<] 188 | 'co''cl'd+ 'c'<%e1c%e0>c'cs'e4. >'cl'b16'cm'b16< 189 | 'co''c'f'cl'c'cm'c'c''cs'>g4< 'co'c 'cs'>g4. g<'co'c'cl'c 190 | 'c'f'cl'c'c'c'cs'c+4 'cl'c 'c'c+4 d4. 'co''cl'c 191 | 'c'%e1%e0'cl'c'c'c'cs'c4 'co''cl'f 'c'<%e1c%e0>'cm'd+'c'g+'cl'c'cs'c'co'c 192 | 'cs''c'>%e1a%e0<'co''cl'e'cm'e'cs''c'>a4.< %e1a4 %e0[>b<'co''cl'd'cs''c'] 193 | ## 51 194 | %e1f+'cl''co'%e0d+'cs'c'c'c'co'd+'cl'd+ 'c''cs'>b2^< 'cl''co'f 195 | 'c''cs'%e1f+4 %e0'co'e'cs'>a+4 'cl'a+< 'c'e2^ 'cl'f 196 | 'co''c'%e1f+4 %e0d+f4 d+ 'cs'%e1a+4 'co'%e0f+'cm'd'c'f+4 197 | 'cs'a+4. c4 'co'g+ 'cs'g+4 d+4 g+'cs''cl'f+ 198 | ## 59 199 | 'co''c'c'cs''cm'c'cl'c'co''c'g+4 c 'cs'c+4 'co''cl'c+'c'f'cs'>b'cl'a+< 200 | 'co''c'c'cl'c+f'c'g+'cl'f'cs'>a+< ['cs''c'c+'co''cl'd]'c'd+'cl'd 201 | 'c'%e1f+'cm'%e0d'cl'c+'cs''c'c4 'co'd 'cs'>b<'co''cm'c4. 'cs'c'co''cl'c 202 | 'c''cs'%e1d'cl''co'%e0c+'cs'>a<'c'd4 'co''cl'd 'cs''c'%e1f'co'%e0g'cm'c+4 'cs'd'co'd 203 | 'c'%e1d+4 %e0['cl'c'c'd+4] 'cs''cm'>a<'co''c'e'cl'd+'cs''cm'>b 204 | 'c'a<'co''cl'c'cm'c+'cs'>b4< 'co'c 'cs''c'>b16 'gt1' d8.^2 205 | ##71 206 | %r15>>g1^1^1 207 | ; 208 | 209 | {6} 210 | [9 $ 'gt1-o1' %e1 l2 211 | c.c.] 212 | ## 19 213 | 'str-o1' p80 214 | 's*v.67' g2. 's' g+2. 's*v1.33' g2. 's*v1.67' g+2. 215 | 'sf*v,2.5' g2. 'sf*v,3.5' g+2. 'sf*v,3' g2. 'sf' g+2. 216 | ## 27 217 | < p64 ['s*v2''sf*v,3'c2.%r15d2.%r] 218 | 'sf'c2. p80 's*v2''sf*v.67,3.33'g+2.'sf*v,2.67'g2.'sf*v,1.5'f2. 219 | 'sf*v,3'e2. g+2. e2. 'sf*v,1.5'c2. 220 | ## 39 221 | l8 [p56('sf*v,2'c2.'sf*v,2.5'c2.'sf*v,3'c2.'sf*v,3.5'c2.) j2,649 222 | 'hrn'r4.fafa<[3%r7cj3%r25^]'str*v1.5']$649 p56 'sf*v,2.67'(c2.'sf*v,1.5'c2.) 223 | ## 51 224 | 's*v1.5''sf*v,2'(c2.'sf*v,2.5'c2.'sf*v,3'c2.'sf*v,3.5'c2.) 225 | 'sf*v,4'(d+2.'sf*v,3'd+2.'sf*v,2.15'd+2.'sf*v,1.5'd+2. 226 | ## 59 227 | 'sf*v,3'(>a+2.'sf*v,4'a+2.'sf*v,2.67'a+2.'sf*v,1.5'a+2.<) 228 | 'sf*v,2.25'(f+2f+2f+2) 's*v3''sf*v,2'(c+2.'sf*v,1.5'c+2.) 229 | 'sf*v,2.5'(c2.'sf*v,3.5'c2.)'sf*v,5'(d2.'sf*v,1.5'd2.) 230 | ##71 231 | %r17 ^1. [10 232 | ; 233 | 234 | {7} 235 | [9 $ 'gt2-o1' %e1 l8 236 | 'i'cg> 'i'cg+>] 237 | ## 19 238 | 'str+o1' p40 v0 l8 239 | ['sf*v,2.4'([3e>g<]) ([3f>g+<]) ([3e>g<]) 'sf'([3f>g+<])] 240 | ## 27 241 | p56 ['s''sf*v,2.67'c2. 's*v4'%r16f2.%r] 242 | p40 's*v.5''sf*v,2.3'([3e>g<]) ([3f>g+<]) ([3e>g<]) 'sf'([3c>f<]) 243 | 's*v.5''sf*v,2.2'([3e>g<]) ([3f>a+<]) ([3e>g<]) 'sf'([3c>f<]) 244 | >## 39 245 | p48 [('s*v1.5''sf*v,2'd+2.'sf*v,2.5'd+2.'sf*v,3'd+2.'sf*v,3.5'd+2.) 246 | j2 (c2.'sf*v,1.5'c2.)] 'sf*v,2.67'e2.'sf*v,1.5'd+2. 247 | ## 51 248 | 's*v1.5''sf*v,2'(d+2.'sf*v,2.5'd+2.'sf*v,3'd+2.'sf*v,3.5'd+2.) 249 | 'mba-p56' [r2.d+8r8c+8r8j2>a+8r8<]d+8r8 'str*v1.5'p48 250 | ## 59 251 | 'sf*v,3'(c+2.'sf*v,4'c+2.'sf*v,2.67'c+2.'sf*v,1.5'c+2.) 252 | 'sf*v,3'(c+2c+2c+2) 'sf*v,2'(d+2.'sf*v,1.5'd+2.) 253 | p64 'sf*v,2'(>a2.'sf*v,3'a2.)'sf*v,5'(b2.b2'sf*v.5,1.5'b4) 254 | ##71 255 | %r16 ^1. [10 256 | ; 257 | 258 | {8} 259 | $ [18r2.] 260 | ## 19 261 | 'gt2-o1' %e1 l8 262 | ['i'cg> 'i'cg+>]a+> 263 | ['i'cg> 'i'cg+>] 264 | ## 27 265 | ['i'>g+> 'i'>a+] 266 | ['i'cg> 'i'cg+>] 267 | 'i'cg> 'i'cg+> 'i'cg> 'i'cg+> 268 | ## 39 269 | 'str-o1' p64 270 | [('s*v1.5''sf*v,2'g+2.'sf*v,2.5'g+2.'sf*v,3'g+2.'sf*v,3.5'g+2.) 271 | j2 (a2.'sf*v,1.5'a2.)] 'sf*v,2.67'g2.'sf*v,1.5'g+2. 272 | ## 51 273 | 's*v1.5''sf*v,2'(g+2.'sf*v,2.5'g+2.'sf*v,3'g+2.'sf*v,3.5'g+2.) 274 | 'sf*v,4'(a+2.'sf*v,3'a+2.'sf*v,2.15'a+2.'sf*v,1.5'a+2.) 275 | ## 59 276 | 's''sf*v,2'(>a+2.'sf*v,4'a+2.'sf*v,2.5'a+2.'sf'a+2.<) 277 | 'sf*v,2'(a+2.'sf*v,3'a+2.'sf*v,2.25'a+2.'sf*v,1.5'a+2.) 278 | r1r1r4 'gt1-o1*v1.33' d32g8..^2 279 | ##71 280 | ^1^1^1 281 | ; 282 | -------------------------------------------------------------------------------- /mass_extract.py: -------------------------------------------------------------------------------- 1 | ## mfvitools mass extractor tool 2 | ## extract mml, samples, and SPC from multiple tracks all at once, with tags 3 | ## set up what to extract in a config file, then run mass_extract.py [CONFIGFILE] 4 | 5 | import sys, os, configparser 6 | from build_spc import build_spc, load_data_from_rom, read_pointer 7 | from mfvi2mml import akao_to_mml, byte_insert 8 | 9 | SPC_ENGINE_OFFSET = 0x5070E 10 | STATIC_BRR_OFFSET = 0x51EC7 11 | STATIC_PTR_OFFSET = 0x52016 12 | STATIC_ENV_OFFSET = 0x52038 13 | STATIC_PITCH_OFFSET = 0x5204A 14 | 15 | POINTER_TO_BRR_POINTERS = 0x50222 16 | POINTER_TO_BRR_LOOPS = 0x5041C 17 | POINTER_TO_BRR_ENV = 0x504DE 18 | POINTER_TO_BRR_PITCH = 0x5049C 19 | POINTER_TO_INST_TABLE = 0x501E3 20 | POINTER_TO_SEQ_POINTERS = 0x50539 21 | 22 | def text_insert(data, position, text, length): 23 | new_data = bytearray(length) 24 | new_data = byte_insert(new_data, 0, bytes(text, "utf-8"), maxlength = length) 25 | return bytearray(byte_insert(data, position, new_data)) 26 | 27 | if __name__ == "__main__": 28 | print(f"mfvitools mass extractor tool") 29 | print(f" created by emberling") 30 | print() 31 | 32 | if len(sys.argv) >= 2: 33 | fn = sys.argv[1] 34 | else: 35 | print("HOW TO USE:") 36 | print("Make a config file to set up the files you want to extract. ") 37 | print("Example txt file: ") 38 | print("................................................................") 39 | print("[ff6.smc] ") 40 | print("01: prelude; The Prelude; Final Fantasy VI; Nobuo Uematsu; M.Akao") 41 | print("09: gau; Gau's Theme; Final Fantasy VI; Nobuo Uematsu; M.Akao ") 42 | print(" ") 43 | print("[rotds.smc] ") 44 | print("19: xb_gaur; Gaur Plains; Xenoblade; ACE+; Jackimus ") 45 | print("1A: lal_boss; Megalomania; Live A Live; Yoko Shimomura; Gi Nattak") 46 | print("................................................................") 47 | print("Format is [id]: file; title; game; composer; arranger/converter ") 48 | print("You can process more than one game at the same time. ") 49 | print("Files will include the ROM name, e.g. ff6_gau.mml, rotds_xb_gaur.mml") 50 | print("Rename your ROM as appropriate to keep filenames manageable. ") 51 | print(" ") 52 | print("You can also just enter a ROM directly instead of a config file ") 53 | print("to extract all music with no titles or metadata. ") 54 | 55 | 56 | print("Enter config filename..") 57 | fn = input(" > ").replace('"','').strip() 58 | 59 | config = configparser.ConfigParser() 60 | 61 | # is this a rom or a config file? 62 | cfg_size = os.path.getsize(fn) 63 | if cfg_size >= 0x300000: 64 | try: 65 | with open(fn, 'rb') as f: 66 | rom = f.read() 67 | except IOError: 68 | print(f"ERROR: Couldn't load ROM file {fn}") 69 | os.exit() 70 | if len(rom) % 0x10000 == 0x200: 71 | rom = rom[0x200:] 72 | number_of_songs = rom[0x53C5E] 73 | 74 | virt_config = {fn: {f"{n:02X}": f"{n:02X}" for n in range(number_of_songs)}} 75 | config.read_dict(virt_config) 76 | else: 77 | config.read(fn) 78 | 79 | for romfile in config.sections(): 80 | 81 | romid = os.path.basename(romfile).split('.')[0].strip().replace(' ', '_') 82 | 83 | try: 84 | with open(romfile, 'rb') as f: 85 | rom = f.read() 86 | except IOError: 87 | print(f"ERROR: Couldn't load ROM file {romfile}") 88 | continue 89 | if len(rom) % 0x10000 == 0x200: 90 | rom = rom[0x200:] 91 | print(f"Loaded {romfile} with header.") 92 | else: 93 | print(f"Loaded {romfile} without header.") 94 | 95 | for song_idx_string in config[romfile]: 96 | try: 97 | song_idx = int(song_idx_string.strip(), 16) 98 | except ValueError: 99 | print(f"ERROR: invalid index {song_idx_string}") 100 | continue 101 | 102 | spc = build_spc(rom, song_idx) 103 | 104 | ## Build MML 105 | brr_pointer_offset = read_pointer(rom, POINTER_TO_BRR_POINTERS) 106 | brr_loop_offset = read_pointer(rom, POINTER_TO_BRR_LOOPS) 107 | brr_env_offset = read_pointer(rom, POINTER_TO_BRR_ENV) 108 | brr_pitch_offset = read_pointer(rom, POINTER_TO_BRR_PITCH) 109 | inst_table_offset = read_pointer(rom, POINTER_TO_INST_TABLE) 110 | 111 | # Extract sequence from ROM and convert to MML 112 | loc = read_pointer(rom, POINTER_TO_SEQ_POINTERS) 113 | loc += song_idx * 3 114 | loc = read_pointer(rom, loc) 115 | seq = load_data_from_rom(rom, loc) 116 | try: 117 | mml = akao_to_mml(seq, force_short_header=True) 118 | except IndexError: 119 | print(f"Failed to convert sequence {romid}:{song_idx:02X} (sequence too short?)") 120 | continue 121 | 122 | # Extract samples from ROM 123 | #brr_loop, brr_env, brr_pitch, brr_ident = {}, {}, {}, {} 124 | sample_defs = [] 125 | loc = song_idx * 0x20 + inst_table_offset 126 | inst_table = rom[loc:loc+0x20] 127 | for i in range(16): 128 | inst_id = int.from_bytes(inst_table[i*2:i*2+2], "little") 129 | if inst_id: 130 | inst_idx = inst_id - 1 131 | loc = brr_loop_offset + 2 * inst_idx 132 | brr_loop = int.from_bytes(rom[loc:loc+2], "big") 133 | 134 | loc = brr_env_offset + 2 * inst_idx 135 | brr_env = int.from_bytes(rom[loc:loc+2], "big") 136 | 137 | loc = brr_pitch_offset + 2 * inst_idx 138 | brr_pitch = int.from_bytes(rom[loc:loc+2], "big") 139 | 140 | brr_pointer = read_pointer(rom, brr_pointer_offset + 3 * inst_idx) 141 | brr_data = load_data_from_rom(rom, brr_pointer) 142 | 143 | brr_ident = f"{len(brr_data) // 9:04}_{sum(brr_data) % pow(16,6):06X}" 144 | 145 | os.makedirs(os.path.join("brr", romid), exist_ok = True) 146 | bfn = f"brr/{romid}/{brr_ident}.brr" 147 | try: 148 | with open(bfn, "wb") as f: 149 | f.write(brr_data) 150 | except IOError: 151 | print(f"ERROR: Couldn't write sample {romid}:{song_idx:02X}:{i + 0x20:02X} as {bfn}") 152 | continue 153 | 154 | # Build definition 155 | prg = i + 0x20 156 | sample_defs.append(f"#BRR 0x{prg:02X} 0x{inst_id:02X}; {bfn}, {brr_loop:04X}, {brr_pitch:04X}, {brr_env:04X}") 157 | 158 | out_mml = [] 159 | 160 | ## Deal with metadata 161 | meta_cfg = config[romfile][song_idx_string].split(';') 162 | while len(meta_cfg) < 5: 163 | meta_cfg.append("") 164 | for i in range(len(meta_cfg)): 165 | meta_cfg[i] = meta_cfg[i].strip() 166 | 167 | songfn = romid + '_' + meta_cfg[0] 168 | 169 | if meta_cfg[1]: 170 | out_mml.append(f"#TITLE {meta_cfg[1]}") 171 | spc = text_insert(spc, 0x2E, meta_cfg[1], 0x20) 172 | spc[0x23] = 0x1A 173 | if meta_cfg[2]: 174 | out_mml.append(f"#ALBUM {meta_cfg[2]}") 175 | spc = text_insert(spc, 0x4E, meta_cfg[2], 0x20) 176 | spc[0x23] = 0x1A 177 | if meta_cfg[3]: 178 | out_mml.append(f"#COMPOSER {meta_cfg[3]}") 179 | spc = text_insert(spc, 0xB1, meta_cfg[3], 0x20) 180 | spc[0x23] = 0x1A 181 | if meta_cfg[4]: 182 | out_mml.append(f"#ARRANGED {meta_cfg[4]}") 183 | spc = text_insert(spc, 0x6E, meta_cfg[4], 0x10) 184 | spc[0x23] = 0x1A 185 | spc = byte_insert(spc, 0xAC, b"\x35\x30\x30\x30") 186 | 187 | ## MML surgery 188 | out_mml.append("") 189 | for line in sample_defs: 190 | out_mml.append(line) 191 | out_mml.append("") 192 | 193 | mml = [line for line in mml if not line.startswith("#WAVE")] 194 | out_mml = out_mml + mml 195 | out_mml = "\n".join(out_mml) 196 | 197 | ## file output 198 | 199 | this_fn = songfn + ".spc" 200 | try: 201 | with open(this_fn, "wb") as f: 202 | f.write(spc) 203 | except IOError: 204 | print("ERROR: failed to write {this_fn}") 205 | 206 | this_fn = songfn + ".mml" 207 | try: 208 | with open(this_fn, "w") as f: 209 | f.write(out_mml) 210 | except IOError: 211 | print("ERROR: failed to write {this_fn}") 212 | 213 | -------------------------------------------------------------------------------- /mfvi2mml.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys, traceback 3 | import mfvitbl 4 | from mmltbl import * 5 | 6 | jump_bytes = [0xF5, 0xF6, 0xFC] 7 | 8 | def byte_insert(data, position, newdata, maxlength=0, end=0): 9 | while position > len(data): 10 | data = data + b"\x00" 11 | if end: 12 | maxlength = end - position + 1 13 | if maxlength and len(data) > maxlength: 14 | newdata = newdata[:maxlength] 15 | return data[:position] + newdata + data[position+len(newdata):] 16 | 17 | 18 | def int_insert(data, position, newdata, length, reversed=True): 19 | n = int(newdata) 20 | l = [] 21 | while len(l) < length: 22 | l.append(n & 0xFF) 23 | n = n >> 8 24 | if not reversed: l.reverse() 25 | return byte_insert(data, position, bytes(l), length) 26 | 27 | def warn(fileid, cmd, msg): 28 | print(f"{fileid}: WARNING: in {cmd:<10}: {msg}") 29 | 30 | def clean_end(): 31 | print("Processing ended.") 32 | input("Press enter to close.") 33 | quit() 34 | 35 | def akao_to_mml(data, inst=None, fileid='akao', force_short_header=False): 36 | 37 | def unskew(base): 38 | base -= unskew.addr 39 | if base < 0: base += 0x10000 40 | return base + 0x26 41 | 42 | mml = ["# Converted from binary by mfvitools", ""] 43 | 44 | ## process inst 45 | if inst is not None: 46 | for slot in range(0,0x10): 47 | if len(inst) <= slot*2: break 48 | byte = inst[slot*2] 49 | if byte == 0x00: continue 50 | line = "#WAVE 0x{:02X} -- 0x{:02X}".format(slot+0x20, byte) 51 | mml.append(line) 52 | mml.append("") 53 | 54 | ## process header 55 | #ROM storage needs an extra two-byte header (38 total), SPCrips do not have this 56 | #we can't reliably tell whether the header is included in all cases, but the 57 | #common custom song address base (26 00) is an impossible ROM header so if 58 | #this is the first two bytes, we know it's an SPCrip. 59 | if force_short_header: 60 | data = int_insert(b"\x00\x00", 0, len(data), 2) + data 61 | elif data[0:2] == b"\x26\x00": 62 | print("detected no header (26 00 base)") 63 | data = int_insert(b"\x00\x00", 0, len(data), 2) + data 64 | #alternately, it is almost certainly an SPCrip if $4-5 == $14-15 != $24-25 65 | elif data[4] == data[0x14] and data[0x14] != data[0x24]: 66 | if data[5] == data[0x15] and data[0x15] != data[0x25]: 67 | print("detected no header ($4 == $14 != $24)") 68 | data = int_insert(b"\x00\x00", 0, len(data), 2) + data 69 | unskew.addr = data[2] + (data[3] << 8) 70 | print("unskew.addr {}".format(hex(unskew.addr))) 71 | 72 | #Check end address so that channel pointers at or beyond that point 73 | #don't get treated as active channels 74 | end_addr = unskew(data[4] + (data[5] << 8)) 75 | 76 | channels, r_channels = {}, {} 77 | for c in range(0,16): 78 | caddr = unskew(data[6 + c*2] + (data[7 + c*2] << 8)) 79 | if c >= 8: 80 | if caddr == channels[c-8]: 81 | continue 82 | channels[c] = caddr 83 | for k, v in channels.items(): 84 | r_channels[v] = k 85 | 86 | #some padding so we don't read beyond end of data 87 | data += b"\x00\x00\x00" 88 | 89 | loc = 0x26 90 | jumps = {} 91 | nextjump = 1 92 | while loc < len(data)-3: 93 | byte = data[loc] 94 | if byte in [0xF5, 0xF6, 0xFC]: 95 | jloc = loc+2 if byte == 0xF5 else loc+1 96 | target = unskew(data[jloc] + (data[jloc+1] << 8)) 97 | jumps[target] = nextjump 98 | nextjump += 1 99 | bytelen = 1 100 | if byte in byte_tbl: bytelen += byte_tbl[byte][0] 101 | loc += bytelen 102 | for j, i in jumps.items(): 103 | print("jump id {} is at {}".format(i, hex(j))) 104 | 105 | loc = 0x26 106 | measure = 0 107 | sinceline = 0 108 | line = "" 109 | foundjumps = [] 110 | while loc < len(data)-3: 111 | byte = data[loc] 112 | bytelen = 1 113 | # IF channel points here 114 | if loc in r_channels: 115 | for chnl in [c for c in channels if channels[c] == loc]: 116 | if loc >= end_addr: 117 | print(f"Ignoring channel pointer {chnl+1} (0x{loc:04X}) - past stated EOF (0x{end_addr:04X})") 118 | else: 119 | line += "\n{%d}\nl16" % (chnl+1) 120 | # IF jump points here 121 | if loc in jumps: 122 | line += " $%d " % jumps[loc] 123 | foundjumps.append(jumps[loc]) 124 | # IF this is a jump 125 | if byte in jump_bytes: 126 | paramlen = byte_tbl[byte][0] 127 | s = byte_tbl[byte][1] 128 | params = [] 129 | late_add_param = None 130 | if byte == 0xF5: 131 | params.append(data[loc+1]) 132 | tloc = loc + 2 133 | else: tloc = loc + 1 134 | dest = unskew(data[tloc] + (data[tloc+1] << 8)) 135 | bytelen += paramlen 136 | if dest in jumps: 137 | params.append(jumps[dest]) 138 | else: 139 | params.append("{N/A}") 140 | warncmd = "" 141 | for d in data[loc:loc+bytelen]: 142 | warncmd += f"{d:2X} " 143 | warn(fileid, warncmd, f"Error parsing jump to {dest:X}") 144 | print(jumps) 145 | while params: 146 | s += str(params.pop(0)) 147 | if params: 148 | s += "," 149 | line += s 150 | if byte in [0xEB, 0xF6]: #segment enders 151 | line += "\n\nl16" 152 | else: 153 | line += " " 154 | # 155 | elif byte in byte_tbl: 156 | paramlen = byte_tbl[byte][0] 157 | s = byte_tbl[byte][1] 158 | params = [] 159 | for p in range(1,paramlen+1): 160 | params.append(data[loc+p]) 161 | while params: 162 | if byte == 0xDC: 163 | if params[0] >= 32 and params[0] < 48: 164 | s = "|{:X}".format(params[0] % 16) 165 | break 166 | if byte == 0xE2: #loop 167 | params[0] += 1 168 | s += str(params.pop(0)) 169 | if params: 170 | s += "," 171 | if byte == 0xC8: #portamento 172 | if params[0] >= 128: 173 | s += "-" 174 | params[0] = 256 - params[0] 175 | else: 176 | s += "+" 177 | line += s 178 | if byte in [0xEB, 0xF6]: #segment enders 179 | line += "\n\nl16" 180 | bytelen += paramlen 181 | elif byte <= 0xC3: 182 | note = mfvitbl.notes[int(byte//14)].lower() 183 | length = r_length_tbl[byte%14] 184 | line += note + length 185 | measure += mfvitbl.lengths[byte%14] 186 | if measure >= 0xC0: 187 | line += " " 188 | sinceline += measure 189 | measure = 0 190 | if sinceline >= 0xC0 * 4 or len(line) >= 64: 191 | mml.append(line) 192 | line = "" 193 | sinceline = 0 194 | loc += bytelen 195 | mml.append(line) 196 | 197 | # If main start exists but alternate does not exist for a channel pair, 198 | # set up alternate starts at EOF 199 | eof_append = "" 200 | for chnl in [c for c in channels if c >= 8 and channels[c] == end_addr 201 | and c - 8 in channels and channels[c-8] != end_addr]: 202 | eof_append += "{%d} " % (chnl+1) 203 | if eof_append: 204 | mml.append(eof_append + "##EOF") 205 | 206 | for k, v in jumps.items(): 207 | if v not in foundjumps: 208 | warn(fileid, "{} {}".format(k,v), "Jump destination never found") 209 | return mml 210 | 211 | if __name__ == "__main__": 212 | 213 | print("mfvitools AKAO SNESv4 to MML converter") 214 | print() 215 | 216 | if len(sys.argv) >= 2: 217 | fn = sys.argv[1] 218 | else: 219 | print("If you have both data and instrument set files, named *_data.bin") 220 | print("and *_inst.bin respectively, you can enter only the prefix") 221 | print() 222 | print("Enter AKAO filename..") 223 | fn = input(" > ").replace('"','').strip() 224 | 225 | if fn.endswith("_data.bin"): fn = fn[:-9] 226 | prefix = False 227 | try: 228 | with open(fn + "_data.bin", 'rb') as df: 229 | data = df.read() 230 | prefix = True 231 | except: 232 | try: 233 | with open(fn, 'rb') as df: 234 | data = df.read() 235 | except IOError: 236 | print("Couldn't open {}".format(fn)) 237 | clean_end() 238 | inst = None 239 | if prefix: 240 | try: 241 | with open(fn + "_inst.bin", 'rb') as instf: 242 | inst = instf.read() 243 | except IOError: 244 | pass 245 | 246 | try: 247 | mml = akao_to_mml(data, inst) 248 | except Exception: 249 | traceback.print_exc() 250 | clean_end() 251 | 252 | try: 253 | with open(fn + ".mml", 'w') as mf: 254 | for line in mml: 255 | mf.write(line + "\n") 256 | except IOError: 257 | print("Error writing {}.mml".format(fn)) 258 | clean_end() 259 | 260 | print("OK") 261 | clean_end() 262 | -------------------------------------------------------------------------------- /hex_tools/mfvisplit.py: -------------------------------------------------------------------------------- 1 | from shutil import copyfile 2 | import StringIO 3 | import random 4 | from mfvitbl import notes, lengths, codes 5 | 6 | skipone = {"\xC4", "\xC6", "\xCF", "\xD6", "\xD9", "\xDA", "\xDB", "\xDC", "\xDD", "\xDE", "\xDF", "\xE0", 7 | "\xE2", "\xE8", "\xE9", "\xEA", "\xF0", "\xF2", "\xF4", "\xFD", "\xFE"} 8 | skiptwo = {"\xC5", "\xC7", "\xC8", "\xCD", "\xF1", "\xF3", "\xF7", "\xF8"} 9 | skipthree = {"\xC9", "\xCB"} 10 | 11 | source = raw_input("Please input the file name of your CLEAN music data" 12 | "ROM dump, excluding _data.bin:\n> ").strip() 13 | 14 | sourcefile = source + "_data.bin" 15 | 16 | try: 17 | fin = open(sourcefile, 'rb') 18 | except IOError: 19 | raw_input("Couldn't open input file. Press enter to quit. ") 20 | quit() 21 | 22 | def read_word(f): 23 | low, high = 0, 0 24 | low = ord(f.read(1)) 25 | high = ord(f.read(1)) 26 | return low + (high * 0x100) 27 | 28 | def write_word(i, f): 29 | low = i & 0xFF 30 | high = (i & 0xFF00) >> 8 31 | f.write(chr(low)) 32 | f.write(chr(high)) 33 | 34 | def strmod(s, p, c): #overwrites part of a string 35 | while len(s) < p: 36 | s = s + " " 37 | if len(s) == p: 38 | return s[:p] + c 39 | else: 40 | return s[:p] + c + s[p+len(c):] 41 | 42 | def bytes(n): # int-word to two char-bytes 43 | low = n & 0xFF 44 | high = (n & 0xFF00) >> 8 45 | return chr(high) + chr(low) 46 | 47 | fin.seek(0,2) 48 | size = fin.tell() 49 | assert size <= 0xFFFF 50 | 51 | fin.seek(2) 52 | if read_word(fin) != 0x26: 53 | raw_input("Please clean me2 dump before unrolling") 54 | quit() 55 | 56 | fin.seek(6) 57 | channels = [] 58 | for c in xrange(0,16): 59 | channels.append(read_word(fin)) 60 | 61 | pieces = [""] 62 | current, skip = 0, 0 63 | jumppoints = {} 64 | origins = [0x26] 65 | 66 | pos = 0x26 67 | fin.seek(pos) 68 | while pos < size: 69 | byte = fin.read(1) 70 | if not byte: break 71 | 72 | pieces[current] += byte 73 | if byte in {"\xF6", "\xF5", "\xFA", "\xFC"} and not skip: 74 | if byte in ["\xF5", "\xFA"]: pieces[current] += fin.read(1) 75 | pos = fin.tell() 76 | dest = read_word(fin) 77 | jumppoints[pos] = dest 78 | pieces[current] += chr(dest & 0xFF) + chr((dest & 0xFF00) >> 8) 79 | fin.seek(pos+2) 80 | 81 | if skip: 82 | skip -= 1 83 | else: 84 | if byte in skipone: skip = 1 85 | elif byte in skiptwo: skip = 2 86 | elif byte in skipthree: skip = 3 87 | elif byte in ["\xF6", "\xEB"]: 88 | origins.append(fin.tell()) 89 | pieces.append("") 90 | current += 1 91 | pos = fin.tell() 92 | 93 | def convert_jump(old_dest, warn = False): 94 | global pieces 95 | global origins 96 | remainder = old_dest 97 | target = 0 98 | 99 | for s in pieces: 100 | if remainder >= len(s): 101 | target += 1 102 | remainder -= len(s) 103 | for i, o in enumerate(origins): 104 | x = i 105 | if old_dest < o: 106 | x = i - 1 107 | break 108 | remainder = old_dest - origins[x] 109 | 110 | if warn and remainder >= 0xFFF: print "warning!! sequence too long (>0xFFF), can't repack jump destinations" 111 | if warn and x >= 0xF: print "warning!! too many sequences (>0xF), can't repack jump destinations" 112 | return remainder + ( x << 12 ) 113 | 114 | for p in jumppoints: 115 | remainder = p 116 | target_piece = 0 117 | 118 | for s in pieces: 119 | if remainder >= len(s): 120 | target_piece += 1 121 | remainder -= len(s) 122 | for i, o in enumerate(origins): 123 | x = i 124 | if p < o: 125 | x = i - 1 126 | break 127 | #print "jump point at {} {}".format(x+1, hex(p - origins[x])) 128 | dest = bytes(convert_jump(jumppoints[p],warn=True)) 129 | print "{} {} {}".format(x, p - origins[x], ord(dest[0])+ord(dest[1])) 130 | pieces[x] = strmod(pieces[x], p - origins[x], dest) 131 | #pieces[target_piece] = pieces[target_piece][:remainder] + chr((dest & 0xFF00) >> 8) + chr(dest & 0xFF) + pieces[target_piece][remainder+2:] 132 | 133 | try: 134 | fout = open(source + "00", 'wb') 135 | except IOError: 136 | raw_input("Couldn't open output file '{}'. Press enter to quit. ".format(fn)) 137 | quit() 138 | for c in channels: 139 | fout.write(bytes(convert_jump(c))) 140 | fout.close() 141 | 142 | scribe = StringIO.StringIO() 143 | asterisks = list("!@#$%^&*+=") 144 | stars = asterisks[:] 145 | random.shuffle(asterisks) 146 | 147 | for i, s in enumerate(pieces): 148 | #record it 149 | ppos, xpos = 0, 0 150 | noteline, starline, headline = "", "", "" 151 | fxlines = [" "] 152 | measure = "|..........." + "-..........." * 3 153 | star = "*" 154 | lastcode = 0 155 | nest = 0 156 | looping, lpos = [0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0] 157 | while ppos < len(s): 158 | byte = ord(s[ppos]) 159 | if byte <= 0xC3: 160 | note = notes[int(byte / 14)] 161 | writepos = int(xpos / 4) 162 | xpos += lengths[byte % 14] 163 | noteline = strmod(noteline, writepos, note) 164 | elif byte in codes: 165 | writepos = int(xpos/4) 166 | e = ppos + codes[byte][0] 167 | args = "" 168 | while ppos < e: 169 | ppos += 1 170 | argval = ord(s[ppos]) 171 | args = args + " " + hex(argval) 172 | if writepos != lastcode: 173 | if not stars: stars = asterisks[:] 174 | star = stars.pop() 175 | starline = strmod(starline, writepos, star) 176 | mycode = codes[byte][1] 177 | if mycode == "LoopEnd" and looping[nest]: mycode = "Loop{}".format([l for l in looping if l > 0]) 178 | fxtxt = star + mycode + args 179 | for ix, l in enumerate(fxlines): 180 | nextline = False 181 | while len(fxlines[ix]) < writepos + len(fxtxt): 182 | fxlines[ix] = fxlines[ix] + " " 183 | if len(set(fxlines[ix][writepos:writepos+len(fxtxt)])) > 1: nextline = True 184 | if not nextline: 185 | fxlines[ix] = strmod(fxlines[ix], writepos, fxtxt) 186 | break 187 | lastcode = writepos 188 | if byte == 0xE2: 189 | nest += 1 190 | looping[nest] = argval 191 | lpos[nest] = ppos 192 | elif byte == 0xE3 and nest: 193 | if looping[nest] > 0: 194 | looping[nest] -= 1 195 | ppos = lpos[nest] 196 | else: 197 | looping[nest] = 0 198 | lpos[nest] = 0 199 | nest -= 1 200 | ppos += 1 201 | if len(set(fxlines[-1])) > 1: fxlines.append(" ") 202 | if (len(headline) <= len(noteline)) or (len(headline) <= len(starline)): 203 | headline = headline + measure 204 | headline = strmod(headline, writepos, hex(ppos)[2:]) 205 | while len(headline) < len(noteline): 206 | headline = headline + measure 207 | headline = strmod(headline, 0, "** SEGMENT {}".format(i+1)) 208 | scribe.write(headline + "\n") 209 | scribe.write(noteline + "\n") 210 | scribe.write(starline + "\n") 211 | for l in fxlines: scribe.write(l + "\n") 212 | 213 | #write it 214 | if len(s): 215 | fn = source + "%02d" % (i+1) 216 | try: 217 | fout = open(fn, 'wb') 218 | except IOError: 219 | raw_input("Couldn't open output file '{}'. Press enter to quit. ".format(fn)) 220 | quit() 221 | fout.write(s) 222 | fout.close() 223 | 224 | # calculate channel duration pre- and post-loop 225 | for i, c in enumerate(channels): 226 | startpoint = convert_jump(c) 227 | seg = startpoint >> 12 228 | ppos = startpoint & 0xFFF 229 | xpos = 0 230 | nest = 0 231 | jumppoint, endpoint = 0, 0 232 | looping, lpos = [0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0] 233 | jdescr, edescr = "", "" 234 | keepgoing = True 235 | while keepgoing: 236 | if len(pieces) <= seg: 237 | keepgoing = False 238 | break 239 | if ppos >= len(pieces[seg]): 240 | keepgoing = False 241 | if jumppoint: 242 | edescr += "Ended without re-jumping at " 243 | endpoint = xpos 244 | else: 245 | jdescr += "Ended without jumping at " 246 | jumppoint = xpos 247 | break 248 | byte = ord(pieces[seg][ppos]) 249 | if byte <= 0xC3: 250 | xpos += lengths[byte % 14] 251 | elif byte == 0xE2: 252 | ppos += 1 253 | nest += 1 254 | looping[nest] = ord(pieces[seg][ppos]) 255 | lpos[nest] = ppos 256 | elif byte == 0xE3 and nest: 257 | looping[nest] -= 1 258 | ppos = lpos[nest] 259 | if looping[nest] <= 0: 260 | looping[nest] = 0 261 | lpos[nest] = 0 262 | nest -= 1 263 | elif byte == 0xF6: 264 | if not jumppoint: 265 | jdescr += "First jump occurs after " 266 | jumppoint = xpos 267 | args = [ord(pieces[seg][ppos+1]), ord(pieces[seg][ppos+2])] 268 | print map(hex, args) 269 | seg = args[0] >> 4 270 | ppos = args[1] + ((args[0] & 0xF) << 8) 271 | continue 272 | else: 273 | edescr += "Loop lasts " 274 | endpoint = xpos - jumppoint 275 | keepgoing = False 276 | elif byte in codes: 277 | ppos += codes[byte][0] 278 | ppos += 1 279 | print "CHANNEL {}:".format(i+1) 280 | print jdescr + "{} frames / {} beats / {} measures / {} phrases".format(jumppoint, jumppoint/48, jumppoint/192, jumppoint/768) 281 | print edescr + "{} frames / {} beats / {} measures / {} phrases".format(endpoint, endpoint/48, endpoint/192, endpoint/768) 282 | print 283 | 284 | 285 | try: 286 | sout = open(source + ".txt", 'w') 287 | sout.write(scribe.getvalue()) 288 | sout.close() 289 | except IOError: 290 | print scribe 291 | 292 | scribe.close() 293 | 294 | for p in sorted(jumppoints): print "{} {} {}".format(hex(p), hex(jumppoints[p]), hex(convert_jump(jumppoints[p]))) 295 | raw_input("Press enter to quit. ") 296 | -------------------------------------------------------------------------------- /experimental/sxc2mml.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys, os, itertools 3 | 4 | DEBUG_WRITE_FULL_HEX = False 5 | DEBUG_SUPER_VERBOSE_NOTES = False 6 | CONFIG_NOTE_LENGTH_COMPENSATION = True 7 | 8 | def clean_end(): 9 | print("Processing ended.") 10 | input("Press enter to close.") 11 | quit() 12 | 13 | def write_hex(bin): 14 | s = "" 15 | for b in bin: 16 | s += f"{b:02X} " 17 | return s.strip() 18 | 19 | def specify_note_duration(note, dur): 20 | target_note_table = [0xC0, 0x60, 0x40, 0x48, 0x30, 0x20, 0x24, 0x18, 0x10, 0x0C, 0x08, 0x06, 0x04, 0x03] 21 | ff6_duration_table = ["1", "2", "3", "4.", "4", "6", "8.", "8", "12", "16", "24", "32", "48", "64"] 22 | key = {target_note_table[i]: ff6_duration_table[i] for i in range(len(target_note_table))} 23 | 24 | if not dur: 25 | return "" 26 | 27 | solution = [] 28 | if dur in target_note_table: 29 | solution = [dur] 30 | target_note_table = [t for t in target_note_table if t <= dur] 31 | if not solution: 32 | for c in itertools.combinations(target_note_table, 2): 33 | if sum(c) == dur: 34 | solution = c 35 | break 36 | # if CONFIG_EXPAND_NOTES_TO_THREE and not solution: 37 | # for c in itertools.combinations(target_note_table, 3): 38 | # if sum(c) == dur: 39 | # solution = c 40 | # break 41 | if solution: 42 | text = "" 43 | for i, s in enumerate(solution): 44 | text += "^" if i else f"{note}" 45 | text += f"{key[s]}" 46 | else: 47 | text = f"&{dur}{note}" 48 | 49 | return text 50 | 51 | command_length_table_SFC = { 52 | 1: [0xFB], 53 | 2: [0xF3, 0xFC], 54 | 3: [0xF0, 0xF1, 0xF2, 0xF4, 0xF5, 0xF7, 0xF8, 0xF9, 0xFA, 0xFE], 55 | 4: [0xF6] 56 | } 57 | command_length_table_S2C = { 58 | 1: [0xFB], 59 | 2: [0xF3, 0xFC], 60 | 3: [0xF0, 0xF1, 0xF2, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFE], 61 | 4: [] 62 | } 63 | command_length_table = command_length_table_SFC 64 | 65 | def handle_pattern(ptr): 66 | loc = ptr 67 | pdata = b"" 68 | line = "" 69 | while True: 70 | #print(f"DEBUG: reading sequence at {loc:04X}") 71 | cmd = data[loc] 72 | if DEBUG_SUPER_VERBOSE_NOTES: 73 | print(f"{write_hex(data[loc:loc+4])} > ", end="") 74 | if cmd == 0xFD: # end pattern 75 | pdata = data[ptr:loc+1] 76 | break 77 | elif cmd in range(0x60): # note with parameters 78 | line += handle_full_note(data[loc:loc+4]) 79 | loc += 4 80 | elif cmd in range(0x80, 0xE0): # note without parameters 81 | line += handle_note(data[loc] - 0x80, state_delta, state_length, state_velocity) 82 | loc += 1 83 | elif cmd == 0xFF: # meta commands 84 | line += handle_meta_command(data[loc+1], data[loc+2]) 85 | loc += 3 86 | elif cmd in command_length_table[1]: # 1 byte commands 87 | line += handle_command(cmd, bytes([data[loc]])) 88 | loc += 1 89 | elif cmd in command_length_table[2]: # 2 byte commands 90 | line += handle_command(cmd, data[loc:loc+2]) 91 | loc += 2 92 | elif cmd in command_length_table[3]: # 3 byte commands & unknown 93 | line += handle_command(cmd, data[loc:loc+3]) 94 | loc += 3 95 | elif cmd in command_length_table[4]: # 4 byte commands 96 | line += handle_command(cmd, data[loc:loc+4]) 97 | loc += 4 98 | elif cmd in range(0x60, 0x80): # unknown 99 | print(f"unknown bytecode {write_hex(data[loc:loc+4])}") 100 | line += f"'{write_hex(data[loc:loc+4])}'" 101 | loc += 4 102 | else: # unknown 103 | print(f"unknown bytecode {data[loc]:02X}") 104 | line += f"'{data[loc]:02X}'" 105 | loc += 1 106 | if DEBUG_WRITE_FULL_HEX: 107 | line = f"## {write_hex(pdata)}\n" + line 108 | return line 109 | 110 | def get_note_key(key): 111 | note_table = ["c", "c+", "d", "d+", "e", "f", "f+", "g", "g+", "a", "a+", "b"] 112 | if key < 0: 113 | return "^" 114 | return note_table[key % 12] 115 | 116 | def handle_full_note(code): 117 | return " " + handle_note(code[0], code[1], code[2], code[3]) 118 | 119 | def handle_note(key, delta, length, velocity): 120 | global state_octave, state_delta, state_length, state_last_key, state_velocity, state_output_volume, state_slur, state_retrigger, state_remainder, DEBUG_SUPER_VERBOSE_NOTES 121 | 122 | key += pattern_transpose 123 | 124 | octave = (key // 12) 125 | note = get_note_key(key) 126 | 127 | if DEBUG_SUPER_VERBOSE_NOTES: 128 | print(note + f" {delta=} {length=}", end=" :: ") 129 | 130 | before_this_note_text = "" 131 | after_this_note_text = "" 132 | if state_last_key == key and state_remainder >= 0 and not state_retrigger: 133 | note = "^" 134 | if state_slur and state_remainder < 0: 135 | before_this_note_text += ')' 136 | state_slur = False 137 | if length > delta: 138 | state_last_key = key 139 | if not state_slur: 140 | before_this_note_text += '(' 141 | state_slur = True 142 | else: 143 | state_last_key = -1 144 | if state_slur: 145 | after_this_note_text += ')' 146 | state_slur = False 147 | state_retrigger = False 148 | 149 | state_length = length 150 | state_delta = delta 151 | state_remainder = max(-1, length - delta) 152 | if CONFIG_NOTE_LENGTH_COMPENSATION and length - delta <= 3: 153 | if length / delta >= 0.75: 154 | length = delta 155 | length = min(length, delta) 156 | 157 | note_text = before_this_note_text + specify_note_duration(note, length) + after_this_note_text 158 | if delta - length: 159 | note_text += specify_note_duration("r", delta - length) 160 | 161 | if octave == state_octave: 162 | octave_text = "" 163 | elif octave == state_octave + 1: 164 | octave_text = "<" 165 | state_octave += 1 166 | elif octave == state_octave - 1: 167 | octave_text = ">" 168 | state_octave -= 1 169 | else: 170 | octave_text = f"o{octave} " 171 | state_octave = octave 172 | 173 | output_volume = int(velocity * (1 + state_volume) / 128) 174 | if output_volume != state_output_volume: 175 | volume_text = f"v{output_volume}" 176 | state_output_volume = output_volume 177 | else: 178 | volume_text = "" 179 | state_velocity = velocity 180 | 181 | if DEBUG_SUPER_VERBOSE_NOTES: 182 | print(octave_text + volume_text + note_text + f" :: {state_delta=} {state_length=} {state_last_key=} {state_slur=} {state_remainder=}") 183 | if input() == "end": 184 | DEBUG_SUPER_VERBOSE_NOTES = False 185 | 186 | return octave_text + volume_text + note_text 187 | 188 | def handle_command(cmd, code): 189 | global state_volume, state_output_volume, state_octave, state_retrigger, state_remainder, state_last_key 190 | note = "^" if state_remainder > 0 else "r" 191 | if cmd == 0xF1: # track volume 192 | state_volume = code[2] 193 | text = "" 194 | if state_remainder > 0: 195 | output_volume = int(state_velocity * (1 + state_volume) / 128) 196 | if output_volume != state_output_volume: 197 | text += f"v{output_volume}" 198 | state_output_volume = output_volume 199 | elif cmd == 0xF2: # pan 200 | text = f"p{code[2]}" 201 | elif cmd == 0xF3: # rest 202 | text = "" 203 | elif cmd == 0xF6 and format == "S2C": # detune 204 | text = f"k{code[2]-0x80}" 205 | note = get_note_key(state_last_key) 206 | #if not code[1]: 207 | # state_retrigger = True 208 | elif cmd == 0xF7: # program change 209 | used_programs.add(code[2]) 210 | text = f"'Prog{code[2]:02X}'" 211 | elif cmd == 0xFB: # loop start 212 | loopid = len(used_loops) 213 | state_loops.append(loopid) 214 | used_loops.add(loopid) 215 | state_octave = -1 216 | text = f"'Loop{loopid:02}'" 217 | elif cmd == 0xFC: # loop end 218 | if state_loops: 219 | loopid = state_loops.pop() 220 | if code[1]: 221 | placeholders[f"'Loop{loopid:02}'"] = f"[{code[1]}" 222 | text = "]" 223 | else: 224 | placeholders[f"'Loop{loopid:02}'"] = "$" 225 | text = ";\n" 226 | else: 227 | text = "" 228 | else: 229 | text = f"'CMD {write_hex(code)}' " 230 | try: 231 | if len(code) >= 3 or cmd == 0xF3: 232 | text += specify_note_duration(note, code[1]) 233 | state_remainder = max(-1, state_remainder - code[1]) 234 | return text 235 | except UnboundLocalError: 236 | print(f"ERROR: UnboundLocalError in handle_command {write_hex(code)}") 237 | return f"'ULE {write_hex(code)}'" 238 | 239 | 240 | def handle_meta_command(cmd, param): 241 | if cmd == 0x01: # EFB 242 | return f"%b0,{param}" 243 | elif cmd == 0x03: # enable echo 244 | return "%e1" 245 | elif cmd == 0x04: # disable echo 246 | return "%e0" 247 | elif cmd == 0x0B: # ADSR sustain rate (R) 248 | return f"%r{param}" 249 | else: 250 | return f"'META {cmd:02X}:{param}'" 251 | 252 | if __name__ == "__main__": 253 | print("mfvitools Neverland SFC/S2C to MML converter") 254 | 255 | if len(sys.argv) >= 2: 256 | fn = sys.argv[1] 257 | else: 258 | print("Enter Neverland SPC filename..") 259 | fn = input(" > ").replace('"','').strip() 260 | 261 | try: 262 | with open(fn, 'rb') as f: 263 | spc = f.read() 264 | except IOError: 265 | print("Error reading file {}".format(fn)) 266 | clean_end() 267 | 268 | spc = spc[0x100:] 269 | mml = [] 270 | 271 | print() 272 | for loc in range(0x10000): 273 | format = None 274 | if spc[loc:loc+3] == b"SFC": 275 | format = "SFC" 276 | elif spc[loc:loc+3] == b"S2C": 277 | format = "S2C" 278 | command_length_table = command_length_table_S2C 279 | if format: 280 | name = spc[loc+4:loc+16] 281 | timecode = spc[loc+3] 282 | print(f"Possible match found: {format} at 0x{loc:04X} - {name}") 283 | print("Type 'n' to skip, or press enter to accept") 284 | entry = input(" > ").lower() 285 | if entry and entry[0] == "n": 286 | continue 287 | else: 288 | break 289 | if not format: 290 | print("No sequence found. Exiting.") 291 | clean_end() 292 | 293 | if format == "SFC": 294 | data = spc 295 | head = loc 296 | else: 297 | data = spc[loc:] 298 | head = 0 299 | used_programs = set() 300 | used_loops = set() 301 | placeholders = {} 302 | 303 | tracks = [] 304 | for t in range(8): 305 | tracks.append(int.from_bytes(data[head + 0x20 + t*2 : head + 0x20 + t*2 + 2], "little")) 306 | 307 | # tempo (est.) 308 | # BERSERKER 0x35 (53) ~= 190bpm ~= 315.8 ms/beat ~= 6.58 ms/tick 309 | # PRAYER BELLS 0x4a (74) ~= 135bpm ~= 444.4 ms/beat ~= 9.26 ms/tick 310 | # PRIPHEA (L2) 0x64 (100) ~= 100bpm ~= 600 ms/beat ~= 12.5 ms/tick 311 | # CALMING DAYS 0x76 (118) ~= 90bpm ~= 666.7 ms/beat ~= 13.89 ms/tick 312 | # estimate: timecode * 6 = ms/beat 313 | tempo = int((1 / (timecode * 6)) * 1000 * 60) 314 | 315 | for track in range(8): 316 | mml.append(f"{{Track {track+1}}}") 317 | if track == 0: 318 | mml.append(f"t{tempo}") 319 | pattern_loc = tracks[track] 320 | 321 | state_octave = -1 322 | state_volume = 0 323 | state_program = 0 324 | state_delta = 0 325 | state_length = 0 326 | state_remainder = -1 327 | state_velocity = 0 328 | state_output_volume = 0 329 | state_last_key = -1 330 | state_loops = [] 331 | state_slur = False 332 | state_retrigger = False 333 | 334 | while pattern_loc <= len(data) - 3: 335 | pattern_transpose = 0 336 | if data[pattern_loc] == 0xFF: 337 | mml.append(";\n") 338 | break 339 | if data[pattern_loc] >= 0x80: 340 | pattern_transpose = data[pattern_loc] - 0x80 341 | #print(f"DEBUG: transposing next pattern by {pattern_transpose}") 342 | pattern_loc += 1 343 | #print(f"DEBUG: reading pattern at {pattern_loc:04X}") 344 | pattern_ptr = int.from_bytes(data[pattern_loc:pattern_loc + 2], "big") 345 | mml.append(f"{{'{pattern_ptr:X}'}}" + handle_pattern(pattern_ptr)) 346 | pattern_loc += 2 347 | 348 | for k, v in placeholders.items(): 349 | for i, line in enumerate(mml): 350 | mml[i] = line.replace(k, v) 351 | 352 | prepend = [""] 353 | for i, p in enumerate(used_programs): 354 | prepend.append(f"#WAVE 0x{0x20+i:02X} 0x00 # program {p:02X}") 355 | prepend.append("") 356 | for i, p in enumerate(used_programs): 357 | prepend.append(f"#def Prog{p:02X}= |{i:X} %k12") 358 | prepend.append("\n#cdef ( %l1\n#cdef ) %l0\n") 359 | mml = prepend + mml 360 | 361 | fn = fn.rpartition('.')[0] + '.mml' 362 | try: 363 | with open(fn, 'w') as f: 364 | f.write("\n".join(mml)) 365 | except IOError: 366 | print("Error writing file {}".format(fn)) 367 | clean_end() 368 | 369 | clean_end() -------------------------------------------------------------------------------- /mfvitrace.py: -------------------------------------------------------------------------------- 1 | try: 2 | import mfvitbl 3 | except ImportError: 4 | from . import mfvitbl 5 | 6 | # Multiply total length by this amount to compensate for any slowdown, etc 7 | FUDGE_FACTOR = 1.005 8 | MAX_TICKS = 255 * 48 * 999 9 | VERBOSE = False 10 | 11 | def measure(ticks): 12 | measures = ticks // 192 13 | beats = 1 + (ticks % 192) // 48 14 | ticks = (ticks % 192) % 48 15 | return f"{measures}.{beats}.{ticks}" 16 | 17 | class TrackState(): 18 | def __init__(self, id, data, start, addr_base): 19 | self.addr_base = addr_base 20 | self.loc = self.addr(start) 21 | self.stopped = False 22 | self.data = data 23 | self.segment = 0 24 | self.ticks = 0 25 | self.delta = 0 26 | self.id = id 27 | 28 | self.stack = [] 29 | self.jump_records = {} 30 | self.last_new_jump = None 31 | self.jump_records[None] = [] 32 | 33 | def stop(self): 34 | self.stopped = True 35 | self.delta = 0 36 | self.stack = [] 37 | 38 | def get_state(self): 39 | return (self.loc, self.stopped, self.delta, list(self.stack)) 40 | 41 | def tick(self): 42 | if not self.stopped: 43 | self.delta -= 1 44 | self.ticks += 1 45 | if self.delta < 0: 46 | print("somehow, delta < 0") 47 | if self.delta == 0: 48 | self.loc += 1 49 | 50 | def addr(self, address): 51 | # Convert a raw address to the equivalent index in "data" 52 | address -= self.addr_base 53 | if address < 0: 54 | address += 0x10000 55 | return address + 0x24 56 | 57 | def acquire_delta(self): 58 | # Advance the track pointer until the next event which has a 59 | # nonzero duration (delta-time). 60 | # Also, return any global tempo changes encountered during that time. 61 | tempo_changes = [None, None, None] 62 | if self.stopped or self.delta > 0: 63 | return tempo_changes 64 | 65 | while True: 66 | loc = self.loc 67 | bytecode = self.data[loc] 68 | if bytecode < 0xC4: 69 | if not self.delta: 70 | self.delta = mfvitbl.lengths[bytecode % 14] 71 | #print(f"[{self.id}] {bytecode:02X} -> delta {self.delta}") 72 | return tempo_changes 73 | #print(f"[{self.id}] {self.loc+0x1C02:04X} {bytecode:02X}") 74 | 75 | cmdlen = 1 76 | if bytecode in mfvitbl.codes: 77 | cmdlen += mfvitbl.codes[bytecode][0] 78 | next = loc + cmdlen 79 | 80 | if bytecode == 0xE2: 81 | # Loop start 82 | count = 1 83 | repeats = self.data[loc+1] 84 | target = self.loc + 2 85 | self.stack.append((count, repeats, target)) 86 | #print(f"[{self.id}] loop start") 87 | 88 | if len(self.stack) > 4: 89 | print(f"[{self.id}] {self.loc+0x1C02:04X} WARNING: Loop stack overflow") 90 | self.stack = self.stack[-4:] 91 | 92 | elif bytecode == 0xE3: 93 | # Loop end 94 | count, repeats, target = self.stack.pop() 95 | repeats -= 1 96 | count += 1 97 | if repeats >= 0: 98 | next = target 99 | self.stack.append((count, repeats, target)) 100 | #print(f"[{self.id}] loop end") 101 | 102 | elif bytecode in [0xEB, 0xEC, 0xED, 0xEE, 0xEF, 0xFD, 0xFE, 0xFF]: 103 | # End track 104 | self.stop() 105 | return tempo_changes 106 | 107 | elif bytecode == 0xE8: 108 | # Set next note duration 109 | self.delta = self.data[loc+1] 110 | 111 | elif bytecode == 0xF0: 112 | # Set tempo 113 | tempo_changes[0] = self.data[loc+1] 114 | 115 | elif bytecode == 0xF1: 116 | # Tempo fade 117 | tempo_changes[1] = self.data[loc+1] 118 | tempo_changes[2] = self.data[loc+2] 119 | 120 | elif bytecode == 0xF5 and self.stack: 121 | # Loop break / volta 122 | condition = self.data[loc+1] 123 | vtarget = self.addr(int.from_bytes(self.data[loc+2:loc+4], "little")) 124 | count, repeats, ltarget = self.stack.pop() 125 | if condition == count: 126 | next = vtarget 127 | if condition != count or repeats > 0: 128 | self.stack.append((count, repeats, ltarget)) 129 | if VERBOSE: 130 | print(f"[{self.id}] {self.loc+0x1C02:04X} volta {count}/{condition} :: {self.stack}") 131 | if condition == count: 132 | print(f" Jumping to {vtarget+0x1C02:04X}") 133 | 134 | elif bytecode == 0xF6: 135 | # Jump 136 | target = self.addr(int.from_bytes(self.data[loc+1:loc+3], "little")) 137 | next = target 138 | if VERBOSE: 139 | print(f"[{self.id}] {self.loc+0x1C02:04X} jump to {target+0x1C02:04X}") 140 | print(f" {self.ticks - self.segment} ticks since start or last jump") 141 | self.segment = self.ticks 142 | 143 | jump_record = (loc, target, tuple(self.stack)) 144 | #print(jump_record) 145 | 146 | if jump_record in self.jump_records: 147 | self.jump_records[jump_record].append(self.ticks) 148 | else: 149 | self.jump_records[jump_record] = [self.ticks] 150 | self.last_new_jump = jump_record 151 | #print(f"{self.jump_records=}") 152 | 153 | self.loc = next 154 | 155 | 156 | # Read an AKAO4 (FF6) binary sequence and return its approximate length in seconds 157 | def mfvi_trace(data, iterations=2, long_header=False): 158 | if long_header: 159 | data = data[2:] 160 | 161 | addr_base = int.from_bytes(data[0:2], "little") 162 | addr_end = int.from_bytes(data[2:4], "little") 163 | tracks = [] 164 | tempo_sets = {} 165 | tempo_fades = {} 166 | loop_ticks = {} 167 | loop_lengths = {} 168 | initial_segments = {} 169 | for trackid in range(8): 170 | loc = 4 + trackid * 2 171 | addr = int.from_bytes(data[loc:loc+2], "little") 172 | track = TrackState(trackid+1, data, addr, addr_base) 173 | if addr == addr_end: 174 | track.stop() 175 | tracks.append(track) 176 | 177 | for track in tracks: 178 | ticks = 0 179 | tempo_sets[track.id] = {} 180 | tempo_fades[track.id] = {} 181 | while ticks < MAX_TICKS: 182 | tempo_cmds = track.acquire_delta() 183 | 184 | # Test if we've encountered the endpoint 185 | if track.stopped or len(track.jump_records[track.last_new_jump]) >= iterations: 186 | if track.stopped: 187 | segment = 0 188 | elif iterations < 2: 189 | segment = track.jump_records[track.last_new_jump][0] 190 | else: 191 | segment = track.jump_records[track.last_new_jump][-1] - track.jump_records[track.last_new_jump][-2] 192 | print(f"breaking track {track.id} at {ticks} with delta {segment} ({measure(segment)})") 193 | loop_ticks[track.id] = segment 194 | loop_lengths[track.id] = segment 195 | initial_segments[track.id] = ticks - segment 196 | break 197 | 198 | # Handle tempo changes 199 | if tempo_cmds[0]: 200 | tempo_sets[track.id][ticks] = tempo_cmds[0] 201 | if tempo_cmds[2]: 202 | tempo_fades[track.id][ticks] = (tempo_cmds[1], tempo_cmds[2]) 203 | 204 | # Advance time 205 | ticks += 1 206 | track.tick() 207 | 208 | # Extend loops until everything is in phase 209 | longest_ticks = max(loop_ticks.values()) 210 | while longest_ticks < MAX_TICKS: 211 | shorter_track_found = False 212 | for id in loop_ticks: 213 | while 0 < loop_ticks[id] < longest_ticks: 214 | loop_ticks[id] += loop_lengths[id] 215 | longest_ticks = max(loop_ticks[id], longest_ticks) 216 | shorter_track_found = True 217 | print(f" {loop_ticks[id]:5} ", end="") 218 | print() 219 | if not shorter_track_found: 220 | print(longest_ticks) 221 | break 222 | total_ticks = longest_ticks + max(initial_segments.values()) 223 | print(f"{total_ticks=}") 224 | 225 | # Figure out tempo over all ticks 226 | # first, pull out tempo commands onto a unified timeline 227 | tempo_timeline = {} 228 | for id in tempo_sets: 229 | for tick, tempo in tempo_sets[id].items(): 230 | tempo_timeline[tick] = (tempo, None, None) 231 | if tick >= initial_segments[id]: 232 | vtick = tick + loop_lengths[id] 233 | while vtick < total_ticks: 234 | tempo_timeline[tick] = (tempo, None, None) 235 | vtick += loop_lengths[id] 236 | for id in tempo_fades: 237 | for tick, (dur, target) in tempo_fades[id].items(): 238 | tempo = tempo_timeline[tick][0] if tick in tempo_timeline else None 239 | tempo_timeline[tick] = (tempo, dur, target) 240 | if tick >= initial_segments[id]: 241 | vtick = tick + loop_lengths[id] 242 | while vtick < total_ticks: 243 | tempo = tempo_timeline[tick][0] if tick in tempo_timeline else None 244 | tempo_timeline[tick] = (tempo, dur, target) 245 | vtick += loop_lengths[id] 246 | for k in sorted(tempo_timeline): 247 | print(f"{k:5}:: {tempo_timeline[k]}") 248 | 249 | tempo = 0 250 | tempo_increment = 0 251 | tick_length = 1.0 252 | tempo_target = None 253 | duration = 0.0 254 | for ticks in range(total_ticks): 255 | prev_tempo = tempo 256 | if tempo_target: 257 | tempo += tempo_increment 258 | if ((tempo_increment > 0 and tempo >= tempo_target) or 259 | (tempo_increment < 0 and tempo <= tempo_target)): 260 | tempo = tempo_target 261 | tempo_target = None 262 | tempo_increment = 0 263 | if ticks in tempo_timeline: 264 | tt, dur, target = tempo_timeline[ticks] 265 | if tt: 266 | tempo = tt 267 | if target: 268 | tempo_increment = (target - tempo) / dur 269 | tempo_target = target 270 | if prev_tempo != tempo: 271 | bpm = 60000000.0 / (48 * (125 * 0x27)) * (tempo / 256.0) 272 | tick_length = 1 / (bpm * 48 / 60) 273 | duration += tick_length 274 | print(duration) 275 | return min(duration, 999) 276 | 277 | # Read an AKAO4 (FF6) binary sequence and return its approximate length in seconds 278 | # (the time it takes to reach an identical state for the nth time) 279 | def _mfvi_trace(data, iterations=2, long_header=False): 280 | if long_header: 281 | data = data[2:] 282 | 283 | addr_base = int.from_bytes(data[0:2], "little") 284 | addr_end = int.from_bytes(data[2:4], "little") 285 | tick = 0 286 | tracks = [] 287 | tick_tempos = {} 288 | states = [] 289 | loops_found = 0 290 | tempo = 1 291 | tempo_increment = 0 292 | tempo_target = None 293 | song_length = 0 294 | prev_tempo = 0 295 | tick_length = 0 296 | for trackid in range(8): 297 | loc = 4 + trackid * 2 298 | addr = int.from_bytes(data[loc:loc+2], "little") 299 | track = TrackState(trackid+1, data, addr, addr_base) 300 | if addr == addr_end: 301 | track.stop() 302 | tracks.append(track) 303 | 304 | while tick < MAX_TICKS: 305 | # Advance each track until delta is nonzero 306 | for track in tracks: 307 | tempo_changes = track.acquire_delta() 308 | 309 | # Handle tempo changes 310 | new_tempo, tempo_fade, tempo_new_target = tempo_changes 311 | if new_tempo: 312 | tempo = new_tempo 313 | tick_tempos[tick] = tempo 314 | tempo_target = None 315 | if tempo_target: 316 | tempo += tempo_increment 317 | if ((tempo_target <= tempo and tempo_increment >= 0) or 318 | (tempo_target >= tempo and tempo_increment <= 0)): 319 | tempo = tempo_target 320 | tempo_target = None 321 | tick_tempos[tick] = tempo 322 | if tempo_fade: 323 | tempo_target = tempo_new_target 324 | tempo_increment = (tempo_new_target - tempo) / tempo_fade 325 | 326 | # Compare and record states once everything has a delta 327 | state = [tempo, tempo_increment, tempo_target] 328 | state += [track.get_state() for track in tracks] 329 | if state in states: 330 | loops_found += 1 331 | if loops_found >= iterations: 332 | print(tick_tempos) 333 | break 334 | states = [] 335 | if VERBOSE: 336 | print(f"{tick} {state}") 337 | else: 338 | if tick % (192 * 32) == 0: 339 | print() 340 | if tick % (192 * 8) == 0: 341 | print(" ", end="", flush=True) 342 | if tick % 192 == 0: 343 | print(".", end="", flush=True) 344 | 345 | states.append(state) 346 | tick += 1 347 | for track in tracks: 348 | track.tick() 349 | 350 | # Calc tempo and increment duration 351 | if tempo != prev_tempo: 352 | print(f"Tempo change at {tick} to {tempo}") 353 | # tempo formula from https://github.com/vgmtrans/vgmtrans/blob/master/src/main/formats/AkaoSnesSeq.cpp 354 | bpm = 60000000.0 / (48 * (125 * 0x27)) * (tempo / 256.0) 355 | tick_length = 1 / (bpm * 48 / 60) 356 | print(f"{bpm=} {tick_length=}") 357 | prev_tempo = tempo 358 | song_length += tick_length * FUDGE_FACTOR 359 | if song_length > 999: 360 | break 361 | 362 | # After tracing entire song, recalculate tick lengths 363 | song_length = 0 364 | tempo = 1 365 | for i in range(tick): 366 | if i in tick_tempos: 367 | tempo = tick_tempos[i] 368 | print(f"Tempo change at {i} to {tempo}") 369 | # tempo formula from https://github.com/vgmtrans/vgmtrans/blob/master/src/main/formats/AkaoSnesSeq.cpp 370 | bpm = 60000000.0 / (48 * (125 * 0x27)) * (tempo / 256.0) 371 | tick_length = 1 / (bpm * 48 / 60) 372 | print(f"{bpm=} {tick_length=}") 373 | song_length += tick_length * FUDGE_FACTOR 374 | print(f"Song length {int(song_length // 60)}:{round(song_length % 60):02} ({song_length} sec.)") 375 | 376 | return min(song_length, 999) 377 | 378 | if __name__ == "__main__": 379 | import sys 380 | 381 | with open(sys.argv[1], "rb") as f: 382 | spc = f.read() 383 | 384 | data = spc[0x1D00:0x4900] 385 | mfvi_trace(data) 386 | -------------------------------------------------------------------------------- /brr2sf2.py: -------------------------------------------------------------------------------- 1 | ## bulk BRR-to-SF2 converter 2 | ## expands waveforms beyond their original length to 3 | ## properly capture the effect BRR filters have on looping 4 | ## made with reference to vgmtrans & brrtools source 5 | ## and BRR docs/code on wiki.superfamicom.org 6 | 7 | import os, sys, configparser, traceback, re 8 | from math import log, modf 9 | import mml2mfvi 10 | 11 | SAMPLE_EXTRA_ITERATIONS = 1 12 | SAMPLE_MIN_SIZE = 4000 13 | USE_ID_IN_NAMES = False 14 | USE_LISTFILE_TRANSPOSE = True 15 | SORT_BY_BLOCKSIZE = False 16 | 17 | # SPC700 sustain level -> conventional sf2 units (1 = 0.04dB) 18 | attenuation_table = { 19 | 7: 0, 20 | 6: 29, 21 | 5: 63, 22 | 4: 102, 23 | 3: 150, 24 | 2: 213, 25 | 1: 301, 26 | 0: 452, 27 | -1: 2500 } 28 | 29 | ATTEN_SILENCE = 2500 30 | 31 | attack_table = { 32 | 0: 4100, 33 | 1: 2600, 34 | 2: 1500, 35 | 3: 1000, 36 | 4: 640, 37 | 5: 380, 38 | 6: 260, 39 | 7: 160, 40 | 8: 96, 41 | 9: 64, 42 | 10: 40, 43 | 11: 24, 44 | 12: 16, 45 | 13: 10, 46 | 14: 6, 47 | 15: 0 } 48 | 49 | decay_table = { 50 | 0: 1200, 51 | 1: 740, 52 | 2: 440, 53 | 3: 290, 54 | 4: 180, 55 | 5: 110, 56 | 6: 74, 57 | 7: 37 } 58 | 59 | release_table = { 60 | 1: 38000, 61 | 2: 38000, 62 | 3: 24000, 63 | 4: 19000, 64 | 5: 14000, 65 | 6: 12000, 66 | 7: 9400, 67 | 8: 7100, 68 | 9: 5900, 69 | 10: 4700, 70 | 11: 3500, 71 | 12: 2900, 72 | 13: 2400, 73 | 14: 1800, 74 | 15: 1500, 75 | 16: 1200, 76 | 17: 880, 77 | 18: 740, 78 | 19: 590, 79 | 20: 440, 80 | 21: 370, 81 | 22: 290, 82 | 23: 220, 83 | 24: 180, 84 | 25: 150, 85 | 26: 110, 86 | 27: 92, 87 | 28: 74, 88 | 29: 55, 89 | 30: 37, 90 | 31: 28 } 91 | 92 | pre = 0 93 | prepre = 0 94 | 95 | clamp = lambda nmin, n, nmax: nmin if n < nmin else (nmax if n > nmax else n) 96 | 97 | def text_clamp(text, length): 98 | if len(text) > length: 99 | text = text[:length] 100 | text = bytearray(text, "latin-1") 101 | while len(text) < length: 102 | text += b'\x00' 103 | return text 104 | 105 | class BrrSample: 106 | def __init__(self, idx, text): 107 | # parse text - from insertmfvi "Sample.init_from_listfile" 108 | text = [s.strip() for s in text.split(',')] 109 | if not text: 110 | return None 111 | self.filename = os.path.join(listpath, text[0]) 112 | try: 113 | looptext = text[1].lower().strip() 114 | except IndexError: 115 | looptext = "0000" 116 | print(f"SAMPLEINIT: no loop point specified for sample {text[0]}, using 0000") 117 | try: 118 | pitchtext = text[2].lower().strip() 119 | except IndexError: 120 | pitchtext = "0000" 121 | print(f"SAMPLEINIT: no tuning data specified for sample {text[0]}, using 0000") 122 | try: 123 | envtext = text[3].lower().strip() 124 | except IndexError: 125 | envtext = "ffe0" 126 | print(f"SAMPLEINIT: no envelope data specified for sample {text[0]}, using 15/7/7/0") 127 | 128 | selfloop = mml2mfvi.parse_brr_loop(looptext) 129 | selftuning = mml2mfvi.parse_brr_tuning(pitchtext) 130 | selfadsr = mml2mfvi.parse_brr_env(envtext) 131 | 132 | try: 133 | extratext = text[4].strip() 134 | except IndexError: 135 | extratext = "" 136 | extratext = re.sub(r"\{.*\}", "", extratext) 137 | coarsetune = re.search(r"\[[0-9+-]+\]", extratext) 138 | extratext = re.sub(r"\[.*\]", "", extratext) 139 | self.name = extratext.strip() 140 | if not self.name: 141 | self.name = self.filename 142 | if '.' in self.name: 143 | self.name = self.name.rpartition('.')[0] 144 | if '/' in self.name: 145 | self.name = self.name.rpartition('/')[2] 146 | if '\\' in self.name: 147 | self.name = self.name.rpartition('\\')[2] 148 | if coarsetune is not None and USE_LISTFILE_TRANSPOSE: 149 | coarsetune = coarsetune.group(0)[1:-1].strip() 150 | try: 151 | self.coarsetune = int(coarsetune) * -1 152 | except ValueError: 153 | self.coarsetune = None 154 | else: 155 | self.coarsetune = None 156 | 157 | # load data - from insertmfvi "Sample.load()" 158 | brr = None 159 | try: 160 | with open(self.filename, "rb") as f: 161 | brr = f.read() 162 | except FileNotFoundError: 163 | try: 164 | with open(self.filename + ".brr", "rb") as f: 165 | brr = f.read() 166 | self.filename += ".brr" 167 | except FileNotFoundError: 168 | try: 169 | with open(self.filename.strip(), "rb") as f: 170 | brr = f.read() 171 | self.filename = self.filename.strip() 172 | except FileNotFoundError: 173 | try: 174 | with open(self.filename.strip() + ".brr", "rb") as f: 175 | brr = f.read() 176 | self.filename = self.filename.strip() + ".brr" 177 | except: 178 | print(f"LOADBRR: couldn't open file {self.filename}") 179 | if brr: 180 | if len(brr) % 9 == 2: 181 | header = brr[0:2] 182 | brr = brr[2:] 183 | header_value = int.from_bytes(header, "little") 184 | if header_value != len(brr): 185 | if header_value % 9 and header_value < len(brr): 186 | # looks like an AddmusicK-style loop point header 187 | print(f"LOADBRR: Found embedded loop point {header_value:04X} in {self.filename}") 188 | if isinstance(brr.loop, bytes): 189 | print(f" Externally specified loop point {int.from_bytes(brr.loop, 'little'):04X} takes precedence") 190 | else: 191 | print(f" using this") 192 | self.loop = header_value.to_bytes(2, "little") 193 | if len(brr) % 9: 194 | print(f"LOADBRR: {self.filename}: bad file format") 195 | print(" BRRs must be a multiple of 9 bytes, with optional 2-byte header") 196 | self.brr = brr 197 | 198 | # cleanup, adapted from ff6spc2sf2 199 | self.idx = idx 200 | self.sdta_offset = None 201 | self.sdta_end = None 202 | 203 | self.offset = 0 204 | self.loffset = int.from_bytes(selfloop, "little") 205 | ad = selfadsr[0] 206 | sr = selfadsr[1] 207 | self.attack = ad & 0x0F 208 | self.decay = (ad >> 4) & 0b111 209 | self.sustain = (sr >> 5) 210 | self.release = sr & 0x1F 211 | scale = int.from_bytes(selftuning, "big") 212 | if scale < 0x8000: scale += 0x10000 213 | self.pitch_scale = scale / 0x10000 214 | 215 | loc = self.offset 216 | while loc < (0xF500 - 9): 217 | if brr[loc] & 1: 218 | break 219 | loc += 9 220 | self.is_looped = True if brr[loc] & 0b10 else False 221 | end = loc + 9 222 | self.length = (end - self.offset) // 9 223 | self.llength = (end - self.loffset) // 9 if self.is_looped else 0 224 | self.alength = (self.length - self.llength) 225 | 226 | loop = (self.loffset - self.offset) / 9 227 | print(f"BRR sample {idx:02X}:") 228 | print(f" end {end} self.loffset {self.loffset:04X}") 229 | print(f" @{self.offset:04X} || length {self.length} ({self.length*16})") 230 | if self.is_looped: 231 | print(f" Attack {self.alength} then loop {self.llength} ({self.alength * 16} -> {self.llength * 16})") 232 | else: 233 | print(f" Loop not active") 234 | print(f" {self.attack:02} {self.decay} {self.sustain} {self.release:02}") 235 | print(f" Pitch mult {self.pitch_scale}") 236 | 237 | def is_valid(self): 238 | # invalidate if negative loop 239 | if self.loffset < self.offset: 240 | return False 241 | # invalidate if loop not multiple of 9 bytes (warn) 242 | if (self.loffset - self.offset) % 9: 243 | print("WARNING: Sample {self.idx:02X} loop point is invalid!") 244 | # invalidate if bad ranges (warn) 245 | for i in range(self.length): 246 | brr_range = self.brr[self.offset + i * 9] >> 4 247 | if brr_range > 12: 248 | print(f"WARNING: Sample {self.idx:02X} has invalid range nybble {brr_range} at block {i}") 249 | # return False 250 | return True 251 | 252 | def get_pcm(self): 253 | global pre, prepre 254 | pre = 0 255 | prepre = 0 256 | # print(f"getpcm {self.idx:02X}") 257 | 258 | pcm = bytearray() 259 | loc = self.offset 260 | loops = 0 261 | while True: 262 | block = self.brr[loc:loc+9] 263 | if len(block) != 9: 264 | print(f"WARNING: truncated block at {loc:04X} - expected length 9, got length {len(block)}") 265 | print(f"aborting processing for this sample, press enter to continue") 266 | input() 267 | break 268 | pcm_samples = decode_block(block) 269 | print(".", end="") 270 | for p in pcm_samples: 271 | pcm.extend(p.to_bytes(2, "little", signed=True)) 272 | 273 | if block[0] & 0b11 == 0b11: 274 | if loops >= SAMPLE_EXTRA_ITERATIONS and len(pcm) >= SAMPLE_MIN_SIZE: 275 | valid = self.validate_loop(pcm) 276 | if valid: 277 | self.llength *= valid 278 | print(f"\n Loop extended by {valid}x (total iterations {loops+1})") 279 | print(f" loop size {self.llength}, sample size {len(pcm)}") 280 | break 281 | loops += 1 282 | loc = self.loffset 283 | # print(f" adding {loops+1}rd iteration") 284 | continue 285 | elif block[0] & 1: 286 | break 287 | 288 | loc += 9 289 | if loc > self.offset + self.length * 9: 290 | print(f"why are we at {loc:04X} when sample {self.idx:02X} at {self.offset:04X} is only {self.length} blocks long? we should have gotten there by {self.offset + self.length * 9:04X}") 291 | input() 292 | return pcm 293 | 294 | def validate_loop(self, pcm): 295 | llen = self.llength * 16 296 | lsp = len(pcm) - llen 297 | lscale = 1 298 | while True: 299 | lsp = len(pcm) - llen * lscale 300 | prelsp = lsp - llen * lscale 301 | if prelsp <= self.length: 302 | break 303 | #print(f" trying {lscale}x ({lsp} / {len(pcm)})") 304 | if pcm[lsp:] == pcm[prelsp:lsp]: 305 | #print(f" found match with period {lscale}x original loop size") 306 | return lscale 307 | lscale += 1 308 | return False 309 | 310 | def get_tuning(self): 311 | semitones = 12 * (log(self.pitch_scale, 10) / log(2, 10)) 312 | cents, semitones = modf(semitones) 313 | key = int(69 - semitones) 314 | cents = int(round(cents * 100)) 315 | #if cents < 0: 316 | # cents += 100 317 | # key += 1 318 | #if cents > 50: 319 | # key -= 1 320 | # cents = (100 - cents) + 0x80 321 | return key, cents 322 | 323 | def get_key_range(self): 324 | semitones = 12 * (log(self.pitch_scale, 10) / log(2, 10)) 325 | cents, semitones = modf(semitones) 326 | key = int(93 - semitones) 327 | if cents > 0: 328 | key -= 1 329 | if self.coarsetune: 330 | key -= self.coarsetune 331 | max = clamp(0, key, 127) 332 | min = clamp(0, 0 - self.coarsetune, max) if self.coarsetune else 0 333 | return (max << 8) + min 334 | 335 | def decode_block(block): 336 | global pre, prepre 337 | assert len(block) == 9 338 | head = block[0] 339 | filtermode = (head & 0b1100) >> 2 340 | shiftrange = head >> 4 341 | # print(f"filter {filtermode} shift {shiftrange}") 342 | 343 | nybs = [] 344 | pcms = [] 345 | for byt in block[1:]: 346 | nybs.append(byt >> 4) 347 | nybs.append(byt & 0x0F) 348 | # print(nybs) 349 | for n in nybs: 350 | if n >= 8: 351 | n -= 16 352 | if shiftrange > 13: 353 | pcm = (-1 if n < 0 else 1) << 11 354 | else: 355 | pcm = n << shiftrange >> 1 356 | debug_shifted = pcm 357 | 358 | if filtermode == 0: 359 | filter = 0 360 | elif filtermode == 1: 361 | filter = pre + ((-1 * pre) >> 4) 362 | elif filtermode == 2: 363 | filter = (pre << 1) + ((-1*((pre << 1) + pre)) >> 5) - prepre + (prepre >> 4) 364 | elif filtermode == 3: 365 | filter = (pre << 1) + ((-1*(pre + (pre << 2) + (pre << 3))) >> 6) - prepre + (((prepre << 1) + prepre) >> 4) 366 | pcm += filter 367 | debug_filtered = pcm 368 | 369 | pcm = clamp(-0x8000, pcm, 0x7FFF) 370 | if pcm > 0x3FFF: 371 | pcm -= 0x8000 372 | elif pcm < -0x4000: 373 | pcm += 0x8000 374 | # print(f"{debug_shifted} -> {debug_filtered} ({filter}) -> {pcm}") 375 | pcms.append(pcm) 376 | prepre = pre 377 | pre = pcm 378 | return pcms 379 | 380 | def chunkify(data, name): 381 | return bytearray(name, "latin-1") + len(data).to_bytes(4, "little") + data 382 | 383 | def generator(id, val, signed=True): 384 | return bytearray(id.to_bytes(2, "little") + val.to_bytes(2, "little", signed=signed)) 385 | 386 | def timecents(ms): 387 | s = ms / 1000 388 | tc = int(round(1200 * log(s, 2))) 389 | return clamp(-0x8000, tc, 0x7FFF) 390 | 391 | def attenuate(pct): 392 | if pct >= 1: 393 | return 0 394 | if pct <= 0: 395 | return ATTEN_SILENCE 396 | p_ref = 0.00002 397 | db_ref = 20 * log(1 / p_ref, 10) 398 | db_pct = 20 * log(pct / p_ref, 10) 399 | return int((db_ref - db_pct) // .04) 400 | 401 | ## ----------------------------- 402 | 403 | ## if len(sys.argv) >= 2: 404 | ## spcfn = sys.argv[1] 405 | ## else: 406 | ## print("spc filename: ") 407 | ## spcfn = input() 408 | ## 409 | ## spcfn = spcfn.strip('"') 410 | ## with open(spcfn, "rb") as f: 411 | ## spc = f.read()[0x100:] 412 | ## 413 | ## brrs = {} 414 | ## for i in range(32, 48): 415 | ## s = BrrSample(i) 416 | ## if s.is_valid(): 417 | ## brrs[i] = s 418 | ## 419 | ## print (f"Accepted samples: {[f'{k:02X}' for k in brrs.keys()]}") 420 | 421 | try: 422 | print("mfvitools brr2sf2") 423 | print("usage: brr2sf2.py LISTFILE [sort] [id] [@SAMPLEPATH]") 424 | print() 425 | 426 | if len(sys.argv) >= 2: 427 | listfn = sys.argv[1] 428 | else: 429 | print("BRR list filename:") 430 | listfn = input() 431 | listfn = listfn.strip('"').strip() 432 | listpath, listname = os.path.split(listfn) 433 | 434 | if len(sys.argv) >= 3: 435 | if "sort" in [a.strip() for a in sys.argv[2:]]: 436 | SORT_BY_BLOCKSIZE = True 437 | if "id" in [a.strip() for a in sys.argv[2:]]: 438 | USE_ID_IN_NAMES = True 439 | for arg in [a.strip() for a in sys.argv[2:]]: 440 | if len(arg) and arg[0] == "@": 441 | listpath = arg[1:] 442 | 443 | #listfile = configparser.ConfigParser() 444 | #listfile.read(listfn) 445 | with open(listfn, "r") as f: 446 | listfile = f.readlines() 447 | listfile = [l for l in listfile if len(l) and l[0] != '[' and l[0] != '#' and l[0] != ';'] 448 | listdefs = {} 449 | #if 'Samples' in listfile: 450 | # listdefs.update(listfile['Samples']) 451 | #if 'BRR' in listfile: 452 | # listdefs.update(listfile['BRR']) 453 | #if 'BRRs' in listfile: 454 | # listdefs.update(listfile['BRRs']) 455 | #if 'Instruments' in listfile: 456 | # listdefs.update(listfile['Instruments']) 457 | 458 | brrs = {} 459 | used_ids = set() 460 | for full_line in listfile: 461 | if not full_line.strip(): 462 | continue 463 | id, _, line = full_line.partition(':') 464 | id = id.strip() 465 | 466 | #brrs = {} 467 | #for id, line in listdefs.items(): 468 | if 'k' in id: 469 | id = ''.join(d for d in id if d.isdigit()) 470 | try: 471 | id = int(id) * 128 472 | except ValueError: 473 | id = 0 474 | else: 475 | try: 476 | id = int(id, 16) 477 | except ValueError: 478 | print(f"LISTFILES: invalid sample id {id}") 479 | id = None 480 | if id is None: 481 | id = 0 482 | if id in used_ids: 483 | for i in range((id // 0x80) * 128, (id // 0x80) * 128 + 0x7F): 484 | print(i) 485 | if i not in used_ids: 486 | id = i 487 | break 488 | if id in used_ids: 489 | for i in range(128 * 128): 490 | if i not in used_ids: 491 | id = i 492 | break 493 | if id in used_ids: 494 | print(f"no free id for {line}") 495 | continue 496 | used_ids.add(id) 497 | brrs[id] = BrrSample(id, line) 498 | # brrs = {k: v for k, v in brrs.items() if v.is_valid() and len(v.brr)} 499 | 500 | if SORT_BY_BLOCKSIZE: 501 | brrs_sorted = {} 502 | for i in range(128): 503 | bank = [] 504 | for j in range(128): 505 | if (i * 128) + j in brrs: 506 | bank.append(brrs[i * 128 + j]) 507 | bank = sorted(bank, key=lambda x: x.length) 508 | for j in range(len(bank)): 509 | bank[j].idx = i * 128 + j 510 | brrs_sorted[i * 128 + j] = bank[j] 511 | brrs = brrs_sorted 512 | 513 | ##### Build sample data chunk 514 | 515 | smp_data = bytearray() 516 | 517 | print("Building waveforms") 518 | for k, s in brrs.items(): 519 | print(f"\nconverting BRR to PCM: {k:02X}", end="") 520 | s.sdta_offset = len(smp_data) // 2 521 | smp_data.extend(s.get_pcm()) 522 | s.sdta_end = len(smp_data) // 2 523 | smp_data.extend(b"\x00\x00" * 46) 524 | if len(smp_data) % 2: 525 | smp_data.append(b"\x00") 526 | 527 | sdta_chunk = chunkify(smp_data, "sdtasmpl") 528 | sdta_list = chunkify(sdta_chunk, "LIST") 529 | 530 | ##### Build articulation data chunk 531 | 532 | sfPresetHeader = bytearray() 533 | sfPresetBag = bytearray() 534 | sfModList = chunkify(b"\x00" * 10, "pmod") 535 | sfGenList = bytearray() 536 | sfInst = bytearray() 537 | sfInstBag = bytearray() 538 | sfInstModList = chunkify(b"\x00" * 10, "imod") 539 | sfInstGenList = bytearray() 540 | sfSample = bytearray() 541 | 542 | i = -1 543 | print("Building soundfont") 544 | for s in brrs.values(): 545 | if s is None: 546 | continue 547 | i += 1 548 | # name = text_clamp(f"brr{s.idx:02X} ({s.length})", 20) 549 | if USE_ID_IN_NAMES: 550 | name_id = f"{s.idx:02X}" 551 | name_block = f"{s.length}" 552 | name_freespace = 18 - len(name_id) - len(name_block) 553 | name = f"{name_id} {s.name[:name_freespace].strip()} {name_block}" 554 | else: 555 | name_block = f"{s.length}" 556 | name_freespace = 19 - len(name_block) 557 | name = f"{s.name[:name_freespace].strip()} {name_block}" 558 | print(name) 559 | name = text_clamp(name, 20) 560 | 561 | # sfSample.achSampleName 562 | sample = bytearray(name) 563 | # sfSample.dwStart 564 | sample += s.sdta_offset.to_bytes(4, "little") 565 | # sfSample.dwEnd 566 | sample += s.sdta_end.to_bytes(4, "little") 567 | # sfSample.dwStartloop 568 | if s.is_looped: 569 | lst = (s.sdta_end - 16) - (s.llength * 16) 570 | else: 571 | lst = s.sdta_offset 572 | sample += lst.to_bytes(4, "little") 573 | # sfSample.dwEndloop 574 | if s.is_looped: 575 | sample += (s.sdta_end - 16).to_bytes(4, "little") 576 | else: 577 | sample += s.sdta_offset.to_bytes(4, "little") 578 | # sfSample.dwSampleRate 579 | sample += int(32000).to_bytes(4, "little") 580 | # sfSample.byOriginalPitch 581 | key, cents = s.get_tuning() 582 | sample.append(key) 583 | # sfSample.chPitchCorrection 584 | sample += cents.to_bytes(1, "little", signed=True) 585 | # sfSample.wSampleLink 586 | # sfSample.sfSampleType 587 | sample += b"\x00\x00\x01\x00" 588 | 589 | sfSample += sample 590 | 591 | # build instrument generators 592 | # two zone operation to mimic SPC700 sustain rate (2nd decay) 593 | dgen, rgen, hgen, common = bytearray(), bytearray(), bytearray(), bytearray() 594 | # ADSR 595 | hold = 0 596 | common += generator(38, timecents(9.75)) # true release 597 | if attack_table[s.attack]: 598 | common += generator(34, timecents(attack_table[s.attack])) 599 | if s.sustain < 7: 600 | decay_share = 1 - (s.sustain + 1) * (1/8) 601 | decay_time = decay_table[s.decay] * decay_share 602 | dgen += generator(37, ATTEN_SILENCE) # sf2 sustain = silence 603 | dgen += generator(36, timecents(decay_time)) # sf2 decay 604 | if s.release > 0: 605 | hold += decay_time 606 | rgen += generator(35, timecents(decay_time)) # sf2 hold 607 | if s.release > 0: 608 | rgen += generator(37, ATTEN_SILENCE) # sf2 sustain = silence 609 | hgen += generator(37, ATTEN_SILENCE) 610 | rgen += generator(36, timecents(release_table[s.release]*2)) 611 | hgen += generator(36, timecents(release_table[s.release]*2)) 612 | hgen += generator(35, timecents(hold + release_table[s.release]*2//3)) 613 | # attenuation 614 | dgen_share = 1 - ((s.sustain + 1) * 1/8) 615 | hgen_share = (1 - dgen_share) * 1/8 if s.release else 0 616 | rgen_share = 1 - (dgen_share + hgen_share) 617 | dgen += generator(48, attenuate(dgen_share)) 618 | rgen += generator(48, attenuate(rgen_share)) 619 | hgen += generator(48, attenuate(hgen_share)) 620 | # vibrato delay 500ms (1 beat at 120bpm) 621 | common += generator(23, timecents(500)) 622 | vfreq = 1 / ((9.75 * 18) / 1000) 623 | vfreq = timecents(vfreq * 1000 / 8.176) 624 | common += generator(24, vfreq) 625 | # loop state (sampleModes) 626 | common += generator(54, 1 if s.is_looped else 0) 627 | # sampleID 628 | common += generator(53, i) 629 | 630 | dgen += common 631 | rgen += common 632 | hgen += common 633 | dgen_bagindex = len(sfInstGenList) // 4 634 | sfInstGenList += dgen 635 | hgen_bagindex = len(sfInstGenList) // 4 636 | sfInstGenList += hgen 637 | rgen_bagindex = len(sfInstGenList) // 4 638 | sfInstGenList += rgen 639 | 640 | # instrument bag 641 | sfInstBag += (dgen_bagindex).to_bytes(2, "little") + b"\x00\x00" 642 | sfInstBag += (hgen_bagindex).to_bytes(2, "little") + b"\x00\x00" 643 | sfInstBag += (rgen_bagindex).to_bytes(2, "little") + b"\x00\x00" 644 | 645 | # instrument header 646 | sfInst += name 647 | sfInst += (i * 3).to_bytes(2, "little") 648 | 649 | # build preset generators 650 | pgen = bytearray() 651 | # max range root+2oct, min c at octave 0 652 | pgen += generator(43, s.get_key_range(), signed=False) 653 | # transpose if specified in listfile 654 | if s.coarsetune: 655 | pgen += generator(51, s.coarsetune) 656 | # instrumentID 657 | pgen += generator(41, i) 658 | 659 | pgen_bagindex = len(sfGenList) // 4 660 | sfGenList += pgen 661 | 662 | # preset bag 663 | sfPresetBag += pgen_bagindex.to_bytes(2, "little") + b"\x00\x00" 664 | 665 | # preset header 666 | sfPresetHeader += name 667 | # preset ID 668 | sfPresetHeader += (s.idx % 0x80).to_bytes(2, "little") 669 | # bank ID 670 | sfPresetHeader += (s.idx // 0x80).to_bytes(2, "little") 671 | # preset bag index 672 | sfPresetHeader += i.to_bytes(2, "little") 673 | # trash 674 | sfPresetHeader += b"\x00" * 12 675 | 676 | # add terminal elements 677 | sfPresetHeader += bytes("EOP", "latin-1") + b"\x00" * 21 + (i+1).to_bytes(2, "little") + b"\x00" * 12 678 | sfInst += bytes("EOI", "latin-1") + b"\x00" * 17 + (i*3+3).to_bytes(2, "little") 679 | sfSample += bytes("EOS", "latin-1") + b"\x00" * 43 680 | sfInstBag += (len(sfInstGenList) // 4).to_bytes(4, "little") 681 | sfPresetBag += (len(sfGenList) // 4).to_bytes(4, "little") 682 | sfInstGenList += b"\x00" * 4 683 | sfGenList += b"\x00" * 4 684 | 685 | # pack 'em up 686 | pdta_chunk = chunkify(sfPresetHeader, "pdtaphdr") 687 | pdta_chunk += chunkify(sfPresetBag, "pbag") 688 | pdta_chunk += sfModList 689 | pdta_chunk += chunkify(sfGenList, "pgen") 690 | pdta_chunk += chunkify(sfInst, "inst") 691 | pdta_chunk += chunkify(sfInstBag, "ibag") 692 | pdta_chunk += sfInstModList 693 | pdta_chunk += chunkify(sfInstGenList, "igen") 694 | pdta_chunk += chunkify(sfSample, "shdr") 695 | pdta_list = chunkify(pdta_chunk, "LIST") 696 | 697 | # INFO_list 698 | info_chunk = bytearray() 699 | info_chunk += chunkify(b"\x02\x00\x04\x00", "INFOifil") 700 | info_chunk += chunkify(bytes("EMU8000", "latin-1") + b"\x00", "isng") 701 | 702 | listname = listname.rpartition('.')[0] 703 | outfn = listname + ".sf2" 704 | listname = bytes(listname, "latin-1") + b"\x00" 705 | if len(listname) % 2: 706 | listname += b"\x00" 707 | info_chunk += chunkify(listname, "INAM") 708 | info_list = chunkify(info_chunk, "sfbkLIST") 709 | 710 | sfbk_chunk = info_list + sdta_list + pdta_list 711 | sfbk_riff = chunkify(sfbk_chunk, "RIFF") 712 | 713 | with open(outfn, "wb+") as f: 714 | f.write(sfbk_riff) 715 | 716 | print("done.") 717 | input() 718 | except: 719 | traceback.print_exc() 720 | input() -------------------------------------------------------------------------------- /experimental_ff5/mml2akao3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys, os, re, traceback, copy 4 | from mmltbl_ff5 import * 5 | 6 | mml_log = "\n" if __name__ == "__main__" else None 7 | 8 | def byte_insert(data, position, newdata, maxlength=0, end=0): 9 | while position > len(data): 10 | data = data + "\x00" 11 | if end: 12 | maxlength = end - position + 1 13 | if maxlength and len(data) > maxlength: 14 | newdata = newdata[:maxlength] 15 | return data[:position] + newdata + data[position+len(newdata):] 16 | 17 | 18 | def int_insert(data, position, newdata, length, reversed=True): 19 | n = int(newdata) 20 | l = [] 21 | while len(l) < length: 22 | l.append(chr(n & 0xFF)) 23 | n = n >> 8 24 | if not reversed: l.reverse() 25 | return byte_insert(data, position, "".join(l), length) 26 | 27 | def warn(fileid, cmd, msg): 28 | global mml_log 29 | m = "{}: WARNING: in {:<10}: {}".format(fileid, cmd, msg) 30 | print(m) 31 | if __name__ == "__main__": mml_log += m + '\n' 32 | 33 | def mlog(msg): 34 | global mml_log 35 | if __name__ == "__main__": mml_log += msg + '\n' 36 | 37 | class Drum: 38 | def __init__(self, st): 39 | s = re.findall('(.)(.[+-]?)\\1=\s*([0-9]?)([a-gr^])([+-]?)\s*(.*)', st) 40 | if s: s = s[0] 41 | mlog("{} -> {}".format(st, s)) 42 | if len(s) >= 6: 43 | self.delim = s[0] 44 | self.key = s[1] 45 | self.octave = int(s[2]) if s[2] else 5 46 | self.note = s[3] + s[4] 47 | s5 = re.sub('\s*', '', s[5]).lower() 48 | params = re.findall("\|[0-9a-f]|@0x[0-9a-f][0-9a-f]|%?[^|0-9][0-9,]*", s5) 49 | par = {} 50 | for p in params: 51 | if p[0] == "@" and len(p) >= 5: 52 | if p[0:3] == "@0x": 53 | par['@0'] = str(int(p[3:5], 16)) 54 | continue 55 | if p[0] == '|' and len(p) >= 2: 56 | par['@0'] = str(int(p[1], 16) + 32) 57 | else: 58 | pre = re.sub('[0-9]+', '0', p) 59 | suf = re.sub('%?[^0-9]', '', p, 1) 60 | if pre in equiv_tbl: 61 | pre = equiv_tbl[pre] 62 | par[pre] = suf 63 | self.params = par 64 | else: 65 | self.delim, self.key, self.octave, self.note, self.params = None, None, None, None, None 66 | mlog("DRUM: [{}] {} -- o{} {} {}".format(self.delim, self.key, self.octave, self.note, self.params)) 67 | 68 | def mml_to_akao(mml, fileid='mml', sfxmode=False, variant=None): 69 | #preprocessor 70 | #returns dict of (data, inst) tuples (4096, 32 bytes max) 71 | #one generated for each #VARIANT directive 72 | 73 | if isinstance(mml, str): 74 | mml = mml.splitlines() 75 | #one-to-one character replacement 76 | transes = [] 77 | for line in mml: 78 | if line.startswith("#REPLACE") and len(line) > 7: 79 | tokens = line[7:].split() 80 | if len(tokens) < 3: continue 81 | if len(tokens[1]) != len(tokens[2]): 82 | warn(fileid, line, "token size mismatch, ignoring excess") 83 | if len(tokens[1]) > len(tokens[2]): 84 | tokens[1] = tokens[1][0:len(tokens[2])] 85 | else: 86 | tokens[2] = tokens[2][0:len(tokens[1])] 87 | transes.append(str.maketrans(tokens[1], tokens[2])) 88 | for trans in transes: 89 | newmml = [] 90 | for line in mml: 91 | newmml.append(line.translate(trans)) 92 | mml = newmml 93 | 94 | #sfxvariant 95 | all_delims = set() 96 | for line in mml: 97 | if line.startswith("#SFXV") and len(line) > 5: 98 | tokens = line[5:].split() 99 | if len(tokens) < 1: continue 100 | if len(tokens) >= 2 and sfxmode: 101 | all_delims.update(tokens[1]) 102 | elif not sfxmode: 103 | all_delims.update(tokens[0]) 104 | #variants 105 | variants = {} 106 | for line in mml: 107 | if line.startswith("#VARIANT") and len(line) > 8: 108 | makedefault = True if not variants else False 109 | tokens = line[8:].split() 110 | if len(tokens) < 1: continue 111 | if len(tokens) == 1: 112 | tokens.append('_default_') 113 | all_delims.update(tokens[0]) 114 | variants[tokens[1]] = tokens[0] 115 | if makedefault: variants["_default_"] = tokens[0] 116 | for k, v in list(variants.items()): 117 | variants[k] = "".join([c for c in all_delims if c not in variants[k]]) 118 | if not variants: 119 | variants['_default_'] = ''.join([c for c in all_delims]) 120 | if variant: 121 | if variant not in variants: 122 | print("mml error: requested unknown variant '{}'\n".format(variant)) 123 | variants = {variant: variants[variant]} 124 | 125 | #generate instruments 126 | isets = {} 127 | for k, v in variants.items(): 128 | iset = {} 129 | for line in mml: 130 | skip = False 131 | if line.startswith("#WAVE") and len(line) > 5: 132 | for c in v: 133 | if c in line: 134 | line = re.sub(c+'.*?'+c, '', line) 135 | line = re.sub('[^x\da-fA-F]', ' ', line[5:]) 136 | tokens = line.split() 137 | if len(tokens) < 2: continue 138 | numbers = [] 139 | for t in tokens[0:2]: 140 | t = t.lower() 141 | base = 16 if 'x' in t else 10 142 | t = t.replace('x' if base == 16 else 'xabcdef', '') 143 | try: 144 | numbers.append(int(t, base)) 145 | except: 146 | warn(fileid, "#WAVE {}, {}".format(tokens[0], tokens[1]), "Couldn't parse token {}".format(t)) 147 | continue 148 | if numbers[0] not in list(range(0x20,0x30)): 149 | warn(fileid, "#WAVE {}, {}".format(hex(numbers[0]), hex(numbers[1])), "Program ID out of range (expected 0x20 - 0x2F / 32 - 47)") 150 | continue 151 | if numbers[1] not in list(range(0, 256)): 152 | warn(fileid, "#WAVE {}, {}".format(hex(numbers[0]), hex(numbers[1])), "Sample ID out of range (expected 0x00 - 0xFF / 0 - 255)") 153 | continue 154 | iset[numbers[0]] = numbers[1] 155 | raw_iset = "\x00" * 0x20 156 | for slot, inst in iset.items(): 157 | raw_iset = byte_insert(raw_iset, (slot - 0x20)*2, chr(inst)) 158 | isets[k] = raw_iset 159 | 160 | 161 | #generate data 162 | datas = {} 163 | for k, v in variants.items(): 164 | datas[k] = mml_to_akao_main(mml, v, fileid) 165 | 166 | output = {} 167 | for k, v in variants.items(): 168 | output[k] = (datas[k], isets[k]) 169 | 170 | return output 171 | 172 | 173 | def mml_to_akao_main(mml, ignore='', fileid='mml'): 174 | mml = copy.copy(mml) 175 | ##final bit of preprocessing 176 | #single character macros 177 | cdefs = {} 178 | for line in mml: 179 | if line.lower().startswith("#cdef"): 180 | li = line[5:] 181 | li = li.split('#')[0].lower().strip() 182 | li = li.split(None, 1) 183 | if len(li) < 2: continue 184 | if len(li[0]) != 1: 185 | warn(fileid, line, "Expected one character for cdef, found {} ({})").format(len(li[0]), li[0]) 186 | continue 187 | cdefs[li[0]] = li[1] 188 | #single quote macros 189 | macros = {} 190 | for line in mml: 191 | if line.lower().startswith("#def"): 192 | line = line[4:] 193 | line = line.split('#')[0].lower() 194 | if not line: continue 195 | pre, sep, post = line.partition('=') 196 | if post: 197 | pre = pre.replace("'", "").strip() 198 | for c in ignore: 199 | try: 200 | post = re.sub(c+".*?"+c, "", post) 201 | except Exception: 202 | c = "\\" + c 203 | post = re.sub(c+".*?"+c, "", post) 204 | post = "".join(post.split()) 205 | macros[pre] = post.lower() 206 | #pan scaling for easy cross game compatibility 207 | pan_scale = 1 208 | for line in mml: 209 | if line.lower().startswith("#scale pan"): 210 | line = line[10:].strip() 211 | try: 212 | pan_scale = float(line) 213 | except: 214 | warn(fileid, line, "invalid pan scale") 215 | 216 | for i, line in enumerate(mml): 217 | while True: 218 | r = re.search("'(.*?)'", line) 219 | if not r: break 220 | mx = r.group(1) 221 | # 222 | m = re.search("([^+\-*]+)", mx).group(1) 223 | tweaks = {} 224 | tweak_text = "" 225 | while True: 226 | twx = re.search("([+\-*])([%a-z]+)([0-9.,]+)", mx) 227 | if not twx: break 228 | tweak_text += twx.group(0) 229 | cmd = twx.group(2) + ''.join([c for c in twx.group(3) if c == ',']) 230 | tweaks[cmd] = (twx.group(1), twx.group(3)) 231 | mx = mx.replace(twx.group(0), "", 1) 232 | # 233 | s = macros[m] if m in macros else "" 234 | p = 0 235 | if tweaks: 236 | # "o,,": ("+", ",1,") 237 | skip = ignore + "\"'{" 238 | sq = list(s) 239 | sr = "" 240 | while sq: 241 | c = sq.pop(0) 242 | if c in skip: 243 | endat = "}" if c=="{" else c 244 | if sq: c += sq.pop(0) 245 | while sq: 246 | cc = sq.pop(0) 247 | if cc == endat: 248 | if endat == "'": c += tweak_text 249 | c += cc 250 | break 251 | else: c += cc 252 | sr += c 253 | continue 254 | if sq and c == "%": 255 | c += sq.pop(0) 256 | d = "" 257 | while sq and sq[0] in "1234567890,.+-x": 258 | d += sq.pop(0) 259 | cmd = c + ''.join([ch for ch in d if ch == ',']) 260 | if cmd in tweaks: 261 | d = d.split(',') 262 | e = tweaks[cmd][1].split(',') 263 | sign = tweaks[cmd][0] 264 | for j, ee in enumerate(e): 265 | if not ee: 266 | c += f"{d[j]}," 267 | continue 268 | try: en = int(ee) 269 | except: 270 | try: en = int(ee,16) 271 | except: 272 | try: en = float(ee) 273 | except: 274 | warn("error parsing {} into {}".format(r.group(0), s)) 275 | en = 0 276 | try: dn = int(d[j]) 277 | except: 278 | try: dn = int(d[j],16) 279 | except: 280 | warn("error parsing {} into {}".format(r.group(0), s)) 281 | dn = 0 282 | if sign is "*": 283 | result = dn * en 284 | elif sign is "-": 285 | result = dn - en 286 | elif sign is "+": 287 | result = dn + en 288 | if result < 0: result = 0 289 | #if ((cmd is "v" or cmd is "p") and j==0) or ((cmd is "v," or cmd is "p,") and j==1): 290 | if ((cmd is "v") and j==0) or ((cmd is "v,") and j==1): 291 | if result > 127: result = 127 292 | else: 293 | if result > 255: result = 255 294 | #apply new values 295 | c += f"{int(result)}," 296 | c = c.rstrip(',') 297 | else: c += d 298 | sr += c 299 | s = sr 300 | 301 | line = line.replace(r.group(0), s, 1) 302 | 303 | mml[i] = line 304 | 305 | #drums 306 | drums = {} 307 | for line in mml: 308 | if line.lower().startswith("#drum"): 309 | s = line[5:].strip() 310 | s = s.split('#')[0].lower() 311 | for c in ignore: 312 | try: 313 | s = re.sub(c+".*?"+c, "", s) 314 | except Exception: 315 | c = "\\" + c 316 | s = re.sub(c+".*?"+c, "", s) 317 | for c in ["~", "/", "`", "\?", "_"]: 318 | s = re.sub(c, '', s) 319 | d = Drum(s.strip()) 320 | if d.delim: 321 | if d.delim not in drums: drums[d.delim] = {} 322 | drums[d.delim][d.key] = d 323 | 324 | for i, line in enumerate(mml): 325 | mml[i] = line.split('#')[0].lower() 326 | 327 | m = list(" ".join(mml)) 328 | targets, channels, pendingjumps = {}, {}, {} 329 | data = "\x00" * 0x16 330 | defaultlength = 8 331 | thissegment = 1 332 | next_jumpid = 1 333 | state = {} 334 | jumpout = [] 335 | 336 | while len(m): 337 | command = m.pop(0) 338 | 339 | #single character macros 340 | if command in cdefs: 341 | repl = list(cdefs[command] + " ") 342 | m = repl + m 343 | #conditionally executed statements 344 | if command in ignore: 345 | while len(m): 346 | next = m.pop(0) 347 | if next == command: 348 | break 349 | continue 350 | #inline comment // channel marker 351 | elif command == "{": 352 | thisnumber = "" 353 | numbers = [] 354 | while len(m): 355 | command += m.pop(0) 356 | if command[-1] in "1234567890": 357 | thisnumber += command[-1] 358 | elif thisnumber: 359 | numbers.append(int(thisnumber)) 360 | thisnumber = "" 361 | if command[-1] == "}": break 362 | for n in numbers: 363 | if n <= 8 and n >= 1: 364 | channels[n] = len(data) 365 | continue 366 | #drum mode 367 | elif command in drums: 368 | mls, dms = [], [] 369 | drumset = drums[command] 370 | while len(m): 371 | if m[0] != command: 372 | dms.append(m.pop(0)) 373 | else: 374 | m.pop(0) 375 | break 376 | dbgdms = "".join(dms) 377 | lockstate = False 378 | silent = False 379 | if len(dms): 380 | if dms[0] in "1234567890": 381 | state["o0"] = dms.pop(0) 382 | elif dms[0] == ">": 383 | co = dms.pop(0) 384 | while dms[0] == ">": 385 | co += dms.pop(0) 386 | state["o0"] += len(co) 387 | elif dms[0] == "<": 388 | co = dms.pop(0) 389 | while dms[0] == "<": 390 | co += dms.pop(0) 391 | state["o0"] -= len(co) 392 | while len(dms): 393 | dcom = dms.pop(0) 394 | if len(dms): 395 | if dms[0] in "+-": 396 | dcom += dms.pop(0) 397 | if dcom == "\\": 398 | lockstate = True if not lockstate else False 399 | elif dcom == ":": 400 | silent = True if not silent else False 401 | elif dcom == "!": 402 | rcom = dms.pop(0) 403 | if rcom == "!": 404 | if "o0" in state: 405 | state = {"o0": state["o0"]} 406 | else: 407 | state = {} 408 | continue 409 | if rcom == "%": rcom += dms.pop(0) 410 | while len(dms): 411 | if dms[0] in "0,": 412 | rcom += dms.pop(0) 413 | else: break 414 | if rcom in equiv_tbl: 415 | rcom = equiv_tbl[rcom] 416 | state.pop(rcom, None) 417 | elif dcom in "0123456789^.": 418 | mls.extend(dcom) 419 | elif dcom in drumset: 420 | params = {} 421 | for k, v in drumset[dcom].params.items(): 422 | if lockstate and k != "@0": continue 423 | if k in state: 424 | if state[k] != v: 425 | params[k] = v 426 | elif k == "%y" and not ( "%a0" in state or "%y0" in state or 427 | "%s0" in state or "%r0" in state): 428 | pass 429 | else: 430 | params[k] = v 431 | s = "" 432 | if "%y" in params or "@0" in params: 433 | state.pop("%a0", None) 434 | state.pop("%y0", None) 435 | state.pop("%s0", None) 436 | state.pop("%r0", None) 437 | for k, v in params.items(): 438 | t = (re.sub('[0-9,]', '', k) + v).strip() 439 | s = t + s if k == "@0" else s + t 440 | if k != "%y": 441 | state[k] = v 442 | 443 | if 'o0' in state: 444 | if isinstance(state['o0'], str): state['o0'] = int(state['o0']) 445 | ochg = drumset[dcom].octave - int(state['o0']) 446 | if abs(ochg) <= 1: 447 | if ochg < 0: 448 | s += ">" * abs(ochg) 449 | else: s += "<" * ochg 450 | else: 451 | s += "o{}".format(drumset[dcom].octave) 452 | state['o0'] += ochg 453 | else: 454 | s += "o{}".format(drumset[dcom].octave) 455 | state['o0'] = drumset[dcom].octave 456 | s += drumset[dcom].note 457 | if not silent: mls.extend(list(s)) 458 | mlog("drum: processed {} -> {}".format(dbgdms, "".join(mls))) 459 | mls.extend(m) 460 | m = mls 461 | continue 462 | 463 | #populate command variables 464 | if command == "%": command += m.pop(0) 465 | prefix = command 466 | if len(m): 467 | while m[0] in "1234567890,.+-x": 468 | command += m.pop(0) 469 | if not len(m): break 470 | 471 | #catch @0x before parsing params 472 | if "|" in command: 473 | command = "@0x2" + command[1:] 474 | if "@0x" in command: 475 | while len(command) < 5: 476 | command += m.pop(0) 477 | number = command[-2:] 478 | try: 479 | number = int(number, 16) 480 | except ValueError: 481 | warn(fileid, command, "Invalid instrument {}, falling back to 0x20".format(number)) 482 | number = 0x20 483 | command = "@" + str(number) 484 | 485 | modifier = "" 486 | params = [] 487 | for c in command: 488 | if c in "+-": 489 | modifier = c 490 | thisnumber = "" 491 | is_negative = False 492 | for c in command[len(prefix):] + " ": 493 | if c in "1234567890": 494 | thisnumber += c 495 | elif c == "-" and prefix not in "abcdefg^r": 496 | is_negative = True 497 | elif thisnumber: 498 | params.append(0x100-int(thisnumber) if is_negative else int(thisnumber)) 499 | thisnumber = "" 500 | is_negative = False 501 | dots = len([c for c in command if c == "."]) 502 | 503 | if (prefix, len(params)) not in command_tbl and len(params): 504 | if (prefix + str(params[0]), len(params) - 1) in command_tbl: 505 | prefix += str(params.pop(0)) 506 | 507 | #print "processing command {} -> {} {} mod {} dots {}".format(command, prefix, params, modifier, dots) 508 | #case: notes 509 | if prefix in "abcdefg^r": 510 | pitch = note_tbl[prefix] 511 | if prefix not in "^r": 512 | pitch += 1 if "+" in modifier else 0 513 | pitch -= 1 if "-" in modifier else 0 514 | while pitch < 0: pitch += 0xC 515 | while pitch > 0xB: pitch -= 0xC 516 | if not params: 517 | length = defaultlength 518 | else: 519 | length = params[0] 520 | if dots and str(length)+"." in length_tbl: 521 | akao = chr(pitch * len(length_tbl) + length_tbl[str(length)+"."][0]) 522 | dots -= 1 523 | length *= 2 524 | elif length in length_tbl: 525 | akao = chr(pitch * len(length_tbl) + length_tbl[length][0]) 526 | else: 527 | warn(fileid, command, "Unrecognized note length {}".format(length)) 528 | continue 529 | while dots: 530 | if length*2 not in length_tbl: 531 | warn(fileid, command, "Cannot extend note/tie of length {}".format(length)) 532 | break 533 | dots -= 1 534 | length *= 2 535 | if dots and str(length)+"." in length_tbl: 536 | akao += chr(note_tbl["^"]*len(length_tbl) + length_tbl[str(length)+"."][0]) 537 | dots -= 1 538 | length *= 2 539 | else: 540 | akao += chr(note_tbl["^"]*len(length_tbl) + length_tbl[length][0]) 541 | data += akao 542 | #case: simple commands 543 | elif (prefix, len(params)) in command_tbl: 544 | #special case: loops 545 | if prefix == "[": 546 | if len(params): 547 | params[0] -= 1 548 | else: 549 | params.append(1) 550 | #special case: end loop adds jump target if j,1 is used 551 | if prefix == "]": 552 | while len(jumpout): 553 | pendingjumps[jumpout.pop()] = "jo%d"%next_jumpid 554 | targets["jo%d"%next_jumpid] = len(data) # + 1 (before ']' for ff5) 555 | next_jumpid += 1 556 | #general case 557 | akao = chr(command_tbl[prefix, len(params)]) 558 | #special case: pansweep 559 | if prefix == "p": 560 | if len(params) == 3: 561 | params = params[1:] 562 | #special case: pan scaling 563 | params[-1] = min(255, int(pan_scale * params[-1])) 564 | #general case 565 | while len(params): 566 | if params[0] >= 256: 567 | warn(fileid, command, "Parameter {} out of range, substituting 0".format(params[0])) 568 | params[0] = 0 569 | akao += chr(params.pop(0)) 570 | data += akao 571 | #case: default length 572 | elif prefix == "l" and len(params) == 1: 573 | if params[0] in length_tbl: 574 | defaultlength = params[0] 575 | else: 576 | warn(fileid, command, "Unrecognized note length {}".format(length)) 577 | #case: jump point 578 | elif prefix == "$": 579 | if params: 580 | targets[params[0]] = len(data) 581 | else: 582 | targets["seg%d"%thissegment] = len(data) 583 | #case: end of segment 584 | elif prefix == ";": 585 | defaultlength = 8 586 | state = {} 587 | if params: 588 | if params[0] in targets: 589 | target = targets[params[0]] 590 | else: 591 | target = len(data) 592 | pendingjumps[len(data)+1] = params[0] 593 | else: 594 | if "seg%d"%thissegment in targets: 595 | target = targets["seg%d"%thissegment] 596 | else: 597 | data += CMD_END_TRACK 598 | thissegment += 1 599 | continue 600 | data += CMD_END_LOOP + int_insert(" ",0,target,2) 601 | thissegment += 1 602 | #case: jump out of loop 603 | elif prefix == "j": 604 | if len(params) == 1: 605 | jumpout.append(len(data)+2) 606 | target = len(data) 607 | elif len(params) == 2: 608 | if params[1] in targets: 609 | target = targets[params[1]] 610 | else: 611 | target = len(data) 612 | pendingjumps[len(data)+2] = params[1] 613 | else: continue 614 | if params[0] >= 256: 615 | warn(fileid, command, "Parameter {} out of range, substituting 1".format(params[0])) 616 | params[0] = 1 617 | data += CMD_JUMP_IF_LOOP + chr(params[0]) + int_insert(" ",0,target,2) 618 | #case: hard jump without ending segment 619 | elif prefix == "%j": 620 | if len(params)==1: 621 | if params[0] in targets: 622 | target = targets[params[0]] 623 | else: 624 | target = len(data) 625 | pendingjumps[len(data)+1] = params[0] 626 | else: continue 627 | data += CMD_END_LOOP + int_insert(" ",0,target,2) 628 | #case: conditional jump 629 | elif prefix == ":" and len(params) == 1: 630 | if params[0] in targets: 631 | target = targets[params[0]] 632 | else: 633 | target = len(data) 634 | pendingjumps[len(data)+1] = params[0] 635 | data += CMD_CONDITIONAL_JUMP + int_insert(" ",0,target,2) 636 | elif command.strip(): 637 | warn(fileid, command, "Unrecognized command") 638 | 639 | #insert pending jumps 640 | for k, v in pendingjumps.items(): 641 | if v in targets: 642 | data = int_insert(data, k, targets[v], 2) 643 | else: 644 | warn(fileid, command, "Jump destination {} not found in file".format(v)) 645 | #set up header 646 | header = int_insert("\x00"*0x16, 0, len(data)-2, 2) 647 | header = int_insert(header, 2, 0x16, 2) 648 | for i in range(1,8): 649 | if i not in channels: 650 | channels[i] = len(data)-2 651 | for k, v in channels.items(): 652 | header = int_insert(header, 2 + k*2, v, 2) 653 | header = int_insert(header, 0x14, len(data), 2) 654 | data = byte_insert(data, 0, header, 0x16) 655 | 656 | return data 657 | 658 | def clean_end(): 659 | print("Processing ended.") 660 | input("Press enter to close.") 661 | quit() 662 | 663 | if __name__ == "__main__": 664 | mml_log = "\n" 665 | 666 | print("emberling's MML to AKAO SNESv3 converter") 667 | print() 668 | 669 | if len(sys.argv) >= 2: 670 | fn = sys.argv[1] 671 | else: 672 | print("Enter MML filename..") 673 | fn = input(" > ").replace('"','').strip() 674 | 675 | try: 676 | with open(fn, 'r') as f: 677 | mml = f.readlines() 678 | except IOError: 679 | print("Error reading file {}".format(fn)) 680 | clean_end() 681 | 682 | try: 683 | variants = mml_to_akao(mml) 684 | except Exception: 685 | traceback.print_exc() 686 | clean_end() 687 | 688 | fn = os.path.splitext(fn)[0] 689 | for k, v in variants.items(): 690 | vfn = ".bin" if k in ("_default_", "") else "_{}.bin".format(k) 691 | 692 | thisfn = fn + "_data" + vfn 693 | try: 694 | with open(thisfn, 'wb') as f: 695 | f.write(bytes(v[0], encoding='latin-1')) 696 | except IOError: 697 | print("Error writing file {}".format(thisfn)) 698 | clean_end() 699 | print("Wrote {} - {} bytes".format(thisfn, hex(len(v[0])))) 700 | 701 | thisfn = fn + "_inst" + vfn 702 | try: 703 | with open(thisfn, 'wb') as f: 704 | f.write(bytes(v[1], encoding='latin-1')) 705 | except IOError: 706 | print("Error writing file {}".format(thisfn)) 707 | clean_end() 708 | print("Wrote {}".format(thisfn)) 709 | 710 | try: 711 | with open(os.path.join(os.path.split(sys.argv[0])[0],"mml_log.txt"), 'w') as f: 712 | f.write(mml_log) 713 | except IOError: 714 | print("Couldn't write log file, displaying...") 715 | print(mml_log) 716 | 717 | print("Conversion successful.") 718 | print() 719 | 720 | clean_end() 721 | --------------------------------------------------------------------------------