A port of Audio Defence: Zombie Arena (Somethin' Else, iOS, 2015) to Windows and the Mac: the audio-only shooter you play by listening, turning towards a zombie you can hear and firing before it reaches you. Endless mode, the challenge worlds, the armory, the tarot cards, the Zombiepedia — and the whole of it through a screen reader, because that is how the original was meant to be played.
The original was pulled from the App Store years ago and never came to anything else. It needs a phone you may no longer own, running an OS that will not install it. This port exists so it can be played on a desktop, with a keyboard and NVDA or VoiceOver, by people who cannot see the screen and never needed to.
It is playable from the logo to the last challenge. If you find something that sounds wrong, say so — most of what is fixed in here was found by someone playing it and describing what they heard.
The port is built for a screen reader, not adapted to one afterwards. It speaks through NVDA when NVDA is running; through JAWS, ZDSR, Narrator, ZoomText, System Access, Window-Eyes, PC-Talker, Boy PC Reader or Sense Reader when one of those is, tried in that order (through the Prism library); and through the SAPI 5 voice when none is — nothing else is required, and there is no visual mode worth using. A screen reader started or closed while the game runs is followed within a few seconds. The game is the same whichever speaks: with SAPI 5 you get every screen and the spoken game exactly as an NVDA player does.
That choice is automatic. Settings → Speech → Speech output picks one instead — NVDA, JAWS, ZDSR, Narrator, ZoomText, System Access, Window-Eyes, PC-Talker, Boy PC Reader, Sense Reader or SAPI 5 — with Enter for the next and Shift+Enter for the previous, and Automatic, the default, at the start of the list. The one you pick is the only one used: while it is not running the game is silent, even with another screen reader running. The row itself always tells you what you picked, through the automatic choice when the one you picked cannot speak ("JAWS is not running, so the game will be silent until it is"), so you can step on to another without being left in silence.
While SAPI 5 is what speaks — chosen, or Automatic with no screen reader running — five more rows follow it in the same tab, for SAPI 5 itself. They come and go by themselves as a screen reader starts or closes:
| row | what it does |
|---|---|
| SAPI 5 voice | the voice set in Control Panel (the default), then every installed voice |
| SAPI 5 rate | -10 to 10, starting where Control Panel has it |
| SAPI 5 rate boost | faster again than the rate allows — only for a voice that can, which the game tries once per voice to find out |
| SAPI 5 pitch | -10 to 10, 0 being the voice's own |
| SAPI 5 volume | in steps of 10%, starting where Control Panel has it |
Enter and Shift+Enter change each one, and each change is said in SAPI 5 at the new setting, even while NVDA speaks the rest, so you hear what you chose. Reset all settings puts all five back to Control Panel's.
On the Mac the same page lists Automatic, VoiceOver and System voice: VoiceOver when it is on, the system voice when it is not, and the system voice's own five rows — voice, rate, pitch and volume, with no rate boost — in place of SAPI 5's. See On the Mac.
Every screen the original offered a VoiceOver user is here, read in the order VoiceOver read it, with the same labels and hints, and a good number of places where the original said the wrong thing or nothing at all have been fixed. They are all listed under How faithful this is.
The game can be played in another language. Settings → Miscellaneous → Language chooses it, and the choice is kept with your other settings. English is the default, and a player who never opens that row sees and hears exactly what the port always showed.
What stays English is the recorded audio — the announcer calling out your kills, and the game's own spoken lines. Those are sound files, not text, so no translation can reach them.
A language is a single file, localization/<code>.json: a flat map from the
English phrase to the phrase in that language. Nothing is compiled and no code
is written — the phrases are data.
- run
py tools/make_language.py, with nothing after it. It writeslocalization/template.json, holding every phrase the port can put in front of a player, each one empty, so you have the list rather than having to find it - fill in the empty phrases, in any order. An empty one stays English, so the file works from the first line: what is translated is translated, and the rest is not
- you can hear it while you write it. While
template.jsonis there the game offers it in the Language row as Template, being translated — no code chosen, nothing renamed - when it is ready, rename it to the language's code —
de.json,fr.json,ja.json— and add that code and the language's own name toLANGUAGESinaudiodefence/game/parameters.py, which is what the Language row offers - run
py tools/verify_localization.py, which fails if a phrase a player can reach is still English, so a language cannot quietly fall behind as the port grows. It skipstemplate.json, unfinished by definition; ask for it by name —--language template— to see how far you have got
template.json is never committed: it belongs to whoever is writing it.
Run make_language.py again whenever the port gains text. A file that is
already there keeps every phrase translated and only the new ones arrive empty;
nothing is ever removed.
A language arrives with a release, because the phrase files are built into the game. Adding one to a copy you already have means waiting for the next build.
The layer handles what a flat list cannot: %i and %s are substituted and
the counted word is inflected, which Russian needs (1 монета, 2 монеты,
5 монет), and a line the port assembles from pieces is translated piece by
piece.
This is a vibe-coded project, and it says so plainly because the method is the interesting part.
Nobody had the source. What existed was the shipped iOS app: an arm64 binary,
its NIBs, its property lists, and 915 .m4a files. Everything in
audiodefence/ was written by reading that binary method by method and writing
the Python next to it — Claude (Opus 5, at its highest reasoning effort) did
the reading and the writing, prompted by a human who cannot see the screen and
who tested every line of it by ear.
The rule was: copy what the game does, not what it ought to do. Its bugs included. Every ported method carries the address it was written against, so any line can be checked against the disassembly, and every deliberate departure is written down with the address of the method it came from. That rule is for the game itself; what the port adds to it is listed on its own, under How faithful this is.
The toolchain that made it possible lives in tools/ and its output in
analysis/ — a disassembler, an Objective-C class and selector dump, a digest
view that condenses a method to its sends and branches, a NIB layout reader, a
resource lister, and an extractor for the HRTF that was embedded in the binary.
The game's own binaural impulse responses are in assets/hrtf/, which is why it
sounds like the original rather than like a generic 3D mixer.
Two lessons paid for in bugs, recorded here because they are the whole method in miniature:
- A summary is not the source. The digest tool used to drop ARC lines, which hid
every early
returncompiled as a tail call — and a wave ambience spent weeks muted because of it. Read the listing when control flow matters. - The tester is the instrument. Several faults were only ever visible by ear: music that vanished for a whole round, a sound that played a metre short, an ambience that outlived the game that started it. None of them would have been found by reading code.
game/ holds the original's own data and audio — the narration, the zombies,
the weapons, the music, the playlists that arrange them. That is Somethin'
Else's work, not ours. It is in the repository so the port can run and be
built from a clone, and a build made by compiler.py carries it too.
Nothing of theirs is claimed here, this project is not affiliated with them, and if they want it taken down it comes down.
There are two ways to play, and only one of them needs Python.
The built game is on this repository's Releases page: a folder with
AudioDefence.exe in it, and the game's own data beside it. Unzip it and run
the executable — nothing to install, no Python, 64-bit Windows and a pair of
headphones. readme.html and changelog.txt are in the same folder.
You only have to do that once. From then on the game tells you when a new version is out and installs it for you, downloading only the files that changed — see Updates below.
On the Mac it is the same folder with AudioDefence.app in it instead, in
AudioDefenceMac-<version>.zip on the same release — see
On the Mac.
From source is everything below: the repository as it stands, run with the Python you have. That is the one to take if you want to read the code, change it, or build the executable yourself.
These are for running from source. A built game from Releases needs none of them.
64-bit Python 3.12 or newer on Windows. The simplest way to get it is the Python install manager from python.org, which is what that site now leads with: install it, then
py install 3.14
and py is the command for everything below. A traditional python.org
installer works just as well — it brings py with it — so if you already have
Python, you have what you need.
Then the packages to play:
py -m pip install pygame-ce numpy av comtypes prismatoid
| package | what needs it |
|---|---|
pygame-ce |
the window, the keyboard and the frame loop (audiodefence/__main__.py, ui/) |
numpy |
the audio maths: decoding, mixing, the Freeverb reverb (s3d/) |
av (PyAV) |
decodes the game's .m4a sounds (s3d/decoder.py) — without it no sound plays |
comtypes |
the SAPI 5 voice, used when no screen reader is running (platform/speech.py); skip it if you always play with NVDA |
prismatoid |
Prism, for speech through the screen readers other than NVDA — JAWS, ZoomText, System Access and the rest (platform/speech.py); it brings cffi with it. Without it those players hear the SAPI 5 voice instead; skip it if you always play with NVDA |
Nothing else is imported outside the standard library. OpenAL Soft
(vendor/openal/soft_oal.dll) and the NVDA controller client
(vendor/nvda/nvdaControllerClient64.dll) ship with the repository, so there
is nothing to install for either, and no system OpenAL is used.
One more package is needed only to redo the reverse engineering, never to play:
py -m pip install capstone
| package | what needs it |
|---|---|
capstone |
the arm64 disassembler (tools/disasm.py, and tools/extract_hrtf.py) |
numpy and av are used by the tools as well — numpy by tools/build_hrtf.py
and tools/reverb_calibrate.py, av for the sound durations in
tools/resources.py — but you already have both for playing.
And one more only if you want to build an executable rather than run from source (Building an executable, below):
py -m pip install pyinstaller
py AudioDefence.py
That is the whole story: the logo, the opener, then the main menu. The launcher can be double-clicked, and it takes the options below:
py AudioDefence.py --endless
py AudioDefence.py --challenge tutorial_1
py AudioDefence.py --help
Settings, saves and the log live in %APPDATA%\AudioDefence (on the Mac,
~/Library/Application Support/AudioDefence), in three files:
| file | what is in it |
|---|---|
save.json |
progress: coins, diamonds, weapons, power-ups, missions, challenges, statistics |
settings.json |
control scheme, button mode, turn sensitivity, menu arrows, cursor memory, tutorial text, the announcer, the game volume, menu music volume, the update check and a version you skipped, how strong the vibration and the trigger feel are, and whether hints name keys or controller buttons, and which controller's, and the speech output and SAPI 5's voice, rate, rate boost, pitch and volume |
keys.json |
the key bindings, and each kind of controller's, by its name |
Deleting the folder starts a fresh profile — the first run then begins on Gyro with the default key bindings.
The Mac build is the same port — the same engine, the same game data, the same
OpenAL Soft and the game's own HRTF — with the Mac's own speech, folders and
app. Nothing in the game itself asks which it is on:
audiodefence/platform/host.py holds every choice that differs.
| Windows | Mac | |
|---|---|---|
| the game | AudioDefence.exe, with game/ beside it |
AudioDefence.app, with the game's data inside it |
| speech | NVDA, the other screen readers through Prism, SAPI 5 | VoiceOver, then the system voice |
| settings, saves, log | %APPDATA%\AudioDefence |
~/Library/Application Support/AudioDefence |
| OpenAL Soft | vendor/openal/soft_oal.dll |
vendor/openal-mac/libopenal.dylib |
| release zip | AudioDefence-Win-<version>.zip |
AudioDefenceMac-<version>.zip |
| leaving the game | Alt+F4, or Quit | Cmd+Q, or Quit |
Unzip AudioDefenceMac-<version>.zip and move the AudioDefence folder
somewhere of your own — your Applications folder will do — then open
AudioDefence.app in it. The app is not signed with an Apple developer
certificate, so the first time macOS refuses to open it: open System
Settings → Privacy & Security, and choose Open Anyway for AudioDefence
near the bottom. You only do that once. It runs on Apple silicon Macs.
Move the folder before you first open the game. An app opened where it was downloaded is run by macOS from a temporary copy, and from there it cannot update itself; it tells you so if you ask it to.
With VoiceOver on, the game speaks through it, in your own voice and at your own rate, and a braille display shows the same lines. It speaks to VoiceOver by AppleScript, which VoiceOver allows only when asked to: in VoiceOver Utility → General, tick Allow VoiceOver to be controlled with AppleScript. The first time the game speaks, macOS asks whether AudioDefence may control VoiceOver — say OK. Until both are done the game still speaks, as announcements VoiceOver reads while the game's window has focus, but they can be cut short by VoiceOver's own speech.
VoiceOver's Quick Nav takes the arrow keys for itself, and the game needs them: if the arrows seem to do nothing, press Left and Right arrow together to turn Quick Nav off.
With VoiceOver off, the game speaks with the system voice — the one set in System Settings → Accessibility → Spoken Content — and Settings → Speech lists its voice, rate, pitch and volume, as it lists SAPI 5's on Windows.
In the menus Command works as Control does with the arrows: Command with the menu's arrows goes to the first or last element, and with the other pair to the first or last tab.
The Mac uses uv for Python and the packages,
which the repository's pyproject.toml and uv.lock pin:
brew install uv
uv run AudioDefence.py
uv run makes .venv with the right Python and packages the first time; the
options are the same as on Windows. pygame-ce, numpy and av are the same
three, pyobjc-framework-cocoa takes the place of comtypes and
prismatoid for speech, and PyInstaller comes with them for building.
AudioDefence.command can be double-clicked in Finder to do the same.
uv run compiler.py
or double-click compiler.command. It is the same compiler with the same
menu and flags, less the one-file build, which on a Mac would unpack itself
on every launch. It leaves dist/AudioDefence holding AudioDefence.app,
readme.html, changelog.txt and license.txt, and zips it into
dist/AudioDefenceMac-<version>.zip, keeping the app's links and execute
bits so Finder unzips a working app. The game's data goes inside the app, at
Contents/Resources/game, less the original's iOS executable and its code
signature, and the app is signed again, ad hoc, once it is complete.
Put the Mac zip on the same release as the Windows one, in either order. Each
build's updater takes the zip made for it, by its name. Keep the names exactly
as the compiler makes them. GitHub lists a release's files alphabetically,
not in the order they were uploaded, and a Windows build from before the Mac
port takes the first zip it finds. AudioDefence-Win- sorts before
AudioDefenceMac- because a dash comes before any letter. A Mac zip called
AudioDefence-Mac- would come first, and those older Windows builds would
install it over themselves.
vendor/openal-mac/libopenal.dylib is OpenAL Soft 1.25.2 built for arm64,
the same version as the Windows DLL. tools/build_openal_mac.sh rebuilds it
from source (it needs cmake and the Xcode command line tools), for an Intel
Mac with --arch x86_64.
In the menus, the port stands in for VoiceOver: it reads the elements of a screen the way VoiceOver reads them, with their labels, hints and "button".
| key | what it does in the menus |
|---|---|
| Right / Tab | next element |
| Left / Shift+Tab | previous element |
| Ctrl+Right / Ctrl+Left, Ctrl+Tab / Ctrl+Shift+Tab, End / Home | last / first element |
| Down / Up | next / previous tab or category |
| Ctrl+Down / Ctrl+Up | last / first tab or category |
| Enter | activate |
| Shift+Enter | a row's second action, where it has one |
| Escape / Backspace | back |
| Page Up / Page Down | menu music louder / quieter |
Hold a key that moves one element — the arrows, Tab or Shift+Tab — and it repeats, so a long list can be walked through without tapping. It starts repeating after about a third of a second. Nothing else repeats: a jump to an end has nowhere to go, Enter must act once, and a key held in a game belongs to the game.
In a game, these are the defaults; all of them can be rebound in Settings → Keyboard, and "Restore default keys" puts them back.
| key | what it does in a game |
|---|---|
| Space | fire: tap for a single shot, hold for continuous fire |
| Left Ctrl or Right Ctrl | melee |
| W, or Up arrow under Gesture | next weapon |
| R, or Down arrow under Gesture | reload |
| Left / Right arrow | turn |
| Escape | pause: Resume, Restart challenge (in a challenge), End Game |
| Enter | skip the narration |
| T | read the challenge timer |
Those two follow the control scheme, because the scheme changes what the action is. Under Gesture you are swiping: up switches weapon, down reloads, and the arrows stand for the swipes. Under Button you are pressing the four corner buttons, where nothing is directional, so they are letters — W and R. Rebinding either one changes it for the scheme you are in and leaves the other alone; the rest of the keys are one binding for both.
An action can hold more than one key. On a binding row, Enter adds a key, Shift+Enter replaces every key that action has, and Delete removes the one you added last — an action is never left with no key at all. Key names are spoken the way people say them, so the two Enter keys are "Enter" and "Numpad Enter".
The arrows that move through a screen are a setting: Left and Right by default, or Up and Down if you prefer them, in Settings → Miscellaneous → Menu arrows. Whichever pair you choose, Ctrl with it jumps to the first or last element, and Tab, Shift+Tab, Home and End work either way.
The other pair changes tab. In the armory, Down and Up step through Weapons, Loadout and Powerup without walking to the bottom of the list first, and each one is named before it reads what you land on; Settings opens straight inside Aiming, and the same two keys move it to Controls, Sound, Keyboard and Miscellaneous — there is no list of categories to go through first. Choose Up and Down for moving through a screen and the two swap over, so the tabs land on Left and Right. Ctrl with the tab pair jumps to the first or last tab, exactly as Ctrl with the other pair jumps to the first or last element; Tab stays with the elements, so Ctrl+Tab and Ctrl+Shift+Tab jump to the last and first element from wherever you are. The ends hold rather than wrap, as everywhere else.
Settings speaks every change and clicks like the original's buttons. Escape leaves it; the turn sensitivity and the key bindings live in Aiming and Keyboard. Settings → Miscellaneous → Reset all settings puts every setting back to how a new profile starts, except your key bindings, which Keyboard has its own Restore default keys for. Quit is on the main menu.
A controller works in the menus and in play: a DualSense, a DualShock 4, an Xbox or Switch Pro controller, or most others Windows recognises. Plug it in before or after starting the game; it says so when one connects or goes. The buttons are named as your controller names them — Cross and R2 on a PlayStation pad, A and RT on an Xbox one.
In the menus it stands in for the keys:
| controller | what it does in the menus |
|---|---|
| D-pad or either stick | the arrows: move through the screen, and change tab on the other pair |
| Cross (A) | Enter: activate |
| Square (X) | Shift+Enter: a row's second action |
| Circle (B) | Escape: back |
| L1 / R1 | previous / next row, without reaching for the D-pad |
| Cross (A) held, with a direction or L1 / R1 | the first or last row, or the first or last tab |
| L2 / R2 | menu music quieter / louder |
| Triangle (Y) | Delete, on a key binding row |
In a game:
| controller | what it does in a game |
|---|---|
| either stick, sideways | turn — a small push turns slowly, all the way as fast as the arrow keys |
| D-pad left / right | turn, at the same speed as the arrow keys (Alternate turn left / right — the only turning you can rebind) |
| R2 | fire |
| R1 | melee |
| L1, or a stick flicked up or D-pad up under Gesture | next weapon |
| L2, or a stick flicked down or D-pad down under Gesture | reload |
| Options | pause, and Options again to resume |
| Cross (A) | skip the narration |
| Square | read the challenge timer |
Under Gesture a stick flicked up or down is the swipe, as the Up and Down arrows are on the keyboard; a stick pushed more sideways than up or down only turns, so turning does not switch weapons by accident. Shaking the controller is shaking the phone: under Gesture it swings your melee weapon, as it did in the original, and under Button it does nothing, as it did not. It needs a pad that can feel movement — a DualSense, a DualShock 4 or a Switch Pro.
A controller that can vibrate lets you feel the game:
- the heartbeat when a zombie is close, on every beat and harder the closer it is;
- a hit, as hard as it hurt: a big gun's hit is felt more than a small one's, a melee blow is a heavier thud, and several hits at once are firmer still. A shot the Shield zombie's shield takes is a light knock, and a miss is felt as nothing;
- a kill, whatever did it;
- an explosion — a Farty going off, a rocket, a power-up's blast — harder the closer it is, and the zombies it hurts;
- the tornado's gust pushing the zombies back;
- and your own death, a long heavy rumble when a zombie gets you.
A DualSense on USB goes further: its grips play the game's own heartbeat — the very recording you hear, felt in your hands — and its own knocks, thuds and rumbles for the rest, through the fine haptics PlayStation games use rather than plain rumble. Over Bluetooth Windows does not offer that, and it rumbles like any other pad. On a DualSense the triggers change too while you play: R2 is shaped like a pistol's trigger: a long take-up with nothing in it, then the wall, where it holds; pressing through the wall breaks it and the shot goes at the break, not when you first meet it. Letting it back out to just under the wall is the reset, and it fires again from there, so you can shoot without letting go all the way. Under Button, L2 pulls against a light spring where it reloads. In the menus and on the pause screen they are plain again, and they are set back when the game closes.
Settings → Miscellaneous → Fine haptics (on by default) chooses how a controller that has both plays what you feel: through the fine haptics in its grips, or through its motors like any other pad. Turn it off to feel a DualSense as an ordinary controller. With no such controller connected, the motors are used whatever it says.
Settings → Miscellaneous sets how strong Joystick vibration and the Trigger feel are: Off, Light, Medium (the default) or Strong, with Enter for the next and Shift+Enter for the previous. Each step of Joystick vibration gives a pulse at the new strength so you can feel it, and each step of the Trigger feel puts that feel on the triggers for eight seconds so you can squeeze R2 and try it there and then. The Trigger feel is only for a DualSense — no other controller has one.
Settings → Joystick names the controller that is connected, and lets you change which button does what, the way Keyboard does for keys: Enter (Cross) on an action adds a button, Shift+Enter (Square) replaces them all, Delete (Triangle) removes the last one, and Escape on the keyboard cancels. Next weapon and Reload are set for the control scheme you are in, as on the keyboard. A stick pushed sideways cannot be bound — it turns — and neither can the PS or Xbox button, which Windows or Steam often keeps. Each kind of controller keeps its own buttons, by the name it gives itself — a DualSense's, a DualShock 4's, an Xbox pad's — made from the defaults the first time it is connected, so setting up one never changes another. With two or more kinds connected, the Controller row says which one's buttons are listed ("DualSense Wireless Controller, 1 of 2"), and Enter or Shift+Enter goes to the next or previous. Restore default buttons puts that controller's buttons back, and Reset all settings leaves them alone. With no controller connected the buttons are not listed. If Steam is running it may take the controller over and present it as an Xbox pad; the game still works, but for your own controller's button names, turn Steam Input off for the game.
Settings → Miscellaneous → Names in hints and tutorial chooses whether the hints and the tutorial text name the keyboard's keys (the default) or the connected controller's buttons, as that controller calls them: "Press Cross to copy the results", "use the R2 button to fire your weapon" on a PlayStation pad, "Press A" and "RT" on an Xbox one. Enter or Shift+Enter switches it. So do the other lines that name a key: the tab and category keys ("L1 and R1 change tab, D-pad left and right move through it"), the Button and Gesture rows, and "Press Cross to skip intro". With two or more kinds of controller connected, Controller for names under it chooses which one. With no controller connected the row is dimmed and it is Keyboard keys: starting the game without one, or unplugging the last one wherever you are, sets it back to Keyboard keys and saves that, so choose Controller buttons again when you next play with one.
Every screen at the end of a run has a Copy results button, read straight after the results and before the screen's other buttons: after an endless game, after a challenge you completed, and after one you failed. It puts a short list on the clipboard as plain text, ready to paste into a message, and says "Results copied to clipboard." After an Endless game it looks like this:
Audio Defence Endless Statistics
Tarot cards: Electric Shield, Zombies With Helmets! and Lucky Shot
Coins Earned: 7802
Diamonds Earned: 14
Score: 11882144
Kills: 162
Accuracy: 76.53 %
Survival time: 17:04
Maximum combo: 483
After a challenge the heading is Audio Defence Challenge Statistics, then the challenge's name and whether you completed or failed it, then the stars, rewards and statistics that screen shows. There are no tarot cards in a challenge.
The lines are taken from the screen itself rather than worked out again: each results screen reads exactly these lines, one row each, and only the heading is not on it, since the screen has its own title. The game's version is never in it. The original had one way to share a score, a Twitter sheet, which needed an account and an iPhone; this needs neither.
The failed screen used to tell you nothing at all — the original shows a tip, and two buttons. It now reads your results first, the same kills, accuracy and tim
0 comments
log in to comment.