├── .gitignore ├── CHANGES.txt ├── LICENSE ├── README.md ├── audio ├── decentsnare.wav ├── kickgen.wav └── selnow.wav ├── docs ├── init.txt └── repocard.xcf ├── lorom256k.cfg ├── makefile ├── obj └── snes │ └── index.txt ├── spc.cfg ├── src ├── bg.s ├── blarggapu.s ├── global.inc ├── init.s ├── main.s ├── musicseq.s ├── pentlyseq.inc ├── player.s ├── ppuclear.s ├── snes.inc ├── snesheader.s ├── spc-65c02.inc ├── spc-ca65.inc ├── spcheader.s └── spcimage.s ├── tilesets ├── bggfx.png └── swinging2.png └── tools ├── fixchecksum.py ├── getpalette.py ├── karplus.py ├── makehat.py ├── pilbmp2nes.py ├── wav2brr.py └── zipup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw[a-p] 2 | *.o 3 | *.brr 4 | *~ 5 | *.chrgb 6 | *.chrsfc 7 | /obj/snes/*.s 8 | /obj/snes/*.wav 9 | /zip.in 10 | /map.txt 11 | /spcmap.txt 12 | /lorom-template.sfc 13 | /lorom-template.spc 14 | /lorom-template.dbg 15 | /lorom-template-*.zip 16 | __pycache__/ 17 | *.pyc 18 | 19 | # this is Super NES homebrew, not DSiWare 20 | *.DS_Store 21 | 22 | 23 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | 0.06 (2016-11-17) 2 | * wav2brr: option to produce a looped BRR 3 | * wav2brr: option to skip pre- and de-emphasis filtering 4 | * spc-ca65.inc: instructions that use DEFAULT_ABS work in 5 | .proc blocks 6 | * Added Karplus-Strong synthesizer to tools 7 | * Sets DSP registers based on note number and instrument number, 8 | which could form the basis for a music engine 9 | * Converted all Python tools to Python 3 10 | * Extra explanation for OBSEL, nmi_handler, pseudo hi-res, 11 | and interlace (requested by nicklausw) 12 | * snes.inc has doc comments for seta/setxy (requested by Pokun) 13 | * Experimental checksum correction tool, not enabled by default 14 | * Now on GitHub 15 | 16 | 0.05 (2015-02-13): The "Ho-ly-$#!+" Code Review 17 | * fixed branch bug in collision introduced during translation 18 | from 6502 to "idiomatic" 65816 (reported by thefox) 19 | * added overall map of source code to README.md 20 | (requested by Espozo) 21 | * comment readability improvements 22 | * added doc comments before more functions (requested by Espozo) 23 | * more consistent use of seta/setxy macros 24 | to change processor word size 25 | * split code related to the background and the player 26 | into separate files (requested by Espozo) 27 | * compensated for missing background line 0 28 | * explained unfamiliar register names 29 | * renamed .h files to .inc so as not to confuse with C headers 30 | (requested by koitsu) 31 | * rewrote init code based on koitsu's InitializeSNES macro 32 | * indented forward branches (requested by Espozo) 33 | * moved ROM segments to $808000 to allow use of fast ROM 34 | (requested by koitsu) 35 | * imported the direct page base address as a symbol 36 | (requested by koitsu) 37 | * broke snes.inc into functional groups 38 | * added register name aliases from Martin Korth's fullsnes.htm 39 | (requested by koitsu) 40 | 41 | 0.04a (2015-01-28) 42 | * corrected missing lorom256k.cfg file 43 | 44 | 0.04 (2014-11-09): 45 | * expanded to 256 KiB to demonstrate far operation 46 | * moved parts into separate banks to show use of multiple banks 47 | * snesheader: added symbolic names for mappers, regions, and 48 | memory sizes and speeds 49 | * wav2brr.py: added proper command line options including 50 | decompression of BRR back to WAV 51 | * spcimage.s: fixed stya macro (reported by doppel) 52 | 53 | 0.03 (2014-10-03): 54 | * wav2brr.py: made NumPy dependency optional 55 | * added other explanatory comments to main.s 56 | * Corrected some errors related to BSS in linker config file 57 | * snes.h: macros for controlling accumulator and index width 58 | (requested by Bisqwit) 59 | * snes.h: macro for nametable X, Y location (requested by Bisqwit) 60 | * New template for SPC files 61 | 62 | 0.02 (2014-09-24): 63 | * converted README to Markdown 64 | * added instructions for installing build environment to README 65 | (requested by whicker) 66 | * corrected a few factual errors in README related to differences 67 | between the NES and the Super NES 68 | * added a wav file and wav to BRR converter 69 | * removed Encyclopedia Dramatica "goes where" meme from index.txt 70 | 71 | 0.01 (2014-09-21): 72 | * initial release 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The zlib License 2 | ================ 3 | 4 | Copyright (c) 2017 Damian Yerrick 5 | 6 | This software is provided 'as-is', without any express or implied warranty. In 7 | no event will the authors be held liable for any damages arising from the use of 8 | this software. 9 | 10 | Permission is granted to anyone to use this software for any purpose, including 11 | commercial applications, and to alter it and redistribute it freely, subject to 12 | the following restrictions: 13 | 14 | 1. The origin of this software must not be misrepresented; you must not claim 15 | that you wrote the original software. If you use this software in a product, 16 | an acknowledgment in the product documentation would be appreciated but is 17 | not required. 18 | 19 | 2. Altered source versions must be plainly marked as such, and must not be 20 | misrepresented as being the original software. 21 | 22 | 3. This notice may not be removed or altered from any source distribution. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | IF THIS FILE HAS NO LINE BREAKS: View it in a web browser. 2 | (Some text editors do not understand UNIX-style line breaks.) 3 | 4 | LoROM template 5 | ============== 6 | 7 | This is a minimal working program for the Super Nintendo 8 | Entertainment System using the LoROM (mode $20) mapper. 9 | 10 | Concepts illustrated: 11 | 12 | * internal header (with correct checksum) and init code 13 | * setting up a static background 14 | * loading data from multiple 32 KiB banks in a LoROM 15 | * structure of a game loop 16 | * automatic controller reading 17 | * 8.8 fixed-point arithmetic 18 | * acceleration-based character movement physics 19 | * sprite drawing and animation, with horizontal flipping 20 | * makefile-controlled conversion of graphics to tile data in 21 | both 2-bit-per-pixel and 4-bit-per-pixel formats 22 | * booting the S-SMP (SPC700 audio CPU) 23 | * writing SPC700 code using 65C02 syntax using blargg's 24 | [SPC700 macro pack] for ca65 25 | * use of SPC700 timers to control playback 26 | * reading a sequence of pitches and converting them to frequencies 27 | * makefile-controlled compression of sampled sound to BRR format 28 | * creating an SPC700 state file for SPC music players 29 | 30 | Concepts not illustrated: 31 | 32 | * a 512-byte header for obsolete floppy-based copiers 33 | * S-CPU/S-SMP communication to play sound effects or change songs 34 | 35 | [SPC700 macro pack]: http://forums.nesdev.com/viewtopic.php?p=121690#p121690 36 | 37 | Setting up the build environment 38 | -------------------------------- 39 | Building this demo requires cc65, Python, Pillow, GNU Make, and GNU 40 | Coreutils. For detailed instructions to set up a build environment, 41 | see [nrom-template]. 42 | 43 | [nrom-template]: https://github.com/pinobatch/nrom-template 44 | 45 | Organization of the program 46 | --------------------------- 47 | 48 | ### Include files 49 | 50 | * `snes.inc`: Register definitions and useful macros for the S-CPU 51 | * `global.inc`: S-CPU global variable and function declarations 52 | * `spc-ca65.inc`: Macro library to produce SPC700 instructions 53 | * `spc-65c02.inc`: Macro library to use 65C02 syntax with the SPC700 54 | 55 | ### Source code files (65816) 56 | 57 | * `snesheader.s`: Super NES internal header 58 | * `init.s`: PPU and CPU I/O initialization code 59 | * `main.s`: Main program 60 | * `bg.s`: Background graphics setup 61 | * `player.s`: Player sprite graphics setup and movement 62 | * `ppuclear.s`: Useful subroutines for interacting with the S-PPU 63 | * `blarggapu.s`: Send a sound driver to the S-SMP 64 | 65 | ### Source code files (SPC700) 66 | 67 | * `spcheader.s`: Header for the `.spc` file; unused in `.sfc` 68 | * `spcimage.s`: Sound driver 69 | 70 | Each source code file is made up of subroutines that start with 71 | `.proc` and end with `.endproc`. See the [ca65 Users Guide] for 72 | what these mean. 73 | 74 | [ca65 Users Guide]: http://cc65.github.io/doc/ca65.html 75 | 76 | The tools 77 | --------- 78 | The `tools` folder contains a few essential command-line programs 79 | written in Python to convert asset data (graphics, audio, etc.) into 80 | a form usable by the Super NES. The makefile contains instructions 81 | to run these programs whenever the original asset data changes. 82 | 83 | * `pilbmp2nes.py` converts bitmap images in PNG or BMP format 84 | into tile data usable by several classic video game consoles. 85 | It has several options to control the data format; use 86 | `pilbmp2nes.py --help` from the command prompt to see them all. 87 | * `wav2brr.py` converts an uncompressed wave file to the BRR (bit 88 | rate reduction) format, a lossy audio codec based on ADPCM used 89 | by the S-DSP (the audio chip in the Super NES). 90 | * `karplus.py` generates a plucked string sound, used for the 91 | bass sample. 92 | * `hihat.py` generates a noise sample. 93 | 94 | Greets 95 | ------ 96 | 97 | * [Super Nintendo Development Wiki] contributors 98 | * Martin Korth (nocash) for [Fullsnes] doc and [NO$SNS] emulator 99 | * Shay Green (blargg) for APU examples and SPC700 macro pack 100 | * Jeremy Chadwick (koitsu) for more code organization tips 101 | 102 | [Super Nintendo Development Wiki]: http://wiki.superfamicom.org/ 103 | [Fullsnes]: http://problemkaputt.de/fullsnes.htm 104 | [NO$SNS]: http://problemkaputt.de/sns.htm 105 | 106 | Legal 107 | ----- 108 | The demo is distributed under the zlib License, reproduced below: 109 | 110 | > Copyright 2017 Damian Yerrick 111 | > 112 | > This software is provided 'as-is', without any express or implied 113 | > warranty. In no event will the authors be held liable for any 114 | > damages arising from the use of this software. 115 | > 116 | > Permission is granted to anyone to use this software for any 117 | > purpose, including commercial applications, and to alter it and 118 | > redistribute it freely, subject to the following restrictions: 119 | > 120 | > 1. The origin of this software must not be misrepresented; you must 121 | > not claim that you wrote the original software. If you use this 122 | > software in a product, an acknowledgment in the product 123 | > documentation would be appreciated but is not required. 124 | > 125 | > 2. Altered source versions must be plainly marked as such, and must 126 | > not be misrepresented as being the original software. 127 | > 128 | > 3. This notice may not be removed or altered from any source 129 | > distribution. 130 | 131 | A work's "source" form is the preferred form of a work for making 132 | modifications to it. -------------------------------------------------------------------------------- /audio/decentsnare.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinobatch/lorom-template/04ca25f1e09c96b6d360922af4c7ba1dd8bdda60/audio/decentsnare.wav -------------------------------------------------------------------------------- /audio/kickgen.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinobatch/lorom-template/04ca25f1e09c96b6d360922af4c7ba1dd8bdda60/audio/kickgen.wav -------------------------------------------------------------------------------- /audio/selnow.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinobatch/lorom-template/04ca25f1e09c96b6d360922af4c7ba1dd8bdda60/audio/selnow.wav -------------------------------------------------------------------------------- /docs/init.txt: -------------------------------------------------------------------------------- 1 | A valid Super NES program must write to all writable ports in the 2 | S-CPU I/O and S-PPU at the start of a program. This way, the 3 | machine starts in a known state. The code in init.s establishes 4 | the following initial state: 5 | 6 | S-CPU registers 7 | 8 | A, X, Y: unspecified 9 | Program bank: main >> 16 10 | Data bank: unspecified (programs might want to PHK PLB) 11 | P: decimal mode off, width 16-bit 12 | S: $01FF 13 | D: $0000 14 | 15 | S-CPU I/O 16 | 17 | 4200=00 Disable vblank NMI and htime/vtime IRQ 18 | 4201=FF Set pin 6 of controller ports high 19 | 4202=00 Multiply 0 by 0 20 | 4203=00 21 | 4204=00 Divide 0 by 0 (OH SHI--) 22 | 4205=00 23 | 4206=00 24 | 4207=00 htime = top of picture 25 | 4208=00 26 | 4209=00 vtime = left side of scanline 27 | 420A=00 28 | 420B=00 Stop DMA copies 29 | 420C=00 Stop HDMA 30 | 420D=00 or 01 Access $808000-$FFFFFF as slow or fast ROM 31 | (depending on value in internal header) 32 | 33 | S-PPU 34 | 35 | 2100=80 Forced blanking 36 | 2101=00 Sprites 8x8 and 16x16, patterns at $0000-$1FFF 37 | 2102=00 OAM address: $0000 38 | 2103=00 39 | 2104: skip OAM write port 40 | 2105=00 Background mode 0, all layers using 8x8 pixel tiles 41 | 2106=00 Mosaic off 42 | 2107=00 BG1 nametable at $0000, 1x1 screen 43 | 2108=00 BG2 nametable at $0000, 1x1 screen 44 | 2109=00 BG3 nametable at $0000, 1x1 screen 45 | 210A=00 BG4 nametable at $0000, 1x1 screen 46 | 210B=00 BG1 and BG2 tiles at $0000 47 | 210C=00 BG3 and BG4 tiles at $0000 48 | 210D=00 00 BG1 scroll at (0, 1). The S-PPU skips the first line 49 | 210E=00 00 of the picture, so Y=0 means start at line 1 of BG. 50 | 210F=00 00 BG2 scroll at (0, 1) 51 | 2110=00 00 52 | 2111=00 00 BG3 scroll at (0, 1) 53 | 2112=00 00 54 | 2113=00 00 BG4 scroll at (0, 1) 55 | 2114=00 00 56 | 2115=80 Add 1 word to VRAM address after high byte write 57 | 2116=00 VRAM address starts at 0 58 | 2117=00 59 | 2118-9: skip VRAM write port 60 | 211A=00 61 | 211B=00 01 Set the mode 7 matrix to the identity matrix 62 | 211C=00 00 [ 1.0 0.0 ] 63 | 211D=00 00 [ 0.0 1.0 ] 64 | 211E=00 01 65 | 211F=00 00 Mode 7 scroll at (0, 0) 66 | 2120=00 00 67 | 2121=00 CGRAM address = 0 68 | 2122: skip CGRAM write port 69 | 2123=00 Disable windows on BG1 and BG2 70 | 2124=00 Disable windows on BG3 and BG4 71 | 2125=00 Disable windows on sprites and blending 72 | 2126=00 Window 1 left side = 0 73 | 2127=00 Window 1 right side = 0 74 | 2128=00 Window 2 left side = 0 75 | 2129=00 Window 2 right side = 0 76 | 212A=00 Combine background windows using OR logic 77 | 212B=00 Combine sprites and blending using OR logic 78 | 212C=00 Enable no layers on main screen 79 | 212D=00 Enable no layers on sub screen 80 | 212E=00 Disable no layers on main screen within the window 81 | 212F=00 Disable no layers on sub screen within the window 82 | 2130=30 Disable blending and 332 palette 83 | 2131=00 Disable blending for all layers 84 | 2132=E0 Set entire COLDATA to 0 85 | 2133=00 Disable interlace and pseudo-hires, 224 lines 86 | 87 | -------------------------------------------------------------------------------- /docs/repocard.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinobatch/lorom-template/04ca25f1e09c96b6d360922af4c7ba1dd8bdda60/docs/repocard.xcf -------------------------------------------------------------------------------- /lorom256k.cfg: -------------------------------------------------------------------------------- 1 | # ca65 linker config for 256 KiB (2 Mbit) sfc file 2 | 3 | # Physical areas of memory 4 | MEMORY { 5 | # I usually reserve $000000-$00000F for local variables 6 | # allocated just below the .proc statement of a subroutine. 7 | # The rest is open for global variables. 8 | ZEROPAGE: start = $000010, size = $00F0; 9 | 10 | # Make sure to change BSS based on where you put 11 | # the stack and how big you expect it to get. 12 | # Unlike on the NES, we include shadow OAM in BSS here 13 | # because there's not as much of a benefit to having it 14 | # page-aligned. 15 | BSS: start = $000200, size = $1E00; 16 | BSS7E: start = $7E2000, size = $E000; 17 | BSS7F: start = $7F0000, size =$10000; 18 | 19 | # The fast ROM area starts at $808000. 20 | # It's mirrored into the slow ROM area. 21 | ROM0: start = $808000, size = $8000, type = ro, file = %O, fill=yes, fillval=$FF; 22 | ROM1: start = $818000, size = $8000, type = ro, file = %O, fill=yes, fillval=$FF; 23 | ROM2: start = $828000, size = $8000, type = ro, file = %O, fill=yes, fillval=$FF; 24 | ROM3: start = $838000, size = $8000, type = ro, file = %O, fill=yes, fillval=$FF; 25 | ROM4: start = $848000, size = $8000, type = ro, file = %O, fill=yes, fillval=$FF; 26 | ROM5: start = $858000, size = $8000, type = ro, file = %O, fill=yes, fillval=$FF; 27 | ROM6: start = $868000, size = $8000, type = ro, file = %O, fill=yes, fillval=$FF; 28 | ROM7: start = $878000, size = $8000, type = ro, file = %O, fill=yes, fillval=$FF; 29 | 30 | # The sound processor has its own address space 31 | SPCZEROPAGE:start = $0010, size = $00E0; 32 | SPCRAM: start = $0200, size = $FDC0; 33 | } 34 | 35 | # Logical areas code/data can be put into. 36 | SEGMENTS { 37 | # Read-only areas for main CPU 38 | CODE: load = ROM0, type = ro, align = $100; 39 | RODATA: load = ROM0, type = ro, align = $100; 40 | SNESHEADER: load = ROM0, type = ro, start = $80FFB0; 41 | CODE1: load = ROM1, type = ro, align = $100, optional=yes; 42 | RODATA1: load = ROM1, type = ro, align = $100, optional=yes; 43 | CODE2: load = ROM2, type = ro, align = $100, optional=yes; 44 | RODATA2: load = ROM2, type = ro, align = $100, optional=yes; 45 | CODE3: load = ROM3, type = ro, align = $100, optional=yes; 46 | RODATA3: load = ROM3, type = ro, align = $100, optional=yes; 47 | CODE4: load = ROM4, type = ro, align = $100, optional=yes; 48 | RODATA4: load = ROM4, type = ro, align = $100, optional=yes; 49 | CODE5: load = ROM5, type = ro, align = $100, optional=yes; 50 | RODATA5: load = ROM5, type = ro, align = $100, optional=yes; 51 | CODE6: load = ROM6, type = ro, align = $100, optional=yes; 52 | RODATA6: load = ROM6, type = ro, align = $100, optional=yes; 53 | CODE7: load = ROM7, type = ro, align = $100, optional=yes; 54 | RODATA7: load = ROM7, type = ro, align = $100, optional=yes; 55 | 56 | # Read-only areas for sound CPU 57 | SPCIMAGE: load = ROM7, run=SPCRAM, align = $100, define=yes; 58 | 59 | # Areas for variables for main CPU 60 | ZEROPAGE: load = ZEROPAGE, type = zp, define=yes; 61 | BSS: load = BSS, type = bss, align = $100, optional=yes; 62 | BSS7E: load = BSS7E, type = bss, align = $100, optional=yes; 63 | BSS7F: load = BSS7F, type = bss, align = $100, optional=yes; 64 | 65 | # Areas for variables for sound CPU 66 | SPCZEROPAGE:load = SPCZEROPAGE, type=zp, optional=yes; 67 | SPCBSS: load = SPCRAM, type = bss, align = $100, optional=yes; 68 | 69 | } 70 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # 3 | # Makefile for LoROM template 4 | # Copyright 2014-2015 Damian Yerrick 5 | # 6 | # Copying and distribution of this file, with or without 7 | # modification, are permitted in any medium without royalty 8 | # provided the copyright notice and this notice are preserved. 9 | # This file is offered as-is, without any warranty. 10 | # 11 | 12 | # These are used in the title of the SFC program and the zip file. 13 | title = lorom-template 14 | version = 0.06 15 | 16 | # Space-separated list of asm files without .s extension 17 | # (use a backslash to continue on the next line) 18 | objlist = \ 19 | snesheader init main bg player \ 20 | ppuclear blarggapu spcimage musicseq 21 | objlistspc = \ 22 | spcheader spcimage musicseq 23 | brrlist = \ 24 | karplusbassloop hat kickgen decentsnare 25 | 26 | AS65 := ca65 27 | LD65 := ld65 28 | CFLAGS65 := -g 29 | objdir := obj/snes 30 | srcdir := src 31 | imgdir := tilesets 32 | 33 | # If it's not bsnes, it's just BS. But I acknowledge that being 34 | # stuck on an old Atom laptop is BS. Atom N450 can't run bsnes at 35 | # full speed, but the Atom-based Pentium N3710 can. 36 | ifndef SNESEMU 37 | #SNESEMU := xterm -e zsnes -d 38 | SNESEMU := bsnes 39 | endif 40 | 41 | # game-music-emu by blargg et al. 42 | # Using paplay-based wrapper from 43 | # https://forums.nesdev.com/viewtopic.php?f=6&t=16218 44 | SPCPLAY := gmeplay 45 | 46 | ifdef COMSPEC 47 | PY := py.exe 48 | else 49 | PY := 50 | endif 51 | 52 | # Calculate the current directory as Wine applications would see it. 53 | # yep, that's 8 backslashes. Apparently, there are 3 layers of escaping: 54 | # one for the shell that executes sed, one for sed, and one for the shell 55 | # that executes wine 56 | # TODO: convert to use winepath -w 57 | wincwd := $(shell pwd | sed -e "s'/'\\\\\\\\'g") 58 | 59 | # .PHONY means these targets aren't actual filenames 60 | .PHONY: all run nocash-run spcrun dist clean 61 | 62 | # When you type make without a target name, make will try 63 | # to build the first target. So unless you're trying to run 64 | # NO$SNS in Wine, you should move run above nocash-run. 65 | run: $(title).sfc 66 | $(SNESEMU) $< 67 | 68 | # Per Martin Korth on 2014-09-16: NO$SNS requires absolute 69 | # paths because he screwed up and made the filename processing 70 | # too clever. 71 | # Not default 72 | nocash-run: $(title).sfc 73 | wine "C:\\Program Files (x86)\\nocash\\no\$$sns.exe" "Z:$(wincwd)\\$(title).sfc" 74 | 75 | # Special target for just the SPC700 image 76 | spcrun: $(title).spc 77 | $(SPCPLAY) $< 78 | 79 | all: $(title).sfc $(title).spc 80 | 81 | clean: 82 | -rm $(objdir)/*.o $(objdir)/*.chrsfc $(objdir)/*.chrgb 83 | -rm $(objdir)/*.wav $(objdir)/*.brr $(objdir)/*.s 84 | 85 | dist: zip 86 | zip: $(title)-$(version).zip 87 | $(title)-$(version).zip: zip.in all README.md $(objdir)/index.txt 88 | $(PY) tools/zipup.py $< $(title)-$(version) -o $@ 89 | -advzip -z3 $@ 90 | 91 | # Build zip.in from the list of files in the Git tree 92 | zip.in: 93 | git ls-files | grep -e "^[^.]" > $@ 94 | echo zip.in >> $@ 95 | echo $(title).sfc >> $@ 96 | echo $(title).spc >> $@ 97 | 98 | $(objdir)/index.txt: makefile 99 | echo "Files produced by build tools go here. (This file's existence forces the unzip tool to create this folder.)" > $@ 100 | 101 | # Rules for ROM 102 | 103 | objlisto = $(foreach o,$(objlist),$(objdir)/$(o).o) 104 | objlistospc = $(foreach o,$(objlistspc),$(objdir)/$(o).o) 105 | brrlisto = $(foreach o,$(brrlist),$(objdir)/$(o).brr) 106 | 107 | map.txt $(title).sfc: lorom256k.cfg $(objlisto) 108 | $(LD65) -o $(title).sfc --dbgfile $(title).dbg -m map.txt -C $^ 109 | $(PY) tools/fixchecksum.py $(title).sfc 110 | 111 | spcmap.txt $(title).spc: spc.cfg $(objlistospc) 112 | $(LD65) -o $(title).spc -m spcmap.txt -C $^ 113 | 114 | $(objdir)/%.o: $(srcdir)/%.s $(srcdir)/snes.inc $(srcdir)/global.inc 115 | $(AS65) $(CFLAGS65) $< -o $@ 116 | 117 | $(objdir)/%.o: $(objdir)/%.s 118 | $(AS65) $(CFLAGS65) $< -o $@ 119 | 120 | $(objdir)/mktables.s: tools/mktables.py 121 | $< > $@ 122 | 123 | # Files that depend on extra included files 124 | $(objdir)/bg.o: \ 125 | $(objdir)/bggfx.chrgb 126 | $(objdir)/player.o: \ 127 | $(objdir)/swinging2.chrsfc 128 | $(objdir)/spcimage.o: $(brrlisto) 129 | $(objdir)/musicseq.o $(objdir)/spcimage.o: src/pentlyseq.inc 130 | 131 | # Rules for CHR data 132 | 133 | # .chrgb (CHR data for Game Boy) denotes the 2-bit tile format 134 | # used by Game Boy and Game Boy Color, as well as Super NES 135 | # mode 0 (all planes), mode 1 (third plane), and modes 4 and 5 136 | # (second plane). 137 | $(objdir)/%.chrgb: tilesets/%.png 138 | $(PY) tools/pilbmp2nes.py --planes=0,1 $< $@ 139 | 140 | $(objdir)/%.chrsfc: tilesets/%.png 141 | $(PY) tools/pilbmp2nes.py "--planes=0,1;2,3" $< $@ 142 | 143 | # Rules for audio 144 | $(objdir)/%.brr: audio/%.wav 145 | $(PY) tools/wav2brr.py $< $@ 146 | $(objdir)/%.brr: $(objdir)/%.wav 147 | $(PY) tools/wav2brr.py $< $@ 148 | $(objdir)/%loop.brr: audio/%.wav 149 | $(PY) tools/wav2brr.py --loop $< $@ 150 | $(objdir)/%loop.brr: $(objdir)/%.wav 151 | $(PY) tools/wav2brr.py --loop $< $@ 152 | $(objdir)/karplusbass.wav: tools/karplus.py 153 | $(PY) $< -o $@ -n 1024 -p 64 -r 4186 -e square:1:4 -a 30000 -g 1.0 -f .5 154 | $(objdir)/hat.wav: tools/makehat.py 155 | $(PY) $< $@ 156 | -------------------------------------------------------------------------------- /obj/snes/index.txt: -------------------------------------------------------------------------------- 1 | Files produced by build tools go here. (This file's existence forces the unzip tool to create this folder.) 2 | -------------------------------------------------------------------------------- /spc.cfg: -------------------------------------------------------------------------------- 1 | # ca65 linker config for SPC file (Super NES music) 2 | 3 | # Physical areas of memory 4 | MEMORY { 5 | SPCZEROPAGE: start=$0010, size=$00E0; 6 | SPCHEADER: start=$0000, size=$0300, fill=yes, fillval=$00; 7 | SPCRAM: start=$0200, size=$FE00, fill=yes, fillval=$00; 8 | SPCFOOTER: start=$0000, size=$0100, fill=yes, fillval=$00; 9 | } 10 | 11 | # Logical areas code/data can be put into. 12 | SEGMENTS { 13 | SPCZEROPAGE: load = SPCZEROPAGE, type=zp; 14 | SPCHEADER: load = SPCHEADER; 15 | SPCIMAGE: load = SPCRAM, align = $100; 16 | SPCBSS: load = SPCRAM, type = bss, align = $100, optional=yes; 17 | SPCFOOTER: load = SPCFOOTER, align = $100; 18 | } 19 | -------------------------------------------------------------------------------- /src/bg.s: -------------------------------------------------------------------------------- 1 | .include "snes.inc" 2 | .include "global.inc" 3 | .smart 4 | 5 | .segment "CODE5" 6 | .proc load_bg_tiles 7 | phb ; Save old program bank and push new bank 8 | phk 9 | plb 10 | 11 | ; Copy background palette to the S-PPU. 12 | ; We perform the copy using DMA (direct memory access), which has 13 | ; four steps: 14 | ; 1. Set the destination address in the desired area of memory, 15 | ; be it CGRAM (palette), OAM (sprites), or VRAM (tile data and 16 | ; background maps). 17 | ; 2. Tell the DMA controller which area of memory to copy to. 18 | ; 3. Tell the DMA controller the starting address to copy from. 19 | ; 4. Tell the DMA controller how big the data is in bytes. 20 | ; ppu_copy uses the current data bank as the source bank 21 | ; for the copy, so set the source bank. 22 | seta8 23 | stz CGADDR ; Seek to the start of CGRAM 24 | setaxy16 25 | lda #DMAMODE_CGDATA 26 | ldx #palette & $FFFF 27 | ldy #palette_size 28 | jsl ppu_copy 29 | 30 | ; This demo uses background mode 0, in which each of four 2bpp 31 | ; layers gets its own palette, at word offsets $00-$1F, $20-$3F, 32 | ; $40-$5F, and $60-$7F in CGRAM. (The other modes with 2bpp 33 | ; backgrounds use only $00-$1F.) We just finished copying the 34 | ; main palette to $00; also copy it to $20 for the second layer 35 | ; in demo modes that use it. The palettes could be different, 36 | ; but in this demo they're the same. 37 | seta8 38 | lda #$20 39 | sta CGADDR 40 | setaxy16 41 | lda #DMAMODE_CGDATA 42 | ldx #layer1_palette & $FFFF 43 | ldy #layer1_palette_size 44 | jsl ppu_copy 45 | 46 | ; Copy background tiles to PPU. 47 | ; PPU memory is also word addressed because the low and high bytes 48 | ; are actually on separate SRAM chips. 49 | ; In background mode 0, all background tiles are 2 bits per pixel, 50 | ; which take 16 bytes or 8 words per tile. 51 | setaxy16 52 | stz PPUADDR ; we will start video memory at $0000 53 | lda #DMAMODE_PPUDATA 54 | ldy #chr_bin_size 55 | ldx #chr_bin & $FFFF 56 | jsl ppu_copy 57 | 58 | plb 59 | rtl 60 | .endproc 61 | 62 | .proc draw_bg 63 | ; This demo's background tile set includes glyphs at ASCII code 64 | ; points $20 (space) through $5F (underscore). Clear the map 65 | ; to all spaces. 66 | setaxy16 67 | ldx #$6000 68 | ldy #' ' | (1 << 10) 69 | jsl ppu_clear_nt 70 | 71 | ; The screen spans rows 0-27, of which rows 23-27 are the ground. 72 | ; Draw the top of the ground using a grass tile 73 | setaxy16 74 | lda #$6000|NTXY(0, 23) 75 | sta PPUADDR 76 | lda #$000B 77 | ldx #32 78 | floorloop1: 79 | sta PPUDATA 80 | dex 81 | bne floorloop1 82 | 83 | ; Draw areas buried under the floor as solid color 84 | lda #$0001 85 | ldx #4*32 86 | floorloop2: 87 | sta PPUDATA 88 | dex 89 | bne floorloop2 90 | 91 | ; Draw blocks on the sides, in vertical columns 92 | seta8 93 | lda #VRAM_DOWN|INC_DATAHI 94 | sta PPUCTRL 95 | setaxy16 96 | 97 | ; At position (2, 19) (VRAM $6262) and (28, 19) (VRAM $627C), 98 | ; draw two columns of two blocks each, each block being 4 tiles: 99 | ; 0C 0D 100 | ; 0E 0F 101 | ldx #2 102 | 103 | colloop: 104 | txa 105 | ora #$6000 | NTXY(0, 19) 106 | sta PPUADDR 107 | 108 | ; Draw $0C $0E $0C $0E or $0D $0F $0D $0F depending on column 109 | and #$0001 110 | ora #$040C ; palette 1 111 | ldy #4 112 | tileloop: 113 | sta PPUDATA 114 | eor #$02 115 | dey 116 | bne tileloop 117 | 118 | ; Columns 2, 3, 28, and 29 only 119 | inx 120 | cpx #4 ; Skip columns 4 through 27 121 | bne not4 122 | ldx #28 123 | not4: 124 | cpx #30 125 | bcc colloop 126 | 127 | ; The Super NES has no attribute table. Yay. 128 | 129 | seta8 130 | lda #INC_DATAHI ; Set the data direction back to how 131 | sta PPUCTRL ; the rest of the program expects it 132 | rtl 133 | .endproc 134 | 135 | .segment "RODATA5" 136 | chr_bin: 137 | .incbin "obj/snes/bggfx.chrgb" 138 | chr_bin_size = * - chr_bin 139 | 140 | ; The background palette 141 | palette: 142 | .word RGB(15,23,31),RGB(12,12, 0),RGB(14,23, 0),RGB(16,31, 0) 143 | .word RGB( 0, 0,15),RGB(15,15, 0),RGB(23,23, 8),RGB(31,31,16) 144 | palette_size = * - palette 145 | 146 | layer1_palette := palette 147 | layer1_palette_size = palette_size 148 | -------------------------------------------------------------------------------- /src/blarggapu.s: -------------------------------------------------------------------------------- 1 | ; High-level interface to SPC-700 bootloader 2 | ; Originally by "blargg" (Shay Green) 3 | ; https://wiki.superfamicom.org/how-to-write-to-dsp-registers-without-any-spc-700-code 4 | ; 5 | ; 1. Call spc_wait_boot 6 | ; 2. To upload data: 7 | ; A. Call spc_begin_upload 8 | ; B. Call spc_upload_byte any number of times 9 | ; C. Go back to A to upload to different addr 10 | ; 3. To begin execution, call spc_execute 11 | ; 12 | ; Have your SPC code jump to $FFC0 to re-run bootloader. 13 | ; Be sure to call spc_wait_boot after that. 14 | 15 | .include "snes.inc" 16 | .smart 17 | .i16 18 | 19 | .segment "CODE2" 20 | 21 | ; Waits for SPC to finish booting. Call before first 22 | ; using SPC or after bootrom has been re-run. 23 | ; Preserved: X, Y 24 | .proc spc_wait_boot 25 | ; Clear command port in case it already has $CC at reset 26 | seta8 27 | stz APU0 28 | 29 | ; Wait for the SPC to signal it's ready with APU0=$AA, APU1=$BB 30 | seta16 31 | lda #$BBAA 32 | waitBBAA: 33 | cmp APU0 34 | bne waitBBAA 35 | seta8 36 | rts 37 | .endproc 38 | 39 | ; Starts upload to SPC addr Y and sets Y to 40 | ; 0 for use as index with spc_upload_byte. 41 | ; Preserved: X 42 | .proc spc_begin_upload 43 | seta8 44 | sty APU2 45 | 46 | ; Tell the SPC to set the start address. The first value written 47 | ; to APU0 must be $CC, and each subsequent value must be nonzero 48 | ; and at least $02 above the index LSB previously written to $00. 49 | ; Adding $22 always works because APU0 starts at $AA. 50 | lda APU0 51 | clc 52 | adc #$22 53 | bne @skip ; ensure nonzero, as zero means start execution 54 | inc a 55 | @skip: 56 | sta APU1 57 | sta APU0 58 | 59 | ; Wait for acknowledgement 60 | @wait: 61 | cmp APU0 62 | bne @wait 63 | 64 | ; Initialize index into block 65 | ldy #0 66 | rts 67 | .endproc 68 | 69 | ; Uploads byte A to SPC and increments Y. The low byte of Y 70 | ; must not change between calls, as it 71 | ; Preserved: X 72 | .proc spc_upload_byte 73 | sta APU1 74 | 75 | ; Signal that it's ready 76 | tya 77 | sta APU0 78 | iny 79 | 80 | ; Wait for acknowledgement 81 | @wait: 82 | cmp APU0 83 | bne @wait 84 | rts 85 | .endproc 86 | 87 | ; Starts executing at SPC addr Y 88 | ; Preserved: X, Y 89 | .proc spc_execute 90 | sty APU2 91 | stz APU1 92 | lda APU0 93 | clc 94 | adc #$22 95 | sta APU0 96 | 97 | ; Wait for acknowledgement 98 | @wait: 99 | cmp APU0 100 | bne @wait 101 | rts 102 | .endproc 103 | 104 | .export spc_write_dsp, spc_boot_apu 105 | 106 | ; Writes high byte of X to SPC-700 DSP register in low byte of X 107 | .proc spc_write_dsp 108 | phx 109 | ; Just do a two-byte upload to $00F2-$00F3, so we 110 | ; set the DSP address, then write the byte into that. 111 | ldy #$00F2 112 | jsr spc_begin_upload 113 | pla 114 | jsr spc_upload_byte ; low byte of X to $F2 115 | pla 116 | jsr spc_upload_byte ; high byte of X to $F3 117 | rtl 118 | .endproc 119 | 120 | .import spc_entry 121 | .import __SPCIMAGE_RUN__, __SPCIMAGE_LOAD__, __SPCIMAGE_SIZE__ 122 | 123 | .proc spc_boot_apu 124 | jsr spc_wait_boot 125 | 126 | ; Upload sample to SPC at $200 127 | ldy #__SPCIMAGE_RUN__ 128 | jsr spc_begin_upload 129 | : 130 | tyx 131 | lda f:__SPCIMAGE_LOAD__,x 132 | jsr spc_upload_byte 133 | cpy #__SPCIMAGE_SIZE__ 134 | bne :- 135 | ldy #spc_entry 136 | jsr spc_execute 137 | rtl 138 | .endproc 139 | 140 | -------------------------------------------------------------------------------- /src/global.inc: -------------------------------------------------------------------------------- 1 | ; main.s 2 | .global main, nmi_handler, irq_handler 3 | .globalzp oam_used 4 | 5 | ; ppuclear.s 6 | .global ppu_copy_oam, ppu_pack_oamhi, ppu_clear_oam 7 | .global ppu_copy, ppu_clear_nt, ppu_vsync 8 | .global OAM, OAMHI 9 | 10 | ; bg.s 11 | .global load_bg_tiles, draw_bg 12 | 13 | ; player.s 14 | .global load_player_tiles, move_player, draw_player_sprite 15 | .globalzp player_xlo, player_xhi, player_dxlo, player_yhi 16 | .globalzp player_frame_sub, player_frame, player_facing 17 | 18 | ; blarggapu.s 19 | .global spc_boot_apu 20 | -------------------------------------------------------------------------------- /src/init.s: -------------------------------------------------------------------------------- 1 | .p816 2 | .export resetstub 3 | .import main, nmi_handler, __ZEROPAGE_RUN__ 4 | .smart 5 | 6 | ; Mask off low byte to allow use of $000000-$00000F as local variables 7 | ZEROPAGE_BASE := __ZEROPAGE_RUN__ & $FF00 8 | 9 | ; Make sure these conform to the linker script (e.g. lorom256.cfg). 10 | STACK_BASE := $0100 11 | STACK_SIZE = $0100 12 | LAST_STACK_ADDR := STACK_BASE + STACK_SIZE - 1 13 | 14 | PPU_BASE := $2100 15 | CPUIO_BASE := $4200 16 | 17 | ; MMIO is mirrored into $21xx, $42xx, and $43xx of all banks $00-$3F 18 | ; and $80-$BF. To make it work no matter the current data bank, we 19 | ; can use a long address in a nonzero bank. 20 | ; Bit 0 of MEMSEL enables fast ROM access above $808000. 21 | MEMSEL := $80420D 22 | 23 | ; Bit 4 of the byte at $FFD5 in the cartridge header specifies 24 | ; whether a game should be manufactured with slow or fast ROM. 25 | ; The init code enables fast ROM if this bit is true. 26 | map_mode := $00FFD5 27 | 28 | ; A tiny stub in bank $00 needs to set interrupt priority to 1, 29 | ; leave 6502 emulation mode, and long jump to the rest of init code 30 | ; in another bank. This should set 16-bit mode, turn off decimal 31 | ; mode, set the stack pointer, load a predictable state into writable 32 | ; MMIO ports of the S-PPU and S-CPU, and set the direct page base. 33 | ; For explanation of the values that this writes, see docs/init.txt 34 | ; 35 | ; For advanced users: Long stretches of STZ are a useful place to 36 | ; shuffle code when watermarking your binary. 37 | 38 | .segment "CODE" 39 | .proc resetstub 40 | sei ; turn off IRQs 41 | clc 42 | xce ; turn off 6502 emulation mode 43 | cld ; turn off decimal ADC/SBC 44 | jml reset_fastrom ; Bank $00 is not fast, but its mirror $80 is 45 | .endproc 46 | 47 | .segment "CODE7" 48 | .proc reset_fastrom 49 | rep #$30 ; 16-bit AXY 50 | ldx #LAST_STACK_ADDR 51 | txs ; set the stack pointer 52 | 53 | ; Initialize the CPU I/O registers to predictable values 54 | lda #CPUIO_BASE 55 | tad ; temporarily move direct page to S-CPU I/O area 56 | lda #$FF00 57 | sta $00 ; disable NMI and HVIRQ; don't drive controller port pin 6 58 | stz $02 ; clear multiplier factors 59 | stz $04 ; clear dividend 60 | stz $06 ; clear divisor and low byte of hcount 61 | stz $08 ; clear high bit of hcount and low byte of vcount 62 | stz $0A ; clear high bit of vcount and disable DMA copy 63 | stz $0C ; disable HDMA and fast ROM 64 | 65 | ; Initialize the PPU registers to predictable values 66 | lda #PPU_BASE 67 | tad ; temporarily move direct page to PPU I/O area 68 | 69 | ; first clear the regs that take a 16-bit write 70 | lda #$0080 71 | sta $00 ; Forced blank, brightness 0, sprite size 8/16 from VRAM $0000 72 | stz $02 ; OAM address = 0 73 | stz $05 ; BG mode 0, no mosaic 74 | stz $07 ; BG 1-2 map 32x32 from VRAM $0000 75 | stz $09 ; BG 3-4 map 32x32 from VRAM $0000 76 | stz $0B ; BG tiles from $0000 77 | stz $16 ; VRAM address $0000 78 | stz $23 ; disable BG window 79 | stz $26 ; clear window 1 x range 80 | stz $28 ; clear window 2 x range 81 | stz $2A ; clear window mask logic 82 | stz $2C ; disable all layers on main and sub 83 | stz $2E ; disable all layers on main and sub in window 84 | ldx #$0030 85 | stx $30 ; disable color math and mode 3/4/7 direct color 86 | ldy #$00E0 87 | sty $32 ; clear RGB components of COLDATA; disable interlace+pseudo hires 88 | 89 | ; now clear the regs that need 8-bit writes 90 | sep #$20 91 | sta $15 ; still $80: add 1 to VRAM pointer after high byte write 92 | stz $1A ; enable mode 7 wrapping and disable flipping 93 | stz $21 ; set CGRAM address to color 0 94 | stz $25 ; disable obj and math window 95 | 96 | ; The scroll registers $210D-$2114 need double 8-bit writes 97 | .repeat 8, I 98 | stz $0D+I 99 | stz $0D+I 100 | .endrepeat 101 | 102 | ; As do the mode 7 registers, which we set to the identity matrix 103 | ; [ $0100 $0000 ] 104 | ; [ $0000 $0100 ] 105 | lda #$01 106 | stz $1B 107 | sta $1B 108 | stz $1C 109 | stz $1C 110 | stz $1D 111 | stz $1D 112 | stz $1E 113 | sta $1E 114 | stz $1F 115 | stz $1F 116 | stz $20 117 | stz $20 118 | 119 | ; Set fast ROM if the internal header so requests 120 | lda f:map_mode 121 | and #$10 122 | beq not_fastrom 123 | inc a 124 | not_fastrom: 125 | sta MEMSEL 126 | 127 | rep #$20 128 | lda #ZEROPAGE_BASE 129 | tad ; return direct page to real zero page 130 | 131 | ; Unlike on the NES, we don't have to wait 2 vblanks to do 132 | ; any of the following remaining tasks. 133 | ; * Fill or clear areas of VRAM that will be used 134 | ; * Clear areas of WRAM that will be used 135 | ; * Load palette data into CGRAM 136 | ; * Fill shadow OAM and then copy it to OAM 137 | ; * Boot the S-SMP 138 | ; The main routine can do these in any order. 139 | jml main 140 | .endproc 141 | 142 | -------------------------------------------------------------------------------- /src/main.s: -------------------------------------------------------------------------------- 1 | .include "snes.inc" 2 | .include "global.inc" 3 | .smart 4 | .export main, nmi_handler 5 | 6 | USE_PSEUDOHIRES = 0 7 | USE_INTERLACE = 0 8 | USE_AUDIO = 1 9 | 10 | .segment "ZEROPAGE" 11 | nmis: .res 1 12 | oam_used: .res 2 13 | 14 | .segment "BSS" 15 | 16 | OAM: .res 512 17 | OAMHI: .res 512 18 | ; OAMHI contains bit 8 of X (the horizontal position) and the size 19 | ; bit for each sprite. It's a bit wasteful of memory, as the 20 | ; 512-byte OAMHI needs to be packed by software into 32 bytes before 21 | ; being sent to the PPU, but it makes sprite drawing code much 22 | ; simpler. The OBC1 coprocessor used in the game Metal Combat: 23 | ; Falcon's Revenge performs the same packing function in hardware, 24 | ; possibly as a copy protection method. 25 | 26 | .segment "CODE" 27 | ;; 28 | ; Minimalist NMI handler that only acknowledges NMI and signals 29 | ; to the main thread that NMI has occurred. 30 | .proc nmi_handler 31 | ; Because the INC and BIT instructions can't use 24-bit (f:) 32 | ; addresses, set the data bank to one that can access low RAM 33 | ; ($0000-$1FFF) and the PPU ($2100-$213F) with a 16-bit address. 34 | ; Only banks $00-$3F and $80-$BF can do this, not $40-$7D or 35 | ; $C0-$FF. ($7E can access low RAM but not the PPU.) But in a 36 | ; LoROM program no larger than 16 Mbit, the CODE segment is in a 37 | ; bank that can, so point the data bank at the program bank. 38 | phb 39 | phk 40 | plb 41 | 42 | seta8 43 | inc a:nmis ; Increase NMI count to notify main thread 44 | bit a:NMISTATUS ; Acknowledge NMI 45 | 46 | ; And restore the previous data bank value. 47 | plb 48 | rti 49 | .endproc 50 | 51 | ;; 52 | ; This program doesn't use IRQs either. 53 | .proc irq_handler 54 | rti 55 | .endproc 56 | 57 | .segment "CODE1" 58 | ; init.s sends us here 59 | .proc main 60 | 61 | ; In the same way that the CPU of the Commodore 64 computer can 62 | ; interact with a floppy disk only through the CPU in the 1541 disk 63 | ; drive, the main CPU of the Super NES can interact with the audio 64 | ; hardware only through the sound CPU. When the system turns on, 65 | ; the sound CPU is running the IPL (initial program load), which is 66 | ; designed to receive data from the main CPU through communication 67 | ; ports at $2140-$2143. Load a program and start it running. 68 | .if ::USE_AUDIO 69 | jsl spc_boot_apu 70 | .endif 71 | 72 | jsl load_bg_tiles ; fill pattern table 73 | jsl draw_bg ; fill nametable 74 | jsl load_player_tiles 75 | 76 | ; In LoROM no larger than 16 Mbit, all program banks can reach 77 | ; the system area (low RAM, PPU ports, and DMA ports). 78 | ; This isn't true of larger LoROM or of HiROM (without tricks). 79 | phk 80 | plb 81 | 82 | ; Program the PPU for the display mode 83 | seta8 84 | stz BGMODE ; mode 0 (four 2-bit BGs) with 8x8 tiles 85 | stz BGCHRADDR ; bg planes 0-1 CHR at $0000 86 | 87 | ; OBSEL needs the start of the sprite pattern table in $2000-word 88 | ; units. In other words, bits 14 and 13 of the address go in bits 89 | ; 1 and 0 of OBSEL. 90 | lda #$4000 >> 13 91 | sta OBSEL ; sprite CHR at $4000, sprites are 8x8 and 16x16 92 | lda #>$6000 93 | sta NTADDR+0 ; plane 0 nametable at $6000 94 | sta NTADDR+1 ; plane 1 nametable also at $6000 95 | ; set up plane 0's scroll 96 | stz BGSCROLLX+0 97 | stz BGSCROLLX+0 98 | lda #$FF 99 | sta BGSCROLLY+0 ; The PPU displays lines 1-224, so set scroll to 100 | sta BGSCROLLY+0 ; $FF so that the first displayed line is line 0 101 | 102 | ; Pseudo hi-res and interlace are optional hardware features. 103 | ; This demo doesn't use them much, but you can enable them when 104 | ; building it to see what they look like. They're described at 105 | ; https://wiki.superfamicom.org/registers 106 | 107 | .if ::USE_PSEUDOHIRES 108 | ; set up plane 1's scroll, offset by 4 pixels, to show 109 | ; the half-pixels of pseudohires 110 | lda #4 111 | sta BGSCROLLX+2 112 | stz BGSCROLLX+2 113 | lda #$FF 114 | sta BGSCROLLY+2 115 | sta BGSCROLLY+2 116 | lda #%00000010 ; enable plane 1 for left halves 117 | sta BLENDSUB 118 | phbit = SUB_HIRES ; split horizontal pixels 119 | .else 120 | phbit = 0 121 | .endif 122 | 123 | .if ::USE_INTERLACE 124 | ilbit = INTERLACE 125 | .else 126 | ilbit = 0 127 | .endif 128 | 129 | lda #phbit|ilbit 130 | sta PPURES 131 | lda #%00010001 ; enable sprites and plane 0 132 | sta BLENDMAIN 133 | lda #VBLANK_NMI|AUTOREAD ; but disable htime/vtime IRQ 134 | sta PPUNMI 135 | 136 | ; Set up game variables, as if it were the start of a new level. 137 | stz player_facing 138 | stz player_dxlo 139 | lda #184 140 | sta player_yhi 141 | setaxy16 142 | stz player_frame_sub 143 | lda #48 << 8 144 | sta player_xlo 145 | 146 | forever: 147 | 148 | jsl move_player 149 | 150 | ; Draw the player to a display list in main memory 151 | setaxy16 152 | stz oam_used 153 | jsl draw_player_sprite 154 | 155 | ; Mark remaining sprites as offscreen, then convert sprite size 156 | ; data from the convenient-to-manipulate format described by 157 | ; psycopathicteen to the packed format that the PPU actually uses. 158 | ldx oam_used 159 | jsl ppu_clear_oam 160 | jsl ppu_pack_oamhi 161 | 162 | ; Backgrounds and OAM can be modified only during vertical blanking. 163 | ; Wait for vertical blanking and copy prepared data to OAM. 164 | jsl ppu_vsync 165 | jsl ppu_copy_oam 166 | seta8 167 | lda #$0F 168 | sta PPUBRIGHT ; turn on rendering 169 | 170 | ; wait for control reading to finish 171 | lda #$01 172 | padwait: 173 | bit VBLSTATUS 174 | bne padwait 175 | 176 | ; This is where you'd usually update the scroll position. 177 | ; The scrolling registers are write-twice: first write bits 7-0 178 | ; then write bits 9-8 to the same address. 179 | stz BGSCROLLX 180 | stz BGSCROLLX 181 | 182 | jmp forever 183 | .endproc 184 | -------------------------------------------------------------------------------- /src/musicseq.s: -------------------------------------------------------------------------------- 1 | .include "pentlyseq.inc" 2 | .segment "SPCIMAGE" 3 | 4 | ; Music data ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 5 | 6 | ; TODO: Make a version of pentlyas.py that can output this format 7 | 8 | music_inst_table: 9 | ; name lv rv frq smp att dec sus lvl 10 | INST KICK, 11, 10, 3, 6, 31, 17, 0, 8 11 | INST SNARE, 10, 15, 3, 7, 31, 17, 0, 8 12 | INST HAT, 3, 5, 3, 5, 31, 17, 0, 8 13 | INST PLING_8, 3, 10, 1, 0, 31, 19, 15, 2 14 | INST PLING_4, 7, 7, 1, 1, 31, 19, 15, 2 15 | INST PLING_2, 9, 6, 1, 2, 31, 17, 15, 2 16 | INST BASSGUITAR, 12, 12, 1, 4, 31, 17, 15, 8 17 | INST BASSCLAR, 12, 12, 1, 2, 21, 21, 27, 4 18 | INST HORNBLAT, 3, 9, 1, 0, 21, 21, 27, 4 19 | INST XYLSHORT, 10, 6, 1, 2, 31, 23, 20, 2 20 | INST XYLMED, 10, 6, 1, 2, 31, 19, 17, 2 21 | 22 | pently_patterns: 23 | ; patterns 0-4: 2 AM 24 | .addr PPDAT_0200_sq1, PPDAT_0200_sq2, PPDAT_0200_bass, PPDAT_0200_drums 25 | ; pattern 4: cleared 26 | .addr PPDAT_round_clear_sq1 27 | ; patterns 5-9: 3 AM 28 | .addr PPDAT_0300_drum, PPDAT_0300_bass1, PPDAT_0300_bass2, PPDAT_0300_sq2_1 29 | .addr PPDAT_0300_sq1_1, PPDAT_0300_sq2_2 30 | ; patterns 10-14: 4 AM 31 | .addr PPDAT_0400_sq1, PPDAT_0400_sq2, PPDAT_0400_drums, PPDAT_0400_drums_2 32 | ; patterns 15-18: 4 PM 33 | .addr PPDAT_1600__sq1, PPDAT_1600__sq2, PPDAT_1600__tri, PPDAT_1600__drums 34 | ; patterns 19-22: 5 AM 35 | .addr PPDAT_0500_sqloop, PPDAT_0500_triloop, PPDAT_0500_melody 36 | .addr PPDAT_restore_vol 37 | 38 | pently_songs: 39 | .addr PSDAT_0500, PSDAT_0200, PSDAT_0300, PSDAT_0400, PSDAT_round_clear, PSDAT_1600 40 | 41 | ;____________________________________________________________________ 42 | ; 2am theme 43 | ; This is the famous first eight bars of the second movement of 44 | ; Beethoven's "Pathetique" done in 9/8 like ACWW 2am. 45 | 46 | PSDAT_0200: 47 | setTempo 300 48 | playPat 0, 0, 24, 4 49 | playPat 1, 1, 24, 4 50 | playPat 2, 2, 12, 4 51 | waitRows 36 52 | playDrumPat 3, 3 53 | waitRows 144 54 | dalSegno 55 | 56 | PPDAT_0200_sq1: 57 | .byt REST|D_D8, N_GS|D_D4, REST|D_D8, N_FS|D_D4 58 | .byt REST|D_D8, N_GS|D_D4, REST|D_D8, N_FS|D_D4 59 | .byt REST|D_D8, N_GS|D_D4, REST|D_D8, N_FS|D_D4 60 | .byt REST|D_D8, N_GS|D_D4, REST|D_D8, N_FS|D_D4 61 | .byt REST|D_D8, N_A|D_D4, REST|D_D8, N_B|D_D8, N_DSH|D_D8 62 | .byt REST|D_D8, N_GS|D_D4, REST|D_D8, N_GS|D_D4 63 | .byt REST|D_D8, N_GS|D_D4, REST|D_D8, N_GS|D_D4 64 | .byt REST|D_D8, N_A|D_D4, REST|D_D8, N_E|D_D4 65 | .byt REST|D_D8, N_FS|D_D4, REST|D_D8, N_D|D_D4 66 | .byt REST|D_D8, N_D|D_D4, REST|D_D8, N_CS|D_D4 67 | .byt PATEND 68 | 69 | PPDAT_0200_sq2: 70 | .byt REST|D_D8, N_CSH|D_D4, REST|D_D8, N_B|D_D4 71 | .byt REST|D_D8, N_CSH|D_D4, REST|D_D8, N_B|D_D4 72 | .byt N_CSH|D_2, REST, N_B|D_2, REST 73 | .byt N_EH|D_2, N_TIE, REST|D_D4, N_DH|D_D8 74 | .byt N_CSH|D_4, N_TIE, N_EH|D_4, N_AH|D_D8, N_BH|D_D4 75 | .byt N_EH|D_2, N_TIE, REST|D_D4, N_FH|D_D8 76 | .byt N_FSH|D_2, REST, N_B|D_D4, N_CSH|D_8, N_DH 77 | .byt N_EH|D_2, REST, N_AS|D_2, REST 78 | .byt N_DH|D_2, REST, N_CSH|D_8, N_B|D_D8, N_A|D_D8, N_GS 79 | .byt N_B|D_2, REST, N_A|D_2, REST 80 | .byt PATEND 81 | 82 | PPDAT_0200_bass: 83 | .byt N_A|D_2, REST, N_G|D_2, REST 84 | .byt N_A|D_2, REST, N_G|D_2, REST 85 | .byt N_A|D_2, REST, N_G|D_2, REST 86 | .byt N_A|D_2, REST, N_G|D_2, REST 87 | .byt N_FS|D_2, REST, N_B|D_2, REST 88 | .byt N_E|D_2, REST, N_E|D_2, REST 89 | .byt N_DH|D_2, REST, N_DH|D_2, REST 90 | .byt N_CSH|D_2, REST, N_FS|D_2, REST 91 | .byt N_B|D_2, REST, N_E|D_2, REST 92 | .byt N_A|D_2, REST, N_A|D_2, REST 93 | .byt PATEND 94 | 95 | PPDAT_0200_drums: 96 | .byt PD_KICK|D_D8, PD_HAT|D_8, PD_HAT, PD_HAT|D_D8, PD_SNARE|D_D8, PD_HAT|D_D4 97 | .byt PATEND 98 | 99 | ;____________________________________________________________________ 100 | ; 3am theme 101 | 102 | PSDAT_0300: 103 | playDrumPat 0, 5 104 | playPat 3, 6, 0, BASSCLAR 105 | waitRows 48 106 | playPat 1, 8, 24, PLING_2 107 | playPat 2, 9, 24, PLING_2 108 | waitRows 96 109 | playPat 3, 7, 0, BASSCLAR 110 | playPat 1, 10, 19, PLING_2 111 | playPat 2, 10, 24, PLING_2 112 | waitRows 96 113 | stopPat 1 114 | stopPat 2 115 | dalSegno 116 | 117 | PPDAT_0300_drum: 118 | .byt PD_KICK|D_D8, PD_HAT|D_8, PD_KICK, PD_SNARE|D_8, PD_HAT, PD_HAT|D_8, PD_HAT 119 | .byt PD_KICK|D_8, PD_HAT, PD_HAT|D_8, PD_KICK, PD_SNARE|D_D8, PD_HAT|D_D8 120 | .byt PATEND 121 | 122 | PPDAT_0300_bass1: 123 | .byt N_E|D_8, REST, N_EH|D_8, N_E|D_8, REST, N_EH|D_8, REST|D_8 124 | .byt REST|D_D2 125 | .byt N_A|D_8, REST, N_AH|D_8, N_A|D_8, REST, N_AH|D_8, REST|D_8 126 | .byt REST|D_D2 127 | .byt PATEND 128 | 129 | PPDAT_0300_bass2: 130 | .byt N_B|D_8, REST, N_BH|D_8, N_B|D_8, REST, N_BH|D_8, REST|D_8 131 | .byt REST|D_D2 132 | .byt N_A|D_8, REST, N_AH|D_8, N_A|D_8, REST, N_AH|D_8, REST|D_8 133 | .byt REST|D_D2 134 | .byt PATEND 135 | 136 | PPDAT_0300_sq2_1: 137 | .byt N_EH|D_D2, N_DH|D_D2, N_CSH|D_D2, N_CH|D_D2 138 | .byt PATEND 139 | 140 | PPDAT_0300_sq1_1: 141 | .byt N_CSH|D_D2, N_B|D_D2, N_A|D_D2, N_G|D_D2 142 | .byt PATEND 143 | 144 | PPDAT_0300_sq2_2: 145 | .byt REST|D_D8, N_B|D_4, N_TIE, N_DH|D_4 146 | .byt N_CSH|D_4, N_TIE, N_A|D_4, N_TIE|D_D8 147 | .byt REST|D_D8, N_A|D_4, N_TIE, N_CH|D_4 148 | .byt N_B|D_4, N_TIE, N_G|D_4, N_TIE|D_D8 149 | .byt PATEND 150 | 151 | ;____________________________________________________________________ 152 | ; 4am theme 153 | 154 | PSDAT_0400: 155 | setTempo 300 156 | playPat 1, 11, 12, XYLSHORT 157 | playPat 2, 12, 12, XYLSHORT 158 | waitRows 6 159 | playDrumPat 0, 13 160 | waitRows 84 161 | playDrumPat 0, 14 162 | segno 163 | waitRows 192 164 | dalSegno 165 | 166 | PPDAT_0400_sq1: 167 | .byt INSTRUMENT, XYLSHORT, N_FH|D_8, N_EH|D_4, N_DH|D_D8 168 | .byt INSTRUMENT, HORNBLAT, N_DH, REST|D_2, REST|D_D4 169 | .byt INSTRUMENT, XYLSHORT, N_EH|D_8, N_DH|D_4, N_CH|D_D8 170 | .byt INSTRUMENT, HORNBLAT, N_CH, REST|D_2, REST|D_D4 171 | .byt INSTRUMENT, XYLSHORT, N_DH|D_8, N_CH|D_4, N_BB|D_D8 172 | .byt INSTRUMENT, HORNBLAT, N_BB, REST|D_2, REST|D_D4 173 | .byt INSTRUMENT, XYLSHORT, N_BB|D_8, N_A|D_4, N_G|D_D8 174 | .byt INSTRUMENT, HORNBLAT, N_DH, REST|D_2, REST|D_D4 175 | .byt $FF 176 | PPDAT_0400_sq2: 177 | .byt INSTRUMENT, XYLSHORT, N_AH|D_D8, N_GH|D_D8, N_FH|D_D8 178 | .byt INSTRUMENT, HORNBLAT, N_FH, REST|D_2, REST|D_D4 179 | .byt INSTRUMENT, XYLSHORT, N_GH|D_D8, N_FH|D_D8, N_EH|D_D8 180 | .byt INSTRUMENT, HORNBLAT, N_EH, REST|D_2, REST|D_D4 181 | .byt INSTRUMENT, XYLSHORT, N_FH|D_D8, N_EH|D_D8, N_DH|D_D8 182 | .byt INSTRUMENT, HORNBLAT, N_DH, REST|D_2, REST|D_D4 183 | .byt INSTRUMENT, XYLSHORT, N_DH|D_D8, N_CH|D_D8, N_B|D_D8 184 | .byt INSTRUMENT, HORNBLAT, N_GH, REST|D_2, REST|D_D4 185 | .byt $FF 186 | PPDAT_0400_drums_2: 187 | .byt PD_KICK|D_D8, PD_HAT|D_D8, PD_SNARE|D_D8, PD_HAT|D_8, PD_KICK 188 | PPDAT_0400_drums: 189 | .byt PD_KICK|D_D8, PD_HAT|D_D4, PD_HAT|D_8, PD_KICK 190 | .byt $FF 191 | 192 | ;____________________________________________________________________ 193 | ; 5am theme 194 | ; 195 | ; This is melodies from the third movement of Beethoven's 196 | ; "Pathetique" combined with rhythms from ACPG 5am. 197 | ; At the time I transcribed this, there were about 400 bytes left 198 | ; in the RODATA segment of the PRG ROM. 199 | 200 | PSDAT_0500: 201 | setTempo 300 202 | playDrumPat 0, 5 203 | playPat 2, 19, 24, PLING_4 204 | playPat 1, 19, 19, PLING_4 205 | waitRows 96 206 | playPat 4, 21, 24, XYLMED 207 | playPat 3, 20, 0, BASSCLAR 208 | waitRows 190 209 | playPat 2, 22, 24, PLING_4 210 | playPat 1, 22, 19, PLING_4 211 | waitRows 2 212 | stopPat 3 213 | stopPat 4 214 | dalSegno 215 | 216 | PPDAT_restore_vol: 217 | .byt CHVOLUME, 4, REST|D_D2 218 | .byt PATEND 219 | 220 | PPDAT_0500_sqloop: 221 | .byt INSTRUMENT, PLING_4 222 | .byt REST|D_8, N_E, N_A|D_8, N_B, N_CSH|D_8, REST, N_DH|D_8, N_B|D_8 223 | .byt INSTRUMENT, PLING_8 224 | .byt REST, N_E, N_A|D_8, N_B, N_CSH|D_8, REST, N_DH|D_8, N_B|D_8 225 | .byt REST, REST|D_D4, REST|D_1 226 | .byt INSTRUMENT, PLING_4 227 | .byt REST|D_8, N_E, N_A|D_8, N_B, N_CSH|D_8, REST, N_B|D_8, N_G|D_8 228 | .byt INSTRUMENT, PLING_8 229 | .byt REST, N_E, N_A|D_8, N_B, N_CSH|D_8, REST, N_B|D_8, N_G|D_8 230 | .byt REST, REST|D_D4, CHVOLUME, 3, REST|D_1 231 | .byt PATEND 232 | PPDAT_0500_triloop: 233 | .byt N_A|D_8, REST, N_G|D_8, N_FS|D_8, REST, N_G|D_8, REST|D_2 234 | .byt N_FS|D_8, REST, N_F|D_8, N_E|D_8 235 | .byt REST|D_D8, REST|D_4, REST|D_1 236 | .byt PATEND 237 | PPDAT_0500_melody: 238 | .byt REST, N_CSH, N_DH, N_CSH, N_B, N_CSH, N_DH, N_EH|D_8, N_EH|D_8, N_EH|D_D8 239 | .byt REST|D_8, REST|D_1, REST|D_1 240 | .byt N_EH|D_8, N_CSH, N_DH|D_8, N_EH|D_D8, N_DH, N_CSH|D_8, N_FSH|D_D8 241 | .byt REST|D_8, REST|D_1, REST|D_1 242 | .byt N_CSH|D_8, N_A, N_B|D_8, N_CSH|D_D8, N_B, N_A|D_8, N_DH|D_D8 243 | .byt REST|D_8, REST|D_1, REST|D_1 244 | .byt N_E|D_D8, N_E|D_D8, N_E|D_D8, N_E|D_8, N_A|D_D8 245 | .byt REST|D_8, REST|D_1, REST|D_1 246 | .byt PATEND 247 | 248 | ;____________________________________________________________________ 249 | ; round cleared theme 250 | 251 | PSDAT_round_clear: 252 | setTempo 60 253 | playPat 0, 4, 12, PLING_2 254 | playPat 1, 4, 21, PLING_2 255 | waitRows 5 256 | fine 257 | 258 | PPDAT_round_clear_sq1: 259 | .byt N_F, N_A, N_G, N_C|D_8 260 | .byt PATEND 261 | 262 | ;____________________________________________________________________ 263 | ; 4pm theme 264 | 265 | PSDAT_1600: 266 | setTempo 300 267 | playPat 1, 15, 12, XYLSHORT 268 | playPat 2, 16, 12, XYLSHORT 269 | playPat 3, 17, 0, BASSGUITAR 270 | playDrumPat 0, 18 271 | waitRows 48 272 | dalSegno 273 | 274 | PPDAT_1600__sq2: 275 | .byt N_FH|D_D8, N_CH|D_D8, N_FH|D_D8, N_CH|D_8, N_FH 276 | .byt N_EBH|D_D8, N_BB|D_D8, N_EBH|D_D8, N_BB|D_8, N_EBH 277 | .byt N_FH|D_D8, N_DH|D_D8, N_FH|D_D8, N_DH|D_8, N_FH 278 | .byt N_GH|D_D8, N_DH|D_D8, N_GH|D_D8, N_CH|D_8, N_GH 279 | .byt PATEND 280 | PPDAT_1600__sq1: 281 | .byt N_AH|D_D4, N_AH|D_D4 282 | .byt N_AH|D_D4, N_AH|D_D4 283 | .byt N_AH|D_D4, N_AH|D_4, N_TIE, N_AH 284 | .byt N_BBH|D_D4, N_BBH|D_4, N_TIE, N_BBH 285 | .byt PATEND 286 | PPDAT_1600__tri: 287 | .byt N_FH|D_8, REST|D_D4, N_CH|D_8, REST|D_8 288 | .byt N_EBH|D_8, REST|D_D4, N_BB|D_8, REST|D_8 289 | .byt N_DH|D_8, REST|D_D4, N_A|D_8, REST|D_8 290 | .byt N_GH|D_8, REST|D_D4, N_CH|D_8, REST|D_8 291 | .byt PATEND 292 | PPDAT_1600__drums: 293 | .byt PD_KICK|D_D8, PD_SNARE|D_D8, PD_HAT|D_8, PD_KICK, PD_SNARE|D_D8 294 | .byt PATEND 295 | -------------------------------------------------------------------------------- /src/pentlyseq.inc: -------------------------------------------------------------------------------- 1 | .ifndef PENTLYSEQ_INC 2 | PENTLYSEQ_INC = 1 3 | 4 | ; Instrument format 5 | ; $00: Left volume (bit 7-4) and right volume (bit 3-0), 6 | ; used for balance and panning 7 | ; $01: Length of period in 16-sample units, or note 24 freq / 4186 Hz 8 | ; $02: Sample number 9 | ; $03: Attack and decay rates 10 | ; $04: Sustain threshold and rate 11 | ; $05-$07: Not used yet 12 | .macro INST name, lvol, rvol, freqscale, samplenum, attack, decay, sustain, sustainlevel 13 | name = (* - music_inst_table) / 8 14 | .ident(.concat("PD_", .string(name))) = (* - music_inst_table) 15 | .assert (0 <= (lvol) && (lvol) <= 15), error, "left volume must be 0-15" 16 | .assert (0 <= (rvol) && (rvol) <= 15), error, "right volume must be 0-15" 17 | .assert (0 <= (rvol) && (rvol) <= 15), error, "right volume must be 0-15" 18 | .assert (1 <= (attack) && (attack) <= 31), error, "attack must be 1-31" 19 | .assert (attack & 1), error, "attack must be odd" 20 | .assert (16 <= (decay) && (decay) <= 31), error, "decay must be 17-31" 21 | .assert (0 <= (sustain) && (sustain) <= 31), error, "sustain must be 0-31" 22 | .assert (1 <= (sustainlevel) && (sustainlevel) <= 8), error, "sustainlevel must be 1-8" 23 | .byte ((lvol) << 4) | (rvol) 24 | .byte freqscale 25 | .byte samplenum 26 | .byte ((attack) >> 1) | (((decay) >> 1) << 4) 27 | .byte (sustain) | (((sustainlevel) - 1) << 5) 28 | .byte $00,$00,$00 29 | .endmacro 30 | 31 | CON_PLAYPAT = $00 ; next: pattern, transpose, instrument 32 | CON_WAITROWS = $20 ; next: number of rows to wait minus 1 33 | CON_FINE = $21 ; stop music now 34 | CON_SEGNO = $22 ; set loop point 35 | CON_DALSEGNO = $23 ; jump to loop point. if no point was set, jump to start of song. 36 | CON_NOTEON = $28 37 | CON_SETTEMPO = $30 ; low bits: bits 10-8 of tempo in rows/min; next: bits 7-0 of tempo 38 | 39 | ; Conductor macros 40 | .macro playPat ch, patid, transpose, instrument 41 | .byt CON_PLAYPAT|ch, patid, transpose, instrument 42 | .endmacro 43 | .macro playDrumPat ch, patid 44 | .byt CON_PLAYPAT|ch, patid, $80, $80 45 | .endmacro 46 | .macro stopPat ch 47 | .byt CON_PLAYPAT|ch, 255, 0, 0 48 | .endmacro 49 | .macro waitRows n 50 | .byt CON_WAITROWS, (n)-1 51 | .endmacro 52 | .macro fine 53 | .byt CON_FINE 54 | .endmacro 55 | .macro segno 56 | .byt CON_SEGNO 57 | .endmacro 58 | .macro dalSegno 59 | .byt CON_DALSEGNO 60 | .endmacro 61 | .macro setTempo rowsPerMin 62 | .local irpm 63 | irpm = rowsPerMin 64 | .byt CON_SETTEMPO|>irpm, 32 on a line) since the last vblank end 62 | ; +-------- 1: sliver overflow (>34 on a line) since the last vblank end 63 | ; this parallels bit 5 of $2002 on NES 64 | 65 | PPUSTATUS2 := $213F 66 | ; 76543210 PPU compositor status 67 | ; || |++++- PPU2 version (1-3, not counting minor versions of 3) 68 | ; || +----- 1: PPU is configured for 50 Hz (PAL) 69 | ; |+------- 1: GETXY has happened since last PPUSTATUS2 read 70 | ; +-------- Toggles every vblank; reflects top/bottom interlace field 71 | 72 | ; S-PPU sprites ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 73 | 74 | OBSEL := $2101 75 | ; 76543210 76 | ; ||||| ++- Sprite main pattern table (0=$0000, 1=$2000, 2=$4000, 3=$6000) 77 | ; |||++---- Alt pattern table offset (0=$1000, 1=$2000, 2=$3000, 3=$4000) 78 | ; +++------ 0: 8/16; 1: 8/32; 2: 8/64; 3: 16/32; 4: 16/32; 5: 32/64 79 | ; (all sprites are square and 2D-mapped) 80 | OBSIZE_8_16 = $00 81 | OBSIZE_8_32 = $20 82 | OBSIZE_8_64 = $40 83 | OBSIZE_16_32 = $60 84 | OBSIZE_16_64 = $80 85 | OBSIZE_32_64 = $A0 86 | 87 | OAMADDR := $2102 ; 16-bit, 128 sprites followed by high-X/size table 88 | OAMDATA := $2104 89 | OAMDATARD := $2138 90 | ; Parallels NES $2003, except apparently word-addressed. 91 | ; OAM random access is working here, unlike on NES. 92 | ; If bit 15 is set, value at start of frame apparently also 93 | ; controls which sprites are in front 94 | 95 | ; S-PPU background configuration ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 96 | 97 | BGMODE := $2105 98 | ; 76543210 99 | ; |||||+++- 0: 4 planes 2 bpp 100 | ; ||||| 1: 2 planes 4 bpp, 1 plane 2 bpp 101 | ; ||||| 2: 2 planes 4 bpp, OPT 102 | ; ||||| 3: 1 plane 8 bpp, 1 plane 4 bpp 103 | ; ||||| 4: 1 plane 8 bpp, 1 plane 2 bpp, OPT 104 | ; ||||| 5: 1 plane 4 bpp, 1 plane 2 bpp, hires 105 | ; ||||| 6: 1 plane 4 bpp, OPT, hires 106 | ; ||||| 7: 1 plane rot/scale 107 | ; ||||+---- In mode 1, set plane 2 high-prio in front of all others 108 | ; |||+----- Plane 0 tile size (0: 8x8, 1: 16x16) 109 | ; ||+------ Plane 1 tile size (0: 8x8, 1: 16x16) 110 | ; |+------- Plane 2 tile size (0: 8x8, 1: 16x16) 111 | ; +-------- Plane 3 tile size (0: 8x8, 1: 16x16) 112 | ; Modes 5 and 6 use 16x8 instead of 8x8 113 | ; Mode 7 always uses 8x8 114 | 115 | MOSAIC := $2106 116 | ; 76543210 117 | ; |||||||+- Apply mosaic to plane 0 (or mode 7 high-prio horizontal) 118 | ; ||||||+-- Apply mosaic to plane 1 (or mode 7 high-prio vertical) 119 | ; |||||+--- Apply mosaic to plane 2 120 | ; ||||+---- Apply mosaic to plane 3 121 | ; ++++----- Pixel size minus 1 (0=1x1, 15=16x16) 122 | 123 | NTADDR := $2107 ; through $210A 124 | ; 76543210 125 | ; ||||||+- Nametable width (0: 1 screen, 1: 2 screens) 126 | ; |||||+-- Nametable height (0: 1 screen, 1: 2 screens) 127 | ; +++++--- Nametable base address in $400 units 128 | ; Each nametable in modes 0-6 is 32 rows, each 32 spaces long. 129 | .define NTXY(xc,yc) ((xc)|((yc)<<5)) 130 | 131 | BGCHRADDR := $210B 132 | ; FEDCBA98 76543210 133 | ; ||| ||| ||| +++- Pattern table base address for plane 0 134 | ; ||| ||| +++----- Same for plane 1 135 | ; ||| +++---------- Same for plane 2 136 | ; +++-------------- Same for plane 3 137 | 138 | M7SEL := $211A 139 | ; 76543210 140 | ; || || 141 | ; || |+- Flip screen horizontally 142 | ; || +-- Flip screen vertically 143 | ; ++------- 0: repeat entire mode 7 plane 144 | ; 2: transparent outside; 3: tile $00 repeating outside 145 | M7_HFLIP = $01 146 | M7_VFLIP = $02 147 | M7_WRAP = $00 148 | M7_NOWRAP = $80 149 | M7_BORDER00 = $C0 150 | 151 | ; S-PPU scrolling ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 152 | 153 | BGSCROLLX := $210D ; double write low then high (000-3FF m0-6, 000-7FF m7) 154 | BGSCROLLY := $210E ; similar. reg 210F-2114 are same for other planes 155 | ; Hi-res scrolling in modes 5-6 moves by whole (sub+main) pixels in X 156 | ; but half scanlines in Y. 157 | ; The top visible line is the line below the value written here. 158 | ; For example, in 224-line mode, if 12 is written, lines 13 through 159 | ; 237 of the background are visible. This differs from the NES. 160 | ; 161 | ; Mode 7 uses this value as the center of rotation. This differs 162 | ; from the GBA, which fixes the center of rotation at the top left. 163 | 164 | ; 211B-2120 control mode 7 matrix; to be documented later 165 | 166 | ; S-PPU VRAM data port ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 167 | 168 | PPUCTRL := $2115 169 | ; 76543210 170 | ; | ||++- VRAM address increment (1, 32, 128, 128) 171 | ; | ++--- Rotate low bits of address left by 3 (off, 8, 9, or 10) 172 | ; +-------- 0: Increment after low data port access; 1: after high 173 | ; Corresponds to bit 2 of $2000 on NES 174 | VRAM_DOWN = $01 175 | VRAM_M7DOWN = $02 176 | INC_DATAHI = $80 177 | 178 | PPUADDR := $2116 ; Word address, not double-write anymore 179 | PPUDATA := $2118 180 | PPUDATAHI := $2119 181 | PPUDATARD := $2139 ; Same dummy read as on NES is needed 182 | PPUDATARDHI := $213A 183 | 184 | ; S-PPU palette ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 185 | 186 | CGADDR := $2121 187 | CGDATA := $2122 ; 5-bit BGR, write twice, low byte first 188 | CGDATARD := $213B ; 5-bit BGR, read twice, low byte first 189 | .define RGB(r,g,b) ((r)|((g)<<5)|((b)<<10)) 190 | 191 | ; S-PPU window ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 192 | 193 | BG12WINDOW := $2123 194 | BG34WINDOW := $2124 195 | OBJWINDOW := $2125 196 | ; 76543210 197 | ; ||||||++- 0: disable window 1 on BG1/BG3/OBJ; 2: enable; 3: enable outside 198 | ; ||||++--- 0: disable window 2 on BG1/BG3/OBJ; 2: enable; 3: enable outside 199 | ; ||++----- 0: disable window 1 on BG2/BG4; 2: enable; 3: enable outside 200 | ; ++------- 0: disable window 2 on BG2/BG4; 2: enable; 3: enable outside 201 | 202 | WINDOW1L := $2126 203 | WINDOW1R := $2127 204 | WINDOW2L := $2128 205 | WINDOW2R := $2129 206 | 207 | BGWINDOP := $212A ; Window op is how windows are combined when both 208 | OBJWINDOP := $212B ; windows 1 and 2 are enabled. 209 | ; 76543210 210 | ; ||||||++- Window op for plane 0 or sprites (0: or, 1: and, 2: xor, 3: xnor) 211 | ; ||||++--- Window op for plane 1 or color window 212 | ; ||++----- Window op for plane 2 213 | ; ++------- Window op for plane 3 214 | 215 | ; S-PPU blending (or "color math") ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 216 | 217 | ; The main layer enable reg, corresponding to PPUMASK on the NES, 218 | ; is BLENDMAIN. 219 | BLENDMAIN := $212C ; Layers enabled for main input of blending 220 | BLENDSUB := $212D ; Layers enabled for sub input of blending 221 | WINDOWMAIN := $212E ; Windows enabled for main input of blending 222 | WINDOWSUB := $212F ; Windows enabled for sub input of blending 223 | ; 76543210 224 | ; ||||+- plane 0 225 | ; |||+-- plane 1 226 | ; ||+--- plane 2 227 | ; |+---- plane 3 228 | ; +----- sprites 229 | ; BLENDMAIN roughly parallels NES $2001 bits 4-3, 230 | ; except that turning off both bits doesn't disable rendering. 231 | ; (Use PPUBRIGHT for that.) 232 | 233 | ; PPU1 appears to generate a stream of (main, sub) pairs, which 234 | ; PPU2 combines to form output colors. 235 | 236 | ; Blending parameters not documented yet. Wait for a future demo. 237 | 238 | ; When BGMODE is 0-6 (or during vblank in mode 7), a fast 16x8 239 | ; signed multiply is available, finishing by the next CPU cycle. 240 | M7MCAND := $211B ; write low then high 241 | M7MUL := $211C ; 8-bit factor 242 | M7PRODLO := $2134 243 | M7PRODHI := $2135 244 | M7PRODBANK := $2136 245 | 246 | GETXY := $2137 ; read while $4201 D7 is set: populate x and y coords 247 | XCOORD := $213C ; used with light guns, read twice 248 | YCOORD := $213D ; also read twice 249 | 250 | ; SPC700 communication ports ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 251 | 252 | APU0 := $2140 253 | APU1 := $2141 254 | APU2 := $2142 255 | APU3 := $2143 256 | 257 | ; S-CPU interrupt control ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 258 | 259 | PPUNMI := $4200 260 | ; 76543210 261 | ; | || +- Automatically read controllers in first 4 lines of vblank 262 | ; | ++----- 0: no IRQ (and acknowledge any pending); 1: IRQs at HTIME; 263 | ; | 2: one IRQ at (0, VTIME); 3: one IRQ at (HTIME, VTIME) 264 | ; +-------- 1: Enable NMI at start of vblank 265 | VBLANK_NMI = $80 266 | HTIME_IRQ = $10 267 | VTIME_IRQ = $20 268 | HVTIME_IRQ = $30 269 | AUTOREAD = $01 270 | 271 | HTIME := $4207 272 | HTIMEHI := $4208 273 | VTIME := $4209 274 | VTIMEHI := $420A 275 | 276 | NMISTATUS := $4210 ; read to acknowledge NMI 277 | ; 76543210 278 | ; | |||| 279 | ; | ++++- DMA controller version (1, 2) where v1 has an HDMA glitch 280 | ; +-------- 1: Vblank has started since last read (like $2002.d7 on NES) 281 | 282 | TIMESTATUS := $4211 ; bit 7: htime/vtime IRQ occurred since last read; 283 | ; read to acknowledge 284 | 285 | VBLSTATUS := $4212 286 | ; 76543210 287 | ; || +- 0: Controller reading finished; 1: busy 288 | ; |+------- 1: In hblank (including hblank of vblank) 289 | ; +-------- 1: In vblank 290 | 291 | ROMSPEED := $420D ; 0: slow ROM everywhere; 1: fast ROM in banks 80-FF 292 | ; (requires 120ns or faster PRG ROM) 293 | 294 | ; S-CPU controller I/O ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 295 | 296 | ; Manual controller reading behaves almost exactly as on Famicom. 297 | ; For games using up to 2 standard controllers, these aren't needed, 298 | ; as you can enable controller autoreading along with vblank NMIs. 299 | ; But for games using (multitap, mouse, etc.), you will need to 300 | ; read the extra bits separately after the autoreader finishes. 301 | JOY0 := $4016 302 | JOY1 := $4017 303 | 304 | ; In addition to the common strobe, each controller port has an 305 | ; additional output bit that can be used as, say, a chip select 306 | ; for SPI peripherals. 307 | JOYOUT := $4201 308 | ; 76543210 309 | ; |+------- Controller 1 pin 6 output 310 | ; +-------- Controller 2 pin 6 output 311 | 312 | ; Results of the autoreader 313 | JOY1CUR := $4218 ; Bit 0: used by standard controllers 314 | JOY2CUR := $421A 315 | JOY1B1CUR := $421C ; Bit 1: used by multitap and a few oddball 316 | JOY2B1CUR := $421E ; input devices 317 | ; FEDCBA98 76543210 318 | ; BYSRUDLR AXLRTTTT 319 | ; |||||||| ||||++++- controller type (0: controller, 1: mouse) 320 | ; |||||||| ||++----- shoulder buttons 321 | ; ++-------++------- right face buttons 322 | ; ||++++---------- Control Pad 323 | ; ++-------------- center face buttons 324 | KEY_B = $8000 325 | KEY_Y = $4000 326 | KEY_SELECT = $2000 327 | KEY_START = $1000 328 | KEY_UP = $0800 329 | KEY_DOWN = $0400 330 | KEY_LEFT = $0200 331 | KEY_RIGHT = $0100 332 | KEY_A = $0080 333 | KEY_X = $0040 334 | KEY_L = $0020 335 | KEY_R = $0010 336 | 337 | ; It is also possible to read the controller by reading only the 338 | ; upper 8 bits in 8-bit mode (m=1). This may be the path of least 339 | ; resistance for porting NES software to the Super NES. Be careful 340 | ; not to mix _HI addresses with non-_HI constants or vice versa. 341 | ; A, X, L, and R cannot be read this way. 342 | JOY1CUR_HI := JOY1CUR + 1 343 | JOY2CUR_HI := JOY2CUR + 1 344 | KEY_HI_B = .hibyte(KEY_B) 345 | KEY_HI_Y = .hibyte(KEY_Y) 346 | KEY_HI_SELECT = .hibyte(KEY_SELECT) 347 | KEY_HI_START = .hibyte(KEY_START) 348 | KEY_HI_UP = .hibyte(KEY_UP) 349 | KEY_HI_DOWN = .hibyte(KEY_DOWN) 350 | KEY_HI_LEFT = .hibyte(KEY_LEFT) 351 | KEY_HI_RIGHT = .hibyte(KEY_RIGHT) 352 | 353 | ; S-CPU multiply and divide ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 354 | 355 | ; Multiply unit. Also good for shifting pixels when drawing 356 | ; text in a proportional font. 357 | CPUMCAND := $4202 ; unchanged by multiplications 358 | CPUMUL := $4203 ; write here to fill CPUPROD 8 cycles later 359 | CPUPROD := $4216 360 | CPUPRODHI := $4217 361 | 362 | ; Divide unit 363 | CPUNUM := $4204 364 | CPUNUMHI := $4205 365 | CPUDEN := $4206 ; write divisor to fill CPUQUOT/CPUREM 16 cycles later 366 | CPUQUOT := $4214 367 | CPUQUOTHI := $4215 368 | CPUREM := CPUPROD 369 | CPUREMHI := CPUPRODHI 370 | 371 | ; S-CPU DMA ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 372 | 373 | COPYSTART := $420B ; writes of 1 << n start a DMA copy on channel n 374 | HDMASTART := $420C ; writes of 1 << n start HDMA on channel n 375 | ; Don't run a DMA copy while HDMA is enabled, or you might run into 376 | ; a defect in revision 1 of the S-CPU that causes crashing. 377 | 378 | ; There are 8 DMA channels. 379 | ; Registers for channels 1-7 start at $4310, $4320, ... 380 | DMAMODE := $4300 381 | ; 76543210 382 | ; || ||+++- PPU address offset pattern 383 | ; || || 0: 0 1: 01 2: 00 3: 0011 4: 0123 5: 0101 384 | ; || ++---- Memcpy only: 0: increment; 1: fixed; 2: decrement 385 | ; |+------- HDMA only: 1: Table contains pointers 386 | ; +-------- Direction (0: read CPU write PPU; 1: read PPU write CPU) 387 | DMA_LINEAR = $00 388 | DMA_01 = $01 389 | DMA_00 = $02 ; For HDMA to double write ports; copies can use linear 390 | DMA_0011 = $03 ; For HDMA to scroll positions and mode 7 matrices 391 | DMA_0123 = $04 ; For HDMA to window registers 392 | DMA_0101 = $05 ; Not sure how this would be useful for HDMA 393 | DMA_FORWARD = $00 394 | DMA_CONST = $08 395 | DMA_BACKWARD = $10 396 | DMA_INDIRECT = $40 397 | DMA_READPPU = $80 398 | 399 | DMAPPUREG := $4301 400 | DMAADDR := $4302 401 | DMAADDRHI := $4303 402 | DMAADDRBANK := $4304 403 | DMALEN := $4305 ; number of bytes, not number of transfers; 0 means 65536 404 | DMALENHI := $4306 405 | 406 | ; A future demo that includes HDMA effects would include port definitions. 407 | ;HDMAINDBANK := $4307 408 | ;HDMATABLELO := $4308 409 | ;HDMATABLEHI := $4309 410 | ;HDMALINE := $430A 411 | 412 | ; composite values for use with 16-bit writes to DMAMODE 413 | DMAMODE_PPULOFILL = (= -128 && @distance <= 127, error, "branch out of range" 280 | .byte <@distance 281 | @next: 282 | .endmacro 283 | 284 | .macro _op_branch inst, target 285 | _branch_offset {.byte inst}, target 286 | .endmacro 287 | 288 | .macro bbs val, target 289 | _branch_offset {_op_bit $03, $20, val}, target 290 | .endmacro 291 | 292 | .macro bbc val, target 293 | _branch_offset {_op_bit $13, $20, val}, target 294 | .endmacro 295 | 296 | .macro dbnz val, target 297 | .if .xmatch ({val}, y) 298 | _op_branch $fe, target 299 | .else 300 | _op_branch {$6e, val}, (target) 301 | .endif 302 | .endmacro 303 | 304 | .macro cbne val, target 305 | .if .xmatch (.right (2, {val}), +x) 306 | _branch_offset {.byte $de, .left (.tcount ({val})-2, {val})}, target 307 | .else 308 | _branch_offset {.byte $2e, val}, target 309 | .endif 310 | .endmacro 311 | 312 | .define bpl _op_branch $10, ; target 313 | .define bra _op_branch $2f, ; target 314 | .define bmi _op_branch $30, ; target 315 | .define bvc _op_branch $50, ; target 316 | .define bvs _op_branch $70, ; target 317 | .define bcc _op_branch $90, ; target 318 | .define bcs _op_branch $B0, ; target 319 | .define bne _op_branch $D0, ; target 320 | .define beq _op_branch $f0, ; target 321 | 322 | ;**** OP !abs 323 | 324 | .macro _op_abs op, val; **** 325 | .if .xmatch (.left (1, {val}), !) 326 | .byte op 327 | .word .right (.tcount ({val})-1, {val}) 328 | .elseif ::DEFAULT_ABS && (!.xmatch (.left (1, {val}), <)) 329 | _op_warn_default_abs 330 | .byte op 331 | .word val 332 | .else 333 | .assert 0, error, "unsupported addressing mode" 334 | .endif 335 | .endmacro 336 | 337 | .define tset1 _op_abs $0E, ; abs 338 | .define tclr1 _op_abs $4E, ; abs 339 | .define call _op_abs $3F, ; abs 340 | 341 | .macro jmp_ val; **** 342 | .if .xmatch (.left (2, {val}), [!) && .xmatch (.right (3, {val}), +x]) 343 | .byte $1f 344 | .word .mid (2, .tcount ({val})-5, {val}) 345 | .elseif ::DEFAULT_ABS && .xmatch (.left (1, {val}), [) && .xmatch (.right (3, {val}), +x]) 346 | _op_warn_default_abs 347 | .byte $1f 348 | .word .mid (1, .tcount ({val})-4, {val}) 349 | .else 350 | _op_abs $5f, val 351 | .endif 352 | .endmacro 353 | 354 | ;**** $1FFF.bit 355 | 356 | .macro _op_mbit op, val 357 | .local @begin, @addr 358 | @begin: 359 | .repeat .tcount ({val}), i 360 | .if .xmatch (.mid (i, 1, {val}), .) 361 | @addr = .left (i, {val}) 362 | .assert 0 <= @addr && @addr <= $1FFF, error, "address exceeds 13 bits" 363 | .byte op 364 | .word (.right (.tcount ({val})-(i+1), {val}))*$2000 + @addr 365 | .endif 366 | .endrepeat 367 | ; TODO: report error during assembly rather than linking 368 | .assert (*-@begin) = 3, error, "unsupported addressing mode" 369 | .endmacro 370 | 371 | .macro _op_mbit_c op, carry, val 372 | .if .xmatch (carry, c) 373 | _op_mbit op, val 374 | .else 375 | .assert 0, error, "destination must be C" 376 | .endif 377 | .endmacro 378 | 379 | .macro _op_mbit_inv op, carry, val 380 | .if .xmatch (.left (1, {val}), /) 381 | _op_mbit_c op+$20, carry, .right (.tcount ({val})-1, {val}) 382 | .else 383 | _op_mbit_c op, carry, val 384 | .endif 385 | .endmacro 386 | 387 | .define not1 _op_mbit $EA, ; abs.bit 388 | .define or1 _op_mbit_inv $0A, ; abs.bit 389 | .define and1 _op_mbit_inv $4A, ; abs.bit 390 | .define eor1 _op_mbit_inv $8A, ; abs.bit 391 | 392 | .macro mov1 dest, src 393 | .if .xmatch ({src}, c) 394 | _op_mbit_c $CA, src, dest 395 | .else 396 | _op_mbit_c $AA, dest, src 397 | .endif 398 | .endmacro 399 | 400 | ;**** OP dp 401 | 402 | .macro _op_dp op, dp 403 | .byte op, (dp) 404 | .endmacro 405 | 406 | .define decw _op_dp $1a, ; dp 407 | .define incw _op_dp $3a, ; dp 408 | 409 | ;**** OP reg 410 | 411 | .macro _op_one_reg op, reg, err, val 412 | .if .xmatch ({val}, reg) 413 | .byte op 414 | .else 415 | .assert 0, error, err 416 | .endif 417 | .endmacro 418 | 419 | .macro _op_w op, reg, val 420 | _op_one_reg op, ya, "only supports ya", reg 421 | .byte val 422 | .endmacro 423 | 424 | .define cmpw _op_w $5A, ; dp 425 | .define addw _op_w $7A, ; dp 426 | .define subw _op_w $9A, ; dp 427 | 428 | .macro movw dest, src 429 | .if .xmatch ({src}, ya) 430 | _op_w $DA, src, dest 431 | .else 432 | _op_w $BA, dest, src 433 | .endif 434 | .endmacro 435 | 436 | .macro div dest, src 437 | .if .xmatch ({dest}, ya) && .xmatch ({src}, x) 438 | .byte $9e 439 | .else 440 | .assert 0, error, "only supports ya, x" 441 | .endif 442 | .endmacro 443 | 444 | .define xcn _op_one_reg $9f, a, "only supports a", 445 | .define das _op_one_reg $BE, a, "only supports a", 446 | .define daa _op_one_reg $DF, a, "only supports a", 447 | .define mul _op_one_reg $CF, ya, "only supports ya", 448 | 449 | ;**** Unique 450 | 451 | .macro tcall val 452 | .assert 0 <= (val) && (val) <= 15, error, "invalid value" 453 | .byte (val)*$10 + $01 454 | .endmacro 455 | 456 | .macro pcall val 457 | .byte $4f, (val) 458 | .endmacro 459 | 460 | ;**** Implied 461 | 462 | .macro _op_implied op 463 | .byte op 464 | .endmacro 465 | 466 | .define nop _op_implied $00 467 | .define brk _op_implied $0f 468 | .define clrp _op_implied $20 469 | .define setp _op_implied $40 470 | .define clrc _op_implied $60 471 | .define ret _op_implied $6f 472 | .define reti _op_implied $7f 473 | .define setc _op_implied $80 474 | .define ei _op_implied $A0 475 | .define di _op_implied $C0 476 | .define clrv _op_implied $E0 477 | .define notc _op_implied $ED 478 | .define sleep _op_implied $ef 479 | .define stop _op_implied $ff 480 | 481 | .ifndef CASPC_65XX 482 | .define and and_ 483 | .define eor eor_ 484 | .define adc adc_ 485 | .define sbc sbc_ 486 | .define cmp cmp_ 487 | 488 | .define dec dec_ 489 | .define inc inc_ 490 | .define lsr lsr_ 491 | .define asl asl_ 492 | .define rol rol_ 493 | .define ror ror_ 494 | 495 | .define jmp jmp_ 496 | .endif 497 | 498 | ;.endif 499 | -------------------------------------------------------------------------------- /src/spcheader.s: -------------------------------------------------------------------------------- 1 | ; 2 | ; This file contains the metadata for an SPC700 save state file, as 3 | ; used by Super NES music players for PC. It is not used at all in 4 | ; the .sfc file. 5 | ; 6 | .import spc_entry 7 | HAS_TITLE = 1 8 | 9 | .segment "SPCHEADER" 10 | spcheaderstart: 11 | .byte "SNES-SPC700 Sound File Data v0.30", 26, 26 12 | .if HAS_TITLE 13 | .byte 26 14 | .else 15 | .byte 27 16 | .endif 17 | .byte 30 ; format version 18 | .assert *-spcheaderstart = $25, error, "magic wrong size" 19 | 20 | .addr spc_entry 21 | .byte 0, 0, 0, 0, $EF ; A, X, Y, P, S 22 | .assert *-spcheaderstart = $2C, error, "registers wrong size" 23 | 24 | .if HAS_TITLE 25 | .res (spcheaderstart + $2E - *) 26 | tracktitle: .byte "Selnow" 27 | .res (spcheaderstart + $4E - *) 28 | albumtitle: .byte "Pino's LoROM template" 29 | .res (spcheaderstart + $6E - *) 30 | rippername: .byte "Pino P" 31 | .res (spcheaderstart + $7E - *) 32 | tracksubtitle: .byte 0 33 | .res (spcheaderstart + $9E - *) 34 | builddate: .byte 0 35 | .res (spcheaderstart + $A9 - *) 36 | tracktime_s: .byte "100" 37 | .res (spcheaderstart + $AC - *) 38 | fadetime_ms: .byte "0" 39 | .res (spcheaderstart + $B1 - *) 40 | artistname: .byte "Pino P" 41 | .res (spcheaderstart + $D1 - *) 42 | muted_ch: .byte %00000000 43 | emulator: .byte 0 ; 0=other, 1=zsnes, 2=snes9x 44 | .endif 45 | 46 | .segment "SPCFOOTER" 47 | .res 128, $00 ; dsp regs 48 | 49 | -------------------------------------------------------------------------------- /src/spcimage.s: -------------------------------------------------------------------------------- 1 | .setcpu "none" 2 | .include "spc-65c02.inc" 3 | .include "pentlyseq.inc" 4 | 5 | .macro stya addr 6 | .local addr1 7 | addr1 = addr 8 | .assert addr <= $00FE, error, "stya works only in zero page" 9 | movw sample_dir 160 | stya DSPADDR 161 | 162 | lda #8000/TIMER_HZ ; S-Pently will use 125 Hz 163 | sta TIMERPERIOD 164 | lda #%10000001 165 | sta TIMEREN 166 | lda TIMERVAL 167 | rts 168 | .endproc 169 | 170 | .proc pently_start_music 171 | pha 172 | ; Start the song 173 | lda #<300 174 | ldy #>300 175 | stya music_tempoLo 176 | lda #0 177 | sta conductorWaitRows 178 | 179 | pla 180 | asl a 181 | tax 182 | lda pently_songs,x 183 | sta conductorPos 184 | sta conductorSegnoLo 185 | lda pently_songs+1,x 186 | sta conductorPos+1 187 | sta conductorSegnoHi 188 | ; falls through 189 | .endproc 190 | 191 | .proc pently_stop_all_tracks 192 | ldx #NUM_CHANNELS - 1 193 | initpatternloop: 194 | lda #silentPattern 197 | sta (60 * TIMER_HZ) 239 | sta tempo_counter+1 240 | 241 | jsr play_conductor 242 | ldx #NUM_CHANNELS - 1 243 | : 244 | lda defaultGraceTime,x 245 | sta graceTime,x 246 | bne nr_notGraceTime 247 | jsr processTrackPattern 248 | nr_notGraceTime: 249 | dex 250 | bpl :- 251 | jmp keyoff_ready_notes 252 | 253 | between_rows: 254 | ldx #NUM_CHANNELS - 1 255 | : 256 | .if 1 257 | lda graceTime,x 258 | beq br_notGraceTime 259 | sec 260 | sbc #1 261 | sta graceTime,x 262 | bne br_notGraceTime 263 | jsr processTrackPattern 264 | .endif 265 | br_notGraceTime: 266 | dex 267 | bpl :- 268 | jmp keyoff_ready_notes 269 | .endproc 270 | 271 | .proc play_conductor 272 | lda silentPattern 398 | sta = 64, the playback frequency is unspecified. 586 | ; @param X voice ID (0-7) 587 | ; @param A pitch scale (BRR blocks per period) 588 | ; @param Y semitone number 589 | ; @return A, Y, $00 trashed; X preserved 590 | .proc ch_set_pitch 591 | pitchhi = $00 592 | sta pitchhi 593 | txa 594 | xcn a 595 | ora #DSP_CFREQLO 596 | sta DSPADDR 597 | tya 598 | octave_loop: 599 | cmp #12 600 | bcc octave_done 601 | sbc #12 602 | asl pitchhi 603 | jmp octave_loop 604 | octave_done: 605 | tay 606 | lda notefreqs_lowbyte,y 607 | ldy pitchhi 608 | mul ya 609 | sta DSPDATA 610 | inc DSPADDR 611 | ;clc ; octave_loop always ends with carry clear 612 | tya 613 | adc pitchhi 614 | sta DSPDATA 615 | rts 616 | .endproc 617 | 618 | ;; 619 | ; Sets volume, sample ID, and envelope of channel X to instrument A. 620 | ; @param X channel number (0-7) 621 | ; @param A instrument number 622 | ; @return $02: pointer to instrument; $04: channel volume; 623 | ; A: instrument's pitch scale; Y trashed; X preserved 624 | .proc ch_set_inst 625 | instptr = $02 626 | chvolalt = $04 627 | ldy #8 628 | mul ya 629 | clc 630 | adc #music_inst_table 634 | sta instptr+1 635 | txa 636 | xcn a 637 | sta DSPADDR 638 | 639 | ; Set left channel volume 640 | ldy #0 641 | lda (instptr),y 642 | pha 643 | xcn a 644 | and #$0F ; 0 to 15 645 | ldy len(doubled): 140 | continue 141 | if doubled[csaddress - 7] & 0xEF == mode: 142 | using_mode, using_modename = mode, modename 143 | if args.verbose: 144 | print("%sautodetected %s ($%02x) mapping mode" 145 | % (errpfx, modename, doubled[csaddress - 7]), 146 | file=sys.stderr) 147 | break 148 | if using_mode is None: 149 | print(errpfx+"could not detect mapping mode as lorom, hirom, or exhirom", 150 | file=sys.stderr) 151 | sys.exit(1) 152 | mode_from_file = doubled[csaddress - 7] 153 | modediff = (using_mode ^ mode_from_file) & 0xEF 154 | if modediff: 155 | if args.verbose: 156 | print("%swarning: %s ($%02x) mapping mode doesn't match $%02x mode in file" 157 | % (errpfx, modename, doubled[csaddress - 7]), 158 | file=sys.stderr) 159 | 160 | # Actually calculate the checksum 161 | doubled[csaddress:csaddress + 4] = [0, 0, 255, 255] 162 | bytesum = sum(doubled) 163 | doubled[csaddress + 2] = bytesum & 0xFF 164 | doubled[csaddress + 3] = (bytesum >> 8) & 0xFF 165 | doubled[csaddress + 0] = doubled[csaddress + 2] ^ 0xFF 166 | doubled[csaddress + 1] = doubled[csaddress + 3] ^ 0xFF 167 | if args.verbose: 168 | print("%ssum of bytes is %s ($%08x)" 169 | % (errpfx, bytesum, bytesum), file=sys.stderr) 170 | tcsum = ' '.join('%02x' % b for b in doubled[csaddress:csaddress + 4]) 171 | print("%schecksum bytes are %s" 172 | % (errpfx, tcsum), file=sys.stderr) 173 | 174 | if not args.double: 175 | del doubled[len(data):] 176 | with open(outfilename, 'wb') as outfp: 177 | outfp.write(doubled) 178 | 179 | if __name__=='__main__': 180 | ## try: 181 | ## main('fixchecksum.py --help'.split()) 182 | ## except SystemExit: 183 | ## print("then it'd exit. Good.") 184 | ## else: 185 | ## assert False 186 | ## try: 187 | ## main('fixchecksum.py --version'.split()) 188 | ## except SystemExit: 189 | ## print("then it'd exit. Good.") 190 | ## else: 191 | ## assert False 192 | ## main('fixchecksum.py -v ../lorom-template.sfc fixed.sfc --mode 20'.split()) 193 | main() 194 | -------------------------------------------------------------------------------- /tools/getpalette.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Spits out the indexed palette of an image in some format 4 | 5 | Copyright 2019 Damian Yerrick 6 | Copying and distribution of this file, with or without 7 | modification, are permitted in any medium without royalty 8 | provided the copyright notice and this notice are preserved. 9 | This file is offered as-is, without any warranty. 10 | """ 11 | import sys 12 | import argparse 13 | from PIL import Image 14 | 15 | def get_bgr555(rgbtuples): 16 | return [ 17 | ((b & 0xF8) << 7) | ((g & 0xF8) << 2) | ((r & 0xF8) >> 3) 18 | for r, g, b in rgbtuples 19 | ] 20 | 21 | def format_bgr555_le(rgbtuples): 22 | out = bytearray() 23 | for word in get_bgr555(rgbtuples): 24 | out.append(word & 0xFF) 25 | out.append((word >> 8) & 0xFF) 26 | return bytes(out) 27 | 28 | def format_wordsperline(words, prefix="", n=8): 29 | lines = [] 30 | wordsperline = 8 31 | for i in range(0, len(words), wordsperline): 32 | lines.append("%s%s\n" % (prefix, ",".join(words[i:i + wordsperline]))) 33 | return "".join(lines) 34 | 35 | def format_bgr555_ca65(rgbtuples): 36 | words = ["$%04x" % r for r in get_bgr555(rgbtuples)] 37 | return format_wordsperline(words, " .word ", 8) 38 | 39 | def format_bgr555_rgbasm(rgbtuples): 40 | words = ["$%04x" % r for r in get_bgr555(rgbtuples)] 41 | return format_wordsperline(words, " dw ", 8) 42 | 43 | def format_hextriplet(rgbtuples): 44 | return "".join("#%02x%02x%02x\n" % r for r in rgbtuples) 45 | 46 | formats = { 47 | "bgr555-le": format_bgr555_le, 48 | "bgr555-ca65": format_bgr555_ca65, 49 | "bgr555-rgbasm": format_bgr555_rgbasm, 50 | "hextriplet": format_hextriplet 51 | } 52 | 53 | def parse_slice(s): 54 | s = s.split(':', 1) 55 | if not all(x.isdigit() for x in s): 56 | raise ValueError("nondigits in slice %s" % args.slice) 57 | rside = int(s[-1]) 58 | lside = int(s[0]) if len(s) >= 2 else 0 59 | out = (lside, rside) 60 | if lside > rside: 61 | raise ValueError("slice %d:%d not ascending" % out) 62 | if rside >= 256: 63 | raise ValueError("slice %d:%d beyond 256" % out) 64 | return out 65 | 66 | def parse_argv(argv): 67 | p = argparse.ArgumentParser() 68 | formatchoices = sorted(formats) 69 | p.add_argument("image", 70 | help="half-open interval of palette entries to keep (e.g. 3:9 for 3, 4, ..., 8)") 71 | p.add_argument("-f", "--format", 72 | help="choose format", 73 | choices=formatchoices, default="hextriplet") 74 | p.add_argument("-n", "--slice", 75 | help="count or half-open interval of palette entries to write (e.g. 8 for 0, 1, 2, ..., 7; or 3:11 for 3, 4, 5, ..., 10)", 76 | type=parse_slice, default=(0, 256)) 77 | p.add_argument("-o", "--output", 78 | help="write (default: standard output)", 79 | default="-") 80 | return p.parse_args(argv[1:]) 81 | 82 | def main(argv=None): 83 | args = parse_argv(argv or sys.argv) 84 | im = Image.open(args.image) 85 | if im.mode != 'P': 86 | raise ValueError("%s: does not use indexed color" % args.image) 87 | 88 | palette = im.getpalette() 89 | palette = [tuple(palette[i:i + 3]) for i in range(0, len(palette), 3)] 90 | palette = palette[args.slice[0]:args.slice[1]] 91 | 92 | out = formats[args.format](palette) 93 | out_byteslike = hasattr(out, "decode") 94 | if args.output == '-': 95 | if out_byteslike: 96 | sys.stdout.buffer.write(out) 97 | else: 98 | sys.stdout.write(out) 99 | else: 100 | with open(args.output, "wb" if out_byteslike else "w") as outfp: 101 | outfp.write(out) 102 | 103 | if __name__=='__main__': 104 | ## main(["getpalette.py", "../tilesets/EWJ2cover.png", "-n", "48", "-f", "bgr555-ca65"]) 105 | main() 106 | 107 | -------------------------------------------------------------------------------- /tools/karplus.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import with_statement, division, print_function 3 | from contextlib import closing 4 | import array, wave, sys, os, optparse, random 5 | 6 | help_version = """karplus 0.01 7 | Copyright 2015 Damian Yerrick 8 | Copying and distribution of this file, with or without 9 | modification, are permitted in any medium without royalty 10 | provided the copyright notice and this notice are preserved. 11 | This file is offered as-is, without any warranty. 12 | """ 13 | 14 | help_usage = "Usage: %prog [options] [OUTFILE]" 15 | help_prolog = """ 16 | Generates a plucked-string sound using Karplus-Strong synthesis, 17 | which applies echo at a period within audio frequency. 18 | For an explanation of the theory, see 19 | https://en.wikipedia.org/wiki/Karplus-Strong_string_synthesis 20 | """ 21 | help_epilog = """ 22 | Excitation formulas: 23 | 24 | random is a uniform pseudorandom number generator. 25 | 26 | square:n:d:b produces a square wave with n/d duty cycle, where 0 < n < d. 27 | If b is 0, the square wave is positive; if it is nonzero, the excitation 28 | is balanced. square is a synonym for square:1:2:1. 29 | 30 | saw:n:d:p produces a sawtooth raised to p power occupying the first 31 | n/d of each period, where 1 <= p and 0 < n <= d. saw is a synonym 32 | for saw:1:1:1. 33 | """ 34 | 35 | 36 | def save_wave_as_mono16(filename, freq, data): 37 | data = array.array('h', (min(max(s, -32767), 32767) for s in data)) 38 | little = array.array('H', b'\x01\x00')[0] == 1 39 | if not little: 40 | data.byteswap() 41 | with closing(wave.open(filename, "wb")) as outfp: 42 | outfp.setnchannels(1) 43 | outfp.setsampwidth(2) 44 | outfp.setframerate(freq) 45 | outfp.writeframes(data.tobytes()) 46 | 47 | def get_fir_group_delay(kernel): 48 | """Calculate the group delay of an FIR filter. 49 | 50 | This is valid at low frequencies for all FIR kernels and at all 51 | frequencies for symmetric (linear phase) kernels. 52 | 53 | """ 54 | return sum(a * b for a, b in enumerate(kernel)) / sum(kernel) 55 | 56 | def karplus_echo(wave, delay, decay, kernel, length, rawkernel=False): 57 | """Perform Karplus-Strong string synthesis on an excitation waveform. 58 | 59 | wave -- a modifiable sequence of sample values, usually a list 60 | or a floating point array containing a short burst of noise or 61 | other wideband excitation, to which samples shall be appended 62 | delay -- a number of samples 63 | decay -- a factor used to scale the waveform before filtering it, 64 | usually 0.9 to 1 65 | kernel -- a sequence that will be convolved with the delayed samples. 66 | If rawkernel is not set, make a copy of kernel scaled to have 67 | a sum of 1. 68 | maxlen -- the number of samples 69 | 70 | The effective period in samples is the delay value plus the group 71 | delay of fircoeffs. For example, the kernel [1, 6, 1] has a group 72 | delay of 1. So kernel = [1, 6, 1] and delay = 49 actually produces 73 | a period of 50 samples: 49 from delay and 1 from kernel. 74 | 75 | """ 76 | if not rawkernel: 77 | decay = decay / sum(kernel) 78 | kernel = [c * decay for c in kernel] 79 | delayend = delay + len(kernel) 80 | while len(wave) < length: 81 | prev = wave[-delay:-delayend:-1] 82 | dot = sum(a * b for a, b in zip(kernel, prev)) 83 | wave.append(dot) 84 | 85 | def make_bass_sample(): 86 | """Make a bass guitar sample using Karplus-Strong synthesis. 87 | 88 | It consists of a single cycle of a 25% duty, 64-sample waveform, 89 | echoed 16 times. 90 | 91 | """ 92 | karpbuf = [30000]*16+[-10000]*48 93 | fircoeffs = [1, 6, 1] 94 | # Use 1.00 in an enveloped environment (xm, it, spc) 95 | # Use 0.99 in a non-enveloped environment (mod, s3m, nsf) 96 | decay = 1.00 97 | delay = len(karpbuf) - int(round(get_fir_group_delay(fircoeffs))) 98 | maxlen = 1024 99 | karplus_echo(karpbuf, delay, decay, fircoeffs, maxlen) 100 | outbuf = array.array('h', 101 | (min(32000, max(-32000, int(round(c)))) for c in karpbuf)) 102 | save_wave_as_mono16("karplus.wav", 4186, outbuf) 103 | 104 | # And the other half is deciding what to make. 105 | 106 | def random_excitation(length, amplitude, args=None): 107 | return [random.randrange(-amplitude, amplitude + 1) 108 | for i in range(length)] 109 | 110 | def square_excitation(length, amplitude, args=None): 111 | num = int(args[0]) if len(args) >= 2 else 1 112 | den = int(args[1]) if len(args) >= 2 else 2 113 | highlen = int(round(num * length / den)) 114 | if not 0 < highlen < length: 115 | raise ValueError("duty numerator %d must be strictly between 0 and denominator %d" 116 | % (num, den)) 117 | lowlen = length - highlen 118 | 119 | balanced = int(args[2]) if len(args) >= 3 else 1 120 | if not balanced: 121 | highlevel = amplitude 122 | lowlevel = 0 123 | elif highlen < lowlen: 124 | highlevel = amplitude 125 | lowlevel = -int(round(amplitude * highlen / lowlen)) 126 | else: 127 | highlevel = int(round(amplitude * lowlen / highlen)) 128 | lowlevel = -amplitude 129 | return [highlevel] * highlen + [lowlevel] * lowlen 130 | 131 | def saw_excitation(length, amplitude, args=None): 132 | num = int(args[0]) if len(args) >= 2 else 1 133 | den = int(args[1]) if len(args) >= 2 else 2 134 | power = float(args[2]) if len(args) < 3 else 1 135 | highlen = int(round(num * length / den)) 136 | if not 0 < highlen < length: 137 | raise ValueError("duty numerator %d must be no greater than denominator %d" 138 | % (num, den)) 139 | if not 0 < power: 140 | raise ValueError("exponent %f must be greater than 0" % power) 141 | 142 | ramp = [(i / (highlen - 1)) ** power * amplitude 143 | for i in range(highlen - 1, 0, -2)] 144 | ramp = ramp[::-1] + [-i for i in ramp] 145 | return ramp + [0] * (length - len(ramp)) 146 | 147 | excitation_types = { 148 | 'random': random_excitation, 149 | 'square': square_excitation, 150 | 'saw': saw_excitation 151 | } 152 | 153 | class MultiGrafIndentedHelpFormatter(optparse.IndentedHelpFormatter): 154 | """An optparse.IndentedHelpFormatter supporting multi-paragraph text.""" 155 | 156 | @staticmethod 157 | def paragraphize(text): 158 | """Split text into paragraphs at multiple blank lines. 159 | 160 | Return an iterator of strings each containing one paragraph. 161 | 162 | """ 163 | lines = [] 164 | for line in text.split("\n"): 165 | line = line.rstrip() 166 | if line: 167 | lines.append(line) 168 | elif lines: 169 | yield '\n'.join(lines) 170 | lines = [] 171 | if lines: 172 | yield '\n'.join(lines) 173 | 174 | def _format_text(self, text): 175 | """ 176 | Format multiple paragraphs of free-form text for inclusion 177 | in the help output at the current indentation level. 178 | """ 179 | fmt = super(MultiGrafIndentedHelpFormatter, self)._format_text 180 | return "\n\n".join(fmt(graf) for graf in self.paragraphize(text)) 181 | 182 | def parse_argv(argv): 183 | parser = optparse.OptionParser(version=help_version, usage=help_usage, 184 | description=help_prolog, epilog=help_epilog, 185 | formatter=MultiGrafIndentedHelpFormatter()) 186 | parser.add_option("-o", dest="outfilename", 187 | help="write output to OUTFILE", 188 | metavar="OUTFILE") 189 | parser.add_option("-n", "--length", dest="length", 190 | metavar="PERIOD", type="int", default=1024, 191 | help="set the output length in samples (default: 1024)") 192 | parser.add_option("-p", "--period", dest="period", 193 | metavar="PERIOD", type="int", default=32, 194 | help="set the wave period in samples (default: 32)") 195 | parser.add_option("-r", "--rate", dest="rate", 196 | metavar="RATE", type="int", default=8372, 197 | help="with -d, set the wave sample rate in Hz (default: 8372)") 198 | parser.add_option("-e", "--excitation", dest="excitation", 199 | help="set excitation to FORMULA (default: random)", 200 | metavar="FORMULA", default="random") 201 | parser.add_option("-a", "--amplitude", dest="amplitude", 202 | metavar="VALUE", type="int", default=32000, 203 | help="set excitation amplitude to VALUE (up to 32767; default 32000)") 204 | parser.add_option("-g", "--gain", dest="gain", 205 | metavar="AMOUNT", type="float", default=1.0, 206 | help="feed AMOUNT of the signal back the delay line (default: 1.0; also try .9 or .98)") 207 | parser.add_option("-f", "--lpf-strength", dest="lpfstrength", 208 | metavar="AMOUNT", type="float", default=1.0, 209 | help="set strength of low-pass filter on feedback to AMOUNT (0.0 to 1.33; default: 0.5)") 210 | 211 | (options, pos) = parser.parse_args(argv[1:]) 212 | if not options.outfilename: 213 | try: 214 | outfilename = pos[0] 215 | except IndexError: 216 | parser.error("output file not specified; try %s --help" 217 | % os.path.basename(argv[0])) 218 | if not 4 <= options.period <= options.length: 219 | parser.error("period is too short") 220 | if not 10 <= options.rate <= 128000: 221 | parser.error("output sample rate must be 10 to 128000 Hz") 222 | excitation = options.excitation.split(':') 223 | try: 224 | exciter = excitation_types[excitation[0]] 225 | except KeyError: 226 | parser.error("unknown excitation type %s" % excitation[0]) 227 | exciteargs = excitation[1:] 228 | if options.amplitude > 32767: 229 | parser.error("amplitude must be no greater than 32767") 230 | if not 0.0 <= options.gain <= 1.0: 231 | parser.error("gain must be 0.0 to 1.0") 232 | if not 0.0 <= 3 * options.lpfstrength <= 4.0: 233 | parser.error("filter strength must be 0.0 to 1.33") 234 | 235 | return options, exciter, exciteargs 236 | 237 | def main(argv=None): 238 | argv = argv or sys.argv 239 | options, exciter, exciteargs = parse_argv(argv) 240 | karpbuf = exciter(options.period, options.amplitude, exciteargs) 241 | fircoeffs = [options.lpfstrength, 242 | 4.0 - options.lpfstrength * 2, 243 | options.lpfstrength] 244 | delay = len(karpbuf) - 1 245 | karplus_echo(karpbuf, delay, options.gain, fircoeffs, options.length) 246 | outbuf = array.array('h', 247 | (min(32767, max(-32767, int(round(c)))) 248 | for c in karpbuf)) 249 | save_wave_as_mono16(options.outfilename, options.rate, outbuf) 250 | 251 | if __name__=='__main__': 252 | if 'idlelib' in sys.modules: 253 | ## main([sys.argv[0], '-o', 'test.wav']) 254 | main([sys.argv[0], '-o', 'karplusbass.wav', '-n', '4096', 255 | '-p', '64', '-r', '4186', '-e', 'square:1:4', 256 | '-a', '30000', '-g', '1.0', '-f', '.5']) 257 | else: 258 | main() 259 | -------------------------------------------------------------------------------- /tools/makehat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Hi-hat generator 4 | # 5 | # Copyright 2017 Damian Yerrick 6 | # Copying and distribution of this file, with or without 7 | # modification, are permitted in any medium without royalty 8 | # provided the copyright notice and this notice are preserved. 9 | # This file is offered as-is, without any warranty. 10 | # 11 | from contextlib import closing 12 | import sys 13 | import random 14 | import array 15 | import wave 16 | 17 | def save_wave_as_mono16(filename, freq, data): 18 | data = array.array('h', (min(max(s, -32767), 32767) for s in data)) 19 | little = array.array('H', b'\x01\x00')[0] == 1 20 | if not little: 21 | data.byteswap() 22 | with closing(wave.open(filename, "wb")) as outfp: 23 | outfp.setnchannels(1) 24 | outfp.setsampwidth(2) 25 | outfp.setframerate(freq) 26 | outfp.writeframes(data.tobytes()) 27 | 28 | def main(argv=None): 29 | argv = argv or sys.argv 30 | outfilename = argv[1] 31 | 32 | rands = [random.randrange(2) * (1600 - i) * (1600 - i) for i in range(1600)] 33 | rands.extend([0]) 34 | rands = [int(round((b - a) / 128)) for a, b in zip(rands, rands[1:])] 35 | save_wave_as_mono16(outfilename, 16744, rands) 36 | 37 | if __name__=='__main__': 38 | main() 39 | -------------------------------------------------------------------------------- /tools/pilbmp2nes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Bitmap to multi-console CHR converter using Pillow, the 4 | # Python Imaging Library 5 | # 6 | # Copyright 2014-2025 Damian Yerrick 7 | # Copying and distribution of this file, with or without 8 | # modification, are permitted in any medium without royalty 9 | # provided the copyright notice and this notice are preserved. 10 | # This file is offered as-is, without any warranty. 11 | # 12 | from __future__ import with_statement, print_function, unicode_literals 13 | from PIL import Image 14 | from time import sleep 15 | 16 | def formatTilePlanar(tile, planemap, hflip=False, little=False): 17 | """Turn a tile into bitplanes. 18 | 19 | Planemap opcodes: 20 | 10 -- bit 1 then bit 0 of each tile 21 | 0,1 -- planar interleaved by rows 22 | 0;1 -- planar interlaved by planes 23 | 0,1;2,3 -- SNES/PCE format 24 | 25 | """ 26 | hflip = 7 if hflip else 0 27 | if (tile.size != (8, 8)): 28 | return None 29 | pixels = list(tile.getdata()) 30 | pixelrows = [pixels[i:i + 8] for i in range(0, 64, 8)] 31 | if hflip: 32 | for row in pixelrows: 33 | row.reverse() 34 | out = bytearray() 35 | 36 | planemap = [[[int(c) for c in row] 37 | for row in plane.split(',')] 38 | for plane in planemap.split(';')] 39 | # format: [tile-plane number][plane-within-row number][bit number] 40 | 41 | # we have five (!) nested loops 42 | # outermost: separate planes 43 | # within separate planes: pixel rows 44 | # within pixel rows: row planes 45 | # within row planes: pixels 46 | # within pixels: bits 47 | for plane in planemap: 48 | for pxrow in pixelrows: 49 | for rowplane in plane: 50 | rowbits = 1 51 | thisrow = bytearray() 52 | for px in pxrow: 53 | for bitnum in rowplane: 54 | rowbits = (rowbits << 1) | ((px >> bitnum) & 1) 55 | if rowbits >= 0x100: 56 | thisrow.append(rowbits & 0xFF) 57 | rowbits = 1 58 | out.extend(thisrow[::-1] if little else thisrow) 59 | return bytes(out) 60 | 61 | def pilbmp2chr(im, tileWidth=8, tileHeight=8, 62 | formatTile=lambda im: formatTilePlanar(im, "0;1")): 63 | """Convert a bitmap image into a list of byte strings representing tiles.""" 64 | im.load() 65 | (w, h) = im.size 66 | outdata = [] 67 | for mt_y in range(0, h, tileHeight): 68 | for mt_x in range(0, w, tileWidth): 69 | metatile = im.crop((mt_x, mt_y, 70 | mt_x + tileWidth, mt_y + tileHeight)) 71 | for tile_y in range(0, tileHeight, 8): 72 | for tile_x in range(0, tileWidth, 8): 73 | tile = metatile.crop((tile_x, tile_y, 74 | tile_x + 8, tile_y + 8)) 75 | data = formatTile(tile) 76 | outdata.append(data) 77 | return outdata 78 | 79 | def parse_argv(argv): 80 | # optparse is still used because of how it supports mixing 81 | # named arguments (-i and -o) with positional arguments 82 | from optparse import OptionParser 83 | parser = OptionParser(usage="usage: %prog [options] [-i] INFILE [-o] OUTFILE") 84 | parser.add_option("-i", "--image", dest="infilename", 85 | help="read image from INFILE", metavar="INFILE") 86 | parser.add_option("-o", "--output", dest="outfilename", 87 | help="write CHR data to OUTFILE", metavar="OUTFILE") 88 | parser.add_option("-W", "--tile-width", dest="tileWidth", 89 | help="set width of metatiles", metavar="HEIGHT", 90 | type="int", default=8) 91 | parser.add_option("--packbits", dest="packbits", 92 | help="use Apple PackBits RLE compression if available", 93 | action="store_true", default=False) 94 | parser.add_option("--pb8", dest="use_pb8", 95 | help="use PB8 RLE compression if available", 96 | action="store_true", default=False) 97 | parser.add_option("-H", "--tile-height", dest="tileHeight", 98 | help="set height of metatiles", metavar="HEIGHT", 99 | type="int", default=8) 100 | parser.add_option("-1", dest="planes", 101 | help="set 1bpp mode (default: 2bpp NES)", 102 | action="store_const", const="0", default="0;1") 103 | parser.add_option("-p", "--planes", dest="planes", 104 | help="set the plane map (1bpp: 0) (NES: 0;1) (GB: 0,1) (SMS:0,1,2,3) (TG16/SNES: 0,1;2,3) (MD: 3210)") 105 | parser.add_option("--hflip", dest="hflip", 106 | help="horizontally flip all tiles (most significant pixel on right)", 107 | action="store_true", default=False) 108 | parser.add_option("--little", dest="little", 109 | help="reverse the bytes within each row-plane (needed for GBA and a few others)", 110 | action="store_true", default=False) 111 | parser.add_option("--add", dest="addamt", 112 | help="value to add to each pixel", 113 | type="int", default=0) 114 | parser.add_option("--add0", dest="addamt0", 115 | help="value to add to pixels of color 0 (if different)", 116 | type="int", default=None) 117 | (options, args) = parser.parse_args(argv[1:]) 118 | 119 | tileWidth = int(options.tileWidth) 120 | if tileWidth <= 0: 121 | raise ValueError("tile width '%d' must be positive" % tileWidth) 122 | 123 | tileHeight = int(options.tileHeight) 124 | if tileHeight <= 0: 125 | raise ValueError("tile height '%d' must be positive" % tileHeight) 126 | 127 | # Fill unfilled roles with positional arguments 128 | argsreader = iter(args) 129 | try: 130 | infilename = options.infilename 131 | if infilename is None: 132 | infilename = next(argsreader) 133 | except StopIteration: 134 | raise ValueError("not enough filenames") 135 | 136 | outfilename = options.outfilename 137 | if outfilename is None: 138 | try: 139 | outfilename = next(argsreader) 140 | except StopIteration: 141 | outfilename = '-' 142 | if outfilename == '-': 143 | import sys 144 | if sys.stdout.isatty(): 145 | raise ValueError("cannot write CHR to terminal") 146 | 147 | addamt, addamt0 = options.addamt, options.addamt0 148 | if addamt0 is None: addamt0 = addamt 149 | 150 | return (infilename, outfilename, tileWidth, tileHeight, 151 | options.planes, options.hflip, options.little, 152 | options.packbits, options.use_pb8, addamt, addamt0) 153 | 154 | argvTestingMode = True 155 | 156 | def make_stdout_binary(): 157 | """Ensure that sys.stdout is in binary mode, with no newline translation.""" 158 | 159 | # Recipe from 160 | # http://code.activestate.com/recipes/65443-sending-binary-data-to-stdout-under-windows/ 161 | # via http://stackoverflow.com/a/2374507/2738262 162 | if sys.platform == "win32": 163 | import os, msvcrt 164 | msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) 165 | 166 | def main(argv=None): 167 | import sys 168 | if argv is None: 169 | argv = sys.argv 170 | if (argvTestingMode and len(argv) < 2 171 | and sys.stdin.isatty() and sys.stdout.isatty()): 172 | argv.extend(input('args:').split()) 173 | try: 174 | (infilename, outfilename, tileWidth, tileHeight, 175 | planes, hflip, little, 176 | usePackBits, use_pb8, addamt, addamt0) = parse_argv(argv) 177 | except Exception as e: 178 | sys.stderr.write("%s: %s\n" % (argv[0], str(e))) 179 | sys.exit(1) 180 | 181 | im = Image.open(infilename) 182 | im.load() 183 | if len(im.getbands()) > 1: 184 | print("%s: %s: image must be indexed (not mode %s)" 185 | % (argv[0], infilename, im.mode), file=sys.stderr) 186 | sys.exit(1) 187 | 188 | # Subpalette shift 189 | if addamt or addamt0: 190 | px = bytearray(im.getdata()) 191 | for i in range(len(px)): 192 | thispixel = px[i] 193 | px[i] = thispixel + (addamt if thispixel else addamt0) 194 | im.putdata(px) 195 | 196 | outdata = pilbmp2chr(im, tileWidth, tileHeight, 197 | lambda im: formatTilePlanar(im, planes, hflip, little)) 198 | outdata = b''.join(outdata) 199 | im = None 200 | if use_pb8: 201 | from pb8 import pb8 202 | outdata = pb8(outdata) 203 | elif usePackBits: 204 | from packbits import PackBits 205 | sz = len(outdata) % 0x10000 206 | outdata = PackBits(outdata).flush().tobytes() 207 | outdata = b''.join([chr(sz >> 8), chr(sz & 0xFF), outdata]) 208 | 209 | # Write output file 210 | outfp = None 211 | try: 212 | if outfilename != '-': 213 | outfp = open(outfilename, 'wb') 214 | else: 215 | outfp = sys.stdout 216 | make_stdout_binary() 217 | outfp.write(outdata) 218 | finally: 219 | if outfp and outfilename != '-': 220 | outfp.close() 221 | 222 | if __name__=='__main__': 223 | main() 224 | ## main(['pilbmp2nes.py', '../tilesets/char_pinocchio.png', 'char_pinocchio.chr']) 225 | ## main(['pilbmp2nes.py', '--packbits', '../tilesets/char_pinocchio.png', 'char_pinocchio.pkb']) 226 | -------------------------------------------------------------------------------- /tools/wav2brr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Wave to BRR converter 4 | # Copyright 2014, 2021 Damian Yerrick 5 | # 6 | # Copying and distribution of this file, with or without 7 | # modification, are permitted in any medium without royalty 8 | # provided the copyright notice and this notice are preserved. 9 | # This file is offered as-is, without any warranty. 10 | 11 | from contextlib import closing 12 | import array, wave, sys, os, optparse 13 | try: 14 | import numpy as np 15 | except ImportError: 16 | using_numpy = False 17 | else: 18 | using_numpy = True 19 | 20 | def naive_convolve(fircoeffs, wavdata): 21 | lside = len(fircoeffs) - 1 22 | rside = len(fircoeffs) - lside 23 | return [sum(a * b 24 | for a, b in zip(fircoeffs[max(lside - i, 0):], 25 | wavdata[max(i - lside, 0): i + rside])) 26 | for i in range(len(wavdata) + len(fircoeffs) - 1)] 27 | 28 | convolve = np.convolve if using_numpy else naive_convolve 29 | 30 | def convolve_test(): 31 | fircoeffs = [1/16, 4/16, 6/16, 4/16, 1/16] 32 | data = [0, 1, 0, 0, 0, 1, 2, 3, 4, 5] 33 | print(naive_convolve(fircoeffs, data)) 34 | print(list(np.convolve(fircoeffs, data))) 35 | 36 | def load_wave_as_mono_s16(filename): 37 | little = array.array('H', b'\x01\x00')[0] == 1 38 | with closing(wave.open(filename, "rb")) as infp: 39 | bytedepth = infp.getsampwidth() 40 | if bytedepth not in (1, 2): 41 | raise ValueError("unsupported sampwidth") 42 | n_ch = infp.getnchannels() 43 | datatype = 'h' if bytedepth == 2 else 'B' 44 | freq = infp.getframerate() 45 | length = infp.getnframes() 46 | data = array.array(datatype, infp.readframes(length)) 47 | if datatype == 'B': 48 | # Expand 8 to 16 bit 49 | data = array.array('h', ((c - 128) << 8 for c in data)) 50 | elif not little: 51 | # 16-bit data is little-endian in the wave file; it needs to 52 | # be byteswapped for big-endian platforms 53 | data.byteswap() 54 | if n_ch > 1: 55 | # average all channels 56 | data = array.array('h', (int(round(sum(data[i:i + n_ch]) / n_ch)) 57 | for i in range(0, len(data), n_ch))) 58 | return (freq, data) 59 | 60 | def save_wave_as_mono_s16(filename, freq, data): 61 | data = array.array('h', (min(max(s, -32767), 32767) for s in data)) 62 | little = array.array('H', b'\x01\x00')[0] == 1 63 | if not little: 64 | data.byteswap() 65 | with closing(wave.open(filename, "wb")) as outfp: 66 | outfp.setnchannels(1) 67 | outfp.setsampwidth(2) 68 | outfp.setframerate(freq) 69 | outfp.writeframes(data.tobytes()) 70 | 71 | def brr_preemphasize(wavdata): 72 | """Emphasize a sample's highs to compensate for S-DSP Gaussian interpolation.""" 73 | preem = \ 74 | [0.00417456, -0.017636, 0.03906008, -0.06200069, 0.07113042, 75 | -0.0255472, -0.26998287, 1.52160339, -0.26998287, -0.0255472, 76 | 0.07113042, -0.06200069, 0.03906008, -0.017636, 0.00417456] 77 | return [int(round(s)) for s in convolve(preem, wavdata)[7:-7]] 78 | 79 | def brr_deemphasize(wavdata): 80 | """Approximate the effect of Gaussian interpolation.""" 81 | deem = \ 82 | [372/2048, 1304/2048, 372/2048] 83 | return [int(round(s)) for s in convolve(deem, wavdata)[1:-1]] 84 | 85 | 86 | brr_filters = [ 87 | (0, 0), 88 | (0, 0.9375), 89 | (-0.9375, 1.90625), 90 | (-0.8125, 1.796875) 91 | ] 92 | 93 | def enfilter(wav, coeffs, prevs=[], quant=None): 94 | """Calculates residuals from an IIR predictive filter. 95 | 96 | wav: iterable of sample values 97 | prevs: previous block of quantized and decoded samples 98 | quant: None during test filtering; 2 to 4096 during encoding 99 | 100 | """ 101 | prevs = list(prevs)[-len(coeffs):] 102 | if len(prevs) < len(coeffs): 103 | prevs = ([0] * len(coeffs) + prevs)[-len(coeffs):] 104 | out = [] 105 | for c in wav: 106 | pred = sum(coeff * prev 107 | for coeff, prev in zip(coeffs, prevs[-len(coeffs):])) 108 | rescaled = resid = (c - pred) 109 | if quant: 110 | resid = int(round(resid / quant)) 111 | resid = max(-8, min(7, resid)) 112 | rescaled = resid * quant 113 | out.append(resid) 114 | prevs.append(rescaled + pred) 115 | return out, prevs[-len(coeffs):] 116 | 117 | def defilter(wav, coeffs, prevs=[], quant=1): 118 | """Applies an IIR predictive filter to residuals. 119 | 120 | wav: block of unpacked residuals 121 | prevs: previous block of decoded samples 122 | quant: number by which all residuals shall be multiplied 123 | 124 | """ 125 | prevs = list(prevs)[-len(coeffs):] 126 | if len(prevs) < len(coeffs): 127 | prevs = ([0] * len(coeffs) + prevs)[-len(coeffs):] 128 | out = [] 129 | for resid in wav: 130 | rescaled = resid * quant 131 | pred = sum(coeff * prev 132 | for coeff, prev in zip(coeffs, prevs[-len(coeffs):])) 133 | c = (rescaled + int(round(pred))) 134 | out.append(c) 135 | prevs.append(c) 136 | return out 137 | 138 | CLIP_MAX = 32000 139 | 140 | def encode_brr(wav, looped=False): 141 | prevs = [] 142 | out = bytearray() 143 | for t in range(0, len(wav), 16): 144 | piece = wav[t:t + 16] 145 | 146 | # Occasionally, treble boost may cause a sample to clip. 147 | peak = max(abs(c) for c in piece) 148 | if peak > CLIP_MAX: 149 | ## print("clip peak %d at time %d" % (peak, t)) 150 | piece = [max(min(c, CLIP_MAX), -CLIP_MAX) for c in piece] 151 | 152 | if prevs: 153 | # Calculate the peak residual for this piece with 154 | # each filter, then choose the smallest 155 | trials = [max(abs(resid) 156 | for resid in enfilter(piece, coeffs, prevs)[0]) 157 | for coeffs in brr_filters] 158 | peak, filterid = min((r, i) for (i, r) in enumerate(trials)) 159 | else: 160 | # first block always uses filter 0 161 | peak = max(abs(resid) for resid in piece) 162 | filterid = 0 163 | 164 | logquant = 0 165 | while logquant < 12 and peak >= (7 << logquant): 166 | logquant += 1 167 | resids, prevs = enfilter(piece, brr_filters[filterid], 168 | prevs or [], 1 << (logquant + 0)) 169 | resids.extend([0] * (16 - len(resids))) 170 | byte0 = (logquant << 4) | (filterid << 2) 171 | if t + 16 >= len(wav): 172 | byte0 = byte0 | (3 if looped else 1) 173 | ## print("filter #%d, scale %d,\nresids %s\n%s" 174 | ## % (filterid, 1 << logquant, repr(resids), repr(list(piece)))) 175 | out.append(byte0) 176 | for i in range(0, len(resids), 2): 177 | hinibble = resids[i] & 0x0F 178 | lonibble = resids[i + 1] & 0x0F 179 | out.append((hinibble << 4) | lonibble) 180 | return bytes(out) 181 | 182 | def decode_brr(brrdata): 183 | prevs = [0, 0] 184 | out = [] 185 | for i in range(0, len(brrdata), 9): 186 | piece = bytes(brrdata[i:i + 9]) 187 | logquant = piece[0] >> 4 188 | filterid = (piece[0] >> 2) & 0x03 189 | resids = [((b >> i & 0x0F) ^ 8) - 8 190 | for b in piece[1:] for i in (4, 0)] 191 | decoded = defilter(resids, brr_filters[filterid], 192 | prevs, 1 << (logquant + 0)) 193 | ## print("filter #%d, scale %d,\nresids %s\n%s" 194 | ## % (filterid, 1 << logquant, repr(resids), repr(decoded))) 195 | out.extend(decoded) 196 | prevs = decoded 197 | return out 198 | 199 | # Command line parsing and help ##################################### 200 | # 201 | # Easy is hard. It takes a lot of code and a lot of text to make a 202 | # program self-documenting and resilient to bad input. 203 | 204 | usageText = "usage: %prog [options] [-i] INFILE [-o] OUTFILE" 205 | versionText = """wav2brr 0.04 206 | 207 | Copyright 2014, 2021 Damian Yerrick 208 | Copying and distribution of this file, with or without 209 | modification, are permitted in any medium without royalty provided 210 | the copyright notice and this notice are preserved in all source 211 | code copies. This file is offered as-is, without any warranty. 212 | """ 213 | descriptionText = """ 214 | Audio converter for Super NES S-DSP. 215 | """.strip() 216 | 217 | # 8372 Hz is concert middle C times 32 samples (two BRR blocks) per cycle 218 | DEFAULT_RATE = 8372 219 | 220 | def parse_argv(argv): 221 | parser = optparse.OptionParser(usage=usageText, version=versionText, 222 | description=descriptionText) 223 | parser.add_option("-i", "--input", dest="infilename", 224 | help="read from INFILE", 225 | metavar="INFILE") 226 | parser.add_option("-o", "--output", dest="outfilename", 227 | help="write output to OUTFILE", 228 | metavar="OUTFILE") 229 | parser.add_option("-d", "--decompress", 230 | action="store_true", dest="decompress", default=False, 231 | help="decompress BRR to wave (default: compress wave to BRR)") 232 | parser.add_option("--emphasize", 233 | action="store_true", dest="emph", default=False, 234 | help="read wave, write preemphasized (treble boosted) wave") 235 | parser.add_option("--deemphasize", 236 | action="store_true", dest="deemph", default=False, 237 | help="read wave, write deemphasized wave") 238 | parser.add_option("-r", "--rate", dest="rate", 239 | metavar="RATE", type="int", default=None, 240 | help="sample rate in Hz when writing wave file " 241 | "(default: %d for -d; equal to input rate for --emph and --deemph)" 242 | % (DEFAULT_RATE,)) 243 | parser.add_option("--loop", 244 | action="store_true", dest="loop", default=False, 245 | help="set the BRR's loop bit") 246 | parser.add_option("--skip-filter", 247 | action="store_true", dest="skipfilter", default=False, 248 | help="skip (de)emphasis when (de)compressing") 249 | 250 | (options, pos) = parser.parse_args(argv[1:]) 251 | 252 | if options.rate is not None and not 10 <= options.rate <= 128000: 253 | parser.error("output sample rate must be 10 to 128000 Hz") 254 | 255 | # Fill unfilled roles with positional arguments 256 | pos = iter(pos) 257 | try: 258 | options.infilename = options.infilename or next(pos) 259 | except StopIteration: 260 | parser.error("no input file; try %s --help" 261 | % os.path.basename(sys.argv[0])) 262 | try: 263 | options.outfilename = options.outfilename or next(pos) 264 | except StopIteration: 265 | parser.error("no output file") 266 | 267 | # make sure no trailing arguments 268 | try: 269 | next(pos) 270 | parser.error("too many filenames") 271 | except StopIteration: 272 | pass 273 | return options 274 | 275 | def main(argv=None): 276 | opts = parse_argv(argv or sys.argv) 277 | if opts.emph: 278 | freq, wave = load_wave_as_mono_s16(opts.infilename) 279 | wave = brr_preemphasize(wave) 280 | save_wave_as_mono_s16(opts.outfilename, opts.rate or freq, wave) 281 | elif opts.deemph: 282 | freq, wave = load_wave_as_mono_s16(opts.infilename) 283 | wave = brr_deeemphasize(wave) 284 | save_wave_as_mono_s16(opts.outfilename, opts.rate or freq, wave) 285 | elif opts.decompress: 286 | with open(opts.infilename, 'rb') as infp: 287 | brrdata = infp.read() 288 | out = decode_brr(brrdata) 289 | if not opts.skipfilter: 290 | out = brr_deemphasize(out) 291 | save_wave_as_mono_s16(opts.outfilename, opts.rate or DEFAULT_RATE, out) 292 | else: 293 | freq, wave = load_wave_as_mono_s16(opts.infilename) 294 | if not opts.skipfilter: 295 | wave = brr_preemphasize(wave) 296 | brrdata = encode_brr(wave, looped=opts.loop) 297 | with open(opts.outfilename, 'wb') as outfp: 298 | outfp.write(brrdata) 299 | 300 | if __name__=='__main__': 301 | if "idlelib" in sys.modules: 302 | main() 303 | else: 304 | main() 305 | ## convolve_test() 306 | -------------------------------------------------------------------------------- /tools/zipup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Zipfile packing tool 3 | # 4 | # Copyright 2014-2015 Damian Yerrick 5 | # Copying and distribution of this file, with or without 6 | # modification, are permitted in any medium without royalty 7 | # provided the copyright notice and this notice are preserved. 8 | # This file is offered as-is, without any warranty. 9 | # 10 | import sys 11 | import os 12 | import argparse 13 | import zipfile 14 | import tarfile 15 | 16 | def make_zipfile(outname, filenames, prefix): 17 | with zipfile.ZipFile(outname, "w", zipfile.ZIP_DEFLATED) as z: 18 | for filename in filenames: 19 | z.write(filename, prefix+filename) 20 | 21 | def make_tarfile(outname, filenames, prefix, mode="w"): 22 | with tarfile.open(outname, "w", zipfile.ZIP_DEFLATED) as z: 23 | for filename in filenames: 24 | z.add(filename, prefix+filename) 25 | 26 | def make_tarfile_gz(outname, filenames, prefix): 27 | return make_tarfile(outname, filenames, prefix, mode="w:gz") 28 | 29 | def make_tarfile_bz2(outname, filenames, foldername): 30 | return make_tarfile(outname, filenames, prefix, mode="w:bz2") 31 | 32 | def make_tarfile_xz(outname, filenames, foldername): 33 | return make_tarfile(outname, filenames, prefix, mode="w:xz") 34 | 35 | formathandlers = [ 36 | (".zip", make_zipfile), 37 | (".tar", make_tarfile), 38 | (".tgz", make_tarfile_gz), 39 | (".tar.gz", make_tarfile_gz), 40 | (".tbz", make_tarfile_bz2), 41 | (".tar.bz2", make_tarfile_bz2), 42 | (".txz", make_tarfile_xz), 43 | (".tar.xz", make_tarfile_xz), 44 | ] 45 | 46 | tophelptext = """ 47 | Make a zip or tar archive containing specified files without a tar bomb. 48 | """ 49 | bottomhelptext = """ 50 | 51 | Supported output formats: """+", ".join(x[0] for x in formathandlers) 52 | 53 | def parse_argv(argv): 54 | p = argparse.ArgumentParser( 55 | description=tophelptext, epilog=bottomhelptext 56 | ) 57 | p.add_argument("filelist", 58 | help="name of file containing newline-separated relative " 59 | "paths to files to include, or - for standard input") 60 | p.add_argument("foldername", 61 | help="name of folder in archive (e.g. hello-1.2.5)") 62 | p.add_argument("-o", "--output", 63 | help="path of archive (default: foldername + .zip)") 64 | return p.parse_args(argv[1:]) 65 | 66 | def get_writerfunc(outname): 67 | outbaselower = os.path.basename(outname).lower() 68 | for ext, writerfunc in formathandlers: 69 | if outbaselower.endswith(ext): 70 | return writerfunc 71 | raise KeyError(os.path.splitext(outbaselower)[1]) 72 | 73 | def main(argv=None): 74 | args = parse_argv(argv or sys.argv) 75 | if args.filelist == '-': 76 | filenames = set(sys.stdin) 77 | else: 78 | with open(args.filelist, "r") as infp: 79 | filenames = set(infp) 80 | filenames = set(x.strip() for x in filenames) 81 | filenames = sorted(x for x in filenames if x) 82 | 83 | outname = args.output or args.foldername + ".zip" 84 | writerfunc = get_writerfunc(outname) 85 | writerfunc(outname, filenames, args.foldername+"/") 86 | 87 | if __name__=='__main__': 88 | main() 89 | --------------------------------------------------------------------------------