diff --git a/loader/samples/minexample/Makefile b/loader/samples/minexample/Makefile index 3f2f604..2241425 100644 --- a/loader/samples/minexample/Makefile +++ b/loader/samples/minexample/Makefile @@ -97,6 +97,7 @@ C1541 = c1541 CC1541 = ../../tools/cc1541/cc1541 ZX02 = zx02/build/zx02 ZX02_SRC = zx02 +FURC64 = furC64 PYTHON = python3 MKDIR = mkdir -p @@ -130,6 +131,7 @@ DISKIMAGE = $(BUILDDIR)/$(NAME)-$(_PLATFORM_).d64 AS_FLAGS = -Wa -I../../../shared -I ../../include -u __EXEHDR__ ZX0PRGS = \ + use_this_sid.zx0.prg \ badguy.zx0.prg \ title_320-prepared.zx0.prg \ tower.zx0.prg \ @@ -174,11 +176,11 @@ endif diskimage: $(DISKIMAGE) -$(DISKIMAGE): $(ASSEMBLE) $(CC1541) $(ZX0PRGS) $(LZPRGS) use_this_sid.bin font.bin +$(DISKIMAGE): $(ASSEMBLE) $(CC1541) $(ZX0PRGS) $(LZPRGS) font.bin $(RM) $@ $(CC1541) -n "otomata labs" -i " 2025" \ -f "ys2intro" -w $< \ - -f "sid" -w use_this_sid.bin \ + -f "song0" -w use_this_sid.zx0.prg \ -f "badguy" -w badguy.zx0.prg \ -f "font" -w font.bin \ -f "intrbmp" -w title_320-prepared.zx0.prg \ @@ -224,6 +226,10 @@ $(ZX02): $(PRINTF) "\x00\x90" | cat - $@.tmp > $@ $(RM) $@.tmp +use_this_sid.bin: ys2_port_legato.fur + cd $(FURC64) && ./convert.sh $(abspath $<) + cp $(FURC64)/asm/song.bin $@ + clean: -$(RM) $(ZX0PRGS) $(LZPRGS) -$(RM) *.o $(ASSEMBLE) $(DISKIMAGE) diff --git a/loader/samples/minexample/furC64/.gitignore b/loader/samples/minexample/furC64/.gitignore new file mode 100644 index 0000000..6b8c623 --- /dev/null +++ b/loader/samples/minexample/furC64/.gitignore @@ -0,0 +1,8 @@ +__pycache__ +asm/song.asm +asm/song.bin +*.o +*.lbl +*.lst +*.map +*.prg diff --git a/loader/samples/minexample/furC64/README.md b/loader/samples/minexample/furC64/README.md new file mode 100644 index 0000000..927b587 --- /dev/null +++ b/loader/samples/minexample/furC64/README.md @@ -0,0 +1,42 @@ +# furC64 +a C64/SID sound driver for Furnace + +### **THIS SOUND DRIVER IS CURRENTLY A WIP** + +A SID driver that's easy to make music with? It's more likely than you think. + +* You have to have [Python](https://www.python.org/) and the [CC65 toolchain](https://cc65.github.io/) installed +* You **have** to set the pitch linearity option to "None". You can do this by going to `window -> song -> compatability flags -> Pitch/Playback -> Pitch linearity` and then setting the option to "None". + +* The driver only supports **arpeggio, waveform, duty and cutoff** macros in each instrument and it DOESN'T support LFO and ADSR macros nor delay and step length, **although you can use LFO macros in the duty and cutoff macros (as in range-sweeping)** + +* The furC64 driver only supports these effects: + * 00xx: arpeggio + * 01xx: pitch slide up + * 02xx: pitch slide down + * 03xx: portamento + * 04xx: vibrato + * 09xx: set speed 1 + * 0Bxx: jump to pattern + * 0Dxx: jump to next pattern + * 0Fxx: set speed 2 + * 1Axx: disable/enable envelope reset + * 1Bxx: reset cutoff + * 1Cxx: reset pulse-width + * 4xxx: set filter cutoff + * E1xx: note slide up + * E2xx: note slide down + * E5xx: note fine-pitch + * EAxx: legato + * ECxx: note cut + +when you've finished / want to test out this driver: +* open the terminal/command prompt **to the furC64 directory** +* run `convert.sh your_fur_file.fur` or `convert.bat file.fur` (depending on your OS) +* in the `furC64/asm` directory you'll hopefully see a file called **`furC64-test.prg`** + * that's your .prg file that you can run on hardware or on an emulator like VICE! + +Hopefully you'll have fun with this driver alongside [furNES](https://github.com/AnnoyedArt1256/furNES) :D + +Libraries used: chipchune + diff --git a/loader/samples/minexample/furC64/asm/bin.cfg b/loader/samples/minexample/furC64/asm/bin.cfg new file mode 100644 index 0000000..bf5f6ea --- /dev/null +++ b/loader/samples/minexample/furC64/asm/bin.cfg @@ -0,0 +1,16 @@ +FEATURES { + STARTADDRESS: default = $0801; +} +MEMORY { + ZP: file = "", start = $0002, size = $00FE, define = yes; + MAIN: file = "", start = %S, size = $A000 - %S; + PLAYER: file = %O, start = $A000, size = $5FFA; +} +SEGMENTS { + ZEROPAGE: load = ZP, type = zp, optional = yes; + CODE: load = MAIN, type = rw; + RODATA: load = MAIN, type = ro, optional = yes; + DATA: load = MAIN, type = rw, optional = yes; + BSS: load = MAIN, type = bss, optional = yes, define = yes; + PLAYER: load = PLAYER, type = rw, define = yes; +} diff --git a/loader/samples/minexample/furC64/asm/exe.cfg b/loader/samples/minexample/furC64/asm/exe.cfg new file mode 100644 index 0000000..bb41d47 --- /dev/null +++ b/loader/samples/minexample/furC64/asm/exe.cfg @@ -0,0 +1,22 @@ +FEATURES { + STARTADDRESS: default = $0801; +} +SYMBOLS { + __LOADADDR__: type = import; +} +MEMORY { + ZP: file = "", start = $0002, size = $00FE, define = yes; + LOADADDR: file = %O, start = %S - 2, size = $0002; + MAIN: file = %O, start = %S, size = $A000 - %S; + PLAYER: file = %O, start = $A000, size = $5FFA; +} +SEGMENTS { + ZEROPAGE: load = ZP, type = zp, optional = yes; + LOADADDR: load = LOADADDR, type = ro; + EXEHDR: load = MAIN, type = ro, optional = yes; + CODE: load = MAIN, type = rw; + RODATA: load = MAIN, type = ro, optional = yes; + DATA: load = MAIN, type = rw, optional = yes; + BSS: load = MAIN, type = bss, optional = yes, define = yes; + PLAYER: load = MAIN, run = PLAYER, type = rw, define = yes; +} diff --git a/loader/samples/minexample/furC64/asm/furC64.asm b/loader/samples/minexample/furC64/asm/furC64.asm new file mode 100644 index 0000000..5629d5a --- /dev/null +++ b/loader/samples/minexample/furC64/asm/furC64.asm @@ -0,0 +1,2181 @@ +.feature c_comments + +.define chnum 3 +.define use_zp 0 +.define porta_once 0 +.define compat_hr 0 +.define CHIP_AMT 1 +.define DIGI 0 +.define HR_ADSR 1 +TEST_AD := $00 +TEST_SR := $00 + +;.define compat_hr 1 +;TEST_AD := $0f +;TEST_SR := $0f + + +.if DIGI = 0 +.define outch chnum +.else +.define outch 4 +.endif + +.macro variables +start_vars = * + +mframeW: .res chnum +doMacroW: .res chnum + +; why +env_reset_buf: .res chnum +env_reset: .res chnum + +fil_lo_n2: .res CHIP_AMT +fil_hi_n2: .res CHIP_AMT +fil_st2: .res CHIP_AMT + +legato: .res chnum + +test_reset: .res chnum + +note_tick: .res chnum + +dur: .res chnum + +vibrato_param: .res chnum +vibrato_phase: .res chnum + +absfil: .res CHIP_AMT + +doMacroC: .res CHIP_AMT +mframeC: .res CHIP_AMT + +instF: .res CHIP_AMT + +wav: .res chnum + +hrframe: .res chnum + +hrc: .res chnum + +ad: .res chnum +sr: .res chnum + +patseq: .res chnum*2 + +duty_lo: .res chnum +duty_hi: .res chnum + +nextpat: .res 1 +patind: .res 1 +jumppat: .res 1 +ch: .res 1 +chm: .res 1 +tick: .res 1 +ins: .res chnum +inst_prev: .res chnum +mframeA: .res chnum +mframeD: .res chnum +doMacroA: .res chnum +doMacroD: .res chnum +arp: .res chnum +absarp: .res chnum +effects_temp: .res 2 +tick_speeds: .res 2 +tick_sel: .res 1 +slide_amt: .res chnum +slide_amt_sign: .res chnum +slide_buffer_lo: .res chnum +slide_buffer_hi: .res chnum +note_pitch_lo: .res chnum +note_pitch_hi: .res chnum +note_n: .res chnum +note_temp: .res chnum +note_dest: .res chnum +finepitch: .res chnum +cut_dur: .res chnum +pw_mod_lo: .res chnum +pw_mod_hi: .res chnum +abspw: .res chnum +retrigger_pw: .res chnum +retrigger_fil: .res CHIP_AMT +triggered_fil: .res CHIP_AMT +.if porta_once = 1 +didporta: .res chnum +.endif +arpeff1: .res chnum +arpeff2: .res chnum +arpind: .res chnum +vol: .res CHIP_AMT +res: .res CHIP_AMT +fil_lo: .res CHIP_AMT +fil_hi: .res CHIP_AMT +fil_lo_n: .res CHIP_AMT +fil_hi_n: .res CHIP_AMT +base: .res 1 +chipnum: .res 1 +has_played: .res chnum +vars_len = *-start_vars + +.endmacro + +inst_buffer = ins + +.if chnum > 4 +.define actual_use_zp 0 +.else +.define actual_use_zp use_zp +.endif + +.ZEROPAGE +.if actual_use_zp = 1 +.org $10 +variables +.endif +patzp := $fe +macroIns := patzp +temp := patzp-4 +flags_temp := temp+3 +patloop = 0 + +.segment "CODE" + +.org $080D +jmp $900 +.res $0900-* +.import __PLAYER_LOAD__, __PLAYER_RUN__, __PLAYER_SIZE__ +main: + sei + lda #$35 + sta $01 + + lda #<__PLAYER_LOAD__ + sta temp + lda #>__PLAYER_LOAD__ + sta temp+1 + lda #<__PLAYER_RUN__ + sta temp+2 + lda #>__PLAYER_RUN__ + sta temp+3 + ldy #0 + ldx #>__PLAYER_SIZE__ + beq :+++ +: + lda (temp),y + sta (temp+2),y + iny + bne :- + inc temp+1 + inc temp+3 + dex + bne :- + beq :++ +: + lda (temp),y + sta (temp+2),y + iny +: + cpy #<__PLAYER_SIZE__ + bne :-- + + lda #127 + sta $dc0d + + and $d011 + sta $d011 + + lda $dc0d + lda $dd0d + + lda #irq + sta $ffff + +.if DIGI <> 0 + lda #$0b + sta $d011 + lda #$00 +.else + lda #$10 +.endif + sta $d012 + + lda #0 + sta $d01a + + lda #$63; <(985248/100) + sta $dc04 + lda #$26;>(985248/100) + sta $dc05 + + lda $dc0d + and #$81 + sta $dc0d + + lda #$40 + sta $dc0c + + lda #$81 + sta $dc0d + + +.if DIGI <> 0 +ldx #0 +: + lda real_addr, x + sta $2, x + inx + cpx #nmi_end-nmi + bne :- +.endif + + lda #0 + jsr init + +.if DIGI <> 0 +lda #nmi +sta $fffb + +lda #$01 +sta $dd0d +sta $dd0e + +lda #$88 ; lo +sta $dd04 +lda #0 +sta $dd05 ; hi + +lda $dd0d +lda #$81 +sta $dd0d + +lda #$40 +sta $dd0c +.endif + + cli + jmp * + +irq: + pha + txa + pha + tya + pha + + .if DIGI = 0 + inc $d020 + .endif + + ;lda $d012 + ;sta rastertime + jsr play + + .if DIGI = 0 + dec $d020 + .endif + + ;lda $d012 + ;sec + ;sbc rastertime + ;sta rastertime + + ;asl $d019 + pla + tay + pla + tax + pla + ;rti + jmp $dc0c + +;rastertime: .res 1 + +.if DIGI <> 0 +real_addr = * +.org $2 +nmi: + sta z:ldat+1 +ldn: + lda $200 + sta $d418 + inc ldn+1 +ldat: + lda #0 + jmp $dd0c +NMI_BUFFER: + ldy #0 +NMI_LOOP: + lda z:nmi_add+2 +nmi_cmp: + cmp #0 + bcc nmi_store +nmi_cmp2: + lda #0 + sta z:nmi_add+2 +nmi_none: + lda vol + and #$f0 + ora #10 + sta $200, y + jmp NMI_LOOP_END + +nmi_store: + lda vol + and #$f0 +nmi_add: + ora $1000 + sta $200, y + clc +nmi_freq_lo2: + lda #0 +nmi_freq_lo: + adc #0 + sta nmi_freq_lo2+1 + lda nmi_add+1 +nmi_freq_hi: + adc #1 + sta nmi_add+1 + bcc NMI_LOOP_END + inc nmi_add+2 +NMI_LOOP_END: + iny + cpy #144 + bne NMI_LOOP + rts +nmi_end: +.org real_addr+(nmi_end-nmi) +.endif + +.segment "PLAYER" +.reloc +init: +jmp initaddr +play: +jmp playaddr +.byte " furC64 driver by AArt1256" + +.if actual_use_zp = 0 +variables +.endif +table_1_to_ff: +table_fil: + .byte 0 + .res 15, $ff + +.proc initaddr +.if actual_use_zp = 1 + ldx #vars_len + lda #0 +: + sta start_vars-1, x + dex + bne :- +.endif + + + ldx #(CHIP_AMT)-1 +: + lda #$7f + sta vol, x + lda #$00 + sta res, x + sta retrigger_fil, x + sta triggered_fil, x + lda #$ff + sta fil_lo, x + sta fil_lo_n, x + lda #$07 + sta fil_hi, x + sta fil_hi_n, x + lda #0 + sta doMacroC, x + sta absfil, x + sta fil_lo_n2, x + sta fil_hi_n2, x + sta fil_st2, x + sta mframeC, x + sta has_played, x + dex + bpl :- + + lda #0 + sta tick_sel + + lda ticks_init + sta tick_speeds + lda ticks_init+1 + sta tick_speeds+1 + + lda #0 + sta patind + + ldx #chnum-1 +: + lda #$80 + sta finepitch, x + sta finepitch, x + lda #1 + sta dur, x + lda #$ff + sta cut_dur, x + lda #0 + sta pw_mod_lo, x + sta pw_mod_hi, x +.if porta_once = 1 + sta didporta, x +.endif + sta wav, x + sta retrigger_pw, x + sta abspw, x + sta ins, x + sta inst_prev, x + sta arp, x + sta slide_amt, x + sta slide_amt_sign, x + sta slide_buffer_lo, x + sta slide_buffer_hi, x + sta vibrato_phase, x + sta vibrato_param, x + sta note_dest, x + sta mframeA, x + sta mframeW, x + sta mframeD, x + sta doMacroA, x + sta doMacroW, x + sta doMacroD, x + lda #$ff + sta env_reset_buf, x + lda #8 + sta test_reset, x + lda #4 + sta hrframe, x + sta note_tick, x + dex + bpl :- + + ldx #0 + jsr set_patseq_init + lda #0 + sta tick + sta jumppat + + rts +.endproc + +.macro get_patzp + .local skipW + inc patzp + bne skipW + inc patzp+1 +skipW: + lda (patzp), y +.endmacro + +.macro get_patzp_vol + .local skipW + inc patzp + bne skipW + inc patzp+1 +skipW: + lda vol + and #$f0 + ora (patzp), y +.endmacro + +.macro add_09xx +effectE0: + get_patzp + sta tick_speeds + jmp begnote +.endmacro + + +.macro add_0Fxx +effectE1: + get_patzp + sta tick_speeds+1 + jmp begnote +.endmacro + +.macro add_0Bxx +effectED: + get_patzp + sta patind + lda #$ff + sta jumppat + jmp begnote +.endmacro + + +.macro add_00xx +effectE2: + get_patzp + sta effects_temp+1 + ldx ch + lda #0 + sta arpind, x + lda effects_temp+1 + and #$0f + sta arpeff2, x + lda effects_temp+1 + lsr + lsr + lsr + lsr + sta arpeff1, x + jmp begnote +.endmacro + + +.macro add_01xx +effectE3: + get_patzp + ldx ch + sta slide_amt, x + lda #$ff + sta slide_amt_sign, x + ldx ch + lda #88 + sta note_dest, x +.if porta_once = 1 + lda #$00 + sta didporta, x +.endif + jmp begnote +.endmacro + +.macro add_02xx +effectE4: + get_patzp + ldx ch + sta slide_amt, x + lda #$00 + sta slide_amt_sign, x + ldx ch + lda #0 + sta note_dest, x +.if porta_once = 1 + lda #$00 + sta didporta, x +.endif + jmp begnote +.endmacro + +.macro add_03xx +effectE5: + get_patzp + ldx ch + sta slide_amt, x + ldy #0 + get_patzp + ldx ch + sta note_dest, x + +.if porta_once = 1 + lda #$ff + sta didporta, x +.endif + + lda note_n, x + cmp note_dest, x + bne :+ + lda #0 + sta slide_amt, x + sta slide_amt_sign, x + jmp begnote +: + bcc :+ + lda #$00 + sta slide_amt_sign, x + jmp begnote +: + lda #$ff + sta slide_amt_sign, x + jmp begnote +.endmacro + +.macro add_04xx + .local retskip +effectE6: + get_patzp + ldx ch + sta vibrato_param, x + beq retskip + sta vibrato_phase, x +retskip: + jmp begnote +.endmacro + +.macro add_1Axx +effectF1: + get_patzp + ldx ch + eor #$ff + sta env_reset_buf, x + jmp begnote +.endmacro + + +.macro add_1Bxx + .local retskip +effectE7: + get_patzp + sta effects_temp+1 + and #$0f + tax + lda table_fil, x + ldx ch + beq :+ + ldx chipnum + lda fil_lo_n, x + sta fil_lo, x + lda fil_hi_n, x + sta fil_hi, x + ldx ch +: + lda effects_temp+1 + lsr + lsr + lsr + lsr + tax + lda table_fil, x + ldx chipnum + sta retrigger_fil, x + jmp begnote +.endmacro + +.macro add_1Cxx + .local retskip +effectE8: + get_patzp + sta effects_temp+1 + and #$0f + tax + lda table_fil, x + ldx ch + beq :+ + lda #0 + sta pw_mod_lo, x + sta pw_mod_hi, x +: + lda effects_temp+1 + lsr + lsr + lsr + lsr + tax + lda table_fil, x + ldx ch + sta retrigger_pw, x + jmp begnote +.endmacro + +.macro add_E1xx +effectE9: + get_patzp + ldx ch + asl + asl + sta slide_amt, x + lda #$ff + sta slide_amt_sign, x + ldy #0 + get_patzp + ldx ch + ora #$80 + sta note_dest, x +.if porta_once = 1 + lda #$00 + sta didporta, x +.endif + jmp begnote +.endmacro + +.macro add_E2xx +effectEA: + get_patzp + ldx ch + asl + asl + sta slide_amt, x + lda #$00 + sta slide_amt_sign, x + ldy #0 + get_patzp + ldx ch + ora #$80 + sta note_dest, x +.if porta_once = 1 + lda #$00 + sta didporta, x +.endif + jmp begnote +.endmacro + + +.macro add_E5xx +effectEB: + get_patzp + ldx ch + sta finepitch, x + jmp begnote +.endmacro + +.macro add_ECxx +effectEC: + get_patzp + ldx ch + clc + .if compat_hr = 0 + adc #3 + .else + adc #2 + .endif + sta cut_dur, x + jmp begnote +.endmacro + +.macro add_4xxx +effectEE: + ldx chipnum + lda #0 + sta doMacroC, x + sta fil_hi_n2, x + lda #$ff + sta fil_st2, x + get_patzp + ldx chipnum + sta fil_lo_n2, x + clc + asl fil_lo_n2, x + rol fil_hi_n2, x + clc + asl fil_lo_n2, x + rol fil_hi_n2, x + clc + asl fil_lo_n2, x + rol fil_hi_n2, x + jmp begnote +.endmacro + + +.macro add_EAxx +effectEF: + ldx ch + lda #0 + sta legato, x + jmp begnote + +effectF0: + ldx ch + lda #$ff + sta legato, x + jmp begnote +.endmacro + +add_09xx +add_0Fxx +add_00xx +add_01xx +add_02xx +add_03xx +add_04xx +add_0Bxx +add_1Axx +add_1Bxx +add_1Cxx +add_E1xx +add_E2xx +add_E5xx +add_EAxx +add_ECxx +add_4xxx + +other_effects: + lda effects_temp + cmp #$FB + bne :+ + ldx ch + get_patzp + sta ins, x + jmp begnote +: + cmp #$FC + bne :+ + ldx chipnum + get_patzp_vol + sta vol, x + jmp begnote +: + + cmp #$FD + bne :+ + ldx ch + ldy ins, x + lda insArel, y + sta mframeA, x + lda insDrel, y + sta mframeD, x + ldx ch + .if compat_hr = 0 + lda #3 + .else + lda #2 + .endif + sta cut_dur, x + lda #1 + sta dur, x + ldy #0 + jmp end_advance +: + + cmp #$FE + bne :+ + ldx ch + .if compat_hr = 0 + lda #3 + .else + lda #2 + .endif + sta cut_dur, x + lda #1 + sta dur, x + ldy #0 + jmp end_advance +: + lda effects_temp + cmp #224 + bcc cont_advance + + lda effects_temp + and #$1f + tax + lda eff_lo, x + sta effect_smc+1 + lda eff_hi, x + sta effect_smc+2 + ldx ch + lda effects_temp +effect_smc: + jmp cont_advance + +eff_lo: +.repeat $12, I + .lobytes .ident(.concat ("effect", .sprintf("%02X",I+$e0))) +.endrepeat +eff_hi: +.repeat $12, I + .hibytes .ident(.concat ("effect", .sprintf("%02X",I+$e0))) +.endrepeat + +.macro add_advance_routine +advance: + .local skipD, noIns, noVol, beg, blank2, blank3, hasNote, wait +beg: + lda ch + asl + tax + lda patseq, x + sta patzp + lda patseq+1, x + sta patzp+1 + + ldx ch + dec dur, x + lda dur, x + beq begnote + jmp end_advance +begnote: + + ldy #0 + get_patzp + sta temp + sta effects_temp + and #$80 + beq wait + lda temp + cmp #$ff + beq blank2 + jmp other_effects +cont_advance: + lda temp + and #$7f + ldx ch + + ldy env_reset_buf, x + beq :+ + sta note_n, x +: + sta note_temp, x + + lda note_tick, x + cmp #96 + bne :+ + lda #0 + sta slide_amt, x + sta slide_amt_sign, x +: + lda note_tick, x + cmp #2 + bcc :+ + lda legato, x + bne :+ + lda #0 + sta hrframe, x + lda #$ff + sta hrc, x +: + lda #0 + sta slide_buffer_lo, x + sta slide_buffer_hi, x + sta note_tick, x +.if porta_once = 1 + cmp didporta, x + beq :+ + sta didporta, x + sta slide_amt, x + sta slide_amt_sign, x +: +.endif + ldx ch + lda env_reset_buf, x + sta env_reset, x + lda #1 + sta dur, x + sta has_played, x + ldy #0 + jmp end_advance + +wait: + lda temp + and #$40 + beq :+ + ldx ch + lda temp + and #$3f + sta ins, x + jmp begnote +: + lda temp + ldx ch + sta dur, x +blank2: + lda temp + cmp #$ff + bne end_advance + + lda #$ff + sta nextpat + +end_advance: + lda ch + asl + tax + lda patzp + sta patseq, x + lda patzp+1 + sta patseq+1, x + rts +.endmacro + +add_advance_routine + +.macro add_insarp +insarp: + .local end, skip1, beg, skip2 +beg: + ldx ch + lda doMacroA, x + beq end + + ldx ch + ldy inst_buffer, x + lda insAL, y + sta macroIns + lda insAH, y + sta macroIns+1 + ldx ch + ldy mframeA, x + lda (macroIns), y + cmp #$fe + beq skip2 + cmp #$ff + bne skip1 + iny + lda (macroIns), y + cmp #$ff + beq :+ + sta mframeA, x + jmp beg +: + lda #0 + sta doMacroA, x + rts +skip1: + sec + sbc #128 + sta arp, x + lda #0 + sta absarp, x + inc mframeA, x + rts +skip2: + iny + lda (macroIns), y + sta arp, x + lda #$ff + sta absarp, x + inc mframeA, x + inc mframeA, x +end: + rts +.endmacro + +add_insarp + +.macro add_insduty +insduty: + .local end, skip1, beg, skip2 +beg: + ldx ch + lda doMacroD, x + beq end + + ldx ch + ldy inst_buffer, x + lda insDL, y + sta macroIns + lda insDH, y + sta macroIns+1 + + ;ldx ch + + ldy mframeD, x + lda (macroIns), y + sta temp + cmp #$ff + bne skip1 + iny + lda (macroIns), y + cmp #$ff + beq :+ + sta mframeD, x + jmp beg +: + lda #0 + sta doMacroD, x + rts +skip1: + iny + lda (macroIns), y + sta temp+1 + + lda abspw, x + bne :+ + + lda temp+1 + sec + sbc #$80 + sta temp+1 + lda pw_mod_lo, x + clc + adc temp + sta pw_mod_lo, x + lda pw_mod_hi, x + adc temp+1 + sta pw_mod_hi, x + inc mframeD, x + inc mframeD, x + rts +: + lda temp + sta duty_lo, x + lda temp+1 + sta duty_hi, x + lda #0 + sta pw_mod_lo, x + sta pw_mod_hi, x + inc mframeD, x + inc mframeD, x +end: + rts +.endmacro + +add_insduty + +.macro add_inswave +inswave: + .local end, skip1, beg, skip2 +beg: + ldx ch + lda doMacroW, x + beq end + + ldx ch + ldy inst_buffer, x + lda insWL, y + sta macroIns + lda insWH, y + sta macroIns+1 + ldx ch + ldy mframeW, x + lda (macroIns), y + cmp #$ff + bne skip1 + iny + lda (macroIns), y + cmp #$ff + beq :+ + sta mframeW, x + jmp beg +: + lda #0 + sta doMacroW, x + rts +skip1: + ldx ch + pha + lda wav, x + and #$0f + sta wav, x + pla + ora wav, x + sta wav, x + inc mframeW, x +end: + rts +.endmacro + +add_inswave + +.macro add_inscut +inscut: + .local end, skip1, beg, skip2 +beg: + ldx ch + lda doMacroC, x + beq end + + ldx ch + ldy instF, x + lda insCL, y + sta macroIns + lda insCH, y + sta macroIns+1 + ldx ch + ldy mframeC, x + lda (macroIns), y + sta temp + cmp #$ff + bne skip1 + iny + lda (macroIns), y + cmp #$ff + beq :+ + sta mframeC, x + jmp beg +: + lda #0 + sta doMacroC, x + rts +skip1: + iny + lda (macroIns), y + sta temp+1 + + lda absfil, x + bne :+ + + lda temp+1 + sec + sbc #$80 + sta temp+1 + + lda fil_lo, x + clc + adc temp + sta fil_lo, x + lda fil_hi, x + adc temp+1 + sta fil_hi, x + jmp :++ +: + lda temp + sta fil_lo, x + lda temp+1 + sta fil_hi, x +: + inc mframeC, x + inc mframeC, x +end: + rts +.endmacro + +add_inscut + +.macro cmp16 val1, val2 +/* + .if 0 + lda val1 + sec + sbc val2 + php + lda val1+1 + sbc val2+1 + php + pla + sta macroIns + pla + and #%00000010 + ora #%11111101 + and macroIns + pha + plp + .else + lda val1 + sec + sbc val2 + lda val1+1 + sbc val2+1 + .endif + */ + lda val1 + sec + sbc val2 + lda val1+1 + sbc val2+1 +.endmacro + +doFinepitch: + lda vibrato_param, x + lsr + lsr + lsr + lsr + clc + adc vibrato_phase, x + and #63 + sta vibrato_phase, x + tay + lda vibrato_param, x + and #$0f + ora triangle_lookup, y + tay + + + ; this is NOT a typo! + sec + lda note_pitch_lo, x + sbc finepitch, x + sta temp + lda note_pitch_hi, x + sbc #$fe + sta temp+1 + +; bcs skip_pitch +; lda #0 +; sta temp +; sta temp+1 +;skip_pitch: + +.repeat 3 + lda tri_vibrato_lookup, y + ;lsr + ;clc + ;adc tri_vibrato_lookup, y + eor #$ff + + clc + adc temp + sta temp + lda temp+1 + adc #$ff + sta temp+1 +.endrepeat + rts + +.macro add_do_ch +do_ch_jsr: + .local skipHR1, skipHR2, end + ldx ch + lda hrc, x + beq skipHR1 + + lda #0 + sta hrc, x +.if HR_ADSR <> 0 + lda env_reset, x + beq :+ + lda #0 + sta env_reset, x + + lda #TEST_AD + sta ad, x + lda #TEST_SR + sta sr, x + lda wav, x + and #255^1 + ora test_reset, x + sta wav, x +: +.else + lda inst_buffer, x + tax + lda insFL, x + sta patzp + lda insFH, x + sta patzp+1 + ldx ch + ldy #1 + lda (patzp), y + sta ad, x + iny + lda (patzp), y + ldy base + sta sr, x + lda wav, x + and #255^1 + ora test_reset, x + sta wav, x +.endif + lda #0 + sta doMacroA, x + sta doMacroD, x + sta doMacroW, x + +skipHR1: + + lda hrframe, x + cmp #4 + bne :+ + jmp skipHR2 +: + + inc hrframe, x + +.if compat_hr = 0 + cmp #2 +.else + cmp #1 +.endif + beq :+ + jmp skipHR2 +: + + ldx ch + + ; for 1Axx + lda note_temp, x + sta note_n, x + + lda #0 + sta mframeA, x + sta mframeD, x + sta mframeW, x + lda #$ff + sta doMacroA, x + sta doMacroD, x + sta doMacroW, x + + lda inst_buffer, x + tax + lda insFL, x + sta patzp + lda insFH, x + sta patzp+1 + + ldx ch + ldy #1 + lda (patzp), y + sta ad, x + iny + lda (patzp), y + sta sr, x + + iny + lda (patzp), y + sta duty_lo, x + iny + lda (patzp), y + sta duty_hi, x + + iny + lda (patzp), y + sta flags_temp + and #1 + ;tay + ;lda table_1_to_ff, y + sta abspw, x + + lda flags_temp + and #2 + beq :+ + lda #0 + sta pw_mod_lo, x + sta pw_mod_hi, x +: + + lda flags_temp + and #4 + tay + ldx chipnum + lda res, x + ldx chm + and ch_filter_enable_inv, x + cpy #0 + beq :+ + ora ch_filter_enable, x +: + ldx chipnum + sta res, x + + lda flags_temp + and #8 + beq :++ + ldx chipnum + lda #0 + sta doMacroC, x + lda res, x + and #$0f + ldy #6 + ora (patzp), y + sta res, x + iny + lda vol, x + and #$0f + ora (patzp), y + sta vol, x + iny + lda (patzp), y + sta fil_lo_n, x + iny + lda (patzp), y + sta fil_hi_n, x + + lda flags_temp + and #32 + bne :++ + lda triggered_fil, x + beq :+ + lda retrigger_fil, x + beq :++ +: + lda #$ff + sta triggered_fil, x + lda fil_lo_n, x + sta fil_lo, x + lda fil_hi_n, x + sta fil_hi, x +: + + ldx ch + lda inst_buffer, x + cmp inst_prev, x + beq :+ + ldx chipnum + lda fil_lo_n, x + sta fil_lo, x + lda fil_hi_n, x + sta fil_hi, x + ldx ch + lda inst_buffer, x + sta inst_prev, x +: + + lda flags_temp + and #32 + beq :+ + ldx ch + lda inst_buffer, x + ldx chipnum + sta instF, x + lda #0 + sta mframeC, x + lda #$ff + sta doMacroC, x + + lda flags_temp + and #16 + lsr + tay + lda table_1_to_ff, y + ldy chipnum + sta absfil, y + +: + + lda flags_temp + and #64 + lsr + lsr + lsr + ldx ch + sta test_reset, x + + lda retrigger_pw, x + beq :+ + lda #0 + sta pw_mod_lo, x + sta pw_mod_hi, x +: + + lda mframeW, x + beq :+ + ldy #0 + lda (patzp), y + and #$0f + sta temp + lda wav, x + and #%11110000 + ora #1 + ora temp + sta wav, x + jmp skipHR2 +: + ldx ch + ldy #0 + lda (patzp), y + ora #1 + sta wav, x +skipHR2: + rts + +.endmacro + +add_do_ch + +.macro add_do_ch_DIGI +do_ch_DIGI: + .local skipHR1, skipHR2, skipHR_ADSR, end + + ldx #3 + jsr doFinepitch + +.repeat 4 + clc + lsr temp+1 + ror temp +.endrepeat + + lda temp + sta nmi_freq_lo+1 + lda temp+1 + sta nmi_freq_hi+1 + + ldx ch + lda hrc, x + beq skipHR1 + + lda #0 + sta hrc, x + sta doMacroA, x + sta doMacroD, x + sta doMacroW, x + +skipHR1: + + lda hrframe, x + cmp #4 + bne :+ + jmp skipHR2 +: + + inc hrframe, x + +.if compat_hr = 0 + cmp #2 +.else + cmp #1 +.endif + beq :+ + jmp skipHR_ADSR +: + + ldx ch + lda #0 + sta mframeA, x + sta mframeD, x + sta mframeW, x + lda #$ff + sta doMacroA, x + sta doMacroD, x + sta doMacroW, x + + lda inst_buffer, x + tax + lda insSI, x + tax + lda sampleHS, x + sta nmi_add+2 + lda #0 + sta nmi_add+1 + lda sampleHE, x + sta nmi_cmp+1 + sta nmi_cmp2+1 + +skipHR_ADSR: + +skipHR2: + rts + +.endmacro + +.if DIGI <> 0 + add_do_ch_DIGI +.endif + +.proc playaddr +.if DIGI <> 0 + lda #0 + sta ldn+1 + jsr NMI_BUFFER +.endif + + ldx tick_sel + inc tick + lda tick + cmp tick_speeds, x + bcs :+ + jmp skipseq +: + lda #0 + sta tick + +advance_tick: + + lda tick_sel + eor #1 + sta tick_sel + ldx #(CHIP_AMT)-1 + lda #0 +: + sta fil_st2, x + dex + bpl :- + + + .repeat chnum, I + lda #I + sta ch + lda #I/3 + sta chipnum + jsr advance + .endrepeat + + lda nextpat + beq skipnextpat + lda #0 + sta nextpat + lda jumppat + beq :+ + lda #0 + sta jumppat + jmp :++ +: + inc patind + lda patind + cmp #order0len + bne :+ + lda #0 ; #patloop + sta patind +: + jsr set_patseq + ldx #chnum-1 + lda #1 +durloop: + sta dur, x + dex + bpl durloop + jmp advance_tick +skipnextpat: + lda jumppat + beq :+ + lda #$ff + sta nextpat +: +skipseq: + +.if DIGI = 0 +.repeat chnum, I + ldx #I + stx ch + lda #I .mod 3 + sta chm + lda #(I .mod 3)*7+(I/3)*$20 + sta base + lda #I/3 + sta chipnum + jsr do_ch_jsr + ldx ch + lda #$ff + sta env_reset_buf, x +.endrepeat +.else +.repeat 4, I + ldx #I + stx ch + lda #I .mod 3 + sta chm + lda #(I .mod 3)*7+(I/3)*$20 + sta base + lda #I/3 + sta chipnum + .if I = 3 + jsr do_ch_DIGI + .else + jsr do_ch_jsr + .endif + ldx ch + lda #$ff + sta env_reset_buf, x +.endrepeat +.endif + + +.repeat chnum, I + lda #I + sta ch + jsr insarp + jsr inswave + jsr insduty +.endrepeat + +.repeat CHIP_AMT, I + lda #I + sta ch + jsr inscut + ldx ch + lda fil_st2, x + beq :+ + lda fil_lo_n2, x + sta fil_lo, x + lda fil_hi_n2, x + sta fil_hi, x +: + +.endrepeat + + ldx #chnum-1 +note_cut_loop: + lda cut_dur, x + cmp #$ff + beq note_cut_loop_end + dec cut_dur, x + lda cut_dur, x + bne note_cut_loop_end + + lda #$ff + sta cut_dur, x + + lda wav, x + and #%11111110 + sta wav, x + +note_cut_loop_end: + dex + bpl note_cut_loop + + ldx #chnum-1 +relslide_loop: + lda note_dest, x + and #$80 + beq slide_skip + eor note_dest, x + sta macroIns + lda slide_amt_sign, x + beq positive_slide2 + lda note_n, x + clc + adc macroIns + jsr clamp_note + sta note_dest, x + jmp slide_skip +positive_slide2: + lda note_n, x + sec + sbc macroIns + jsr clamp_note + sta note_dest, x +slide_skip: + dex + bpl relslide_loop + + ldx #chnum-1 +note_loop: + lda absarp, x + beq nrel + lda arp, x + and #127 + jmp nout +nrel: + lda note_n, x + clc + ;adc arp, x +nout: + ;clc + jsr add_arpeff + jsr clamp_note + tay + clc + lda note_table_lo, y + adc slide_buffer_lo, x + sta note_pitch_lo, x + lda note_table_hi, y + adc slide_buffer_hi, x + sta note_pitch_hi, x + dex + bpl note_loop + + ldx #chnum-1 +slide_loop: + lda slide_amt, x + beq slide_loop2 + + lda slide_amt_sign, x + bne positive_slide + sec + lda slide_buffer_lo, x + sbc slide_amt, x + sta slide_buffer_lo, x + bcs :+ + dec slide_buffer_hi, x +: + + ldy note_dest, x + lda note_table_lo, y + sec + sbc note_pitch_lo, x + lda note_table_hi, y + sbc note_pitch_hi, x + bcc slide_loop2 + jmp finish_slide +positive_slide: + clc + lda slide_buffer_lo, x + adc slide_amt, x + sta slide_buffer_lo, x + bcc :+ + inc slide_buffer_hi, x +: + jmp :+ +finish_slide: + lda note_dest, x + sta note_n, x + lda #0 + sta slide_buffer_lo, x + sta slide_buffer_hi, x + sta slide_amt, x + sta slide_amt_sign, x + jmp slide_loop2 +: + ldy note_dest, x + lda note_pitch_lo, x + sec + sbc note_table_lo, y + lda note_pitch_hi, x + sbc note_table_hi, y + bcc slide_loop2 + jmp finish_slide + +slide_loop2: + dex + bpl slide_loop + + ldx #chnum-1 +: + lda note_tick, x + cmp #96 + beq note_tick_loop + inc note_tick, x +note_tick_loop: + + lda has_played, x + bne note_tick_loop2 + lda #3 + sta note_tick, x +note_tick_loop2: + dex + bpl :- + +.if DIGI = 0 +lda vol +sta $d418 +.if chnum >= 6 +lda vol+1 +sta $d438 +.endif +.if chnum >= 9 +lda vol+2 +sta $d458 +.endif +.if chnum >= 12 +lda vol+3 +sta $d478 +.endif +.endif + +lda res +sta $d417 +.if chnum >= 6 +lda res+1 +sta $d437 +.endif +.if chnum >= 9 +lda res+2 +sta $d457 +.endif +.if chnum >= 12 +lda res+3 +sta $d477 +.endif + +lda fil_lo +sta temp +lda fil_hi +sta temp+1 + +clc +lsr temp+1 +ror temp +clc +lsr temp+1 +ror temp +clc +lsr temp+1 +ror temp + +lda temp +sta $d416 + +.if chnum >= 6 +lda fil_lo+1 +sta temp +lda fil_hi+1 +sta temp+1 + +clc +lsr temp+1 +ror temp +clc +lsr temp+1 +ror temp +clc +lsr temp+1 +ror temp + +lda temp +sta $d436 +.endif + +.if chnum >= 9 +lda fil_lo+2 +sta temp +lda fil_hi+2 +sta temp+1 + +clc +lsr temp+1 +ror temp +clc +lsr temp+1 +ror temp +clc +lsr temp+1 +ror temp + +lda temp +sta $d456 +.endif + +.if chnum >= 12 +lda fil_lo+2 +sta temp +lda fil_hi+2 +sta temp+1 + +clc +lsr temp+1 +ror temp +clc +lsr temp+1 +ror temp +clc +lsr temp+1 +ror temp + +lda temp +sta $d476 +.endif + + ldx #chnum-1 +note_loop3: + lda absarp, x + beq nrel3 + lda arp, x + and #127 + jmp nout3 +nrel3: + lda note_n, x + clc + adc arp, x +nout3: + clc + jsr add_arpeff + jsr clamp_note + tay + clc + lda note_table_lo, y + adc slide_buffer_lo, x + sta note_pitch_lo, x + lda note_table_hi, y + adc slide_buffer_hi, x + sta note_pitch_hi, x + dex + bpl note_loop3 + +.repeat CHIP_AMT, J + ldx #2 +: + txa + clc + adc #J*3 + tax + jsr doFinepitch + txa + sec + sbc #J*3 + tax + + ldy ch_mul_tabl, x + lda temp + sta $d400+J*$20,y + lda temp+1 + sta $d401+J*$20, y + + lda ad+J*3, x + sta $d405+J*$20, y + lda sr+J*3, x + sta $d406+J*$20, y + + lda wav+J*3, x + sta $d404+J*$20, y + lda duty_lo+J*3, x + clc + adc pw_mod_lo+J*3, x + sta $d402+J*$20, y + lda duty_hi+J*3, x + adc pw_mod_hi+J*3, x + and #$0f + sta $d403+J*$20, y + + dex + bpl :- +.endrepeat + rts +.endproc + +set_patseq: + stx temp+2 + + ldx patind + + .repeat chnum, I + lda .ident(.concat ("order", .sprintf("%d",I), "L")), x + sta patseq+0+2*I + lda .ident(.concat ("order", .sprintf("%d",I), "H")), x + sta patseq+1+2*I + .endrepeat + + ldx temp+2 + rts + +set_patseq_init: + .repeat chnum, I + ldx patind + lda .ident(.concat ("order", .sprintf("%d",I), "L")), x + sta patseq+0+2*I + lda .ident(.concat ("order", .sprintf("%d",I), "H")), x + sta patseq+1+2*I + .endrepeat + rts + +add_arpeff: + pha + inc arpind, x + ldy arpind, x + lda arp_mod, y + sta arpind, x + tay + pla + cpy #1 + beq arp1 + cpy #2 + beq arp2 + rts + +arp1: + clc + adc arpeff1, x + rts + +arp2: + clc + adc arpeff2, x + rts + +arp_mod: +.byte 0,1,2,0 + +ch_mul_tabl: +.byte 0,7,14 + +clamp_note: + cmp #95 + bcc :+ + lda #95 +: + rts + +note_table_lo: + .incbin "note_lo.bin" +note_table_hi: + .incbin "note_hi.bin" + +ch_filter_enable: +.byte 1, 2, 4 +ch_filter_enable_inv: +.byte 255^1, 255^2, 255^4 + +triangle_lookup: + .repeat 64, I + .if (I+0)&32 + .byte ((32-(((I+0)&63)-32)-1)>>1)<<4 + .else + .byte (((I+0)&63)>>1)<<4 + .endif + .endrepeat + +tri_vibrato_lookup: + .repeat 16, J + .repeat 16, I + ;.byte ((I*((J>>1)-15)*2)/15)+$1f + VAL .set ((I*((J<<1)-15)*2)/3) + .if (VAL+$80) < 0 + .byte 0 + .elseif (VAL+$80) > 255 + .byte $ff + .else + .byte VAL+$80 + .endif + .endrepeat + .endrepeat + +.include "song.asm" + +;.res 4096, $00 \ No newline at end of file diff --git a/loader/samples/minexample/furC64/asm/note_hi.bin b/loader/samples/minexample/furC64/asm/note_hi.bin new file mode 100644 index 0000000..a472973 --- /dev/null +++ b/loader/samples/minexample/furC64/asm/note_hi.bin @@ -0,0 +1,3 @@ + + +  "$')+.147:>AEINRW\bhnu| \ No newline at end of file diff --git a/loader/samples/minexample/furC64/asm/note_lo.bin b/loader/samples/minexample/furC64/asm/note_lo.bin new file mode 100644 index 0000000..ee26c5b --- /dev/null +++ b/loader/samples/minexample/furC64/asm/note_lo.bin @@ -0,0 +1 @@ +&8K^s ,MqBsY,{'Q67YN Ϣmgo:Ddu8&?"ȴqLh8Eh$ \ No newline at end of file diff --git a/loader/samples/minexample/furC64/chipchune/__init__.py b/loader/samples/minexample/furC64/chipchune/__init__.py new file mode 100644 index 0000000..fd53fbd --- /dev/null +++ b/loader/samples/minexample/furC64/chipchune/__init__.py @@ -0,0 +1,18 @@ +""" +:mod:`chipchune` is a Python library for manipulating several +different kinds of chiptune music files. + +Currently supports: + - Furnace (:mod:`chipchune.furnace`) (almost!) + +Plans to support: + - DefleMask (:mod:`chipchune.deflemask`) + - FamiTracker (:mod:`chipchune.famitracker`) + +### Installation + +`pip install git+https://github.com/ZoomTen/chipchune@master` + +""" + +__version__ = "0.0.1" diff --git a/loader/samples/minexample/furC64/chipchune/_util.py b/loader/samples/minexample/furC64/chipchune/_util.py new file mode 100644 index 0000000..4fa2842 --- /dev/null +++ b/loader/samples/minexample/furC64/chipchune/_util.py @@ -0,0 +1,98 @@ +import struct +from enum import Enum +from typing import BinaryIO, Any, cast +import io + +known_sizes = { + 'c': 1, + 'b': 1, 'B': 1, + '?': 1, + 'h': 2, 'H': 2, + 'i': 4, 'I': 4, + 'l': 4, 'L': 4, + 'q': 8, 'Q': 8, + 'e': 2, 'f': 4, + 'd': 8 +} + + +class EnumShowNameOnly(Enum): + """ + Just an Enum, except its string repr is + just the enum's name + """ + def __repr__(self) -> str: + return self.name + + def __str__(self) -> str: + return self.__repr__() + + +class EnumValueEquals(Enum): + """ + Enum that can be compared to its raw value. + """ + def __eq__(self, other: Any) -> bool: + return cast(bool, self.value == other) + + +def truthy_to_boolbyte(value: Any) -> bytes: + """ + If value is truthy, output b'\x01'. Else output b'\x00'. + + :param value: anything + """ + if value: + return b'\x01' + else: + return b'\x00' + + +# these are just to make the typehinter happy +# cast(dolphin, foobar) should've been named trust_me_bro_im_a(dolphin, foobar) + + +def read_int(file: BinaryIO, signed: bool = False) -> int: + """ + 4 bytes + """ + if signed: + return cast(int, struct.unpack(' int: + """ + 2 bytes + """ + if signed: + return cast(int, struct.unpack(' int: + """ + 1 bytes + """ + if signed: + return cast(int, struct.unpack(' float: + """ + 4 bytes + """ + return cast(float, struct.unpack(' str: + """ + variable string (ends in \\x00) + """ + buffer = bytearray() + char = file.read(1) + while char != b'\x00': + buffer += char + char = file.read(1) + return buffer.decode('utf-8') diff --git a/loader/samples/minexample/furC64/chipchune/deflemask/__init__.py b/loader/samples/minexample/furC64/chipchune/deflemask/__init__.py new file mode 100644 index 0000000..9399e49 --- /dev/null +++ b/loader/samples/minexample/furC64/chipchune/deflemask/__init__.py @@ -0,0 +1,3 @@ +""" +soon! +""" \ No newline at end of file diff --git a/loader/samples/minexample/furC64/chipchune/famitracker/__init__.py b/loader/samples/minexample/furC64/chipchune/famitracker/__init__.py new file mode 100644 index 0000000..9399e49 --- /dev/null +++ b/loader/samples/minexample/furC64/chipchune/famitracker/__init__.py @@ -0,0 +1,3 @@ +""" +soon! +""" \ No newline at end of file diff --git a/loader/samples/minexample/furC64/chipchune/furnace/__init__.py b/loader/samples/minexample/furC64/chipchune/furnace/__init__.py new file mode 100644 index 0000000..32575fc --- /dev/null +++ b/loader/samples/minexample/furC64/chipchune/furnace/__init__.py @@ -0,0 +1,24 @@ +""" +Tools to manipulate Furnace .fur files. + +- :mod:`chipchune.furnace.module`: Tools to inspect and manipulate module files. +- :mod:`chipchune.furnace.instrument`: Tools to inspect and manipulate instrument data from within or without the module. +- :mod:`chipchune.furnace.sample`: Tools to inspect and manipulate sample data (might be merged with inst?) +- :mod:`chipchune.furnace.wavetable`: Tools to inspect and manipulate wavetable data +- :mod:`chipchune.furnace.enums`: Various constants that apply to Furnace. +- :mod:`chipchune.furnace.data_types`: Various data types that apply to Furnace. + + +### Example + + from chipchune.furnace.module import FurnaceModule + + module = FurnaceModule("tests/samples/furnace/skate_or_die.143.fur") + + pattern = module.get_pattern(0, 0, 0) + + print(pattern.as_clipboard()) + + for row in pattern.data: + print(row) +""" diff --git a/loader/samples/minexample/furC64/chipchune/furnace/data_types.py b/loader/samples/minexample/furC64/chipchune/furnace/data_types.py new file mode 100644 index 0000000..0d5415e --- /dev/null +++ b/loader/samples/minexample/furC64/chipchune/furnace/data_types.py @@ -0,0 +1,706 @@ +from dataclasses import dataclass, field +from typing import Tuple, List, TypedDict, Any, Union, Dict + +from .enums import ( + ChipType, LinearPitch, LoopModality, DelayBehavior, JumpTreatment, InputPortSet, OutputPortSet, + InstrumentType, MacroCode, OpMacroCode, MacroType, MacroItem, GBHwCommand, WaveFX, ESFilterMode, + SNESSusMode, GainMode, Note +) + + +# modules +@dataclass +class ChipInfo: + """ + Information on a single chip. + """ + type: ChipType + #: shall be a simple dict, no enums needed + flags: Dict[str, Any] = field(default_factory=dict) + panning: float = 0.0 + surround: float = 0.0 + """ + Chip front/rear balance. + """ + volume: float = 1.0 + + +@dataclass +class ModuleMeta: + """ + Module metadata. + """ + name: str = '' + name_jp: str = '' + author: str = '' + author_jp: str = '' + album: str = '' + """ + Can also be the game name or container name. + """ + album_jp: str = '' + sys_name: str = 'Sega Genesis/Mega Drive' + sys_name_jp: str = '' + comment: str = '' + version: int = 0 + tuning: float = 440.0 + + +@dataclass +class TimingInfo: + """ + Timing information for a single subsong. + """ + arp_speed = 1 + clock_speed = 60.0 + highlight: Tuple[int, int] = (4, 16) + speed: Tuple[int, int] = (0, 0) + timebase = 1 + virtual_tempo: Tuple[int, int] = (150, 150) + + +@dataclass +class ChipList: + """ + Information about chips used in the module. + """ + list: List[ChipInfo] = field(default_factory=list) + master_volume: float = 2.0 + + +@dataclass(repr=False) +class ChannelDisplayInfo: + """ + Relating to channel display in Pattern and Order windows. + """ + name: str = '' + abbreviation: str = '' + collapsed: bool = False + shown: bool = True + + def __repr__(self) -> str: + return "ChannelDisplayInfo(name='%s', abbreviation='%s', collapsed=%s, shown=%s)" % ( + self.name, + self.abbreviation, + self.collapsed, + self.shown + ) + + +@dataclass +class ModuleCompatFlags: + """ + Module compatibility flags, a.k.a. "The Motherload" + + Default values correspond with fileOps.cpp in the furnace src. + """ + + # compat 1 + + limit_slides: bool = False + linear_pitch: LinearPitch = field(default_factory=lambda: LinearPitch.FULL_LINEAR) + loop_modality: LoopModality = field(default_factory=lambda: LoopModality.DO_NOTHING) + proper_noise_layout: bool = True + wave_duty_is_volume: bool = False + reset_macro_on_porta: bool = False + legacy_volume_slides: bool = False + compatible_arpeggio: bool = False + note_off_resets_slides: bool = True + target_resets_slides: bool = True + arpeggio_inhibits_portamento: bool = False + wack_algorithm_macro: bool = False + broken_shortcut_slides: bool = False + ignore_duplicates_slides: bool = False + stop_portamento_on_note_off: bool = False + continuous_vibrato: bool = False + broken_dac_mode: bool = False + one_tick_cut: bool = False + instrument_change_allowed_in_porta: bool = True + reset_note_base_on_arpeggio_stop: bool = True + + # compat 2 (>= dev70) + + broken_speed_selection: bool = False + no_slides_on_first_tick: bool = False + next_row_reset_arp_pos: bool = False + ignore_jump_at_end: bool = False + buggy_portamento_after_slide: bool = False + gb_ins_affects_env: bool = True + shared_extch_state: bool = True + ignore_outside_dac_mode_change: bool = False + e1e2_takes_priority: bool = False + new_sega_pcm: bool = True + weird_fnum_pitch_slides: bool = False + sn_duty_resets_phase: bool = False + linear_pitch_macro: bool = True + pitch_slide_speed_in_linear: int = 4 + old_octave_boundary: bool = False + disable_opn2_dac_volume_control: bool = False + new_volume_scaling: bool = True + volume_macro_lingers: bool = True + broken_out_vol: bool = False + e1e2_stop_on_same_note: bool = False + broken_porta_after_arp: bool = False + sn_no_low_periods: bool = False + cut_delay_effect_policy: DelayBehavior = field(default_factory=lambda: DelayBehavior.LAX) + jump_treatment: JumpTreatment = field(default_factory=lambda: JumpTreatment.ALL_JUMPS) + auto_sys_name: bool = True + disable_sample_macro: bool = False + broken_out_vol_2: bool = False + old_arp_strategy: bool = False + + # not-a-compat (>= dev135) + + auto_patchbay: bool = True + + # compat 3 (>= dev138) + + broken_porta_during_legato: bool = False + + broken_fm_off: bool = False + pre_note_no_effect: bool = False + old_dpcm: bool = False + reset_arp_phase_on_new_note: bool = False + ceil_volume_scaling: bool = False + old_always_set_volume: bool = False + old_sample_offset: bool = False + + +@dataclass +class SubSong: + """ + Information on a single subsong. + """ + name: str = '' + comment: str = '' + speed_pattern: List[int] = field(default_factory=lambda: [6]) + """ + Maximum 16 entries. + """ + grooves: List[List[int]] = field(default_factory=list) + timing: TimingInfo = field(default_factory=TimingInfo) + pattern_length = 64 + order: Dict[int, List[int]] = field(default_factory=lambda: { + 0: [0], 1: [0], 2: [0], 3: [0], 4: [0], + 5: [0], 6: [0], 7: [0], 8: [0], 9: [0] + }) + effect_columns: List[int] = field(default_factory=lambda: [ + 1 for _ in range( + ChipType.YM2612.channels + ChipType.SMS.channels + ) + ]) + channel_display: List[ChannelDisplayInfo] = field(default_factory=lambda: [ + ChannelDisplayInfo() for _ in range( + ChipType.YM2612.channels + ChipType.SMS.channels + ) + ]) + + +@dataclass +class FurnaceRow: + """ + Represents a single row in a pattern. + """ + note: Note + octave: int + instrument: int + volume: int + effects: List[Tuple[int, int]] = field(default_factory=list) + + def as_clipboard(self) -> str: + """ + Renders the selected row in Furnace clipboard format (without header!) + + :return: Furnace clipboard data (str) + """ + note_maps = { + Note.Cs: "C#", + Note.D_: "D-", + Note.Ds: "D#", + Note.E_: "E-", + Note.F_: "F-", + Note.Fs: "F#", + Note.G_: "G-", + Note.Gs: "G#", + Note.A_: "A-", + Note.As: "A#", + Note.B_: "B-", + Note.C_: "C-", + } + if self.note == Note.OFF: + note_str = "OFF" + elif self.note == Note.OFF_REL: + note_str = "===" + elif self.note == Note.REL: + note_str = "REL" + elif self.note == Note.__: + note_str = "..." + else: + note_str = "%s%d" % (note_maps[self.note], self.octave) + + vol = ".." if self.volume==0xffff else "%02X" % self.volume + ins = ".." if self.instrument==0xffff else "%02X" % self.instrument + + rep_str = "%s%s%s" + + for fx in self.effects: + cmd, val = fx + cmd_str = ".." if cmd == 0xffff else "%02X" % cmd + val_str = ".." if val == 0xffff else "%02X" % val + rep_str += "%s%s" % (cmd_str, val_str) + + return rep_str % ( + note_str, + ins, vol + ) + "|" + + def __str__(self) -> str: + if self.note == Note.OFF: + note_str = "OFF" + elif self.note == Note.OFF_REL: + note_str = "===" + elif self.note == Note.REL: + note_str = "///" + elif self.note == Note.__: + note_str = "---" + else: + note_str = "%s%d" % (self.note, self.octave) + + vol = "--" if self.volume==0xffff else "%02x" % self.volume + ins = "--" if self.instrument==0xffff else "%02x" % self.instrument + + rep_str = "row data: %s %s %s" + + for fx in self.effects: + cmd, val = fx + cmd_str = "--" if cmd == 0xffff else "%02x" % cmd + val_str = "--" if val == 0xffff else "%02x" % val + rep_str += " %s%s" % (cmd_str, val_str) + + return "<" + rep_str % ( + note_str, + ins, vol + ) + ">" + + +@dataclass +class FurnacePattern: + """ + Represents one pattern in a module. + """ + channel: int = 0 + index: int = 0 + subsong: int = 0 + data: List[FurnaceRow] = field(default_factory=list) # yeah... + name: str = "" + + def as_clipboard(self) -> str: + """ + Renders the selected pattern in Furnace clipboard format. + + :return: Furnace clipboard data + """ + return "org.tildearrow.furnace - Pattern Data\n0\n" + "\n".join([x.as_clipboard() for x in self.data]) + + def __str__(self) -> str: + return "" % ( + self.name if len(self.name) > 0 else "%02x" % self.index, + self.channel, + self.subsong + ) + + +class InputPatchBayEntry(TypedDict): + """ + A patch that has an "input" connector. + """ + set: InputPortSet + """ + The set that the patch belongs to. + """ + port: int + """ + Which port to connect to. + """ + + +class OutputPatchBayEntry(TypedDict): + """ + A patch that has an "output" connector. + """ + set: OutputPortSet + """ + The set that the patch belongs to. + """ + port: int + """ + Which port to connect from. + """ + + +@dataclass +class PatchBay: + """ + A single patchbay connection. + """ + source: OutputPatchBayEntry + dest: InputPatchBayEntry + + +# instruments +@dataclass +class InsFeatureAbstract: + """ + Base class for all InsFeature* classes. Not really to be used. + """ + _code: str = field(init=False) + + def __post_init__(self) -> None: + if len(self._code) != 2: + raise ValueError('No code defined for this instrument feature') + + # def serialize(self) -> bytes: + # raise Exception('Method serialize() has not been overridden...') + + +@dataclass +class InsFeatureName(InsFeatureAbstract, str): + """ + Instrument's name block. Can be used as a string. + """ + _code = 'NA' + name: str = '' + + def __str__(self) -> str: + return self.name + + +@dataclass +class InsMeta: + version: int = 143 + type: InstrumentType = InstrumentType.FM_4OP + + +@dataclass +class InsFMOperator: + am: bool = False + ar: int = 0 + dr: int = 0 + mult: int = 0 + rr: int = 0 + sl: int = 0 + tl: int = 0 + dt2: int = 0 + rs: int = 0 + dt: int = 0 + d2r: int = 0 + ssg_env: int = 0 + dam: int = 0 + dvb: int = 0 + egt: bool = False + ksl: int = 0 + sus: bool = False + vib: bool = False + ws: int = 0 + ksr: bool = False + enable: bool = True + kvs: int = 2 + + +@dataclass +class InsFeatureFM(InsFeatureAbstract): + _code = 'FM' + alg: int = 0 + fb: int = 4 + fms: int = 0 + ams: int = 0 + fms2: int = 0 + ams2: int = 0 + ops: int = 2 + opll_preset: int = 0 + op_list: List[InsFMOperator] = field(default_factory=lambda: [ + InsFMOperator( + tl=42, ar=31, dr=8, + sl=15, rr=3, mult=5, + dt=5 + ), + InsFMOperator( + tl=48, ar=31, dr=4, + sl=11, rr=1, mult=1, + dt=5 + ), + InsFMOperator( + tl=18, ar=31, dr=10, + sl=15, rr=4, mult=1, + dt=0 + ), + InsFMOperator( + tl=2, ar=31, dr=9, + sl=15, rr=9, mult=1, + dt=0 + ), + ]) + + +@dataclass +class SingleMacro: + kind: Union[MacroCode, OpMacroCode] = field(default_factory=lambda: MacroCode.VOL) + mode: int = 0 + type: MacroType = field(default_factory=lambda: MacroType.SEQUENCE) + delay: int = 0 + speed: int = 1 + open: bool = False + data: List[Union[int, MacroItem]] = field(default_factory=list) + + +@dataclass +class InsFeatureMacro(InsFeatureAbstract): + _code = 'MA' + macros: List[SingleMacro] = field(default_factory=lambda: [SingleMacro()]) + + +@dataclass +class InsFeatureOpr1Macro(InsFeatureMacro): + _code = 'O1' + + +@dataclass +class InsFeatureOpr2Macro(InsFeatureMacro): + _code = 'O2' + + +@dataclass +class InsFeatureOpr3Macro(InsFeatureMacro): + _code = 'O3' + + +@dataclass +class InsFeatureOpr4Macro(InsFeatureMacro): + _code = 'O4' + + +@dataclass +class GBHwSeq: + command: GBHwCommand + data: List[int] = field(default_factory=lambda: [0, 0]) + + +@dataclass +class InsFeatureGB(InsFeatureAbstract): + _code = 'GB' + env_vol: int = 15 + env_dir: int = 0 + env_len: int = 2 + sound_len: int = 0 + soft_env: bool = False + always_init: bool = False + hw_seq: List[GBHwSeq] = field(default_factory=list) + + +@dataclass +class GenericADSR: + a: int = 0 + d: int = 0 + s: int = 0 + r: int = 0 + + +@dataclass +class InsFeatureC64(InsFeatureAbstract): + _code = '64' + tri_on: bool = False + saw_on: bool = True + pulse_on: bool = False + noise_on: bool = False + envelope: GenericADSR = field(default_factory=lambda: GenericADSR(a=0, d=8, s=0, r=0)) + duty: int = 2048 + ring_mod: int = 0 + osc_sync: int = 0 + to_filter: bool = False + vol_is_cutoff: bool = False + init_filter: bool = False + duty_is_abs: bool = False + filter_is_abs: bool = False + no_test: bool = False + res: int = 0 + cut: int = 0 + hp: bool = False + lp: bool = False + bp: bool = False + ch3_off: bool = False + + +@dataclass +class SampleMap: + freq: int = 0 + sample_index: int = 0 + + +@dataclass +class DPCMMap: + pitch: int = 0 + delta: int = 0 + + +@dataclass +class InsFeatureAmiga(InsFeatureAbstract): # Sample data + _code = 'SM' + init_sample: int = 0 + use_note_map: bool = False + use_sample: bool = False + use_wave: bool = False + wave_len: int = 31 + sample_map: List[SampleMap] = field(default_factory=lambda: [SampleMap() for _ in range(120)]) + + +@dataclass +class InsFeatureDPCMMap(InsFeatureAbstract): # DPCM sample data + _code = 'NE' + use_map: bool = False + sample_map: List[DPCMMap] = field(default_factory=lambda: [SampleMap() for _ in range(120)]) + + +@dataclass +class InsFeatureX1010(InsFeatureAbstract): + _code = 'X1' + bank_slot: int = 0 + + +@dataclass +class InsFeaturePowerNoise(InsFeatureAbstract): + _code = 'PN' + octave: int = 0 + + +@dataclass +class InsFeatureSID2(InsFeatureAbstract): + _code = 'S2' + noise_mode: int = 0 + wave_mix: int = 0 + volume: int = 0 + + +@dataclass +class InsFeatureN163(InsFeatureAbstract): + _code = 'N1' + wave: int = -1 + wave_pos: int = 0 + wave_len: int = 32 + wave_mode: int = 3 + + +@dataclass +class InsFeatureFDS(InsFeatureAbstract): # Virtual Boy + _code = 'FD' + mod_speed: int = 0 + mod_depth: int = 0 + init_table_with_first_wave: bool = False # compat + mod_table: List[int] = field(default_factory=lambda: [0 for i in range(32)]) + + +@dataclass +class InsFeatureMultiPCM(InsFeatureAbstract): + _code = 'MP' + ar: int = 15 + d1r: int = 15 + dl: int = 0 + d2r: int = 0 + rr: int = 15 + rc: int = 15 + lfo: int = 0 + vib: int = 0 + am: int = 0 + + +@dataclass +class InsFeatureWaveSynth(InsFeatureAbstract): + _code = 'WS' + wave_indices: List[int] = field(default_factory=lambda: [0, 0]) + rate_divider: int = 1 + effect: WaveFX = WaveFX.NONE + enabled: bool = False + global_effect: bool = False + speed: int = 0 + params: List[int] = field(default_factory=lambda: [0, 0, 0, 0]) + one_shot: bool = False # not read? + + +@dataclass +class InsFeatureSoundUnit(InsFeatureAbstract): + _code = 'SU' + switch_roles: bool = False + + +@dataclass +class InsFeatureES5506(InsFeatureAbstract): + _code = 'ES' + filter_mode: ESFilterMode = ESFilterMode.LPK2_LPK1 + k1: int = 0xffff + k2: int = 0xffff + env_count: int = 0 + left_volume_ramp: int = 0 + right_volume_ramp: int = 0 + k1_ramp: int = 0 + k2_ramp: int = 0 + k1_slow: int = 0 + k2_slow: int = 0 + + +@dataclass +class InsFeatureSNES(InsFeatureAbstract): + _code = 'SN' + use_env: bool = True + sus: SNESSusMode = SNESSusMode.DIRECT + gain_mode: GainMode = GainMode.DIRECT + gain: int = 127 + d2: int = 0 + envelope: GenericADSR = field(default_factory=lambda: GenericADSR(a=15, d=7, s=7, r=0)) + + +@dataclass +class InsFeatureOPLDrums(InsFeatureAbstract): + _code = 'LD' + fixed_drums: bool = False + kick_freq: int = 1312 + snare_hat_freq: int = 1360 + tom_top_freq: int = 448 + + +@dataclass +class _InsFeaturePointerAbstract(InsFeatureAbstract): + """ + Also not really to be used. Container for all "list" features. + """ + _code = 'LL' + pointers: Dict[int, int] = field(default_factory=dict) + + +@dataclass +class InsFeatureSampleList(_InsFeaturePointerAbstract): + """ + List of pointers to all samples used by this instrument. + """ + _code = 'SL' + + +@dataclass +class InsFeatureWaveList(_InsFeaturePointerAbstract): + """ + List of pointers to all wave tables used by this instrument. + """ + _code = 'WL' + +@dataclass +class WavetableMeta: + name: str = '' + width: int = 32 + height: int = 32 + +@dataclass +class SampleMeta: + name: str = '' + length: int = 0 + sample_rate: int = 0 + bitdepth: int = 0 + loop_start: int = 0 + loop_end: int = 0 diff --git a/loader/samples/minexample/furC64/chipchune/furnace/enums.py b/loader/samples/minexample/furC64/chipchune/furnace/enums.py new file mode 100644 index 0000000..d8b29c8 --- /dev/null +++ b/loader/samples/minexample/furC64/chipchune/furnace/enums.py @@ -0,0 +1,648 @@ +from chipchune._util import EnumShowNameOnly, EnumValueEquals +from typing import Tuple + +class LinearPitch(EnumShowNameOnly, EnumValueEquals): + """ + Options for :attr:`chipchune.furnace.data_types.ModuleCompatFlags.linear_pitch`. + """ + NON_LINEAR = 0 + ONLY_PITCH_CHANGE = 1 + FULL_LINEAR = 2 + + +class LoopModality(EnumShowNameOnly, EnumValueEquals): + """ + Options for :attr:`chipchune.furnace.data_types.ModuleCompatFlags.loop_modality`. + """ + HARD_RESET_CHANNELS = 0 + SOFT_RESET_CHANNELS = 1 + DO_NOTHING = 2 + + +class DelayBehavior(EnumShowNameOnly, EnumValueEquals): + """ + Options for :attr:`chipchune.furnace.data_types.ModuleCompatFlags.cut_delay_effect_policy`. + """ + STRICT = 0 + BROKEN = 1 + LAX = 2 + + +class JumpTreatment(EnumShowNameOnly, EnumValueEquals): + """ + Options for :attr:`chipchune.furnace.data_types.ModuleCompatFlags.jump_treatment`. + """ + ALL_JUMPS = 0 + FIRST_JUMP_ONLY = 1 + ROW_JUMP_PRIORITY = 2 + + +class Note(EnumShowNameOnly): + """ + All notes recognized by Furnace + """ + __ = 0 + Cs = 1 + D_ = 2 + Ds = 3 + E_ = 4 + F_ = 5 + Fs = 6 + G_ = 7 + Gs = 8 + A_ = 9 + As = 10 + B_ = 11 + C_ = 12 + OFF = 100 + OFF_REL = 101 + REL = 102 + + +class MacroItem(EnumShowNameOnly): + """ + Special values used only in this parser, to allow data editing similar to that + of Furnace itself. + """ + LOOP = 0 + RELEASE = 1 + + +class MacroCode(EnumShowNameOnly, EnumValueEquals): + """ + Marks what aspect of an instrument does a macro change. + """ + + VOL = 0 + """ + Also: + - C64 cutoff + """ + + ARP = 1 + """ + Not applicable to MSM6258 and MSM6295. + """ + + DUTY = 2 + """ + Also: + - AY noise freq + - POKEY audctl + - Mikey duty/int + - MSM5232 group ctrl + - Beeper/Pokemon Mini pulse width + - T6W28 noise type + - Virtual Boy noise length + - PC Engine/Namco/WonderSwan noise type + - SNES noise freq + - Namco 163 waveform pos. + - ES5506 filter mode + - MSM6258/MSM6295 freq. divider + - ADPCMA global volume + - QSound echo level + """ + + WAVE = 3 + """ + Also: + - OPLL patch + - OPZ/OPM lfo1 shape + """ + + PITCH = 4 + + EX1 = 5 + """ + - OPZ/OPM am depth + - C64 filter mode + - SAA1099 envelope + - X1-010 env. mode + - Namco 163 wave length + - FDS mod depth + - TSU cutoff + - ES5506 filter k1 + - MSM6258 clk divider + - QSound echo feedback + - SNES special + - MSM5232 group attack + - AY8930 duty? + """ + + EX2 = 6 + """ + - C64 resonance + - Namco 163 wave update + - FDS mod speed + - TSU resonance + - ES5506 filter k2 + - QSound echo length + - SNES gain + - MSM5232 group decay + - AY3/AY8930 envelope + """ + + EX3 = 7 + """ + - C64 special + - AY/AY8930 autoenv num + - X1-010 autoenv num + - Namco 163 waveload wave + - FDS mod position + - TSU control + - MSM5232 noise + """ + + ALG = 8 + """ + Also: + - AY/AY8930 autoenv den + - X1-010 autoenv den + - Namco 163 waveload pos + - ES5506 control + """ + + FB = 9 + """ + Also: + - AY8930 noise & mask + - Namco 163 waveload len + - ES5506 outputs + """ + + FMS = 10 + """ + Also: + - AY8930 noise | mask + - Namco 163 waveload trigger + """ + + AMS = 11 + + PAN_L = 12 + + PAN_R = 13 + + PHASE_RESET = 14 + + EX4 = 15 + """ + - C64 test/gate + - TSU phase reset timer + - FM/OPM opmask + """ + + EX5 = 16 + """ + - OPZ am depth 2 + """ + + EX6 = 17 + """ + - OPZ pm depth 2 + """ + + EX7 = 18 + """ + - OPZ lfo2 speed + """ + + EX8 = 19 + """ + - OPZ lfo2 shape + """ + + STOP = 255 + """ + Marks end of macro reading. + """ + + +class OpMacroCode(EnumShowNameOnly, EnumValueEquals): + """ + Controls which FM parameter a macro should change. + """ + AM = 0 + AR = 1 + DR = 2 + MULT = 3 + RR = 4 + SL = 5 + TL = 6 + DT2 = 7 + RS = 8 + DT = 9 + D2R = 10 + SSG_EG = 11 + DAM = 12 + DVB = 13 + EGT = 14 + KSL = 15 + SUS = 16 + VIB = 17 + WS = 18 + KSR = 19 + + +class MacroType(EnumShowNameOnly): + """ + Instrument macro type (version 120+). + """ + SEQUENCE = 0 + ADSR = 1 + LFO = 2 + + +class MacroSize(EnumShowNameOnly): + """ + Type of value stored in the instrument file. + """ + _value_: int + num_bytes: int + signed: bool + + UINT8: Tuple[int, int, bool] = (0, 1, False) + INT8: Tuple[int, int, bool] = (1, 1, True) + INT16: Tuple[int, int, bool] = (2, 2, True) + INT32: Tuple[int, int, bool] = (3, 4, True) + + def __new__(cls, id: int, num_bytes: int, signed: bool): # type: ignore[no-untyped-def] + member = object.__new__(cls) + member._value_ = id + setattr(member, 'num_bytes', num_bytes) + setattr(member, 'signed', signed) + return member + + +class GBHwCommand(EnumShowNameOnly): + """ + Game Boy hardware envelope commands. + """ + ENVELOPE = 0 + SWEEP = 1 + WAIT = 2 + WAIT_REL = 3 + LOOP = 4 + LOOP_REL = 5 + + +class SampleType(EnumShowNameOnly): + """ + Sample types used in Furnace + """ + ZX_DRUM = 0 + NES_DPCM = 1 + QSOUND_ADPCM = 4 + ADPCM_A = 5 + ADPCM_B = 6 + X68K_ADPCM = 7 + PCM_8 = 8 + SNES_BRR = 9 + VOX = 10 + PCM_16 = 16 + + +class InstrumentType(EnumShowNameOnly): + """ + Instrument types currently available as of version 144. + """ + STANDARD = 0 + FM_4OP = 1 + GB = 2 + C64 = 3 + AMIGA = 4 + PCE = 5 + SSG = 6 + AY8930 = 7 + TIA = 8 + SAA1099 = 9 + VIC = 10 + PET = 11 + VRC6 = 12 + FM_OPLL = 13 + FM_OPL = 14 + FDS = 15 + VB = 16 + N163 = 17 + KONAMI_SCC = 18 + FM_OPZ = 19 + POKEY = 20 + PC_BEEPER = 21 + WONDERSWAN = 22 + LYNX = 23 + VERA = 24 + X1010 = 25 + VRC6_SAW = 26 + ES5506 = 27 + MULTIPCM = 28 + SNES = 29 + TSU = 30 + NAMCO_WSG = 31 + OPL_DRUMS = 32 + FM_OPM = 33 + NES = 34 + MSM6258 = 35 + MSM6295 = 36 + ADPCM_A = 37 + ADPCM_B = 38 + SEGAPCM = 39 + QSOUND = 40 + YMZ280B = 41 + RF5C68 = 42 + MSM5232 = 43 + T6W28 = 44 + K007232 = 45 + GA20 = 46 + POKEMON_MINI = 47 + SM8521 = 48 + PV1000 = 49 + + +class ChipType(EnumShowNameOnly): + """ + Furnace chip database, either planned or implemented. + Contains console name, chip ID and number of channels. + """ + _value_: int + channels: int + + YMU759 = (0x01, 17) + GENESIS = (0x02, 10) # YM2612 + SN76489 + SMS = (0x03, 4) # SN76489 + GB = (0x04, 4) # LR53902 + PCE = (0x05, 6) # HuC6280 + NES = (0x06, 5) # RP2A03 + C64_8580 = (0x07, 3) # SID r8580 + SEGA_ARCADE = (0x08, 13) # YM2151 + SegaPCM + NEO_GEO_CD = (0x09, 13) + + GENESIS_EX = (0x42, 13) # YM2612 + SN76489 + SMS_JP = (0x43, 13) # SN76489 + YM2413 + NES_VRC7 = (0x46, 11) # RP2A03 + YM2413 + C64_6581 = (0x47, 3) # SID r6581 + NEO_GEO_CD_EX = (0x49, 16) + + AY38910 = (0x80, 3) + AMIGA = (0x81, 4) # Paula + YM2151 = (0x82, 8) # YM2151 + YM2612 = (0x83, 6) # YM2612 + TIA = (0x84, 2) + VIC20 = (0x85, 4) + PET = (0x86, 1) + SNES = (0x87, 8) # SPC700 + VRC6 = (0x88, 3) + OPLL = (0x89, 9) # YM2413 + FDS = (0x8a, 1) + MMC5 = (0x8b, 3) + N163 = (0x8c, 8) + OPN = (0x8d, 6) # YM2203 + PC98 = (0x8e, 16) # YM2608 + OPL = (0x8f, 9) # YM3526 + + OPL2 = (0x90, 9) # YM3812 + OPL3 = (0x91, 18) # YMF262 + MULTIPCM = (0x92, 24) + PC_SPEAKER = (0x93, 1) # Intel 8253 + POKEY = (0x94, 4) + RF5C68 = (0x95, 8) + WONDERSWAN = (0x96, 4) + SAA1099 = (0x97, 6) + OPZ = (0x98, 8) + POKEMON_MINI = (0x99, 1) + AY8930 = (0x9a, 3) + SEGAPCM = (0x9b, 16) + VIRTUAL_BOY = (0x9c, 6) + VRC7 = (0x9d, 6) + YM2610B = (0x9e, 16) + ZX_BEEPER = (0x9f, 6) # tildearrow's engine + + YM2612_EX = (0xa0, 9) + SCC = (0xa1, 5) + OPL_DRUMS = (0xa2, 11) + OPL2_DRUMS = (0xa3, 11) + OPL3_DRUMS = (0xa4, 20) + NEO_GEO = (0xa5, 14) + NEO_GEO_EX = (0xa6, 17) + OPLL_DRUMS = (0xa7, 11) + LYNX = (0xa8, 4) + SEGAPCM_DMF = (0xa9, 5) + MSM6295 = (0xaa, 4) + MSM6258 = (0xab, 1) + COMMANDER_X16 = (0xac, 17) # VERA + BUBBLE_SYSTEM_WSG = (0xad, 2) + OPL4 = (0xae, 42) + OPL4_DRUMS = (0xaf, 44) + + SETA = (0xb0, 16) # Allumer X1-010 + ES5506 = (0xb1, 32) + Y8950 = (0xb2, 10) + Y8950_DRUMS = (0xb3, 12) + SCC_PLUS = (0xb4, 5) + TSU = (0xb5, 8) + YM2203_EX = (0xb6, 9) + YM2608_EX = (0xb7, 19) + YMZ280B = (0xb8, 8) + NAMCO = (0xb9, 3) # Namco WSG + N15XX = (0xba, 8) # Namco 15xx + CUS30 = (0xbb, 8) # Namco CUS30 + MSM5232 = (0xbc, 8) + YM2612_PLUS_EX = (0xbd, 11) + YM2612_PLUS = (0xbe, 7) + T6W28 = (0xbf, 4) + + PCM_DAC = (0xc0, 1) + YM2612_CSM = (0xc1, 10) + NEO_GEO_CSM = (0xc2, 18) # YM2610 CSM + YM2203_CSM = (0xc3, 10) + YM2608_CSM = (0xc4, 20) + YM2610B_CSM = (0xc5, 20) + K007232 = (0xc6, 2) + GA20 = (0xc7, 4) + SM8521 = (0xc8, 3) + M114S = (0xc9, 16) + ZX_BEEPER_QUADTONE: Tuple[int, int] = (0xca, 5) # Natt Akuma's engine + PV_1000: Tuple[int, int] = (0xcb, 3) # NEC D65010G031 + K053260 = (0xcc, 4) + TED = (0xcd, 2) + NAMCO_C140 = (0xce, 24) + NAMCO_C219 = (0xcf, 16) + + NAMCO_C352 = (0xd0, 32) + ESFM = (0xd1, 18) + ES5503 = (0xd2, 32) + POWERNOISE = (0xd4, 4) + DAVE = (0xd5, 6) + NDS = (0xd6, 16) + GBA = (0xd7, 2) + GBA_MINMOD = (0xd8, 16) + BIFURCATOR = (0xd9, 4) + YM2610B_EX = (0xde, 19) + + QSOUND = (0xe0, 19) + + SID2 = (0xf0, 3) # SID2 + FIVEE01 = (0xf1, 5) # 5E01 + PONG = (0xfc, 1) + DUMMY = (0xfd, 1) + + RESERVED_1 = (0xfe, 1) + RESERVED_2 = (0xff, 1) + + def __new__(cls, id: int, channels: int): # type: ignore[no-untyped-def] + member = object.__new__(cls) + member._value_ = id + setattr(member, 'channels', channels) + return member + + def __repr__(self) -> str: + # repr abuse + # about as stupid as "mapping for the renderer"... + return "%s (0x%02x), %d channel%s" % ( + self.name, self._value_, self.channels, + "s" if self.channels != 1 else "" + ) + + +class InputPortSet(EnumShowNameOnly): + """ + Devices which contain an "input" port. + """ + SYSTEM = 0 + NULL = 0xFFF + + +class OutputPortSet(EnumShowNameOnly): + """ + Devices which contain an "output" port. + """ + CHIP_1 = 0 + CHIP_2 = 1 + CHIP_3 = 2 + CHIP_4 = 3 + CHIP_5 = 4 + CHIP_6 = 5 + CHIP_7 = 6 + CHIP_8 = 7 + CHIP_9 = 8 + CHIP_10 = 9 + CHIP_11 = 10 + CHIP_12 = 11 + CHIP_13 = 12 + CHIP_14 = 13 + CHIP_15 = 14 + CHIP_16 = 15 + CHIP_17 = 16 + CHIP_18 = 17 + CHIP_19 = 18 + CHIP_20 = 19 + CHIP_21 = 20 + CHIP_22 = 21 + CHIP_23 = 22 + CHIP_24 = 23 + CHIP_25 = 24 + CHIP_26 = 25 + CHIP_27 = 26 + CHIP_28 = 27 + CHIP_29 = 28 + CHIP_30 = 29 + CHIP_31 = 30 + CHIP_32 = 31 + PREVIEW = 0xFFD + METRONOME = 0xFFE + NULL = 0xFFF + + +class WaveFX(EnumShowNameOnly): + """ + Used in :attr:`chipchune.furnace.data_types.InsFeatureWaveSynth.effect`. + """ + NONE = 0 + + # single waveform + INVERT = 1 + ADD = 2 + SUBTRACT = 3 + AVERAGE = 4 + PHASE = 5 + CHORUS = 6 + + # double waveform + NONE_DUAL = 128 + WIPE = 129 + FADE = 130 + PING_PONG = 131 + OVERLAY = 132 + NEGATIVE_OVERLAY = 133 + SLIDE = 134 + MIX = 135 + PHASE_MOD = 136 + + +class ESFilterMode(EnumShowNameOnly): + """ + Used in :attr:`chipchune.furnace.data_types.InsFeatureES5506.filter_mode`. + """ + HPK2_HPK2 = 0 + HPK2_LPK1 = 1 + LPK2_LPK2 = 2 + LPK2_LPK1 = 3 + + +class GainMode(EnumShowNameOnly): + """ + Used in :attr:`chipchune.furnace.data_types.InsFeatureSNES.gain_mode`. + """ + DIRECT = 0 + DEC_LINEAR = 4 + DEC_LOG = 5 + INC_LINEAR = 6 + INC_INVLOG = 7 + + +class SNESSusMode(EnumShowNameOnly): + """ + Used in :attr:`chipchune.furnace.data_types.InsFeatureSNES.sus`. + """ + DIRECT = 0 + SUS_WITH_DEC = 1 + SUS_WITH_EXP = 2 + SUS_WITH_REL = 3 + + +class _FurInsImportType(EnumShowNameOnly, EnumValueEquals): + """ + Also only used in this parser to differentiate between different types of instrument formats. + """ + # Old format + FORMAT_0_FILE = 0 + FORMAT_0_EMBED = 1 + + # Dev127 format + FORMAT_1_FILE = 2 + FORMAT_1_EMBED = 3 + +class _FurWavetableImportType(EnumShowNameOnly, EnumValueEquals): + """ + Also only used in this parser to differentiate between different types of wavetable formats. + """ + FILE = 0 + EMBED = 1 + +class _FurSampleType(EnumShowNameOnly, EnumValueEquals): + """ + Also only used in this parser to differentiate between different types of sample formats. + """ + PCM_1_BIT = 0 + DPCM = 1 + YMZ = 3 + QSOUND = 4 + ADPCM_A = 5 + ADPCM_B = 6 + K05_ADPCM = 7 + PCM_8_BIT = 8 + BRR = 9 + VOX = 10 + ULAW = 11 + C219 = 12 + IMA = 13 + PCM_16_BIT = 16 diff --git a/loader/samples/minexample/furC64/chipchune/furnace/instrument.py b/loader/samples/minexample/furC64/chipchune/furnace/instrument.py new file mode 100644 index 0000000..0eaf3b0 --- /dev/null +++ b/loader/samples/minexample/furC64/chipchune/furnace/instrument.py @@ -0,0 +1,1572 @@ +from io import BytesIO +from typing import Optional, Union, BinaryIO, TypeVar, Type, List, Dict + +from chipchune._util import read_byte, read_short, read_int, read_str +from .data_types import ( + InsFeatureAbstract, InsFeatureMacro, InsMeta, InstrumentType, InsFeatureName, + InsFeatureFM, InsFeatureOpr1Macro, InsFeatureOpr2Macro, InsFeatureOpr3Macro, InsFeatureOpr4Macro, + InsFeatureC64, InsFeatureGB, GBHwSeq, SingleMacro, InsFeatureAmiga, InsFeatureOPLDrums, InsFeatureSNES, + GainMode, InsFeatureN163, InsFeatureFDS, InsFeatureWaveSynth, _InsFeaturePointerAbstract, InsFeatureSampleList, + InsFeatureWaveList, InsFeatureMultiPCM, InsFeatureSoundUnit, InsFeatureES5506, InsFeatureX1010, GenericADSR, + InsFeatureDPCMMap, InsFeaturePowerNoise, InsFeatureSID2 +) +from .enums import ( + _FurInsImportType, MacroCode, OpMacroCode, MacroItem, MacroType, GBHwCommand, + SNESSusMode, WaveFX, ESFilterMode, MacroSize +) + +FILE_MAGIC_STR = b'-Furnace instr.-' +DEV127_FILE_MAGIC_STR = b'FINS' + +EMBED_MAGIC_STR = b'INST' +DEV127_EMBED_MAGIC_STR = b'INS2' + +T_MACRO = TypeVar('T_MACRO', bound=InsFeatureMacro) # T_MACRO must be subclass of InsFeatureMacro +T_POINTERS = TypeVar('T_POINTERS', bound=_InsFeaturePointerAbstract) + + +class FurnaceInstrument: + def __init__(self, file_name: Optional[str] = None, protocol_version: Optional[int] = 1) -> None: + """ + Creates or opens a new Furnace instrument as a Python object. + + :param file_name: (Optional) + If specified, then it will parse a file as a FurnaceInstrument. If file name (str) is + given, it will load that file. + + Defaults to None. + + :param protocol_version: (Optional) + If specified, it will determine which format the instrument is to be serialized (exported) + to. It is ignored if loading up a file. + + Defaults to 2 (dev127+ ins. format) + """ + self.file_name: Optional[str] = None + """ + Original file name, if the object was initialized with one. + """ + self.protocol_version: Optional[int] = protocol_version + """ + Instrument file "protocol" version. Currently: + - 0: The "unified" instrument format up to Furnace version 126. + - 1: The new "featural" instrument format introduced in version 127. + """ + self.features: List[InsFeatureAbstract] = [] + """ + List of features, regardless of protocol version. + """ + self.meta: InsMeta = InsMeta() + """ + Instrument metadata. + """ + + # self.wavetables: list[] = [] + # self.samples: list[] = [] + + self.__map_to_fn = { + b'NA': self.__load_na_block, + b'FM': self.__load_fm_block, + b'MA': self.__load_ma_block, + b'64': self.__load_c64_block, + b'GB': self.__load_gb_block, + b'SM': self.__load_sm_block, + b'O1': self.__load_o1_block, + b'O2': self.__load_o2_block, + b'O3': self.__load_o3_block, + b'O4': self.__load_o4_block, + b'LD': self.__load_ld_block, + b'SN': self.__load_sn_block, + b'N1': self.__load_n1_block, + b'FD': self.__load_fd_block, + b'WS': self.__load_ws_block, + b'SL': self.__load_sl_block, + b'WL': self.__load_wl_block, + b'MP': self.__load_mp_block, + b'SU': self.__load_su_block, + b'ES': self.__load_es_block, + b'X1': self.__load_x1_block, + b'NE': self.__load_ne_block, + # TODO: No documentation? + #b'EF': self.__load_ef_block, + b'PN': self.__load_pn_block, + b'S2': self.__load_s2_block, + } + + if isinstance(file_name, str): + self.load_from_file(file_name) + + def load_from_file(self, file_name: Optional[str] = None) -> None: + if isinstance(file_name, str): + self.file_name = file_name + if self.file_name is None: + raise RuntimeError('No file name set, either set self.file_name or pass file_name to the function') + + # since we're loading from an uncompressed file, we can just check the file magic number + with open(self.file_name, 'rb') as f: + detect_magic = f.peek(len(FILE_MAGIC_STR))[:len(FILE_MAGIC_STR)] + if detect_magic == FILE_MAGIC_STR: + return self.load_from_stream(f, _FurInsImportType.FORMAT_0_FILE) + elif detect_magic[:len(DEV127_FILE_MAGIC_STR)] == DEV127_FILE_MAGIC_STR: + return self.load_from_stream(f, _FurInsImportType.FORMAT_1_FILE) + else: # uncompressed for sure + raise ValueError('No recognized file type magic') + + def load_from_bytes(self, data: bytes, import_as: Union[int, _FurInsImportType]) -> None: + """ + Load an instrument from a series of bytes. + + :param data: Bytes + :param import_as: int + see :method:`FurnaceInstrument.load_from_stream` + + """ + return self.load_from_stream( + BytesIO(data), + import_as + ) + + def load_from_stream(self, stream: BinaryIO, import_as: Union[int, _FurInsImportType]) -> None: + """ + Load a module from an **uncompressed** stream. + + :param stream: File-like object containing the uncompressed module. + :param import_as: int + - 0 = old format instrument file + - 1 = old format, embedded in module + - 2 = new format instrument file + - 3 = new format, embedded in module + """ + if import_as == _FurInsImportType.FORMAT_0_FILE: + if stream.read(len(FILE_MAGIC_STR)) != FILE_MAGIC_STR: + raise ValueError('Bad magic value for a format 1 file') + self.protocol_version = 0 + self.meta.version = read_short(stream) + read_short(stream) # reserved + ins_data_ptr = read_int(stream) + num_waves = read_short(stream) + num_samples = read_short(stream) + read_int(stream) # reserved + + # these don't exist for format 1 instrs. + self.__wavetable_ptr = [ + read_int(stream) for _ in range(num_waves) + ] + self.__sample_ptr = [ + read_int(stream) for _ in range(num_samples) + ] + + stream.seek(ins_data_ptr) + self.__load_format_0_embed(stream) + # TODO: load wavetables and samples + + elif import_as == _FurInsImportType.FORMAT_0_EMBED: + self.protocol_version = 0 + return self.__load_format_0_embed(stream) + + elif import_as == _FurInsImportType.FORMAT_1_FILE: + if stream.read(len(DEV127_FILE_MAGIC_STR)) != DEV127_FILE_MAGIC_STR: + raise ValueError('Bad magic value for a format 1 file') + self.protocol_version = 1 + self.__load_format_1(stream) + # TODO: load wavetables and samples + + elif import_as == _FurInsImportType.FORMAT_1_EMBED: + if stream.read(len(DEV127_EMBED_MAGIC_STR)) != DEV127_EMBED_MAGIC_STR: + raise ValueError('Bad magic value for a format 1 embed') + self.protocol_version = 1 + ins_data = BytesIO(stream.read(read_int(stream))) + return self.__load_format_1(ins_data) + + else: + raise ValueError('Invalid import type') + + def __str__(self) -> str: + return '' % ( + self.get_name(), self.meta.type + ) + + def __load_format_1(self, stream: BinaryIO) -> None: + # skip headers and magic + self.meta.version = read_short(stream) + self.meta.type = InstrumentType(read_short(stream)) + self.features.clear() + + # add all the features + feat = self.__read_format_1_feature(stream) + while isinstance(feat, InsFeatureAbstract): + self.features.append(feat) + feat = self.__read_format_1_feature(stream) + + def __read_format_1_feature(self, stream: BinaryIO) -> Optional[object]: # subclass InsFeatureAbstract + code = stream.read(2) + if code == b'EN' or code == b'': # eof + return None + + len_block = read_short(stream) + feature_block = BytesIO(stream.read(len_block)) + + # if this fails it might be a malformed file + return self.__map_to_fn[code](feature_block) + + def get_name(self) -> str: + """ + Shortcut to fetch the instrument name. + + :return: Instrument name + """ + name = '' + for i in self.features: + if isinstance(i, InsFeatureName): + name = i # InsFeatureName also subclasses 'str' so it's fine + return name + + # format 1 features + + def __load_na_block(self, stream: BytesIO) -> InsFeatureName: + return InsFeatureName( + read_str(stream) + ) + + def __load_fm_block(self, stream: BytesIO) -> InsFeatureFM: + fm = InsFeatureFM() + + # read base data + data = [read_byte(stream) for _ in range(4)] + + current = data.pop(0) + ops = current & 0b1111 + fm.op_list[0].enable = bool(current & 16) + fm.op_list[1].enable = bool(current & 32) + fm.op_list[2].enable = bool(current & 64) + fm.op_list[3].enable = bool(current & 128) + + current = data.pop(0) + fm.alg = (current >> 4) & 0b111 + fm.fb = current & 0b111 + + current = data.pop(0) + fm.fms2 = (current >> 5) & 0b111 + fm.ams = (current >> 3) & 0b11 + fm.fms = current & 0b111 + + current = data.pop(0) + fm.ams2 = (current >> 6) & 0b11 + if current & 32: + fm.ops = 4 + else: + fm.ops = 2 + fm.opll_preset = current & 31 + + # read operators + for op in range(ops): + data = [read_byte(stream) for _ in range(8)] + + current = data.pop(0) + fm.op_list[op].ksr = bool(current & 128) + fm.op_list[op].dt = (current >> 4) & 7 + fm.op_list[op].mult = current & 15 + + current = data.pop(0) + fm.op_list[op].sus = bool(current & 128) + fm.op_list[op].tl = current & 127 + + current = data.pop(0) + fm.op_list[op].rs = (current >> 6) & 3 + fm.op_list[op].vib = bool(current & 32) + fm.op_list[op].ar = current & 31 + + current = data.pop(0) + fm.op_list[op].am = bool(current & 128) + fm.op_list[op].ksl = (current >> 5) & 3 + fm.op_list[op].dr = current & 31 + + current = data.pop(0) + fm.op_list[op].egt = bool(current & 128) + fm.op_list[op].kvs = (current >> 5) & 3 + fm.op_list[op].d2r = current & 31 + + current = data.pop(0) + fm.op_list[op].sl = (current >> 4) & 15 + fm.op_list[op].rr = current & 15 + + current = data.pop(0) + fm.op_list[op].dvb = (current >> 4) & 15 + fm.op_list[op].ssg_env = current & 15 + + current = data.pop(0) + fm.op_list[op].dam = (current >> 5) & 7 + fm.op_list[op].dt2 = (current >> 3) & 3 + fm.op_list[op].ws = current & 7 + + return fm + + def __common_ma_block(self, stream: BytesIO, macro_class: Type[T_MACRO]) -> T_MACRO: + ma = macro_class() + ma.macros.clear() + read_short(stream) # header size + + target_code: Union[MacroCode, OpMacroCode] + + if macro_class in [InsFeatureOpr1Macro, + InsFeatureOpr2Macro, + InsFeatureOpr3Macro, + InsFeatureOpr4Macro]: + target_code = OpMacroCode(read_byte(stream)) + else: + target_code = MacroCode(read_byte(stream)) + + while target_code != MacroCode.STOP: + new_macro = SingleMacro(kind=target_code) + + length = read_byte(stream) + loop = read_byte(stream) + release = read_byte(stream) + + new_macro.mode = read_byte(stream) + flags = read_byte(stream) + + word_size = MacroSize(flags >> 6 & 0b11) # type: ignore + new_macro.type = MacroType(flags >> 1 & 0b11) + new_macro.open = bool(flags & 1) + new_macro.delay = read_byte(stream) + new_macro.speed = read_byte(stream) + + # adsr and lfo will simply be kept as a list + macro_content: List[Union[int, MacroItem]] = [ + int.from_bytes( + stream.read(word_size.num_bytes), + byteorder='little', + signed=word_size.signed + ) + for _ in range(length) + ] + + if loop != 0xff: # hard limit in new macro + macro_content.insert(loop, MacroItem.LOOP) + + if release != 0xff: # ^ + macro_content.insert(release, MacroItem.RELEASE) + + new_macro.data = macro_content + + ma.macros.append(new_macro) + + if macro_class in [InsFeatureOpr1Macro, + InsFeatureOpr2Macro, + InsFeatureOpr3Macro, + InsFeatureOpr4Macro]: + target_code = OpMacroCode(read_byte(stream)) + else: + target_code = MacroCode(read_byte(stream)) + + return ma + + def __load_ma_block(self, stream: BytesIO) -> InsFeatureMacro: + return self.__common_ma_block(stream, InsFeatureMacro) + + def __load_o1_block(self, stream: BytesIO) -> InsFeatureOpr1Macro: + return self.__common_ma_block(stream, InsFeatureOpr1Macro) + + def __load_o2_block(self, stream: BytesIO) -> InsFeatureOpr2Macro: + return self.__common_ma_block(stream, InsFeatureOpr2Macro) + + def __load_o3_block(self, stream: BytesIO) -> InsFeatureOpr3Macro: + return self.__common_ma_block(stream, InsFeatureOpr3Macro) + + def __load_o4_block(self, stream: BytesIO) -> InsFeatureOpr4Macro: + return self.__common_ma_block(stream, InsFeatureOpr4Macro) + + def __load_c64_block(self, stream: BytesIO) -> InsFeatureC64: + c64 = InsFeatureC64() + + data = [read_byte(stream) for _ in range(4)] + + current = data.pop(0) + c64.duty_is_abs = bool((current >> 7) & 1) + c64.init_filter = bool((current >> 6) & 1) + c64.vol_is_cutoff = bool((current >> 5) & 1) + c64.to_filter = bool((current >> 4) & 1) + c64.noise_on = bool((current >> 3) & 1) + c64.pulse_on = bool((current >> 2) & 1) + c64.saw_on = bool((current >> 1) & 1) + c64.tri_on = bool(current & 1) + + current = data.pop(0) + c64.osc_sync = bool((current >> 7) & 1) + c64.ring_mod = bool((current >> 6) & 1) + c64.no_test = bool((current >> 5) & 1) + c64.filter_is_abs = bool((current >> 4) & 1) + c64.ch3_off = bool((current >> 3) & 1) + c64.bp = bool((current >> 2) & 1) + c64.hp = bool((current >> 1) & 1) + c64.lp = bool(current & 1) + + current = data.pop(0) + c64.envelope.a = (current >> 4) & 0b1111 + c64.envelope.d = current & 0b1111 + + current = data.pop(0) + c64.envelope.s = (current >> 4) & 0b1111 + c64.envelope.r = current & 0b1111 + + c64.duty = read_short(stream) + + c_r = read_short(stream) + c64.cut = c_r & 0b11111111111 + c64.res = (c_r >> 12) & 0b1111 + + return c64 + + def __load_gb_block(self, stream: BytesIO) -> InsFeatureGB: + gb = InsFeatureGB() + + data = [read_byte(stream) for _ in range(4)] + + current = data.pop(0) + gb.env_vol = current & 0b1111 + gb.env_dir = (current >> 4) & 1 + gb.env_len = (current >> 5) & 0b111 + + gb.sound_len = data.pop(0) + + current = data.pop(0) + gb.soft_env = bool(current & 1) + gb.always_init = bool((current >> 1) & 1) + + hw_seq_len = data.pop(0) + for i in range(hw_seq_len): + seq_entry = GBHwSeq( + GBHwCommand(read_byte(stream)) + ) + seq_entry.data = [ + read_byte(stream), + read_byte(stream) + ] + gb.hw_seq.append(seq_entry) + + return gb + + def __load_sm_block(self, stream: BytesIO) -> InsFeatureAmiga: + sm = InsFeatureAmiga() + + sm.init_sample = read_short(stream) + + current = read_byte(stream) + sm.use_wave = bool((current >> 2) & 1) + sm.use_sample = bool((current >> 1) & 1) + sm.use_note_map = bool(current & 1) + + sm.wave_len = read_byte(stream) + + if sm.use_note_map: + for i in range(len(sm.sample_map)): + sm.sample_map[i].freq = read_short(stream) + sm.sample_map[i].sample_index = read_short(stream) + + return sm + + def __load_ld_block(self, stream: BytesIO) -> InsFeatureOPLDrums: + return InsFeatureOPLDrums( + fixed_drums=bool(read_byte(stream) & 1), + kick_freq=read_short(stream), + snare_hat_freq=read_short(stream), + tom_top_freq=read_short(stream) + ) + + def __load_sn_block(self, stream: BytesIO) -> InsFeatureSNES: + sn = InsFeatureSNES() + + data = [read_byte(stream) for _ in range(4)] + + current = data.pop(0) + sn.envelope.d = (current >> 4) & 0b1111 + sn.envelope.a = current & 0b1111 + + current = data.pop(0) + sn.envelope.s = (current >> 4) & 0b1111 + sn.envelope.r = current & 0b1111 + + current = data.pop(0) + sn.use_env = bool((current >> 4) & 1) + sn.sus = SNESSusMode((current >> 3) & 1) + + gain_mode = current & 0b111 + if current < 4: + gain_mode = 0 + sn.gain_mode = GainMode(gain_mode) + + sn.gain = data.pop(0) + + if self.meta.version >= 131: + d2s = read_byte(stream) + sn.sus = SNESSusMode((d2s >> 5 & 0b11)) + sn.d2 = d2s & 31 + + return sn + + def __load_n1_block(self, stream: BytesIO) -> InsFeatureN163: + return InsFeatureN163( + wave=read_int(stream), + wave_pos=read_byte(stream), + wave_len=read_byte(stream), + wave_mode=read_byte(stream) + ) + + def __load_fd_block(self, stream: BytesIO) -> InsFeatureFDS: + fd = InsFeatureFDS( + mod_speed=read_int(stream), + mod_depth=read_int(stream), + init_table_with_first_wave=bool(read_byte(stream)) + ) + for i in range(32): + fd.mod_table[i] = read_byte(stream) + return fd + + def __load_ws_block(self, stream: BytesIO) -> InsFeatureWaveSynth: + return InsFeatureWaveSynth( + wave_indices=[ + read_int(stream), read_int(stream) + ], + rate_divider=read_byte(stream), + effect=WaveFX(read_byte(stream)), + enabled=bool(read_byte(stream) & 1), + global_effect=bool(read_byte(stream) & 1), + speed=read_byte(stream), + params=[ + read_byte(stream), read_byte(stream), + read_byte(stream), read_byte(stream) + ] + ) + + def __common_pointers_block(self, stream: BytesIO, ptr_class: Type[T_POINTERS]) -> T_POINTERS: + pt = ptr_class() + num_entries = read_byte(stream) + + for _ in range(num_entries): + pt.pointers[read_byte(stream)] = -1 + + for i in pt.pointers: + pt.pointers[i] = read_int(stream) + + return pt + + def __load_sl_block(self, stream: BytesIO) -> InsFeatureSampleList: + return self.__common_pointers_block(stream, InsFeatureSampleList) + + def __load_wl_block(self, stream: BytesIO) -> InsFeatureWaveList: + return self.__common_pointers_block(stream, InsFeatureWaveList) + + def __load_mp_block(self, stream: BytesIO) -> InsFeatureMultiPCM: + return InsFeatureMultiPCM( + ar=read_byte(stream), + d1r=read_byte(stream), + dl=read_byte(stream), + d2r=read_byte(stream), + rr=read_byte(stream), + rc=read_byte(stream), + lfo=read_byte(stream), + vib=read_byte(stream), + am=read_byte(stream), + ) + + def __load_su_block(self, stream: BytesIO) -> InsFeatureSoundUnit: + return InsFeatureSoundUnit( + switch_roles=bool(read_byte(stream)) + ) + + def __load_es_block(self, stream: BytesIO) -> InsFeatureES5506: + return InsFeatureES5506( + filter_mode=ESFilterMode(read_byte(stream)), + k1=read_short(stream), + k2=read_short(stream), + env_count=read_short(stream), + left_volume_ramp=read_byte(stream), + right_volume_ramp=read_byte(stream), + k1_ramp=read_byte(stream), + k2_ramp=read_byte(stream), + k1_slow=read_byte(stream), + k2_slow=read_byte(stream) + ) + + def __load_x1_block(self, stream: BytesIO) -> InsFeatureX1010: + return InsFeatureX1010( + bank_slot=read_int(stream) + ) + + def __load_ne_block(self, stream: BytesIO) -> InsFeatureDPCMMap: + sm = InsFeatureDPCMMap() + + sm.use_map = bool(read_byte(stream) & 1) + + if sm.use_map: + for i in range(len(sm.sample_map)): + sm.sample_map[i].pitch = read_byte(stream) + sm.sample_map[i].delta = read_byte(stream) + + return sm + + # TODO: No documentation? + #def __load_ef_block(self, stream: BytesIO) -> InsFeatureESFM: + # pass + + def __load_pn_block(self, stream: BytesIO) -> InsFeaturePowerNoise: + return InsFeaturePowerNoise( + octave=read_byte(stream) + ) + + def __load_s2_block(self, stream: BytesIO) -> InsFeatureSID2: + current_byte = read_byte(stream) + return InsFeatureSID2( + volume=current_byte & 0b1111, + wave_mix=(current_byte >> 4) & 0b11, + noise_mode=(current_byte >> 6) & 0b11 + ) + + # format 0; also used for file because it includes the "INST" header too + + def __load_format_0_embed(self, stream: BinaryIO) -> None: + # load format 0 as a series of format 1 feature blocks + + # aux function... + def add_to_macro_data(macro: List[Union[int, MacroItem]], + loop: Optional[int] = 0xffffffff, + release: Optional[int] = 0xffffffff, + data: Optional[List[int]] = None) -> None: + if data is not None: + macro.extend(data) + if loop is not None and loop != 0xffffffff: # old macros have a 4-byte length + macro.insert(loop, MacroItem.LOOP) + if release is not None and release != 0xffffffff: + macro.insert(release, MacroItem.RELEASE) + + # we check the header here + if stream.read(len(EMBED_MAGIC_STR)) != EMBED_MAGIC_STR: + raise RuntimeError('Bad magic value for a format 0 embed') + + blk_size = read_int(stream) + if blk_size > 0: + ins_data = BytesIO(stream.read(blk_size)) + else: + ins_data = stream + + self.meta.version = read_short(ins_data) # overwrites the file header version + self.meta.type = InstrumentType(read_byte(ins_data)) + + read_byte(ins_data) + + # read all features in one go! + self.features.clear() + + # name, insert immediately + self.features.append( + InsFeatureName(read_str(ins_data)) + ) + + # fm + if True: + fm = InsFeatureFM( + alg=read_byte(ins_data), + fb=read_byte(ins_data), + fms=read_byte(ins_data), + ams=read_byte(ins_data), + ops=read_byte(ins_data), + opll_preset=read_byte(ins_data) + ) + read_short(ins_data) + for i in range(4): + fm.op_list[i].am = bool(read_byte(ins_data)) + fm.op_list[i].ar = read_byte(ins_data) + fm.op_list[i].dr = read_byte(ins_data) + fm.op_list[i].mult = read_byte(ins_data) + fm.op_list[i].rr = read_byte(ins_data) + fm.op_list[i].sl = read_byte(ins_data) + fm.op_list[i].tl = read_byte(ins_data) + fm.op_list[i].dt2 = read_byte(ins_data) + fm.op_list[i].rs = read_byte(ins_data) + fm.op_list[i].dt = read_byte(ins_data) + fm.op_list[i].d2r = read_byte(ins_data) + fm.op_list[i].ssg_env = read_byte(ins_data) + fm.op_list[i].dam = read_byte(ins_data) + fm.op_list[i].dvb = read_byte(ins_data) + fm.op_list[i].egt = bool(read_byte(ins_data)) + fm.op_list[i].ksl = read_byte(ins_data) + fm.op_list[i].sus = bool(read_byte(ins_data)) + fm.op_list[i].vib = bool(read_byte(ins_data)) + fm.op_list[i].ws = read_byte(ins_data) + fm.op_list[i].ksr = bool(read_byte(ins_data)) + en = read_byte(ins_data) + if self.meta.version >= 114: + fm.op_list[i].enable = bool(en) + kvs = read_byte(ins_data) + if self.meta.version >= 115: + fm.op_list[i].kvs = kvs + ins_data.read(10) + self.features.append(fm) + + # gameboy + if True: + gb = InsFeatureGB( + env_vol=read_byte(ins_data), + env_dir=read_byte(ins_data), + env_len=read_byte(ins_data), + sound_len=read_byte(ins_data) + ) + self.features.append(gb) + + # c64 + if True: + c64 = InsFeatureC64( + tri_on=bool(read_byte(ins_data)), + saw_on=bool(read_byte(ins_data)), + pulse_on=bool(read_byte(ins_data)), + noise_on=bool(read_byte(ins_data)), + duty=read_short(ins_data), + ring_mod=read_byte(ins_data), + osc_sync=read_byte(ins_data), + to_filter=bool(read_byte(ins_data)), + init_filter=bool(read_byte(ins_data)), + vol_is_cutoff=bool(read_byte(ins_data)), + res=read_byte(ins_data), + lp=bool(read_byte(ins_data)), + bp=bool(read_byte(ins_data)), + hp=bool(read_byte(ins_data)), + ch3_off=bool(read_byte(ins_data)), + cut=read_short(ins_data), + duty_is_abs=bool(read_byte(ins_data)), + filter_is_abs=bool(read_byte(ins_data)) + ) + c64.envelope = GenericADSR( + a=read_byte(ins_data), + d=read_byte(ins_data), + s=read_byte(ins_data), + r=read_byte(ins_data), + ) + self.features.append(c64) + + # amiga + if True: + amiga = InsFeatureAmiga( + init_sample=read_short(ins_data) + ) + + wave = read_byte(ins_data) + wavelen = read_byte(ins_data) + if self.meta.version >= 82: + amiga.use_wave = bool(wave) + amiga.wave_len = wavelen + + for _ in range(12): + read_byte(ins_data) # reserved + + self.features.append(amiga) + + # standard + if True: + mac = InsFeatureMacro() + + vol_mac = SingleMacro(kind=MacroCode.VOL) + arp_mac = SingleMacro(kind=MacroCode.ARP) + duty_mac = SingleMacro(kind=MacroCode.DUTY) + wave_mac = SingleMacro(kind=MacroCode.WAVE) + + vol_mac.data.clear() + arp_mac.data.clear() + duty_mac.data.clear() + wave_mac.data.clear() + + mac_list: List[SingleMacro] = [vol_mac, arp_mac, duty_mac, wave_mac] + mac.macros = mac_list + + vol_mac_len = read_int(ins_data) + arp_mac_len = read_int(ins_data) + duty_mac_len = read_int(ins_data) + wave_mac_len = read_int(ins_data) + + if self.meta.version >= 17: + pitch_mac = SingleMacro(kind=MacroCode.PITCH) + x1_mac = SingleMacro(kind=MacroCode.EX1) + x2_mac = SingleMacro(kind=MacroCode.EX2) + x3_mac = SingleMacro(kind=MacroCode.EX3) + + pitch_mac.data.clear() + x1_mac.data.clear() + x2_mac.data.clear() + x3_mac.data.clear() + + mac_list.extend([pitch_mac, x1_mac, x2_mac, x3_mac]) + + pitch_mac_len = read_int(ins_data) + x1_mac_len = read_int(ins_data) + x2_mac_len = read_int(ins_data) + x3_mac_len = read_int(ins_data) + + vol_mac_loop = read_int(ins_data) + arp_mac_loop = read_int(ins_data) + duty_mac_loop = read_int(ins_data) + wave_mac_loop = read_int(ins_data) + + if self.meta.version >= 17: + pitch_mac_loop = read_int(ins_data) + x1_mac_loop = read_int(ins_data) + x2_mac_loop = read_int(ins_data) + x3_mac_loop = read_int(ins_data) + + arp_mac_mode = read_byte(ins_data) + old_vol_height = read_byte(ins_data) + old_duty_height = read_byte(ins_data) + + read_byte(ins_data) + + add_to_macro_data(vol_mac.data, + loop=vol_mac_loop, + release=None, + data=[read_int(ins_data) for _ in range(vol_mac_len)]) + + add_to_macro_data(arp_mac.data, + loop=arp_mac_loop, + release=None, + data=[read_int(ins_data) for _ in range(arp_mac_len)]) + + add_to_macro_data(duty_mac.data, + loop=duty_mac_loop, + release=None, + data=[read_int(ins_data) for _ in range(duty_mac_len)]) + + add_to_macro_data(wave_mac.data, + loop=wave_mac_loop, + release=None, + data=[read_int(ins_data) for _ in range(wave_mac_len)]) + + # adjust values + if self.meta.version < 31: + if arp_mac_mode == 0: + for j in range(len(arp_mac.data)): + if isinstance(arp_mac.data[j], int): + arp_mac.data[j] -= 12 + if self.meta.version < 87: + if c64.vol_is_cutoff and not c64.filter_is_abs: + for j in range(len(vol_mac.data)): + if isinstance(vol_mac.data[j], int): + vol_mac.data[j] -= 18 + if c64.duty_is_abs: # TODO + for j in range(len(duty_mac.data)): + if isinstance(duty_mac.data[j], int): + duty_mac.data[j] -= 12 + if self.meta.version < 112: + if arp_mac_mode == 1: # fixed arp! + for i in range(len(arp_mac.data)): + if isinstance(arp_mac.data[i], int): + arp_mac.data[i] |= (1 << 30) + if len(arp_mac.data) > 0: + if arp_mac_loop != 0xffffffff: + if arp_mac_loop == arp_mac_len+1: + arp_mac.data[-1] = 0 + arp_mac.data.append(MacroItem.LOOP) + elif arp_mac_loop == arp_mac_len: + arp_mac.data.append(0) + else: + arp_mac.data.append(0) + + # read more macros + if self.meta.version >= 17: + add_to_macro_data(pitch_mac.data, + loop=pitch_mac_loop, + release=None, + data=[read_int(ins_data) for _ in range(pitch_mac_len)]) + + add_to_macro_data(x1_mac.data, + loop=x1_mac_loop, + release=None, + data=[read_int(ins_data) for _ in range(x1_mac_len)]) + + add_to_macro_data(x2_mac.data, + loop=x2_mac_loop, + release=None, + data=[read_int(ins_data) for _ in range(x2_mac_len)]) + + add_to_macro_data(x3_mac.data, + loop=x3_mac_loop, + release=None, + data=[read_int(ins_data) for _ in range(x3_mac_len)]) + else: + if self.meta.type == InstrumentType.STANDARD: + if old_vol_height == 31: + self.meta.type = InstrumentType.PCE + elif old_duty_height == 31: + self.meta.type = InstrumentType.SSG + + self.features.append(mac) + + # fm macros + if True: + if self.meta.version >= 29: + alg_mac = SingleMacro(kind=MacroCode.ALG) + fb_mac = SingleMacro(kind=MacroCode.FB) + fms_mac = SingleMacro(kind=MacroCode.FMS) + ams_mac = SingleMacro(kind=MacroCode.AMS) + mac_list.extend([alg_mac, fb_mac, fms_mac, ams_mac]) + + alg_mac.data.clear() + fb_mac.data.clear() + fms_mac.data.clear() + ams_mac.data.clear() + + alg_mac_len = read_int(ins_data) + fb_mac_len = read_int(ins_data) + fms_mac_len = read_int(ins_data) + ams_mac_len = read_int(ins_data) + + alg_mac_loop = read_int(ins_data) + fb_mac_loop = read_int(ins_data) + fms_mac_loop = read_int(ins_data) + ams_mac_loop = read_int(ins_data) + + vol_mac.open = bool(read_byte(ins_data)) + arp_mac.open = bool(read_byte(ins_data)) + duty_mac.open = bool(read_byte(ins_data)) + wave_mac.open = bool(read_byte(ins_data)) + pitch_mac.open = bool(read_byte(ins_data)) + x1_mac.open = bool(read_byte(ins_data)) + x2_mac.open = bool(read_byte(ins_data)) + x3_mac.open = bool(read_byte(ins_data)) + + alg_mac.open = bool(read_byte(ins_data)) + fb_mac.open = bool(read_byte(ins_data)) + fms_mac.open = bool(read_byte(ins_data)) + ams_mac.open = bool(read_byte(ins_data)) + + add_to_macro_data(alg_mac.data, + loop=alg_mac_loop, + release=None, + data=[read_int(ins_data) for _ in range(alg_mac_len)]) + + add_to_macro_data(fb_mac.data, + loop=fb_mac_loop, + release=None, + data=[read_int(ins_data) for _ in range(fb_mac_len)]) + + add_to_macro_data(fms_mac.data, + loop=fms_mac_loop, + release=None, + data=[read_int(ins_data) for _ in range(fms_mac_len)]) + + add_to_macro_data(ams_mac.data, + loop=ams_mac_loop, + release=None, + data=[read_int(ins_data) for _ in range(ams_mac_len)]) + + # fm op macros + if True: + if self.meta.version >= 29: + new_ops: Dict[int, InsFeatureMacro] = {} # actual ops + + ops_types: Dict[int, Type[InsFeatureMacro]] = { # classes + 0: InsFeatureOpr1Macro, + 1: InsFeatureOpr2Macro, + 2: InsFeatureOpr3Macro, + 3: InsFeatureOpr4Macro, + } + + ops: Dict[int, Dict[str, Union[int, bool]]] = { # params + 0: {}, + 1: {}, + 2: {}, + 3: {} + } + + for opi in ops: + ops[opi]["am_mac_len"] = read_int(ins_data) + ops[opi]["ar_mac_len"] = read_int(ins_data) + ops[opi]["dr_mac_len"] = read_int(ins_data) + ops[opi]["mult_mac_len"] = read_int(ins_data) + ops[opi]["rr_mac_len"] = read_int(ins_data) + ops[opi]["sl_mac_len"] = read_int(ins_data) + ops[opi]["tl_mac_len"] = read_int(ins_data) + ops[opi]["dt2_mac_len"] = read_int(ins_data) + ops[opi]["rs_mac_len"] = read_int(ins_data) + ops[opi]["dt_mac_len"] = read_int(ins_data) + ops[opi]["d2r_mac_len"] = read_int(ins_data) + ops[opi]["ssg_mac_len"] = read_int(ins_data) + + ops[opi]["am_mac_loop"] = read_int(ins_data) + ops[opi]["ar_mac_loop"] = read_int(ins_data) + ops[opi]["dr_mac_loop"] = read_int(ins_data) + ops[opi]["mult_mac_loop"] = read_int(ins_data) + ops[opi]["rr_mac_loop"] = read_int(ins_data) + ops[opi]["sl_mac_loop"] = read_int(ins_data) + ops[opi]["tl_mac_loop"] = read_int(ins_data) + ops[opi]["dt2_mac_loop"] = read_int(ins_data) + ops[opi]["rs_mac_loop"] = read_int(ins_data) + ops[opi]["dt_mac_loop"] = read_int(ins_data) + ops[opi]["d2r_mac_loop"] = read_int(ins_data) + ops[opi]["ssg_mac_loop"] = read_int(ins_data) + + ops[opi]["am_mac_open"] = read_byte(ins_data) + ops[opi]["ar_mac_open"] = read_byte(ins_data) + ops[opi]["dr_mac_open"] = read_byte(ins_data) + ops[opi]["mult_mac_open"] = read_byte(ins_data) + ops[opi]["rr_mac_open"] = read_byte(ins_data) + ops[opi]["sl_mac_open"] = read_byte(ins_data) + ops[opi]["tl_mac_open"] = read_byte(ins_data) + ops[opi]["dt2_mac_open"] = read_byte(ins_data) + ops[opi]["rs_mac_open"] = read_byte(ins_data) + ops[opi]["dt_mac_open"] = read_byte(ins_data) + ops[opi]["d2r_mac_open"] = read_byte(ins_data) + ops[opi]["ssg_mac_open"] = read_byte(ins_data) + + for opi in ops: + new_op = ops_types[opi]() + new_op.macros = [] + + am_mac = SingleMacro(kind=OpMacroCode.AM) + am_mac.open = bool(ops[opi]["am_mac_open"]) + am_mac.data.clear() + add_to_macro_data(am_mac.data, + loop=ops[opi]["am_mac_loop"], + release=None, + data=[read_int(ins_data) for _ in range(ops[opi]["am_mac_len"])]) + + ar_mac = SingleMacro(kind=OpMacroCode.AR) + ar_mac.open = bool(ops[opi]["ar_mac_open"]) + ar_mac.data.clear() + add_to_macro_data(ar_mac.data, + loop=ops[opi]["ar_mac_loop"], + release=None, + data=[read_int(ins_data) for _ in range(ops[opi]["ar_mac_len"])]) + + dr_mac = SingleMacro(kind=OpMacroCode.DR) + dr_mac.open = bool(ops[opi]["dr_mac_open"]) + dr_mac.data.clear() + add_to_macro_data(dr_mac.data, + loop=ops[opi]["dr_mac_loop"], + release=None, + data=[read_int(ins_data) for _ in range(ops[opi]["dr_mac_len"])]) + + mult_mac = SingleMacro(kind=OpMacroCode.MULT) + mult_mac.open = bool(ops[opi]["mult_mac_open"]) + mult_mac.data.clear() + add_to_macro_data(mult_mac.data, + loop=ops[opi]["mult_mac_loop"], + release=None, + data=[read_int(ins_data) for _ in range(ops[opi]["mult_mac_len"])]) + + rr_mac = SingleMacro(kind=OpMacroCode.RR) + rr_mac.open = bool(ops[opi]["rr_mac_open"]) + rr_mac.data.clear() + add_to_macro_data(rr_mac.data, + loop=ops[opi]["rr_mac_loop"], + release=None, + data=[read_int(ins_data) for _ in range(ops[opi]["rr_mac_len"])]) + + sl_mac = SingleMacro(kind=OpMacroCode.SL) + sl_mac.open = bool(ops[opi]["sl_mac_open"]) + sl_mac.data.clear() + add_to_macro_data(sl_mac.data, + loop=ops[opi]["sl_mac_loop"], + release=None, + data=[read_int(ins_data) for _ in range(ops[opi]["sl_mac_len"])]) + + tl_mac = SingleMacro(kind=OpMacroCode.TL) + tl_mac.open = bool(ops[opi]["tl_mac_open"]) + tl_mac.data.clear() + add_to_macro_data(tl_mac.data, + loop=ops[opi]["tl_mac_loop"], + release=None, + data=[read_int(ins_data) for _ in range(ops[opi]["tl_mac_len"])]) + + dt2_mac = SingleMacro(kind=OpMacroCode.DT2) + dt2_mac.open = bool(ops[opi]["dt2_mac_open"]) + dt2_mac.data.clear() + add_to_macro_data(dt2_mac.data, + loop=ops[opi]["dt2_mac_loop"], + release=None, + data=[read_int(ins_data) for _ in range(ops[opi]["dt2_mac_len"])]) + + rs_mac = SingleMacro(kind=OpMacroCode.RS) + rs_mac.open = bool(ops[opi]["rs_mac_open"]) + rs_mac.data.clear() + add_to_macro_data(rs_mac.data, + loop=ops[opi]["rs_mac_loop"], + release=None, + data=[read_int(ins_data) for _ in range(ops[opi]["rs_mac_len"])]) + + dt_mac = SingleMacro(kind=OpMacroCode.DT) + dt_mac.open = bool(ops[opi]["dt_mac_open"]) + dt_mac.data.clear() + add_to_macro_data(dt_mac.data, + loop=ops[opi]["dt_mac_loop"], + release=None, + data=[read_int(ins_data) for _ in range(ops[opi]["dt_mac_len"])]) + + d2r_mac = SingleMacro(kind=OpMacroCode.D2R) + d2r_mac.open = bool(ops[opi]["d2r_mac_open"]) + d2r_mac.data.clear() + add_to_macro_data(d2r_mac.data, + loop=ops[opi]["d2r_mac_loop"], + release=None, + data=[read_int(ins_data) for _ in range(ops[opi]["d2r_mac_len"])]) + + ssg_mac = SingleMacro(kind=OpMacroCode.SSG_EG) + ssg_mac.open = bool(ops[opi]["ssg_mac_open"]) + ssg_mac.data.clear() + add_to_macro_data(ssg_mac.data, + loop=ops[opi]["ssg_mac_loop"], + release=None, + data=[read_int(ins_data) for _ in range(ops[opi]["ssg_mac_len"])]) + + new_op.macros.extend([ + am_mac, ar_mac, dr_mac, mult_mac, rr_mac, + sl_mac, tl_mac, dt2_mac, rs_mac, dt_mac, + d2r_mac, ssg_mac + ]) # must be in order!! + + new_ops[opi] = new_op + + # release points + if True: + if self.meta.version >= 44: + add_to_macro_data(vol_mac.data, None, read_int(ins_data), None) + add_to_macro_data(arp_mac.data, None, read_int(ins_data), None) + add_to_macro_data(duty_mac.data, None, read_int(ins_data), None) + add_to_macro_data(wave_mac.data, None, read_int(ins_data), None) + add_to_macro_data(pitch_mac.data, None, read_int(ins_data), None) + add_to_macro_data(x1_mac.data, None, read_int(ins_data), None) + add_to_macro_data(x2_mac.data, None, read_int(ins_data), None) + add_to_macro_data(x3_mac.data, None, read_int(ins_data), None) + add_to_macro_data(alg_mac.data, None, read_int(ins_data), None) + add_to_macro_data(fb_mac.data, None, read_int(ins_data), None) + add_to_macro_data(fms_mac.data, None, read_int(ins_data), None) + add_to_macro_data(ams_mac.data, None, read_int(ins_data), None) + + for opi in new_ops: + for i in range(12): + add_to_macro_data(new_ops[opi].macros[i].data, None, read_int(ins_data), None) + + # extended op macros + if True: + if self.meta.version >= 61: + for op in new_ops: + dam_mac = SingleMacro(kind=OpMacroCode.DAM) + dvb_mac = SingleMacro(kind=OpMacroCode.DVB) + egt_mac = SingleMacro(kind=OpMacroCode.EGT) + ksl_mac = SingleMacro(kind=OpMacroCode.KSL) + sus_mac = SingleMacro(kind=OpMacroCode.SUS) + vib_mac = SingleMacro(kind=OpMacroCode.VIB) + ws_mac = SingleMacro(kind=OpMacroCode.WS) + ksr_mac = SingleMacro(kind=OpMacroCode.KSR) + + dam_mac_len = read_int(ins_data) + dvb_mac_len = read_int(ins_data) + egt_mac_len = read_int(ins_data) + ksl_mac_len = read_int(ins_data) + sus_mac_len = read_int(ins_data) + vib_mac_len = read_int(ins_data) + ws_mac_len = read_int(ins_data) + ksr_mac_len = read_int(ins_data) + + dam_mac_loop = read_int(ins_data) + dvb_mac_loop = read_int(ins_data) + egt_mac_loop = read_int(ins_data) + ksl_mac_loop = read_int(ins_data) + sus_mac_loop = read_int(ins_data) + vib_mac_loop = read_int(ins_data) + ws_mac_loop = read_int(ins_data) + ksr_mac_loop = read_int(ins_data) + + dam_mac_rel = read_int(ins_data) + dvb_mac_rel = read_int(ins_data) + egt_mac_rel = read_int(ins_data) + ksl_mac_rel = read_int(ins_data) + sus_mac_rel = read_int(ins_data) + vib_mac_rel = read_int(ins_data) + ws_mac_rel = read_int(ins_data) + ksr_mac_rel = read_int(ins_data) + + dam_mac.open = bool(read_byte(ins_data)) + dvb_mac.open = bool(read_byte(ins_data)) + egt_mac.open = bool(read_byte(ins_data)) + ksl_mac.open = bool(read_byte(ins_data)) + sus_mac.open = bool(read_byte(ins_data)) + vib_mac.open = bool(read_byte(ins_data)) + ws_mac.open = bool(read_byte(ins_data)) + ksr_mac.open = bool(read_byte(ins_data)) + + dam_mac.data.clear() + dvb_mac.data.clear() + egt_mac.data.clear() + ksl_mac.data.clear() + sus_mac.data.clear() + vib_mac.data.clear() + ws_mac.data.clear() + ksr_mac.data.clear() + + add_to_macro_data(dam_mac.data, dam_mac_loop, dam_mac_rel, [ + read_byte(ins_data) for _ in range(dam_mac_len) + ]) + add_to_macro_data(dvb_mac.data, dvb_mac_loop, dvb_mac_rel, [ + read_byte(ins_data) for _ in range(dvb_mac_len) + ]) + add_to_macro_data(egt_mac.data, egt_mac_loop, egt_mac_rel, [ + read_byte(ins_data) for _ in range(egt_mac_len) + ]) + add_to_macro_data(ksl_mac.data, ksl_mac_loop, ksl_mac_rel, [ + read_byte(ins_data) for _ in range(ksl_mac_len) + ]) + add_to_macro_data(sus_mac.data, sus_mac_loop, sus_mac_rel, [ + read_byte(ins_data) for _ in range(sus_mac_len) + ]) + add_to_macro_data(vib_mac.data, vib_mac_loop, vib_mac_rel, [ + read_byte(ins_data) for _ in range(vib_mac_len) + ]) + add_to_macro_data(ws_mac.data, ws_mac_loop, ws_mac_rel, [ + read_byte(ins_data) for _ in range(ws_mac_len) + ]) + add_to_macro_data(ksr_mac.data, ksr_mac_loop, ksr_mac_rel, [ + read_byte(ins_data) for _ in range(ksr_mac_len) + ]) + + new_ops[op].macros.extend([ + dam_mac, dvb_mac, egt_mac, ksl_mac, sus_mac, vib_mac, + ws_mac, ksr_mac + ]) + + # opl drum data + if True: + if self.meta.version >= 63: + opl_drum = InsFeatureOPLDrums( + fixed_drums = bool(read_byte(ins_data)) + ) + read_byte(ins_data) + opl_drum.kick_freq = read_short(ins_data) + opl_drum.snare_hat_freq = read_short(ins_data) + opl_drum.tom_top_freq = read_short(ins_data) + self.features.append(opl_drum) + + # clear macros + if True: + if self.meta.version < 63 and self.meta.type == InstrumentType.PCE: + duty_mac.data.clear() + if self.meta.version < 70 and self.meta.type == InstrumentType.FM_OPLL: + wave_mac.data.clear() + + # sample map + if True: + if self.meta.version >= 67: + note_map = InsFeatureAmiga() + note_map.use_note_map = bool(read_byte(ins_data)) + if note_map.use_note_map: + for i in range(len(note_map.sample_map)): + note_map.sample_map[i].freq = read_int(ins_data) + for i in range(len(note_map.sample_map)): + note_map.sample_map[i].sample_index = read_short(ins_data) + self.features.append(note_map) + + # n163 + if True: + if self.meta.version >= 73: + n163 = InsFeatureN163( + wave=read_int(ins_data), + wave_pos=read_byte(ins_data), + wave_len=read_byte(ins_data), + wave_mode=read_byte(ins_data) + ) + read_byte(ins_data) # reserved + self.features.append(n163) + + # moar macroes + if True: + if self.meta.version >= 76: + pan_l_mac = SingleMacro(kind=MacroCode.PAN_L) + pan_r_mac = SingleMacro(kind=MacroCode.PAN_R) + phase_res_mac = SingleMacro(kind=MacroCode.PHASE_RESET) + x4_mac = SingleMacro(kind=MacroCode.EX4) + x5_mac = SingleMacro(kind=MacroCode.EX5) + x6_mac = SingleMacro(kind=MacroCode.EX6) + x7_mac = SingleMacro(kind=MacroCode.EX7) + x8_mac = SingleMacro(kind=MacroCode.EX8) + + pan_l_mac.data.clear() + pan_r_mac.data.clear() + phase_res_mac.data.clear() + x4_mac.data.clear() + x5_mac.data.clear() + x6_mac.data.clear() + x7_mac.data.clear() + x8_mac.data.clear() + + pan_l_mac_len = read_int(ins_data) + pan_r_mac_len = read_int(ins_data) + phase_res_mac_len = read_int(ins_data) + x4_mac_len = read_int(ins_data) + x5_mac_len = read_int(ins_data) + x6_mac_len = read_int(ins_data) + x7_mac_len = read_int(ins_data) + x8_mac_len = read_int(ins_data) + + pan_l_mac_loop = read_int(ins_data) + pan_r_mac_loop = read_int(ins_data) + phase_res_mac_loop = read_int(ins_data) + x4_mac_loop = read_int(ins_data) + x5_mac_loop = read_int(ins_data) + x6_mac_loop = read_int(ins_data) + x7_mac_loop = read_int(ins_data) + x8_mac_loop = read_int(ins_data) + + pan_l_mac_rel = read_int(ins_data) + pan_r_mac_rel = read_int(ins_data) + phase_res_mac_rel = read_int(ins_data) + x4_mac_rel = read_int(ins_data) + x5_mac_rel = read_int(ins_data) + x6_mac_rel = read_int(ins_data) + x7_mac_rel = read_int(ins_data) + x8_mac_rel = read_int(ins_data) + + pan_l_mac.open = bool(read_byte(ins_data)) + pan_r_mac.open = bool(read_byte(ins_data)) + phase_res_mac.open = bool(read_byte(ins_data)) + x4_mac.open = bool(read_byte(ins_data)) + x5_mac.open = bool(read_byte(ins_data)) + x6_mac.open = bool(read_byte(ins_data)) + x7_mac.open = bool(read_byte(ins_data)) + x8_mac.open = bool(read_byte(ins_data)) + + add_to_macro_data(pan_l_mac.data, pan_l_mac_loop, pan_l_mac_rel, [ + read_int(ins_data) for _ in range(pan_l_mac_len) + ]) + add_to_macro_data(pan_r_mac.data, pan_r_mac_loop, pan_r_mac_rel, [ + read_int(ins_data) for _ in range(pan_r_mac_len) + ]) + add_to_macro_data(phase_res_mac.data, phase_res_mac_loop, phase_res_mac_rel, [ + read_int(ins_data) for _ in range(phase_res_mac_len) + ]) + add_to_macro_data(x4_mac.data, x4_mac_loop, x4_mac_rel, [ + read_int(ins_data) for _ in range(x4_mac_len) + ]) + add_to_macro_data(x5_mac.data, x5_mac_loop, x5_mac_rel, [ + read_int(ins_data) for _ in range(x5_mac_len) + ]) + add_to_macro_data(x6_mac.data, x6_mac_loop, x6_mac_rel, [ + read_int(ins_data) for _ in range(x6_mac_len) + ]) + add_to_macro_data(x7_mac.data, x7_mac_loop, x7_mac_rel, [ + read_int(ins_data) for _ in range(x7_mac_len) + ]) + add_to_macro_data(x8_mac.data, x8_mac_loop, x8_mac_rel, [ + read_int(ins_data) for _ in range(x8_mac_len) + ]) + + mac_list.extend([ + pan_l_mac, pan_r_mac, phase_res_mac, x4_mac, + x5_mac, x6_mac, x7_mac, x8_mac + ]) + + # fds + if True: + if self.meta.version >= 76: + fds = InsFeatureFDS( + mod_speed=read_int(ins_data), + mod_depth=read_int(ins_data), + init_table_with_first_wave=bool(read_byte(ins_data)) + ) + read_byte(ins_data) # reserved + read_byte(ins_data) + read_byte(ins_data) + fds.mod_table = [read_byte(ins_data) for _ in range(32)] + self.features.append(fds) + + # opz + if True: + if self.meta.version >= 77: + fm.fms2 = read_byte(ins_data) + fm.ams2 = read_byte(ins_data) + + # wave synth + if True: + if self.meta.version >= 79: + ws = InsFeatureWaveSynth( + wave_indices=[read_int(ins_data), read_int(ins_data)], + rate_divider=read_byte(ins_data), + effect=WaveFX(read_byte(ins_data)), + enabled=bool(read_byte(ins_data)), + global_effect=bool(read_byte(ins_data)), + speed=read_byte(ins_data), + params=[read_byte(ins_data) for _ in range(4)] + ) + self.features.append(ws) + + # macro moads + if True: + if self.meta.version >= 84: + vol_mac.mode = read_byte(ins_data) + duty_mac.mode = read_byte(ins_data) + wave_mac.mode = read_byte(ins_data) + pitch_mac.mode = read_byte(ins_data) + x1_mac.mode = read_byte(ins_data) + x2_mac.mode = read_byte(ins_data) + x3_mac.mode = read_byte(ins_data) + alg_mac.mode = read_byte(ins_data) + fb_mac.mode = read_byte(ins_data) + fms_mac.mode = read_byte(ins_data) + ams_mac.mode = read_byte(ins_data) + pan_l_mac.mode = read_byte(ins_data) + pan_r_mac.mode = read_byte(ins_data) + phase_res_mac.mode = read_byte(ins_data) + x4_mac.mode = read_byte(ins_data) + x5_mac.mode = read_byte(ins_data) + x6_mac.mode = read_byte(ins_data) + x7_mac.mode = read_byte(ins_data) + x8_mac.mode = read_byte(ins_data) + + # c64 no test + if True: + if self.meta.version >= 89: + c64.no_test = bool(read_byte(ins_data)) + + # multipcm + if True: + if self.meta.version >= 93: + mp = InsFeatureMultiPCM( + ar=read_byte(ins_data), + d1r=read_byte(ins_data), + dl=read_byte(ins_data), + d2r=read_byte(ins_data), + rr=read_byte(ins_data), + rc=read_byte(ins_data), + lfo=read_byte(ins_data), + vib=read_byte(ins_data), + am=read_byte(ins_data) + ) + for _ in range(23): # reserved + read_byte(ins_data) + self.features.append(mp) + + # sound unit + if True: + if self.meta.version >= 104: + amiga.use_sample = bool(read_byte(ins_data)) + su = InsFeatureSoundUnit( + switch_roles=bool(read_byte(ins_data)) + ) + self.features.append(su) + + # gb hw seq + if True: + if self.meta.version >= 105: + gb_hwseq_len = read_byte(ins_data) + gb.hw_seq.clear() + for i in range(gb_hwseq_len): + gb.hw_seq.append( + GBHwSeq( + command=GBHwCommand(read_byte(ins_data)), + data=[read_byte(ins_data), read_byte(ins_data)] + ) + ) + + # additional gb + if True: + if self.meta.version >= 106: + gb.soft_env = bool(read_byte(ins_data)) + gb.always_init = bool(read_byte(ins_data)) + + # es5506 + if True: + if self.meta.version >= 107: + es = InsFeatureES5506( + filter_mode=ESFilterMode(read_byte(ins_data)), + k1=read_short(ins_data), + k2=read_short(ins_data), + env_count=read_short(ins_data), + left_volume_ramp=read_byte(ins_data), + right_volume_ramp=read_byte(ins_data), + k1_ramp=read_byte(ins_data), + k2_ramp=read_byte(ins_data), + k1_slow=read_byte(ins_data), + k2_slow=read_byte(ins_data) + ) + self.features.append(es) + + # snes + if True: + if self.meta.version >= 109: + snes = InsFeatureSNES() + snes.use_env = bool(read_byte(ins_data)) + if self.meta.version >= 118: + snes.gain_mode = GainMode(read_byte(ins_data)) + snes.gain = read_byte(ins_data) + else: + read_byte(ins_data) + read_byte(ins_data) + snes.envelope.a = read_byte(ins_data) + snes.envelope.d = read_byte(ins_data) + snes_env_s = read_byte(ins_data) + snes.envelope.s = snes_env_s & 0b111 + snes.envelope.r = read_byte(ins_data) + snes.sus = SNESSusMode((snes_env_s >> 3) & 1) # ??? + self.features.append(snes) + + # macro speed delay + if True: + if self.meta.version >= 111: + vol_mac.speed = read_byte(ins_data) + arp_mac.speed = read_byte(ins_data) + duty_mac.speed = read_byte(ins_data) + wave_mac.speed = read_byte(ins_data) + pitch_mac.speed = read_byte(ins_data) + x1_mac.speed = read_byte(ins_data) + x2_mac.speed = read_byte(ins_data) + x3_mac.speed = read_byte(ins_data) + alg_mac.speed = read_byte(ins_data) + fb_mac.speed = read_byte(ins_data) + fms_mac.speed = read_byte(ins_data) + ams_mac.speed = read_byte(ins_data) + pan_l_mac.speed = read_byte(ins_data) + pan_r_mac.speed = read_byte(ins_data) + phase_res_mac.speed = read_byte(ins_data) + x4_mac.speed = read_byte(ins_data) + x5_mac.speed = read_byte(ins_data) + x6_mac.speed = read_byte(ins_data) + x7_mac.speed = read_byte(ins_data) + x8_mac.speed = read_byte(ins_data) + + vol_mac.delay = read_byte(ins_data) + arp_mac.delay = read_byte(ins_data) + duty_mac.delay = read_byte(ins_data) + wave_mac.delay = read_byte(ins_data) + pitch_mac.delay = read_byte(ins_data) + x1_mac.delay = read_byte(ins_data) + x2_mac.delay = read_byte(ins_data) + x3_mac.delay = read_byte(ins_data) + alg_mac.delay = read_byte(ins_data) + fb_mac.delay = read_byte(ins_data) + fms_mac.delay = read_byte(ins_data) + ams_mac.delay = read_byte(ins_data) + pan_l_mac.delay = read_byte(ins_data) + pan_r_mac.delay = read_byte(ins_data) + phase_res_mac.delay = read_byte(ins_data) + x4_mac.delay = read_byte(ins_data) + x5_mac.delay = read_byte(ins_data) + x6_mac.delay = read_byte(ins_data) + x7_mac.delay = read_byte(ins_data) + x8_mac.delay = read_byte(ins_data) + + for op in ops: + for i in range(20): + new_ops[op].macros[i].speed = read_byte(ins_data) + for i in range(20): + new_ops[op].macros[i].delay = read_byte(ins_data) + + # old arp mac format + if True: + if self.meta.version < 112: + if arp_mac.mode != 0: + arp_mac.mode = 0 + for i in range(len(arp_mac.data)): + if isinstance(arp_mac.data[i], int): + arp_mac.data[i] ^= 0x40000000 + + # add ops macros at the end + if True: + if self.meta.version >= 29: + for _, op_contents in new_ops.items(): + self.features.append(op_contents) diff --git a/loader/samples/minexample/furC64/chipchune/furnace/module.py b/loader/samples/minexample/furC64/chipchune/furnace/module.py new file mode 100644 index 0000000..e6e7b27 --- /dev/null +++ b/loader/samples/minexample/furC64/chipchune/furnace/module.py @@ -0,0 +1,1111 @@ +import re +import zlib +from io import BytesIO, BufferedReader +from typing import BinaryIO, Optional, Literal, Union, Dict, List + +from chipchune._util import read_byte, read_short, read_int, read_float, read_str +from .data_types import ( + ModuleMeta, ChipList, ModuleCompatFlags, SubSong, PatchBay, ChannelDisplayInfo, + InputPatchBayEntry, OutputPatchBayEntry, ChipInfo, FurnacePattern, FurnaceRow +) +from .enums import ( + ChipType, LinearPitch, InputPortSet, OutputPortSet, LoopModality, + DelayBehavior, JumpTreatment, _FurInsImportType, _FurWavetableImportType, Note +) +from .instrument import FurnaceInstrument +from .wavetable import FurnaceWavetable +from .sample import FurnaceSample + +MAGIC_STR = b'-Furnace module-' +MAX_CHIPS = 32 + + +class FurnaceModule: + """ + Represents a Furnace .fur file. + + When possible, instrument objects etc. will use the latest format as its internal + representation. For example, old instruments will internally be converted into the + "new" instrument-feature-list format. + """ + + def __init__(self, file_name_or_stream: Optional[Union[BufferedReader, str]] = None) -> None: + """ + Creates or opens a new Furnace module as a Python object. + + :param file_name_or_stream: (Optional) + If specified, then it will parse a file as a FurnaceModule. If file name (str) is + given, it will load that file. If a stream (BufferedReader) instead is given, + it will parse it from the stream. + + Defaults to None. + """ + self.file_name: Optional[str] = None + """ + Original file name, if the object was initialized with one. + """ + self.meta: ModuleMeta = ModuleMeta() + """ + Metadata concerning the module. + """ + self.chips: ChipList = ChipList() + """ + List of chips used in the module. + """ + self.compat_flags: ModuleCompatFlags = ModuleCompatFlags() + """ + Compat flags settings within the module. + """ + self.subsongs: List[SubSong] = [SubSong()] + """ + Subsongs contained within the module. Although the first subsong + and the others are internally stored separately, they're organized + into a list here for convenience. + """ + self.patchbay: List[PatchBay] = [] + """ + List of patchbay connections. + """ + self.instruments: List[FurnaceInstrument] = [] + """ + List of all instruments in the module. + """ + self.patterns: List[FurnacePattern] = [] + """ + List of all patterns in the module. + """ + self.wavetables: List[FurnaceWavetable] = [] + + self.samples: List[FurnaceWavetable] = [] + + if isinstance(file_name_or_stream, BufferedReader): + self.load_from_stream(file_name_or_stream) + elif isinstance(file_name_or_stream, str): + self.load_from_file(file_name_or_stream) + + def load_from_file(self, file_name: Optional[str] = None) -> None: + """ + Load a module from a file name. The file may either be compressed or uncompressed. + + :param file_name: If not specified, it will grab from self.file_name instead. + """ + if isinstance(file_name, str): + self.file_name = file_name + if self.file_name is None: + raise RuntimeError('No file name set, either set self.file_name or pass file_name to the function') + with open(self.file_name, 'rb') as f: + detect_magic = f.peek(len(MAGIC_STR))[:len(MAGIC_STR)] + if detect_magic != MAGIC_STR: # this is probably compressed, so try decompressing it first + return self.load_from_bytes( + zlib.decompress(f.read()) + ) + else: # uncompressed for sure + return self.load_from_stream(f) + + @staticmethod + def decompress_to_file(in_name: str, out_name: str) -> int: + """ + Simple zlib wrapper. Decompresses a zlib-compressed .fur + from in_name to out_name. Does not need instantiation. + + :param in_name: input file name + :param out_name: output file name + :return: Results of file.write(). + """ + with open(in_name, 'rb') as fi: + with open(out_name, 'wb') as fo: + return fo.write(zlib.decompress(fi.read())) + + def load_from_bytes(self, data: bytes) -> None: + """ + Load a module from a series of bytes. + + :param data: Bytes + """ + return self.load_from_stream( + BytesIO(data) + ) + + def load_from_stream(self, stream: BinaryIO) -> None: + """ + Load a module from an **uncompressed** stream. + + :param stream: File-like object containing the uncompressed module. + """ + # assumes uncompressed stream + if stream.read(len(MAGIC_STR)) != MAGIC_STR: + raise RuntimeError('Bad magic value; this is not a Furnace file or is corrupt') + + # clear defaults + self.chips.list.clear() + self.patchbay.clear() + self.subsongs[0].order.clear() + self.subsongs[0].speed_pattern.clear() + + self.__read_header(stream) + self.__init_compat_flags() + self.__read_info(stream) + if self.meta.version >= 119: + self.__read_dev119_chip_flags(stream) + self.__read_instruments(stream) + self.__read_wavetables(stream) + self.__read_samples(stream) + if self.meta.version >= 95: + self.__read_subsongs(stream) + self.__read_patterns(stream) + + def get_num_channels(self) -> int: + """ + Retrieve the number of total channels in the module. + + :return: Channel sum across all chips. + """ + num_channels = 0 + for chip in self.chips.list: + num_channels += chip.type.channels + return num_channels + + def get_pattern(self, channel: int, index: int, subsong: int=0) -> Optional[FurnacePattern]: + """ + Gets one pattern object from a module. + + :param channel: Which channel to use (zero-indexed), e.g. to get VRC6 + in a NES+VRC6 module, use `5`. + :param index: The index of the pattern within the subsong. + :param subsong: The subsong number. + :return: FurnacePattern object or None if no such pattern exists. + """ + try: + return next( + filter(lambda x: x.channel==channel and x.index==index and x.subsong==subsong, self.patterns) + ) + except StopIteration: + return None + + def __init_compat_flags(self) -> None: + """ + Initializes appropriate compat flags based on module version + """ + if self.meta.version < 37: + self.compat_flags.limit_slides = True + self.compat_flags.linear_pitch = LinearPitch.ONLY_PITCH_CHANGE + self.compat_flags.loop_modality = LoopModality.HARD_RESET_CHANNELS + if self.meta.version < 43: + self.compat_flags.proper_noise_layout = False + self.compat_flags.wave_duty_is_volume = False + if self.meta.version < 45: + self.compat_flags.reset_macro_on_porta = True + self.compat_flags.legacy_volume_slides = True + self.compat_flags.compatible_arpeggio = True + self.compat_flags.note_off_resets_slides = True + self.compat_flags.target_resets_slides = True + if self.meta.version < 46: + self.compat_flags.arpeggio_inhibits_portamento = True + self.compat_flags.wack_algorithm_macro = True + if self.meta.version < 49: + self.compat_flags.broken_shortcut_slides = True + if self.meta.version < 50: + self.compat_flags.ignore_duplicates_slides = False + if self.meta.version < 62: + self.compat_flags.stop_portamento_on_note_off = True + if self.meta.version < 64: + self.compat_flags.broken_dac_mode = False + if self.meta.version < 65: + self.compat_flags.one_tick_cut = False + if self.meta.version < 66: + self.compat_flags.instrument_change_allowed_in_porta = False + if self.meta.version < 69: + self.compat_flags.reset_note_base_on_arpeggio_stop = False + if self.meta.version < 71: + self.compat_flags.no_slides_on_first_tick = False + self.compat_flags.next_row_reset_arp_pos = False + self.compat_flags.ignore_jump_at_end = True + if self.meta.version < 72: + self.compat_flags.buggy_portamento_after_slide = True + self.compat_flags.gb_ins_affects_env = False + if self.meta.version < 78: + self.compat_flags.shared_extch_state = False + if self.meta.version < 83: + self.compat_flags.ignore_outside_dac_mode_change = True + self.compat_flags.e1e2_takes_priority = False + if self.meta.version < 84: + self.compat_flags.new_sega_pcm = False + if self.meta.version < 85: + self.compat_flags.weird_fnum_pitch_slides = True + if self.meta.version < 86: + self.compat_flags.sn_duty_resets_phase = True + if self.meta.version < 90: + self.compat_flags.linear_pitch_macro = False + if self.meta.version < 97: + self.compat_flags.old_octave_boundary = True + self.compat_flags.disable_opn2_dac_volume_control = True # dev98 + if self.meta.version < 99: + self.compat_flags.new_volume_scaling = False + self.compat_flags.volume_macro_lingers = False + self.compat_flags.broken_out_vol = True + if self.meta.version < 100: + self.compat_flags.e1e2_stop_on_same_note = False + if self.meta.version < 101: + self.compat_flags.broken_porta_after_arp = True + if self.meta.version < 108: + self.compat_flags.sn_no_low_periods = True + if self.meta.version < 110: + self.compat_flags.cut_delay_effect_policy = DelayBehavior.BROKEN + if self.meta.version < 113: + self.compat_flags.jump_treatment = JumpTreatment.FIRST_JUMP_ONLY + if self.meta.version < 115: + self.compat_flags.auto_sys_name = True + if self.meta.version < 117: + self.compat_flags.disable_sample_macro = True + if self.meta.version < 121: + self.compat_flags.broken_out_vol_2 = False + if self.meta.version < 130: + self.compat_flags.old_arp_strategy = True + if self.meta.version < 138: + self.compat_flags.broken_porta_during_legato = True + if self.meta.version < 155: + self.compat_flags.broken_fm_off = True + if self.meta.version < 168: + self.compat_flags.pre_note_no_effect = True + if self.meta.version < 183: + self.compat_flags.old_dpcm = True + if self.meta.version < 184: + self.compat_flags.reset_arp_phase_on_new_note = False + if self.meta.version < 188: + self.compat_flags.ceil_volume_scaling = False + if self.meta.version < 191: + self.compat_flags.old_always_set_volume = True + if self.meta.version < 200: + self.compat_flags.old_sample_offset = True + + # XXX: update my signature whenever a new compat flag block is added + def __read_compat_flags(self, stream: BinaryIO, phase: Literal[1, 2, 3]) -> None: + """ + Reads the set compat flags in the module + """ + if phase == 1: + compat_flags_to_skip = 20 + if self.meta.version < 37: + self.compat_flags.limit_slides = True + self.compat_flags.linear_pitch = LinearPitch.ONLY_PITCH_CHANGE + self.compat_flags.loop_modality = LoopModality.HARD_RESET_CHANNELS + else: # >= 37 + self.compat_flags.limit_slides = bool(read_byte(stream)) + self.compat_flags.linear_pitch = LinearPitch(read_byte(stream)) + self.compat_flags.loop_modality = LoopModality(read_byte(stream)) + compat_flags_to_skip -= 3 + + if self.meta.version >= 43: + self.compat_flags.proper_noise_layout = bool(read_byte(stream)) + self.compat_flags.wave_duty_is_volume = bool(read_byte(stream)) + compat_flags_to_skip -= 2 + + if self.meta.version >= 45: + self.compat_flags.reset_macro_on_porta = bool(read_byte(stream)) + self.compat_flags.legacy_volume_slides = bool(read_byte(stream)) + self.compat_flags.compatible_arpeggio = bool(read_byte(stream)) + self.compat_flags.note_off_resets_slides = bool(read_byte(stream)) + self.compat_flags.target_resets_slides = bool(read_byte(stream)) + compat_flags_to_skip -= 5 + + if self.meta.version >= 47: + self.compat_flags.arpeggio_inhibits_portamento = bool(read_byte(stream)) + self.compat_flags.wack_algorithm_macro = bool(read_byte(stream)) + compat_flags_to_skip -= 2 + + if self.meta.version >= 49: + self.compat_flags.broken_shortcut_slides = bool(read_byte(stream)) + compat_flags_to_skip -= 1 + + if self.meta.version >= 50: + self.compat_flags.ignore_duplicates_slides = bool(read_byte(stream)) + compat_flags_to_skip -= 1 + + if self.meta.version >= 62: + self.compat_flags.stop_portamento_on_note_off = bool(read_byte(stream)) + self.compat_flags.continuous_vibrato = bool(read_byte(stream)) + compat_flags_to_skip -= 2 + + if self.meta.version >= 64: + self.compat_flags.broken_dac_mode = bool(read_byte(stream)) + compat_flags_to_skip -= 1 + + if self.meta.version >= 65: + self.compat_flags.one_tick_cut = bool(read_byte(stream)) + compat_flags_to_skip -= 1 + + if self.meta.version >= 66: + self.compat_flags.instrument_change_allowed_in_porta = bool(read_byte(stream)) + compat_flags_to_skip -= 1 + + if self.meta.version >= 69: + self.compat_flags.reset_note_base_on_arpeggio_stop = bool(read_byte(stream)) + compat_flags_to_skip -= 1 + elif phase == 2: + compat_flags_to_skip = 28 + if self.meta.version >= 70: + self.compat_flags.broken_speed_selection = bool(read_byte(stream)) + compat_flags_to_skip -= 1 + + if self.meta.version >= 71: + self.compat_flags.no_slides_on_first_tick = bool(read_byte(stream)) + self.compat_flags.next_row_reset_arp_pos = bool(read_byte(stream)) + self.compat_flags.ignore_jump_at_end = bool(read_byte(stream)) + compat_flags_to_skip -= 3 + + if self.meta.version >= 72: + self.compat_flags.buggy_portamento_after_slide = bool(read_byte(stream)) + self.compat_flags.gb_ins_affects_env = bool(read_byte(stream)) + compat_flags_to_skip -= 2 + + if self.meta.version >= 78: + self.compat_flags.shared_extch_state = bool(read_byte(stream)) + compat_flags_to_skip -= 1 + + if self.meta.version >= 83: + self.compat_flags.ignore_outside_dac_mode_change = bool(read_byte(stream)) + self.compat_flags.e1e2_takes_priority = bool(read_byte(stream)) + compat_flags_to_skip -= 2 + + if self.meta.version >= 84: + self.compat_flags.new_sega_pcm = bool(read_byte(stream)) + compat_flags_to_skip -= 1 + + if self.meta.version >= 85: + self.compat_flags.weird_fnum_pitch_slides = bool(read_byte(stream)) + compat_flags_to_skip -= 1 + + if self.meta.version >= 86: + self.compat_flags.sn_duty_resets_phase = bool(read_byte(stream)) + compat_flags_to_skip -= 1 + + if self.meta.version >= 90: + self.compat_flags.linear_pitch_macro = bool(read_byte(stream)) + compat_flags_to_skip -= 1 + + if self.meta.version >= 94: + self.compat_flags.pitch_slide_speed_in_linear = read_byte(stream) + compat_flags_to_skip -= 1 + + if self.meta.version >= 97: + self.compat_flags.old_octave_boundary = bool(read_byte(stream)) + compat_flags_to_skip -= 1 + + if self.meta.version >= 98: + self.compat_flags.disable_opn2_dac_volume_control = bool(read_byte(stream)) + compat_flags_to_skip -= 1 + + if self.meta.version >= 99: + self.compat_flags.new_volume_scaling = bool(read_byte(stream)) + self.compat_flags.volume_macro_lingers = bool(read_byte(stream)) + self.compat_flags.broken_out_vol = bool(read_byte(stream)) + compat_flags_to_skip -= 3 + + if self.meta.version >= 100: + self.compat_flags.e1e2_stop_on_same_note = bool(read_byte(stream)) + compat_flags_to_skip -= 1 + + if self.meta.version >= 101: + self.compat_flags.broken_porta_after_arp = bool(read_byte(stream)) + compat_flags_to_skip -= 1 + + if self.meta.version >= 108: + self.compat_flags.sn_no_low_periods = bool(read_byte(stream)) + compat_flags_to_skip -= 1 + + if self.meta.version >= 110: + self.compat_flags.cut_delay_effect_policy = DelayBehavior(read_byte(stream)) + compat_flags_to_skip -= 1 + + if self.meta.version >= 113: + self.compat_flags.jump_treatment = JumpTreatment(read_byte(stream)) + compat_flags_to_skip -= 1 + + if self.meta.version >= 115: + self.compat_flags.auto_sys_name = bool(read_byte(stream)) + compat_flags_to_skip -= 1 + + if self.meta.version >= 117: + self.compat_flags.disable_sample_macro = bool(read_byte(stream)) + compat_flags_to_skip -= 1 + + if self.meta.version >= 121: + self.compat_flags.broken_out_vol_2 = bool(read_byte(stream)) + compat_flags_to_skip -= 1 + + if self.meta.version >= 130: + self.compat_flags.old_arp_strategy = bool(read_byte(stream)) + compat_flags_to_skip -= 1 + + elif phase == 3: + compat_flags_to_skip = 8 + if self.meta.version >= 138: + self.compat_flags.broken_porta_during_legato = bool(read_byte(stream)) + compat_flags_to_skip -= 1 + + if self.meta.version >= 155: + self.compat_flags.broken_fm_off = bool(read_byte(stream)) + compat_flags_to_skip -= 1 + + if self.meta.version >= 168: + self.compat_flags.pre_note_no_effect = bool(read_byte(stream)) + compat_flags_to_skip -= 1 + + if self.meta.version >= 183: + self.compat_flags.old_dpcm = bool(read_byte(stream)) + compat_flags_to_skip -= 1 + + if self.meta.version >= 184: + self.compat_flags.reset_arp_phase_on_new_note = bool(read_byte(stream)) + compat_flags_to_skip -= 1 + + if self.meta.version >= 188: + self.compat_flags.ceil_volume_scaling = bool(read_byte(stream)) + compat_flags_to_skip -= 1 + + if self.meta.version >= 191: + self.compat_flags.old_always_set_volume = bool(read_byte(stream)) + compat_flags_to_skip -= 1 + else: + raise ValueError( + 'Compat flag phase must be in between: 1, 2, 3' + ) + stream.read(compat_flags_to_skip) + + def __read_dev119_chip_flags(self, stream: BinaryIO) -> None: + for i in range(len(self.chips.list)): + # skip if this chip doesn't have flags + if self.__chip_flag_ptr[i] == 0: + continue + stream.seek(self.__chip_flag_ptr[i]) + + if stream.read(4) != b'FLAG': + raise ValueError('No "FLAG" magic') + + # i assume this will grow, you never know + blk_size = read_int(stream) + flag_blk = BytesIO(stream.read(blk_size)) + + # read entries in FLAG + for entry in [flag.split('=') for flag in read_str(flag_blk).split()]: + key = entry[0] + value = entry[1] + # cast by regex + if re.match(r'true', value): + self.chips.list[i].flags[key] = True + elif re.match(r'false', value): + self.chips.list[i].flags[key] = False + elif re.match(r'\d+$', value): + self.chips.list[i].flags[key] = int(value) + elif re.match(r'\d+\.\d+', value): + self.chips.list[i].flags[key] = float(value) + else: # all other values should be treated as a string + self.chips.list[i].flags[key] = value + + @staticmethod + def __convert_old_chip_flags(chip: ChipType, flag: int) -> Dict[str, Union[bool, int]]: + """ + Convert pre-v119 binary chip flags to the newer dict-style form. + + :param chip: ChipType + :param flag: flag value as a 32-bit number + :return: dictionary containing the flag's equivalent values + """ + n = {} + + if chip in [ChipType.GENESIS, ChipType.GENESIS_EX]: + n['clockSel'] = flag & 2147483647 # bits 0-30 + n['ladderEffect'] = bool((flag >> 31) & 1) + elif chip == ChipType.SMS: + cs = flag & 0xff03 + if cs > 0x100: + cs = cs - 252 # 0x100 + 4 + n['clockSel'] = cs + ct = (flag & 0xcc) // 4 + if ct >= 32: + ct -= 24 + elif ct >= 16: + ct -= 12 + n['chipType'] = ct + n['noPhaseReset'] = flag >> 4 + elif chip == ChipType.GB: + n['chipType'] = flag & 0b11 + n['noAntiClick'] = bool((flag >> 3) & 1) + elif chip == ChipType.PCE: + n['clockSel'] = flag & 1 + n['chipType'] = (flag >> 2) & 1 + n['noAntiClick'] = bool((flag >> 3) & 1) + elif chip in [ChipType.NES, ChipType.VRC6, ChipType.FDS, ChipType.MMC5]: + n['clockSel'] = flag & 0b11 + elif chip in [ChipType.C64_8580, ChipType.C64_6581]: + n['clockSel'] = flag & 0b1111 + elif chip == ChipType.SEGA_ARCADE: + n['clockSel'] = flag & 0b11111111 + elif chip in [ChipType.NEO_GEO_CD, ChipType.NEO_GEO, ChipType.NEO_GEO_EX, + ChipType.NEO_GEO_CD_EX, ChipType.YM2610B, ChipType.YM2610B_EX]: + n['clockSel'] = flag & 0b11111111 + elif chip == ChipType.AY38910: + n['clockSel'] = flag & 0b1111 + n['chipType'] = (flag >> 4) & 0b11 + n['stereo'] = bool((flag >> 6) & 1) + n['halfClock'] = bool((flag >> 7) & 1) + n['stereoSep'] = (flag >> 8) & 0b11111111 + elif chip == ChipType.AMIGA: + n['clockSel'] = flag & 1 + n['chipType'] = (flag >> 1) & 1 + n['bypassLimits'] = bool((flag >> 2) & 1) + n['stereoSep'] = (flag >> 8) & 0b1111111 + elif chip == ChipType.YM2151: + n['clockSel'] = flag & 0b11111111 + elif chip in [ChipType.YM2612, ChipType.YM2612_EX, ChipType.YM2612_PLUS, + ChipType.YM2612_PLUS_EX]: + n['clockSel'] = flag & 2147483647 # bits 0-30 + n['ladderEffect'] = bool((flag >> 31) & 1) + elif chip == ChipType.TIA: + n['clockSel'] = flag & 1 + n['mixingType'] = (flag >> 1) & 0b11 + elif chip == ChipType.VIC20: + n['clockSel'] = flag & 1 + elif chip == ChipType.SNES: + n['volScaleL'] = flag & 0b1111111 + n['volScaleR'] = (flag >> 8) & 0b1111111 + elif chip in [ChipType.OPLL, ChipType.OPLL_DRUMS]: + n['clockSel'] = flag & 0b1111 + n['patchSet'] = flag >> 4 # safe + elif chip == ChipType.N163: + n['clockSel'] = flag & 0b1111 + n['channels'] = (flag >> 4) & 0b111 + n['multiplex'] = bool((flag >> 7) & 1) + elif chip in [ChipType.OPN, ChipType.YM2203_EX]: + n['clockSel'] = flag & 0b11111 + n['prescale'] = (flag >> 5) & 0b11 + elif chip in [ChipType.OPL, ChipType.OPL_DRUMS, ChipType.OPL2, ChipType.OPL2_DRUMS, + ChipType.Y8950, ChipType.Y8950_DRUMS]: + n['clockSel'] = flag & 0b11111111 + elif chip in [ChipType.OPL3, ChipType.OPL3_DRUMS]: + n['clockSel'] = flag & 0b11111111 + elif chip == ChipType.PC_SPEAKER: + n['speakerType'] = flag & 0b11 + elif chip == ChipType.RF5C68: + n['clockSel'] = flag & 0b1111 + n['chipType'] = flag >> 4 # safe + elif chip in [ChipType.SAA1099, ChipType.OPZ]: + n['clockSel'] = flag & 0b11 + elif chip == ChipType.AY8930: + n['clockSel'] = flag & 0b1111 + n['stereo'] = bool((flag >> 6) & 1) + n['halfClock'] = bool((flag >> 7) & 1) + n['stereoSep'] = (flag >> 8) & 0b11111111 + elif chip == ChipType.VRC7: + n['clockSel'] = flag & 0b11 + elif chip == ChipType.ZX_BEEPER: + n['clockSel'] = flag & 1 + elif chip in [ChipType.SCC, ChipType.SCC_PLUS]: + n['clockSel'] = flag & 0b11 + elif chip == ChipType.MSM6295: + n['clockSel'] = flag & 0b1111111 + n['rateSel'] = bool((flag >> 7) & 1) + elif chip == ChipType.MSM6258: + n['clockSel'] = flag & 0b11 + elif chip in [ChipType.OPL4, ChipType.OPL4_DRUMS]: + n['clockSel'] = flag & 0b11111111 + elif chip == ChipType.SETA: + n['clockSel'] = flag & 0b1111 + n['stereo'] = bool((flag >> 4) & 1) + elif chip == ChipType.ES5506: + n['channels'] = flag & 0b11111 + elif chip == ChipType.TSU: + n['clockSel'] = flag & 1 + n['echo'] = bool((flag >> 2) & 1) + n['swapEcho'] = bool((flag >> 3) & 1) + n['sampleMemSize'] = (flag >> 4) & 1 + n['pdm'] = bool((flag >> 5) & 1) + n['echoDelay'] = (flag >> 8) & 0b111111 + n['echoFeedback'] = (flag >> 16) & 0b1111 + n['echoResolution'] = (flag >> 20) & 0b1111 + n['echoVol'] = (flag >> 24) & 0b11111111 + elif chip == ChipType.YMZ280B: + n['clockSel'] = flag & 0b11111111 + elif chip == ChipType.PCM_DAC: + n['rate'] = (flag & 0b1111111111111111) + 1 + n['outDepth'] = (flag >> 16) & 0b1111 + n['stereo'] = bool((flag >> 20) & 1) + elif chip == ChipType.QSOUND: + n['echoDelay'] = flag & 0b1111111111111 + n['echoFeedback'] = (flag >> 16) & 0b11111111 + return n + + def __read_header(self, stream: BinaryIO) -> None: + # assuming we passed the magic number check + self.meta.version = read_short(stream) + stream.read(2) # RESERVED + self.__song_info_ptr = read_int(stream) + stream.read(8) # RESERVED + + def __read_info(self, stream: BinaryIO) -> None: + stream.seek(self.__song_info_ptr) + if stream.read(4) != b'INFO': + raise ValueError('No "INFO" magic') + + if self.meta.version < 100: # don't read size prior to 0.6pre1 + stream.read(4) + info_blk = stream + else: + blk_size = read_int(stream) + info_blk = BytesIO(stream.read(blk_size)) + + # info of first subsong + self.subsongs[0].timing.timebase = (read_byte(info_blk) + 1) + self.subsongs[0].timing.speed = ( + read_byte(info_blk), + read_byte(info_blk) + ) + self.subsongs[0].timing.arp_speed = read_byte(info_blk) + self.subsongs[0].timing.clock_speed = read_float(info_blk) + self.subsongs[0].pattern_length = read_short(info_blk) + len_orders = read_short(info_blk) + self.subsongs[0].timing.highlight = ( + read_byte(info_blk), + read_byte(info_blk) + ) + + # global + num_insts = read_short(info_blk) + num_waves = read_short(info_blk) + num_samples = read_short(info_blk) + num_patterns = read_int(info_blk) + + # fetch chip list + for chip_id in info_blk.read(MAX_CHIPS): + if chip_id == 0: + break # seek position is after chips here + self.chips.list.append( + ChipInfo(ChipType(chip_id)) # type: ignore + ) + + # fetch volume + for i in range(MAX_CHIPS): + vol = read_byte(info_blk, True) / 64.0 + if i >= len(self.chips.list): # cut here + continue + self.chips.list[i].volume = vol + + for i in range(MAX_CHIPS): + pan = read_byte(info_blk, True) / 128.0 + if i >= len(self.chips.list): # cut here + continue + self.chips.list[i].panning = pan + + if self.meta.version >= 119: + self.__chip_flag_ptr: List[int] = [ + read_int(info_blk) for _ in range(MAX_CHIPS) + ] + else: + for i in range(MAX_CHIPS): + flag = read_int(info_blk) + if i < len(self.chips.list): + self.chips.list[i].flags.update( + self.__convert_old_chip_flags(self.chips.list[i].type, flag) + ) + + self.meta.name = read_str(info_blk) + self.meta.author = read_str(info_blk) + self.meta.tuning = read_float(info_blk) + + # Compat flags, part I + self.__read_compat_flags(info_blk, 1) + + self.__instrument_ptr = [ + read_int(info_blk) for _ in range(num_insts) + ] + + self.__wavetable_ptr = [ + read_int(info_blk) for _ in range(num_waves) + ] + + self.__sample_ptr = [ + read_int(info_blk) for _ in range(num_samples) + ] + + self.__pattern_ptr = [ + read_int(info_blk) for _ in range(num_patterns) + ] + + num_channels = self.get_num_channels() + + for channel in range(self.get_num_channels()): + self.subsongs[0].order[channel] = [ + read_byte(info_blk) for _ in range(len_orders) + ] + + self.subsongs[0].effect_columns = [ + read_byte(info_blk) for _ in range(num_channels) + ] + + # set up channels display info + self.subsongs[0].channel_display = [ + ChannelDisplayInfo() for _ in range(num_channels) + ] + + for i in range(num_channels): + self.subsongs[0].channel_display[i].shown = bool(read_byte(info_blk)) + + for i in range(num_channels): + self.subsongs[0].channel_display[i].collapsed = bool(read_byte(info_blk)) + + for i in range(num_channels): + self.subsongs[0].channel_display[i].name = read_str(info_blk) + + for i in range(num_channels): + self.subsongs[0].channel_display[i].abbreviation = read_str(info_blk) + + self.meta.comment = read_str(info_blk) + + # Master volume + if self.meta.version >= 59: + self.chips.master_volume = read_float(info_blk) + + # Compat flags, part II + if self.meta.version >= 70: + self.__read_compat_flags(info_blk, 2) + if self.meta.version >= 96: + self.subsongs[0].timing.virtual_tempo = ( + read_short(info_blk), read_short(info_blk) + ) + else: + info_blk.read(4) # reserved in self.meta.version < 96 + + # Subsongs + if self.meta.version >= 95: + self.subsongs[0].name = read_str(info_blk) + self.subsongs[0].comment = read_str(info_blk) + num_extra_subsongs = read_byte(info_blk) + info_blk.read(3) # reserved + self.__subsong_ptr = [ + read_int(info_blk) for _ in range(num_extra_subsongs) + ] + + # Extra metadata + if self.meta.version >= 103: + self.meta.sys_name = read_str(info_blk) + self.meta.album = read_str(info_blk) + # TODO: need to take encoding into account + self.meta.name_jp = read_str(info_blk) + self.meta.author_jp = read_str(info_blk) + self.meta.sys_name_jp = read_str(info_blk) + self.meta.album_jp = read_str(info_blk) + + # New chip mixer and patchbay + if self.meta.version >= 135: + for i in range(len(self.chips.list)): + # new chip volume/panning format takes precedence over the legacy one + # if you save a .fur with this, legacy and new volume/panning formats + # have the same value. different values shouldn't be possible + self.chips.list[i].volume = read_float(info_blk) + self.chips.list[i].panning = read_float(info_blk) + self.chips.list[i].surround = read_float(info_blk) + num_patchbay_connections = read_int(info_blk) + for _ in range(num_patchbay_connections): + src = read_short(info_blk) + dst = read_short(info_blk) + self.patchbay.append( + PatchBay( + dest=InputPatchBayEntry( + set=InputPortSet(src >> 4), + port=src & 0b1111 + ), + source=OutputPatchBayEntry( + set=OutputPortSet(dst >> 4), + port=dst & 0b1111 + ) + ) + ) + + if self.meta.version >= 136: + self.compat_flags.auto_patchbay = bool(read_byte(info_blk)) + + # Compat flags, part III + if self.meta.version >= 138: + self.__read_compat_flags(info_blk, 3) + + # Speed patterns and grooves + if self.meta.version >= 139: + # speed pattern + len_speed_pattern = read_byte(info_blk) + if (len_speed_pattern < 0) or (len_speed_pattern > 16): + raise ValueError('Invalid speed pattern length value') + self.subsongs[0].speed_pattern = [ + read_byte(info_blk) for _ in range(len_speed_pattern) + ] + info_blk.read(16 - len_speed_pattern) # skip that many bytes, because it's always 0x06 + + # groove + len_groove_list = read_byte(info_blk) + for _ in range(len_groove_list): + len_groove = read_byte(info_blk) + self.subsongs[0].grooves.append([ + read_byte(info_blk) for _ in range(len_groove) + ]) + info_blk.read(16 - len_groove) # TODO: i assume the same as above. i hope i'm right + + def __read_instruments(self, stream: BinaryIO) -> None: + for i in self.__instrument_ptr: + if i == 0: + break + stream.seek(i) + new_ins = FurnaceInstrument() + if self.meta.version < 127: # i trust this not to screw up + new_ins.load_from_stream(stream, _FurInsImportType.FORMAT_0_EMBED) + else: + new_ins.load_from_stream(stream, _FurInsImportType.FORMAT_1_EMBED) + self.instruments.append(new_ins) + + def __read_wavetables(self, stream: BinaryIO) -> None: + for i in self.__wavetable_ptr: + if i == 0: + break + stream.seek(i) + new_wt = FurnaceWavetable() + new_wt.load_from_stream(stream, _FurWavetableImportType.EMBED) + self.wavetables.append(new_wt) + + def __read_samples(self, stream: BinaryIO) -> None: + for i in self.__sample_ptr: + if i == 0: + break + stream.seek(i) + new_wt = FurnaceSample() + new_wt.load_from_stream(stream) + self.samples.append(new_wt) + + def __read_patterns(self, stream: BinaryIO) -> None: + for i in self.__pattern_ptr: + if i == 0: + break + stream.seek(i) + + # Old pattern + if self.meta.version < 157: + if stream.read(4) != b'PATR': + raise ValueError('No "PATR" magic') + sz = read_int(stream) + if sz == 0: + patr_blk = stream + else: + patr_blk = BytesIO(stream.read(sz)) + + new_patr = FurnacePattern() + new_patr.channel = read_short(patr_blk) + new_patr.index = read_short(patr_blk) + new_patr.subsong = read_short(patr_blk) + if self.meta.version < 95: + assert new_patr.subsong == 0 + read_short(patr_blk) # reserved + + num_rows = self.subsongs[new_patr.subsong].pattern_length + + for _ in range(num_rows): + row = FurnaceRow( + note=Note(read_short(patr_blk)), + octave=read_short(patr_blk), + instrument=read_short(patr_blk), + volume=read_short(patr_blk) + ) + row.octave += (1 if row.note == Note.C_ else 0) + effect_columns = self.subsongs[new_patr.subsong].effect_columns[new_patr.channel] + row.effects = [ + (read_short(patr_blk), read_short(patr_blk)) for _ in range(effect_columns) + ] + new_patr.data.append(row) + + if self.meta.version >= 51: + new_patr.name = read_str(patr_blk) + + # New pattern + else: + if stream.read(4) != b'PATN': + raise ValueError('No "PATN" magic') + sz = read_int(stream) + if sz == 0: + patr_blk = stream + else: + patr_blk = BytesIO(stream.read(sz)) + + new_patr = FurnacePattern() + new_patr.subsong = read_byte(patr_blk) + new_patr.channel = read_byte(patr_blk) + new_patr.index = read_short(patr_blk) + new_patr.name = read_str(patr_blk) + + num_rows = self.subsongs[new_patr.subsong].pattern_length + effect_columns = self.subsongs[new_patr.subsong].effect_columns[new_patr.channel] + + empty_row = lambda: FurnaceRow(Note.__, 0, 0xffff, 0xffff, [(0xffff,0xffff)] * effect_columns) + + row_idx = 0 + while row_idx < num_rows: + char = read_byte(patr_blk) + # end of pattern + if char == 0xff: + break + # skip N+2 rows + if char & 0x80: + skip = (char & 0x7f) + 2 + row_idx += skip + for _ in range(skip): + new_patr.data.append(empty_row()) + continue + # check if some values present + effect_present_list = [False] * 8 + effect_val_present_list = [False] * 8 + note_present = bool(char & 0x01) + ins_present = bool(char & 0x02) + volume_present = bool(char & 0x04) + effect_present_list[0] = bool(char & 0x08) + effect_val_present_list[0] = bool(char & 0x10) + effect_0_3_present = bool(char & 0x20) + effect_4_7_present = bool(char & 0x40) + if effect_0_3_present: + char = read_byte(patr_blk) + assert effect_present_list[0] == bool(char & 0x01) + assert effect_val_present_list[0] == bool(char & 0x02) + effect_present_list[1] = bool(char & 0x04) + effect_val_present_list[1] = bool(char & 0x08) + effect_present_list[2] = bool(char & 0x10) + effect_val_present_list[2] = bool(char & 0x20) + effect_present_list[3] = bool(char & 0x40) + effect_val_present_list[3] = bool(char & 0x80) + if effect_4_7_present: + char = read_byte(patr_blk) + effect_present_list[4] = bool(char & 0x01) + effect_val_present_list[4] = bool(char & 0x02) + effect_present_list[5] = bool(char & 0x04) + effect_val_present_list[5] = bool(char & 0x08) + effect_present_list[6] = bool(char & 0x10) + effect_val_present_list[6] = bool(char & 0x20) + effect_present_list[7] = bool(char & 0x40) + effect_val_present_list[7] = bool(char & 0x80) + + # actually read present values + note, octave = Note(0), 0 + if note_present: + raw_note = read_byte(patr_blk) + if raw_note == 180: + note = Note.OFF + elif raw_note == 181: + note = Note.OFF_REL + elif raw_note == 182: + note = Note.REL + else: + note = raw_note % 12 + note = 12 if note == 0 else note + note = Note(note) + octave = -5 + raw_note // 12 + + ins, volume = 0xffff, 0xffff + if ins_present: + ins = read_byte(patr_blk) + if volume_present: + volume = read_byte(patr_blk) + + row = FurnaceRow( + note=note, + octave=octave, + instrument=ins, + volume=volume + ) + + row.effects = [(0xffff,0xffff)] * effect_columns + for i, fx_presents in enumerate(zip(effect_present_list, effect_val_present_list)): + if i >= effect_columns: + break + fx_cmd, fx_val = 0xffff, 0xffff + if fx_presents[0]: + fx_cmd = read_byte(patr_blk) + if fx_presents[1]: + fx_val = read_byte(patr_blk) + row.effects[i] = (fx_cmd, fx_val) + + new_patr.data.append(row) + row_idx += 1 + + # fill the rest of the pattern with EMPTY + while row_idx < num_rows: + new_patr.data.append(empty_row()) + row_idx += 1 + + self.patterns.append(new_patr) + + def __read_subsongs(self, stream: BinaryIO) -> None: + for i in self.__subsong_ptr: + if i == 0: + break + stream.seek(i) + if stream.read(4) != b'SONG': + raise ValueError('No "SONG" magic') + subsong_blk = BytesIO(stream.read(read_int(stream))) + new_subsong = SubSong() + new_subsong.order.clear() + new_subsong.speed_pattern.clear() + + new_subsong.timing.timebase = read_byte(subsong_blk) + new_subsong.timing.speed = ( + read_byte(subsong_blk), read_byte(subsong_blk) + ) + new_subsong.timing.arp_speed = read_byte(subsong_blk) + new_subsong.timing.clock_speed = read_float(subsong_blk) + new_subsong.pattern_length = read_short(subsong_blk) + new_subsong_len_orders = read_short(subsong_blk) + new_subsong.timing.highlight = ( + read_byte(subsong_blk), read_byte(subsong_blk) + ) + new_subsong.timing.virtual_tempo = ( + read_short(subsong_blk), read_short(subsong_blk) + ) + new_subsong.name = read_str(subsong_blk) + new_subsong.comment = read_str(subsong_blk) + + num_channels = self.get_num_channels() + + for channel in range(self.get_num_channels()): + new_subsong.order[channel] = [ + read_byte(subsong_blk) for _ in range(new_subsong_len_orders) + ] + + new_subsong.effect_columns = [ + read_byte(subsong_blk) for _ in range(num_channels) + ] + + # set up channels display info + new_subsong.channel_display = [ + ChannelDisplayInfo() for _ in range(num_channels) + ] + + for i in range(num_channels): + new_subsong.channel_display[i].shown = bool(read_byte(subsong_blk)) + + for i in range(num_channels): + new_subsong.channel_display[i].collapsed = bool(read_byte(subsong_blk)) + + for i in range(num_channels): + new_subsong.channel_display[i].name = read_str(subsong_blk) + + for i in range(num_channels): + new_subsong.channel_display[i].abbreviation = read_str(subsong_blk) + + # Speed patterns and grooves + if self.meta.version >= 139: + # speed pattern + len_speed_pattern = read_byte(subsong_blk) + if (len_speed_pattern < 0) or (len_speed_pattern > 16): + raise ValueError('Invalid speed pattern length value') + new_subsong.speed_pattern = [ + read_byte(subsong_blk) for _ in range(len_speed_pattern) + ] + + self.subsongs.append(new_subsong) + + def __str__(self) -> str: + return '' % ( + self.meta.version, self.meta.name, self.meta.author + ) diff --git a/loader/samples/minexample/furC64/chipchune/furnace/sample.py b/loader/samples/minexample/furC64/chipchune/furnace/sample.py new file mode 100644 index 0000000..1139050 --- /dev/null +++ b/loader/samples/minexample/furC64/chipchune/furnace/sample.py @@ -0,0 +1,47 @@ +from io import BytesIO +from typing import Optional, Union, BinaryIO, List + +from chipchune._util import read_short, read_int, read_str +from .data_types import SampleMeta +from .enums import _FurSampleType + +FILE_MAGIC_STR = b'SMP2' + + +class FurnaceSample: + def __init__(self) -> None: + self.meta: SampleMeta = SampleMeta() + """ + Sample metadata. + """ + self.data: bytearray = b'' + """ + Sample data. + """ + + def load_from_stream(self, stream: BinaryIO) -> None: + """ + Load a sample from an **uncompressed** stream. + + :param stream: File-like object containing the uncompressed wavetable. + """ + if stream.read(len(FILE_MAGIC_STR)) != FILE_MAGIC_STR: + raise ValueError('Bad magic value for a wavetable file') + blk_size = read_int(stream) + if blk_size > 0: + smp_data = BytesIO(stream.read(blk_size)) + else: + smp_data = stream + + self.meta.name = read_str(smp_data) + self.meta.length = read_int(smp_data) + read_int(smp_data) # compatablity rate + self.meta.sample_rate = read_int(smp_data) + self.meta.depth = int(smp_data.read(1)[0]) + smp_data.read(1) # loop direction + smp_data.read(1) # flags + smp_data.read(1) # flags 2 + self.meta.loop_start = read_int(smp_data) + self.meta.loop_end = read_int(smp_data) + smp_data.read(16) # sample presence bitfields + self.data = smp_data.read(self.meta.length) diff --git a/loader/samples/minexample/furC64/chipchune/furnace/wavetable.py b/loader/samples/minexample/furC64/chipchune/furnace/wavetable.py new file mode 100644 index 0000000..cec13cb --- /dev/null +++ b/loader/samples/minexample/furC64/chipchune/furnace/wavetable.py @@ -0,0 +1,101 @@ +from io import BytesIO +from typing import Optional, Union, BinaryIO, List + +from chipchune._util import read_short, read_int, read_str +from .data_types import WavetableMeta +from .enums import _FurWavetableImportType + +FILE_MAGIC_STR = b'-Furnace waveta-' +EMBED_MAGIC_STR = b'WAVE' + + +class FurnaceWavetable: + def __init__(self, file_name: Optional[str] = None) -> None: + """ + Creates or opens a new Furnace wavetable as a Python object. + + :param file_name: (Optional) + If specified, then it will parse a file as a FurnaceWavetable. If file name (str) is + given, it will load that file. + + Defaults to None. + """ + self.file_name: Optional[str] = None + """ + Original file name, if the object was initialized with one. + """ + self.meta: WavetableMeta = WavetableMeta() + """ + Wavetable metadata. + """ + self.data: List[int] = [] + """ + Wavetable data. + """ + + if isinstance(file_name, str): + self.load_from_file(file_name) + + def load_from_file(self, file_name: Optional[str] = None) -> None: + if isinstance(file_name, str): + self.file_name = file_name + if self.file_name is None: + raise RuntimeError('No file name set, either set self.file_name or pass file_name to the function') + + # since we're loading from an uncompressed file, we can just check the file magic number + with open(self.file_name, 'rb') as f: + detect_magic = f.peek(len(FILE_MAGIC_STR))[:len(FILE_MAGIC_STR)] + if detect_magic == FILE_MAGIC_STR: + return self.load_from_stream(f, _FurWavetableImportType.FILE) + else: # uncompressed for sure + raise ValueError('No recognized file type magic') + + def load_from_bytes(self, data: bytes, import_as: Union[int, _FurWavetableImportType]) -> None: + """ + Load a wavetable from a series of bytes. + + :param data: Bytes + """ + return self.load_from_stream( + BytesIO(data), + import_as + ) + + def load_from_stream(self, stream: BinaryIO, import_as: Union[int, _FurWavetableImportType]) -> None: + """ + Load a wavetable from an **uncompressed** stream. + + :param stream: File-like object containing the uncompressed wavetable. + :param import_as: int + - 0 = wavetable file + - 1 = wavetable embedded in module + """ + if import_as == _FurWavetableImportType.FILE: + if stream.read(len(FILE_MAGIC_STR)) != FILE_MAGIC_STR: + raise ValueError('Bad magic value for a wavetable file') + version = read_short(stream) + read_short(stream) # reserved + self.__load_embed(stream) + + elif import_as == _FurWavetableImportType.EMBED: + return self.__load_embed(stream) + + else: + raise ValueError('Invalid import type') + + def __load_embed(self, stream: BinaryIO) -> None: + if stream.read(len(EMBED_MAGIC_STR)) != EMBED_MAGIC_STR: + raise RuntimeError('Bad magic value for a wavetable embed') + + blk_size = read_int(stream) + if blk_size > 0: + wt_data = BytesIO(stream.read(blk_size)) + else: + wt_data = stream + + self.meta.name = read_str(wt_data) + self.meta.width = read_int(wt_data) + read_int(wt_data) # reserved + self.meta.height = read_int(wt_data) + 1 # serialized height is 1 lower than actual value + + self.data = [read_int(wt_data) for _ in range(self.meta.width)] diff --git a/loader/samples/minexample/furC64/chipchune/interchange/__init__.py b/loader/samples/minexample/furC64/chipchune/interchange/__init__.py new file mode 100644 index 0000000..ec33a22 --- /dev/null +++ b/loader/samples/minexample/furC64/chipchune/interchange/__init__.py @@ -0,0 +1,7 @@ +""" +Generic format for manipulating to, from, and between tracker formats. + +- :mod:`chipchune.interchange.enums`: Various constants. +- :mod:`chipchune.interchange.furnace`: Adapters for Furnace. + +""" diff --git a/loader/samples/minexample/furC64/chipchune/interchange/enums.py b/loader/samples/minexample/furC64/chipchune/interchange/enums.py new file mode 100644 index 0000000..6d553b6 --- /dev/null +++ b/loader/samples/minexample/furC64/chipchune/interchange/enums.py @@ -0,0 +1,24 @@ +import enum +from chipchune._util import EnumShowNameOnly + +class InterNote(EnumShowNameOnly): + """ + Common note interchange format. + """ + __ = enum.auto() # Signifies a blank space in tracker + C_ = enum.auto() + Cs = enum.auto() + D_ = enum.auto() + Ds = enum.auto() + E_ = enum.auto() + F_ = enum.auto() + Fs = enum.auto() + G_ = enum.auto() + Gs = enum.auto() + A_ = enum.auto() + As = enum.auto() + B_ = enum.auto() + Off = enum.auto() + OffRel = enum.auto() + Rel = enum.auto() + Echo = enum.auto() diff --git a/loader/samples/minexample/furC64/chipchune/interchange/furnace.py b/loader/samples/minexample/furC64/chipchune/interchange/furnace.py new file mode 100644 index 0000000..4f39640 --- /dev/null +++ b/loader/samples/minexample/furC64/chipchune/interchange/furnace.py @@ -0,0 +1,52 @@ +from chipchune.furnace.enums import Note as FurnaceNote +from chipchune.interchange.enums import InterNote + +def furnace_note_to_internote(note: FurnaceNote) -> InterNote: + """ + Convert a Furnace note into an InterNote. + + Raises: + - Exception: If the supplied note is out of range. + """ + if note == FurnaceNote.__: return InterNote.__ + elif note == FurnaceNote.C_: return InterNote.C_ + elif note == FurnaceNote.Cs: return InterNote.Cs + elif note == FurnaceNote.D_: return InterNote.D_ + elif note == FurnaceNote.Ds: return InterNote.Ds + elif note == FurnaceNote.E_: return InterNote.E_ + elif note == FurnaceNote.F_: return InterNote.F_ + elif note == FurnaceNote.Fs: return InterNote.Fs + elif note == FurnaceNote.G_: return InterNote.G_ + elif note == FurnaceNote.Gs: return InterNote.Gs + elif note == FurnaceNote.A_: return InterNote.A_ + elif note == FurnaceNote.As: return InterNote.As + elif note == FurnaceNote.B_: return InterNote.B_ + elif note == FurnaceNote.OFF: return InterNote.Off + elif note == FurnaceNote.OFF_REL: return InterNote.OffRel + elif note == FurnaceNote.REL: return InterNote.Rel + else: + raise Exception("Invalid note value %s" % note) + +def internote_to_furnace_note(note: InterNote) -> FurnaceNote: + """ + Convert an InterNote into a Furnace note. If the equivalent + value is unable to be determined, a blank note `__` is returned. + """ + if note == InterNote.__: return FurnaceNote.__ + elif note == InterNote.C_: return FurnaceNote.C_ + elif note == InterNote.Cs: return FurnaceNote.Cs + elif note == InterNote.D_: return FurnaceNote.D_ + elif note == InterNote.Ds: return FurnaceNote.Ds + elif note == InterNote.E_: return FurnaceNote.E_ + elif note == InterNote.F_: return FurnaceNote.F_ + elif note == InterNote.Fs: return FurnaceNote.Fs + elif note == InterNote.G_: return FurnaceNote.G_ + elif note == InterNote.Gs: return FurnaceNote.Gs + elif note == InterNote.A_: return FurnaceNote.A_ + elif note == InterNote.As: return FurnaceNote.As + elif note == InterNote.B_: return FurnaceNote.B_ + elif note == InterNote.Off: return FurnaceNote.OFF + elif note == InterNote.OffRel: return FurnaceNote.OFF_REL + elif note == InterNote.Rel: return FurnaceNote.REL + else: + return FurnaceNote.__ \ No newline at end of file diff --git a/loader/samples/minexample/furC64/chipchune/utils/__init__.py b/loader/samples/minexample/furC64/chipchune/utils/__init__.py new file mode 100644 index 0000000..88e24f3 --- /dev/null +++ b/loader/samples/minexample/furC64/chipchune/utils/__init__.py @@ -0,0 +1,5 @@ +""" +Utilities for manipulating, converting, etc. tracker data. + +- :mod:`chipchune.utils.conversion`: Conversion tools. +""" \ No newline at end of file diff --git a/loader/samples/minexample/furC64/chipchune/utils/conversion.py b/loader/samples/minexample/furC64/chipchune/utils/conversion.py new file mode 100644 index 0000000..0c6c5b1 --- /dev/null +++ b/loader/samples/minexample/furC64/chipchune/utils/conversion.py @@ -0,0 +1,94 @@ +from chipchune.furnace.module import FurnacePattern +from chipchune.interchange.enums import InterNote +from chipchune.interchange.furnace import furnace_note_to_internote +from typing import Union, List, Tuple +from dataclasses import dataclass, field + +@dataclass +class SequenceEntry: + """ + A representation of a row in note-length format. Such a format is commonly + used across different sound engines and sequenced data. + + A pattern can be turned into a list of SequenceEntries, which should be easier + to convert into a format of your choice. + """ + note: InterNote + length: int + volume: int + """ + Tracker-defined volume; although this should be -1 for undefined values. + """ + octave: int + """ + Tracker-defined octave; although this should be -1 for undefined values. + """ + instrument: int + """ + Tracker-defined instrument number; although this should be -1 for undefined values. + """ + effects: List[Tuple[int, int]] = field(default_factory=list) + """ + Tracker-defined effects list; if undefined, this should be empty. + """ + +def pattern_to_sequence(pattern: Union[FurnacePattern, None]) -> List[SequenceEntry]: + """ + Interface to convert a pattern from tracker rows to a "sequence", which is + really a list of SequenceEntries. + + :param pattern: + A pattern object. Supported types at the moment: `FurnacePattern`. + Anything outside of the supported types will throw a `TypeError`. + """ + if isinstance(pattern, FurnacePattern): + return furnace_pattern_to_sequence(pattern) + else: + raise TypeError("Invalid pattern type; must be one of: FurnacePattern") + +def furnace_pattern_to_sequence(pattern: FurnacePattern) -> List[SequenceEntry]: + converted: List[SequenceEntry] = [] + last_volume = -1 + for i in pattern.data: + note = furnace_note_to_internote(i.note) + effects = i.effects + volume = i.volume + instrument = i.instrument + + if effects == [(65535, 65535)]: + effects = [] + + if volume == 65535: + volume = last_volume + else: + last_volume = volume + + if instrument == 65535: + instrument = -1 + + if note == InterNote.__: + if len(converted) == 0: + converted.append( + SequenceEntry( + note=InterNote.__, + length=1, + volume=volume, + octave=i.octave, + instrument=instrument, + effects=effects, + ) + ) + else: + converted[-1].length += 1 + else: + converted.append( + SequenceEntry( + note=note, + length=1, + volume=volume, + octave=i.octave, + instrument=instrument, + effects=effects, + ) + ) + return converted diff --git a/loader/samples/minexample/furC64/convert.bat b/loader/samples/minexample/furC64/convert.bat new file mode 100644 index 0000000..5044a81 --- /dev/null +++ b/loader/samples/minexample/furC64/convert.bat @@ -0,0 +1,14 @@ +@echo off +if [%1]==[] goto usage +python3 convert_to_asm.py %1 +echo converted .fur file to .asm! +cd asm +cl65 -d -vm -l furC64.lst -g -u __EXEHDR__ -t c64 -C .\c64-asm.cfg -m furC64.map -Ln furC64.lbl -o furC64-test.prg furC64.asm +@echo compiled .prg file at asm/furC64-test.prg +cd .. +goto :eof +:usage +@echo No arguments supplied +@echo Make sure to run this command with an argument +@echo example: convert.bat test_file.fur +exit /B 1 diff --git a/loader/samples/minexample/furC64/convert.sh b/loader/samples/minexample/furC64/convert.sh new file mode 100755 index 0000000..ffcc96b --- /dev/null +++ b/loader/samples/minexample/furC64/convert.sh @@ -0,0 +1,15 @@ +#!/bin/bash +if [ $# -eq 0 ] +then + echo "No arguments supplied" + echo "Make sure to run this command with an argument" + echo "example: convert.sh test_file.fur" +else +python3 convert_to_asm.py $1 +echo "converted .fur file to .asm!" +cd asm +cl65 -d -vm -l furC64.lst -g -u __EXEHDR__ -t c64 -C ./exe.cfg -m furC64.map -Ln furC64.lbl -o furC64-test.prg furC64.asm +cl65 -d -C ./bin.cfg -o song.bin furC64.asm +echo "compiled .prg file at asm/furC64-test.prg" +cd .. +fi diff --git a/loader/samples/minexample/furC64/convert_to_asm.py b/loader/samples/minexample/furC64/convert_to_asm.py new file mode 100644 index 0000000..14ac9aa --- /dev/null +++ b/loader/samples/minexample/furC64/convert_to_asm.py @@ -0,0 +1,780 @@ +from chipchune.furnace.module import FurnaceModule +from chipchune.furnace.data_types import InsFeatureMacro, InsFeatureC64, InsFeatureAmiga +from chipchune.furnace.enums import MacroCode, MacroItem, MacroType +from chipchune.furnace.enums import InstrumentType +import sys + +subsong = 0 +note_transpose = 0 +dups = {} + +print(sys.argv) +module = FurnaceModule(sys.argv[1]) +chnum = module.get_num_channels() + +speed_type = len(module.subsongs[subsong].speed_pattern) + +notes = ["C_","Cs","D_","Ds","E_","F_","Fs","G_","Gs","A_","As","B_"] + +def comp(pat): + i = 0 + o = [] + n = 0 + while i < len(pat): + j = i + k = 0 + if pat[i] >= 0x40 and pat[i] < 128: i += 1 + elif pat[i] == 0xFB: i += 2 + elif pat[i] == 0xFC: i += 2 + elif pat[i] == 0xE0: i += 2 + elif pat[i] == 0xE1: i += 2 + elif pat[i] == 0xE2: i += 2 + elif pat[i] == 0xE3: i += 2 + elif pat[i] == 0xE4: i += 2 + elif pat[i] == 0xE5: i += 3 + elif pat[i] == 0xE6: i += 2 + elif pat[i] == 0xE7: i += 2 + elif pat[i] == 0xE8: i += 2 + elif pat[i] == 0xE9: i += 3 + elif pat[i] == 0xEA: i += 3 + elif pat[i] == 0xEB: i += 2 + elif pat[i] == 0xEC: i += 2 + elif pat[i] == 0xED: i += 2 + elif pat[i] == 0xEE: i += 2 + elif pat[i] == 0xEF: i += 1 + elif pat[i] == 0xF0: i += 1 + elif pat[i] == 0xF1: i += 2 + elif pat[i] == 0xFF: i += 2 + elif pat[i] == 0xFD: + i += 1 + n = 2 + elif pat[i] == 0xFE: + i += 1 + n = 2 + elif pat[i] >= 128: + i += 1 + n = 2 + else: + k = 1 + if n == 0: + o.append(pat[i]) + elif pat[i] > 1: + o.append(pat[i]-1) + #print(i,pat[i]) + i += 1 + n = max(n-1,0) + if k == 0: + o.extend(pat[j:i]) + #print(pat,"\n",o,"\n") + return o + +def conv_pattern(pattern): + out = [0] + oldtemp = [0,0] + r = 0 + bitind = 0 + oldins = -1 + for row in pattern.data: + has03xx = 0 + for l in row.effects: + k = list(l) + if k[0] == 0x03 and k[1] > 0: + has03xx = 1 + break + + temp = [] + notnote = 0 + new_byte = 0 + if row.instrument != 65535 and oldins != row.instrument: + new_byte = 1 + if row.instrument < 0x40: + temp.append(row.instrument+0x40) + else: + temp.append(0xFB) + temp.append(row.instrument) + oldins = row.instrument + if row.volume != 65535: + new_byte = 1 + temp.append(0xFC) + temp.append(row.volume) + + hasEffect = [-1,-1] + has0Dxx = -1 + for l in row.effects: + k = list(l) + if k[1] == 65535: + k[1] = 0 + + if k[0] == 0xD: + new_byte = 1 + has0Dxx = k[1] + continue + if k[0] == 0x0B: + new_byte = 1 + temp.extend([0xED, k[1]]) + has0Dxx = 0 + continue + if (k[0] == 0x09 or k[0] == 0x0F) and (speed_type == 1): + new_byte = 1 + temp.extend([0xE1, k[1]]) + temp.extend([0xE0, k[1]]) + continue + if k[0] == 0x0F and (speed_type == 2): + new_byte = 1 + temp.extend([0xE1, k[1]]) + continue + if k[0] == 0x09 and (speed_type == 2): + new_byte = 1 + temp.extend([0xE0, k[1]]) + continue + if k[0] == 0x00: + new_byte = 1 + temp.extend([0xE2, k[1]]) + continue + if k[0] == 0x01: + new_byte = 1 + temp.extend([0xE3, k[1]]) + continue + if k[0] == 0x02: + new_byte = 1 + temp.extend([0xE4, k[1]]) + continue + if k[0] == 0x03 and k[1] == 0: + new_byte = 1 + temp.extend([0xE4, 0]) + continue + if k[0] == 0x03 and k[1] > 0: + new_byte = 1 + temp.extend([0xE5, k[1], max(min(notes.index(str(row.note))+(row.octave*12)+note_transpose,95),0)]) + continue + if k[0] == 0x04: + new_byte = 1 + temp.extend([0xE6, k[1]]) + continue + if k[0] == 0x1B: + new_byte = 1 + temp.extend([0xE7, k[1]]) + continue + if k[0] == 0x1C: + new_byte = 1 + temp.extend([0xE8, k[1]]) + continue + if k[0] == 0xE1: + new_byte = 1 + temp.extend([0xE9, k[1]>>4, k[1]&15]) + continue + if k[0] == 0xE2: + new_byte = 1 + temp.extend([0xEA, k[1]>>4, k[1]&15]) + continue + if k[0] == 0xE5: + new_byte = 1 + temp.extend([0xEB, k[1]]) + continue + if k[0] == 0xEC: + new_byte = 1 + temp.extend([0xEC, k[1]]) + continue + if (k[0]>>4) == 4: + new_byte = 1 + temp.extend([0xEE, (k[1]|(k[0]&0xf)<<8)>>3]) + continue + if k[0] == 0xEA: + new_byte = 1 + if k[1] == 0: + temp.extend([0xEF]) + else: + temp.extend([0xF0]) + continue + if k[0] == 0x1A: + new_byte = 1 + if k[1] > 0: + temp.extend([0xF1, 0x00]) + else: + temp.extend([0xF1, 0xFF]) + continue + if str(row.note) == "OFF_REL": + notnote = 1 + new_byte = 1 + temp.append(0xFD) + elif str(row.note) == "REL": + notnote = 1 + new_byte = 1 + temp.append(0xFD) + elif str(row.note) == "OFF": + notnote = 1 + new_byte = 1 + temp.append(0xFE) + elif str(row.note) == "__" or (has03xx == 1): + if has03xx == 0: + notnote = 1 + #temp.append(0x80) + else: + new_byte = 1 + temp.append(max(min(notes.index(str(row.note))+(row.octave*12)+note_transpose,95),0)+0x80) + + if new_byte == 1: + temp.append(0) + out.extend(temp) + durpass = False + if out[-1] >= 63: + out.append(0) + if has0Dxx > -1: + out[-1] += 1 + if out[0] == 0: out = out[1:] + out.extend([0xFF, has0Dxx]) + return out + out[-1] += 1 + r += 1 + out.extend([0xFF, 0]) + if out[0] == 0: out = out[1:] + return out + +f = open("asm/song.asm","w") + +relW = [] +relA = [] +relD = [] +relC = [] + +f.write("ticks_init:") +f.write(".byte ") +if speed_type == 1: + f.write(str(module.subsongs[subsong].speed_pattern[0])+", ") + f.write(str(module.subsongs[subsong].speed_pattern[0])+"\n") +elif speed_type == 2: + f.write(str(module.subsongs[subsong].speed_pattern[0])+", ") + f.write(str(module.subsongs[subsong].speed_pattern[1])+"\n") + +f.write("insFL:\n") +f.write(".lobytes ") +for i in range(len(module.instruments)): + f.write("ins"+str(i)+"F") + if i == len(module.instruments)-1: + f.write("\n") + else: + f.write(", ") +f.write("insFH:\n") +f.write(".hibytes ") +for i in range(len(module.instruments)): + f.write("ins"+str(i)+"F") + if i == len(module.instruments)-1: + f.write("\n") + else: + f.write(", ") + +f.write("insAL:\n") +f.write(".lobytes ") +for i in range(len(module.instruments)): + f.write("ins"+str(i)+"A") + if i == len(module.instruments)-1: + f.write("\n") + else: + f.write(", ") +f.write("insAH:\n") +f.write(".hibytes ") +for i in range(len(module.instruments)): + f.write("ins"+str(i)+"A") + if i == len(module.instruments)-1: + f.write("\n") + else: + f.write(", ") + +f.write("insDL:\n") +f.write(".lobytes ") +for i in range(len(module.instruments)): + f.write("ins"+str(i)+"D") + if i == len(module.instruments)-1: + f.write("\n") + else: + f.write(", ") +f.write("insDH:\n") +f.write(".hibytes ") +for i in range(len(module.instruments)): + f.write("ins"+str(i)+"D") + if i == len(module.instruments)-1: + f.write("\n") + else: + f.write(", ") + +f.write("insWL:\n") +f.write(".lobytes ") +for i in range(len(module.instruments)): + f.write("ins"+str(i)+"W") + if i == len(module.instruments)-1: + f.write("\n") + else: + f.write(", ") +f.write("insWH:\n") +f.write(".hibytes ") +for i in range(len(module.instruments)): + f.write("ins"+str(i)+"W") + if i == len(module.instruments)-1: + f.write("\n") + else: + f.write(", ") + +f.write("insCL:\n") +f.write(".lobytes ") +for i in range(len(module.instruments)): + f.write("ins"+str(i)+"C") + if i == len(module.instruments)-1: + f.write("\n") + else: + f.write(", ") +f.write("insCH:\n") +f.write(".hibytes ") +for i in range(len(module.instruments)): + f.write("ins"+str(i)+"C") + if i == len(module.instruments)-1: + f.write("\n") + else: + f.write(", ") + +for i in range(len(module.instruments)): + features = module.instruments[i].features + a = filter( + lambda x: ( + type(x) == InsFeatureMacro + ), features + ) + macros = [] + for j in a: + macros = j.macros + hasWaveMacro = 0 + hasCutMacro = 0 + for j in macros: + kind = j.kind + if kind == MacroCode.WAVE: + hasWaveMacro = 1 + continue + if kind == MacroCode.ALG: + hasCutMacro = 1 + continue + + hasAbsFilter = False + hasAbsDuty = False + written_ins = False + + a = filter( + lambda x: ( + type(x) == InsFeatureMacro + ), features + ) + macros = [] + for j in a: + macros = j.macros + for j in macros: + kind = j.kind + if kind == MacroCode.DUTY and j.type == MacroType.LFO: + hasAbsDuty = True + + a = filter( + lambda x: ( + type(x) == InsFeatureC64 + ), features + ) + for j in a: + wave = j.tri_on + wave |= j.saw_on<<1 + wave |= j.pulse_on<<2 + wave |= j.noise_on<<3 + wave <<= 4 + wave |= j.ring_mod<<2 + wave |= j.osc_sync<<1 + f.write("ins"+str(i)+"F:\n") + f.write(".byte ") + f.write(str(wave)+", ") + ad = (j.envelope.a<<4)|j.envelope.d + sr = (j.envelope.s<<4)|j.envelope.r + f.write(str(ad)+", ") + f.write(str(sr)+", ") + f.write(str(j.duty&0xff)+", ") + f.write(str(j.duty>>8)+", ") + flags = 0 + if j.duty_is_abs: + flags |= 1 + if hasWaveMacro: + flags |= 2 + if j.to_filter: + flags |= 4 + if j.init_filter: + flags |= 8 + if j.filter_is_abs: + flags |= 16 + if hasCutMacro: + flags |= 32 + if not j.no_test: + flags |= 64 + f.write(str(flags)+", ") + fil = 0 + fil = j.lp<<4 + fil |= j.bp<<5 + fil |= j.hp<<6 + fil |= j.ch3_off<<7 + f.write(str(j.res<<4)+", ") + f.write(str(fil)+", ") + f.write(str(j.cut&0xff)+", ") + f.write(str((j.cut>>8)&15)+"\n") + f.write("\n") + written_ins = True + hasAbsDuty |= j.duty_is_abs + hasAbsFilter = j.filter_is_abs + if written_ins == False: + f.write("ins"+str(i)+"F:\n") + f.write(".byte 32, 8, 0, 255, 7, 0\n") + a = filter( + lambda x: ( + type(x) == InsFeatureMacro + ), features + ) + arp = [128,0xFF,0xFF] + duty = [0xFF,0xFF] + wave = [0xFF,0xFF] + cutoff = [0xFF,0xFF] + macros = [] + for j in a: + macros = j.macros + hasRelTotal = [0,0,0,0] + for j in macros: + kind = j.kind + if kind == MacroCode.ARP: + s = j.speed + arp = [] + loop = 0xff + hasRel = 0 + oldlen = 0 + if j.data[-1] == MacroItem.LOOP: + arr = [MacroItem.LOOP, j.data[-2]] + j.data = j.data[:-2] + arr + for k in j.data: + if k == MacroItem.LOOP: + loop = oldlen + elif k == MacroItem.RELEASE: + arp.append(0xFF) + arp.append(loop) + relA.append(len(arp)) + oldlen = max(len(arp),0) + hasRel = 1 + elif (k>>30) > 0: + arp.append(0xFE) + k = abs(k^(1<<30)) + k = max(min(k,95),0) + arp.append(k%120) + oldlen = max(len(arp),0) + else: + if k < 0: + arp.append((k%120)-120+128) + else: + arp.append((k%120)+128) + oldlen = max(len(arp),0) + if hasRel == 0: + relA.append(len(arp)) + hasRelTotal[1] = 1 + len_temp = len(arp) + arp.append(0xFF) + arp.append(loop if loop!=len_temp else 0xff) + if kind == MacroCode.DUTY and j.type == MacroType.LFO: + while type(j.data[0]) is not int: + j.data = j.data[1:] + + s = j.speed + duty = [] + hasRel = 0 + lfo = j.data[13] + while lfo <= 1023: + lfo += j.data[11] + if lfo > 1023: break + k = lfo + if k & 512: + k = 1023-lfo + k >>= 1 + k = j.data[0]+((k+(j.data[1]-j.data[0])*k)>>8) + if (k&0xff) == 255: + duty.append(254) + else: + duty.append(k&0xff) + duty.append(k>>8) + if hasRel == 0: + relD.append(0) + hasRelTotal[2] = 1 + duty.append(0xFF) + duty.append(0) + elif kind == MacroCode.DUTY: + s = j.speed + duty = [] + loop = 0xff + loop2 = 0 + hasRel = 0 + for k in j.data: + if k == MacroItem.LOOP: + loop = loop2 + elif k == MacroItem.RELEASE: + duty.append(0xFF) + duty.append(loop) + relD.append(len(duty)) + hasRel = 1 + else: + loop2 = len(duty)+2 + if hasAbsDuty: + if (k&0xff) == 255: + duty.append(254) + else: + duty.append(k&0xff) + duty.append(k>>8) + else: + k = 32768-(k*4) #4) + duty.append(k&0xff) + duty.append(k>>8) + if hasRel == 0: + relD.append(len(duty)) + hasRelTotal[2] = 1 + len_temp = len(duty) + duty.append(0xFF) + duty.append(loop if loop!=len_temp else 0xff) + if kind == MacroCode.ALG and j.type == MacroType.LFO: + while type(j.data[0]) is not int: + j.data = j.data[1:] + + s = j.speed + cutoff = [] + hasRel = 0 + lfo = j.data[13] + while lfo <= 1023: + lfo += j.data[11] + k = lfo + if k & 512: + k = 1023-lfo + k >>= 1 + k = j.data[0]+((k+(j.data[1]-j.data[0])*k)>>8) + if (k&0xff) == 255: + cutoff.append(254) + else: + cutoff.append(k&0xff) + cutoff.append(k>>8) + if hasRel == 0: + relC.append(len(cutoff)) + hasRelTotal[3] = 1 + cutoff.append(0xFF) + cutoff.append(0) + elif kind == MacroCode.ALG: + s = j.speed + cutoff = [] + loop = 0xff + loop2 = 0 + hasRel = 0 + for k in j.data: + if k == MacroItem.LOOP: + loop = loop2 + elif k == MacroItem.RELEASE: + cutoff.append(0xFF) + cutoff.append(loop) + relC.append(len(cutoff)) + hasRel = 1 + else: + loop2 = len(cutoff)+1 + if hasAbsFilter: + if (k&0xff) == 255: + cutoff.append(254) + else: + cutoff.append(k&0xff) + cutoff.append(k>>8) + else: + k = 32768+k*7 + cutoff.append(k&0xff) + cutoff.append(k>>8) + if hasRel == 0: + relC.append(len(cutoff)) + hasRelTotal[3] = 1 + len_temp = len(cutoff) + cutoff.append(0xFF) + cutoff.append(loop if loop!=len_temp else 0xff) + if kind == MacroCode.WAVE: + s = j.speed + wave = [] + loop = 0xff + loop2 = 0 + hasRel = 0 + for k in j.data: + if k == MacroItem.LOOP: + loop = len(wave) + elif k == MacroItem.RELEASE: + wave.append(0xFF) + wave.append(loop) + relW.append(len(wave)) + hasRel = 1 + else: + wave.append(k<<4) + if hasRel == 0: + relW.append(len(wave)) + hasRelTotal[0] = 1 + len_temp = len(wave) + wave.append(0xFF) + wave.append(loop if loop!=len_temp else 0xff) + if hasRelTotal[0] == 0: + relW.append(0) + if hasRelTotal[1] == 0: + relA.append(0) + if hasRelTotal[2] == 0: + relD.append(0) + if hasRelTotal[3] == 0: + relC.append(0) + wave = str(wave)[1:-1] + duty = str(duty)[1:-1] + arp = str(arp)[1:-1] + cutoff = str(cutoff)[1:-1] + if arp in dups: + f.write("ins"+str(i)+"A = "+dups[arp]+"\n") + else: + f.write("ins"+str(i)+"A:\n") + f.write(".byte "+arp+"\n") + dups[arp] = "ins"+str(i)+"A" + + if duty in dups: + f.write("ins"+str(i)+"D = "+dups[duty]+"\n") + else: + f.write("ins"+str(i)+"D:\n") + f.write(".byte "+duty+"\n") + dups[duty] = "ins"+str(i)+"D" + + if wave in dups: + f.write("ins"+str(i)+"W = "+dups[wave]+"\n") + else: + f.write("ins"+str(i)+"W:\n") + f.write(".byte "+wave+"\n") + dups[wave] = "ins"+str(i)+"W" + + if cutoff in dups: + f.write("ins"+str(i)+"C = "+dups[cutoff]+"\n") + else: + f.write("ins"+str(i)+"C:\n") + f.write(".byte "+cutoff+"\n") + dups[cutoff] = "ins"+str(i)+"C" + +relW = str(relW)[1:-1] +relD = str(relD)[1:-1] +relA = str(relA)[1:-1] +f.write("insArel:\n") +f.write(".byte "+relA+"\n") +f.write("insDrel:\n") +f.write(".byte "+relD+"\n") +f.write("insWrel:\n") +f.write(".byte "+relW+"\n") + +for i in range(chnum): + order = module.subsongs[subsong].order[i] + f.write("order"+str(i)+"len = "+str(len(order))+"\n") + f.write("order"+str(i)+"L:\n") + f.write(".byte ") + for o in range(len(order)): + f.write("<(patCH"+str(i)+"N"+str(order[o])+"-1)") + if o == len(order)-1: + f.write("\n") + else: + f.write(", ") + f.write("order"+str(i)+"H:\n") + f.write(".byte ") + for o in range(len(order)): + f.write(">(patCH"+str(i)+"N"+str(order[o])+"-1)") + if o == len(order)-1: + f.write("\n") + else: + f.write(", ") + +for i in range(chnum): + order = module.subsongs[subsong].order[i] + avail_patterns = filter( + lambda x: ( + x.channel == i and + x.subsong == subsong + ), + module.patterns + ) + for p in avail_patterns: + patnum = p.index + #print(patnum,i) + g = str(comp(conv_pattern(p)))[1:-1] + f.write("patCH"+str(i)+"N"+str(patnum)+":\n") + f.write(".byte "+g+"\n") + +if chnum == 4: + f.write("sampleHS:\n.hibytes ") + for i in range(len(module.samples)): + f.write("PCM"+str(i)) + if i == (len(module.samples)-1): + f.write("\n") + else: + f.write(", ") + + f.write("sampleHE:\n.hibytes ") + for i in range(len(module.samples)): + f.write("PCMe"+str(i)) + if i == (len(module.samples)-1): + f.write("\n") + else: + f.write(", ") + + total_maps = [] + f.write("insSI:\n.byte ") + for i in range(len(module.instruments)): + features = module.instruments[i].features + a = filter( + lambda x: ( + type(x) == InsFeatureAmiga + ), features + ) + init_sample = 0 + for j in a: + if j.init_sample > 0 and j.init_sample != 65535: + init_sample = j.init_sample + break + f.write(str(init_sample)) + if i == (len(module.instruments)-1): + f.write("\n") + else: + f.write(", ") + + f.write(".res 256-(*&$ff), 0\n") + for i in range(len(module.samples)): + sample = [] + rate = max(min(module.samples[i].meta.sample_rate,384000),100) + smp = list(module.samples[i].data) + k = 0 + while k < len(smp): + j = smp[int(k)] + s = float(((int(j)+128)&0xff)) + s = ((s-128)/1.65)+128 + #s = ((s-128)/1.6)+128 + s = int(s/16) + #s = (s>>1)+8 + sample.append(s) + k += rate/7812 + s = sample[-1] + if (len(sample)%256) == 0: + sample.extend([s]*256) + else: + while (len(sample)%256) != 0: + sample.append(s) + f.write("PCM"+str(i)+":\n.byte "+str(sample)[1:-1]+"\n") + f.write("PCMe"+str(i)+":\n") + +f.close() + +# frequency calculation code taken from +# https://codebase64.org/doku.php?id=base:how_to_calculate_your_own_sid_frequency_table + +tuning = module.meta.tuning +f = open("asm/note_lo.bin","wb") +for i in range(96): + hz = tuning * (2**(float(i-57)/12.0)) + cnst = (256**3)/985248.0 # PAL frequency + freq = min(max(hz*cnst,0),0xffff) + f.write(bytearray([int(freq)&0xff])) +f.close() +f = open("asm/note_hi.bin","wb") +for i in range(96): + hz = tuning * (2**(float(i-57)/12.0)) + cnst = (256**3)/985248.0 # PAL frequency + freq = min(max(hz*cnst,0),0xffff) + f.write(bytearray([(int(freq)>>8)&0xff])) +f.close() diff --git a/loader/samples/minexample/guy_amb.fur b/loader/samples/minexample/guy_amb.fur new file mode 100644 index 0000000..2368704 Binary files /dev/null and b/loader/samples/minexample/guy_amb.fur differ diff --git a/loader/samples/minexample/guy_appear.fur b/loader/samples/minexample/guy_appear.fur new file mode 100644 index 0000000..60201fe Binary files /dev/null and b/loader/samples/minexample/guy_appear.fur differ diff --git a/loader/samples/minexample/minexample.s b/loader/samples/minexample/minexample.s index 301e9d3..f87c15d 100644 --- a/loader/samples/minexample/minexample.s +++ b/loader/samples/minexample/minexample.s @@ -165,9 +165,10 @@ code_start: lda #$10 jsr load_8000_zx02 - ldx #sidname - jsr loadraw + ldx #song0name + lda #$a0 + jsr load_8000_zx02 ldx #towername @@ -1246,7 +1247,7 @@ irq_badguy: badguy: .byte "badguy",0 fontname: .byte "font",0 -sidname: .byte "sid", 0 +song0name: .byte "song0", 0 introname: .byte "intrbmp", 0 towername: .byte "tower", 0 towerbeamname: .byte "towerbm", 0 diff --git a/loader/samples/minexample/use_this_sid.bin b/loader/samples/minexample/use_this_sid.bin index 7a9ce8f..07c0641 100644 Binary files a/loader/samples/minexample/use_this_sid.bin and b/loader/samples/minexample/use_this_sid.bin differ diff --git a/loader/samples/minexample/ys2_new.bin b/loader/samples/minexample/ys2_new.bin deleted file mode 100644 index 836c632..0000000 Binary files a/loader/samples/minexample/ys2_new.bin and /dev/null differ diff --git a/loader/samples/minexample/ys2_port_legato.fur b/loader/samples/minexample/ys2_port_legato.fur new file mode 100644 index 0000000..6a638f9 Binary files /dev/null and b/loader/samples/minexample/ys2_port_legato.fur differ diff --git a/loader/samples/minexample/ys2_sid.sid b/loader/samples/minexample/ys2_sid.sid deleted file mode 100644 index ef7c8a6..0000000 Binary files a/loader/samples/minexample/ys2_sid.sid and /dev/null differ diff --git a/loader/samples/minexample/ys2sid.bin b/loader/samples/minexample/ys2sid.bin deleted file mode 100644 index 372de98..0000000 Binary files a/loader/samples/minexample/ys2sid.bin and /dev/null differ