Skip to content

Instantly share code, notes, and snippets.

@windwakr
Last active December 8, 2025 20:42
Show Gist options
  • Select an option

  • Save windwakr/9a2c1d5a8f0806f00a2a67a02a1ae665 to your computer and use it in GitHub Desktop.

Select an option

Save windwakr/9a2c1d5a8f0806f00a2a67a02a1ae665 to your computer and use it in GitHub Desktop.
generates wav files for use with Wappy Dog (Wappy --> DS)
# Generates wav files for Wappy Dog for Wappy-->DS communication
# Sources: US8814627B2 / Game RE
# The game samples the mic at 33,513,982 / 1519 = ~22063Hz
import itertools
import math
import wave
import os
path = "./wappy wavs"
if not os.path.exists(path):
os.makedirs(path)
def genSquare(rate, freq, length):
wave = []
for i in range(int(length * rate)):
sample = math.sin(2.0 * freq * math.pi * i / rate)
if sample > 0.0:
sample = 128+32
else:
sample = 128-32
wave.append(sample)
return bytearray(wave)
def genSilence(rate, length):
wave = [0x80] * int(length * rate)
return bytearray(wave)
sampleRate = 22063
first = genSquare(sampleRate, 2375, 0.05)
second = genSquare(sampleRate, 2475, 0.05)
third = genSquare(sampleRate, 2675, 0.05)
fourth = genSquare(sampleRate, 2775, 0.05)
fifth = genSquare(sampleRate, 2975, 0.05)
toneList = [first, second, third, fourth, fifth]
permutations = list(itertools.product([1, 2, 3, 4, 5], repeat=3))
for perm in permutations:
with wave.open("%s/%d_%d_%d.wav" % (path, perm[0], perm[1], perm[2]), "wb") as af:
arr = bytearray()
# Output is wrapped in short silence
# short 0.1 second silence between the 0.05 second tones
arr.extend(genSilence(sampleRate, 0.25))
arr.extend(toneList[perm[0]-1])
arr.extend(genSilence(sampleRate, 0.1))
arr.extend(toneList[perm[1]-1])
arr.extend(genSilence(sampleRate, 0.1))
arr.extend(toneList[perm[2]-1])
arr.extend(genSilence(sampleRate, 0.25))
af.setsampwidth(1)
af.setnchannels(1)
af.setframerate(sampleRate)
af.writeframesraw(arr)
@windwakr
Copy link
Author

windwakr commented Dec 6, 2025

Generates 125 permutations of Wappy-->DS outputs. According to the patent this is all possible commands?
Sometimes requires feeding the sample in multiple times before it's recognized. Not sure where the flaw lies. I've tried tweaking the frequencies and timings and it still happens. Real recordings seem to use 0.04 and 0.11 second timings, and XX50 frequencies.
Still need to organize and name the outputs.
wappy

@windwakr
Copy link
Author

windwakr commented Dec 7, 2025

Looking more at the game, it performs an FFT on the mic data, does something, then if it detects a valid signal it writes a 32-bit value(1-125) to 0x02088E60. If you use an emulator's memory viewer to pause the game and place a value there the game will act as if it detected input from Wappy.

@windwakr
Copy link
Author

windwakr commented Dec 7, 2025

A Lua script for DeSmuME to play around with feeding the game inputs. Click the buttons and the game will think that command was sent from the Wappy Dog device.

2025-12-06.22-33-15.mp4
local command = 1
local cooldown = 0
local commandCooldown = 0

local downBox = {x = 03, y = 2-192, width = 13, height = 13}
local upBox = {x = 45, y = 2-192, width = 13, height = 13}
local okBox = {x = 62, y = 2-192, width = 13, height = 13}

function checkInside(x, y, box)
    if x >= box.x and x < (box.x+box.width) then
        if y >= box.y and y < (box.y+box.height) then
            return true
        end
    end
    return false
end

function main()
    keys = input.get()
    gui.box(0, 0-192, 77, 16-192, "#20202080")
    gui.box(20, 2-192, 40, 14-192, "#FFFFFF", "#000000")
    gui.text(21, 5-192, string.format("%3d", command))
    
    gui.box(downBox.x, downBox.y, downBox.x+downBox.width-1, downBox.y+downBox.height-1, "#000000")
    gui.line(downBox.x+2, downBox.y+2, downBox.x+6, downBox.y+downBox.height-3, "#FFFFFF")
    gui.line(downBox.x+6, downBox.y+downBox.height-3, downBox.x+downBox.width-3, downBox.y+2, "#FFFFFF")
    
    gui.box(upBox.x, upBox.y, upBox.x+upBox.width-1, upBox.y+upBox.height-1, "#000000")
    gui.line(upBox.x+2, upBox.y+upBox.height-3, upBox.x+6, upBox.y+2, "#FFFFFF")
    gui.line(upBox.x+6, upBox.y+2, upBox.x+upBox.width-3, upBox.y+upBox.height-3, "#FFFFFF")
    
    gui.box(okBox.x, okBox.y, okBox.x+okBox.width-1, okBox.y+okBox.height-1, "#000000")
    gui.text(okBox.x, okBox.y+3, "OK", "#FFFFFF")
    
    if keys["leftclick"] and keys.xmouse and keys.ymouse then
        if checkInside(keys.xmouse, keys.ymouse, downBox) then
            if command > 1 and cooldown == 0 then
                command = command - 1
                cooldown = 6
            end
        end
        if checkInside(keys.xmouse, keys.ymouse, upBox) then
            if command < 125 and cooldown == 0 then
                command = command + 1
                cooldown = 6
            end
        end
        if checkInside(keys.xmouse, keys.ymouse, okBox) then
            if commandCooldown == 0 then
                memory.writedword(0x02088E60, command)
                commandCooldown = 60
            end
        end
    end
    
    if cooldown > 0 then cooldown = cooldown - 1 end
    if commandCooldown > 0 then commandCooldown = commandCooldown - 1 end
end

emu.registerbefore(main)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment