== Creating Assets JoeyLib does not load PNGs, WAVs or MODs at run time. Every asset is converted on the build host into a compact binary the target can use directly, which is what keeps loading fast and the runtime small. This chapter covers each asset type: what you author, which tool bakes it, and which API call loads it. === The Pipeline at a Glance [cols="2,2,2,3", options="header"] |=== |You author |Tool |Baked file |Loaded by |Indexed PNG tile sheet |`assetbake.py --type tile` |`.tbk` |`jlTileBankLoad`, `jlTileBankLoadToSurface` |Indexed PNG sprite sheet |`assetbake.py --type sprite` |`.spr` |`jlSpriteBankLoad` |A `.spr` |`spritebake` |`.spc` |`jlSpriteBankLoadPrecompiled` |Text note notation |`songbake.py` |`.jym` or a C header |`jlMusicPlay` |Protracker `.MOD` |`joeymod` |`.mod`, `.amod`, `.ntp` |`jlAudioPlayMod` |Raw signed 8-bit PCM |(none -- copied) |`.sfx` |`jlAudioPlaySfx` |=== Baked assets are staged into `DATA/` beside the executable, and the program opens them with bare names. === Graphics: Tiles and Sprites ==== Authoring the PNG Both tile and sprite sheets start as an *indexed* PNG with strict rules: * Mode `P` (indexed / palette), *not* RGB or RGBA. * At most *16 colours*. * Width and height both *multiples of 8*. * Palette entry 0 is the transparent / background colour. Remember the library forces it to black on screen. Any decent pixel editor can produce this. In GIMP: *Image > Mode > Indexed*, "Generate optimum palette" with maximum 16 colours, and no dithering -- dithering invents colours and will push you over the limit. Lay the artwork out on a grid. A tile sheet is read as 8x8 cells row by row; a sprite sheet is read as cells of whatever size you declare with `--cell`, also row by row. ==== Baking Tiles [source,console] ---- $ python3 tools/assetbake/assetbake.py --type tile --target atarist \ font.png build/atarist/bin/DATA/font.tbk ---- Tile banks are *per-target*, because the 8x8 block layout is the platform's native pixel format -- plane-major on Amiga and X68000, row-major-with-planes- per-row on ST, chunky 4bpp on DOS and IIgs. You must bake one `.tbk` per platform, and the loader refuses a file baked for a different target rather than drawing garbage. `--target` is required for tiles, and accepts `amiga`, `atarist`, `dos`, `iigs` or `x68000`. ==== Baking Sprites [source,console] ---- $ python3 tools/assetbake/assetbake.py --type sprite --cell 2x3 \ player.png build/atarist/bin/DATA/player.spr ---- `--cell` gives the cel size *in tiles, not pixels*, so `2x3` means 16x24 pixels. The sheet is cut into cels row by row. Sprite `.spr` banks are *cross-target*: the same file works on every platform, because the sprite walker reads chunky 4bpp and converts at draw time. Bake once, ship everywhere. Both formats can embed the 16-entry `$0RGB` palette, and both loaders will hand it back through their `outPalette` parameter so artwork carries its own colours rather than depending on whatever the stage happens to have set. ==== Pre-Compiling Sprites A `.spr` still needs compiling into native blitter code at run time. For a large bank that is a visible startup pause. `spritebake` does it offline: [source,console] ---- $ build/tools/spritebake-atarist player.spr player.spc ---- The result is a *per-platform* `.spc` holding pre-emitted native routines. `jlSpriteBankLoadPrecompiled` copies them straight into the codegen arena and skips compilation entirely. A cel too big for the arena silently loads uncompiled rather than failing, so this is a safe drop-in replacement. The `spritebake` binaries are built per target by `make/tools.mk`, because each one links the emitter for its own CPU while running on the build host. There is a third option if you would rather ship only the small cross-platform `.spr`: compile once on first run and cache the result with `jlSpriteBankSavePrecompiled`, then load the cache on later runs. That trades a one-time startup cost for a much smaller distribution. === Music: the Portable Chip Tracker This is the recommended music path. One authored song plays on all five ports at register-write cost, with no CPU mixing -- which matters enormously on the ST, where `.MOD` playback is only marginally viable. Songs are plain text, one row per line: [source] ---- ; Four bars of Am - F - C - G. tickms 110 loop ; voice0 voice1 voice2 noise A-2:2 A-4:5 --- N16:5 --- C-5:5 E-5:3 off A-3:3 E-4:5 off N2:7 --- A-4:5 C-5:3 off ---- The columns are three tone voices plus an optional fourth noise column. [cols="1,4", options="header"] |=== |Cell |Meaning |`C-4:3` |Note on: note name, octave, then `:` and an attenuation 0..14 where 0 is loudest. Sharps use `#`, as in `C#4:3`. Range `C-0` to `B-7`. The `:atten` part may be omitted and defaults to 0. |`@440:3` |Note on at an exact frequency in Hz, for chip-accurate transcription where equal-tempered note names would round the pitch. |`---` |Hold: this voice is unchanged for this row. |`off` |Silence this voice. |`N4:3` |Noise on (fourth column only): pitch 0..31, where 0 is the brightest hiss and 31 the lowest rumble, plus attenuation. |=== Directives: `tickms N` sets milliseconds per row and is required before the first row (later occurrences change tempo mid-song); `loop` marks the next row as the loop-back target; `repeat N` ... `endrepeat` repeats a block, and may be nested. Bake it either as a binary blob or directly as a C array: [source,console] ---- $ python3 tools/songbake/songbake.py song.song song.jym $ python3 tools/songbake/songbake.py -c gDemoSong song.song song.h ---- The C header form is usually what you want for a small song: it compiles into the program and needs no file I/O at all. [WARNING] ==== On the Atari ST the noise generator shares channel C's volume register with tone voice 2. Author noise hits and voice-2 notes so they do not sound at the same time; `songbake` warns when they overlap. ==== === Music: Protracker Modules For richer music, author a four-channel `.MOD` in any tracker (ProTracker, MilkyTracker, OpenMPT). Then convert per target: [source,console] ---- $ tools/joeymod/joeymod song.mod song.mod # DOS and ST: validated copy $ tools/joeymod/joeymod song.mod song.amod # Amiga $ tools/joeymod/joeymod song.mod song.ntp # Apple IIgs ---- The output extension selects the conversion: * *`.mod`* -- a passthrough copy with the signature validated. DOS and ST detect song end inside libxmp-lite, so the file is used as-is. * *`.amod`* -- the same, plus an `E8FF` stop marker injected at song end. The Amiga replayer, PTPlayer, has no native song-end signal, so without this marker `jlAudioPlayMod(..., loop=false)` loops anyway. Other replayers ignore `E8FF`, so an `.amod` is harmless if it reaches the wrong port. * *`.ntp`* -- NinjaTrackerPlus format for the IIgs, produced by the NinjaTrackerPlus converter the installer's `ntp` component provides (part of `toolchain:iigs`; it needs the PHP command-line interpreter, which the installer's host packages include). Keep modules modest. A four-channel mix is real CPU work on DOS and the ST, and the sample data has to fit in memory alongside your game. === Sound Effects Sound effects are *raw signed 8-bit PCM* with no header at all -- just sample bytes. Convert from a WAV with any audio editor, or on the command line: [source,console] ---- $ sox input.wav -r 11025 -c 1 -b 8 -e signed-integer output.sfx ---- Then play it, telling the library what rate it was recorded at: [source,console] ---- jlAudioPlaySfx(4, sfxBytes, sfxLen, 11025); ---- Choose a rate that suits the machines: 8 to 11 kHz is plenty for game effects and keeps the data small, which matters when the whole thing must fit on a floppy. Two things to know. Prefer *slot 4* if music is playing, because the IIgs maps tracker tone voices onto slots 0 to 2 and the noise voice onto slot 3. And the IIgs plays a sample value of -128 as -127, because its DOC reads the corresponding byte as a wave-end marker -- one level out of 256, inaudible in practice, but it explains a tiny waveform difference if you ever measure one. For long sounds such as speech, use `jlAudioPlaySfxStream` with a fill callback instead of holding the whole sample in memory. === Screens and Backgrounds A whole 320x200 screen can be saved and reloaded in target-native form: [source,c] ---- jlSurfaceSaveFile(surface, "title.sur"); jlSurfaceLoadFile(surface, "title.sur"); ---- The file is 32000 bytes of pixels, 200 SCB bytes and 512 bytes of palette. It is *not* portable between platforms -- no format conversion happens, so the machine that wrote it must be the machine that reads it. It is best used as a runtime cache (a generated title screen, say), not as a shipped asset. For shipped artwork, bake a tile bank and compose the screen with `jlTileBankLoadToSurface`. === Staging Assets into the Build Baked files must end up in `DATA/` next to the executable. Each port's makefile does this with an explicit rule. The simple copy case, from `make/atarist.mk`: [source,makefile] ---- DATA_DIR := $(BINDIR)/DATA DATA_FILES := $(DATA_DIR)/test.mod $(DATA_DIR)/test.sfx $(DATA_DIR)/test.mod: $(REPO_DIR)/assets/test.mod @mkdir -p $(DATA_DIR) cp $< $@ ---- And a bake rule looks like: [source,makefile] ---- $(DATA_DIR)/font.tbk: $(MY_ASSETS)/font.png @mkdir -p $(DATA_DIR) python3 $(REPO_DIR)/tools/assetbake/assetbake.py --type tile --target atarist $< $@ ---- Add the baked files to the port's `all` target so they are built alongside the binaries. Remember that tile banks and `.spc` files need one rule *per platform*, while `.spr`, `.jym` and `.sfx` files are shared. `examples/tilebank` is a working reference: every port's makefile bakes the same PNG for its own target into `DATA/tilebank` (its `TILEBANK_DATA` rule), and the program loads that bank both ways and shows the loader refusing the other targets' bakes. Its `scripts/verify--tilebank.sh` gates run it on every port. === Loading Assets at Run Time [source,c] ---- // Tiles into a caller-owned array. jlTileT tiles[256]; bool valid[256]; uint16_t palette[SURFACE_COLORS_PER_PALETTE]; uint16_t n = jlTileBankLoad("font.tbk", tiles, 256, valid, palette); // Or stream a big bank straight onto a surface, with no caller buffer. uint16_t m = jlTileBankLoadToSurface("font.tbk", fontSurface, palette); // Sprites, pre-compiled. jlSpriteT *cels[32]; uint16_t count = jlSpriteBankLoadPrecompiled("player.spc", cels, 32, palette); // Apply the palette the artwork shipped with. jlPaletteSet(stage, 0, palette); ---- Prefer `jlTileBankLoadToSurface` for anything large. A 1000-tile font sheet is 32000 bytes, which no IIgs program can hold in a local array or in bank-0 BSS, and the streaming loader needs only a small internal scratch buffer. === The Logo The JoeyLib logo lives in `assets/` as both the original 320x200 PNG and a traced SVG. If the source art ever changes, regenerate the vector with: [source,console] ---- $ python3 -m venv /tmp/tracevenv $ /tmp/tracevenv/bin/pip install potracer pillow numpy $ /tmp/tracevenv/bin/python tools/traceLogo.py \ assets/joeylib_logo_-_320x200.png assets/joeylibLogo.svg --colors 5 ---- Five colour clusters measured as the closest match to the original artwork; more clusters start carving the antialiased edges into separate layers, which renders as noise rather than detail.