This file describes the syntax of .dspd scripts in tracks-dsp-raider.
The script language is intentionally small and pragmatic. It is designed for:
- connecting to the proxy
- reading and sending DSP commands
- evaluating GUI captures
- simple conditions and loops
- byte/hex inspection
- diff-based decode workflows
- saving results
The DSL supports a modern C/Java-like style and the legacy syntax. Both can be mixed in the same script.
connect();
ensureSession();
handshake();
let pin = "1234";
login(pin);
let resp = readBlock(0x04);
print(resp.payloadHex);
if (resp.command == 0x24) {
print("read block ok");
} else {
print("unexpected response");
}
for (let ch in 0x04..0x07) {
let block = readBlock(ch);
print(block.commandHex);
}
Legacy syntax (end, kebab-case commands, no ;) remains supported.
Examples:
connect
ensure-session
handshake
let resp = read-block 0x04
print $resp.payloadHex
Most commands/functions can be used in:
- modern camelCase, for example
readBlock(),ensureSession(),saveDiffReport() - legacy kebab-case, for example
read-block(),ensure-session(),save-diff-report()
Examples:
readBlock(0x00)
read-block(0x00)
ensureSession()
ensure-session()
- scripts are plain text files
- recommended extension:
.dspd - encoding:
UTF-8
Comments start with # or //.
# This is a comment
connect
// another commentComments inside strings are not treated as comments.
print("Hello # not a comment");A statement can be separated by:
- newline
;
Examples:
connect
statusconnect(); status(); handshake();connect
status
handshakeStrings use double quotes:
print("Hello World");Supported escape sequences inside strings include:
\"\\\n\r\t
Variables are created with let:
let x = 123;
let name = "Test";
let block = 0x10;The last evaluated expression is also stored in _.
Example:
status();
print(_);Supported base values:
nulltruefalse- integers, for example
123 - hex integers, for example
0x2C - decimals, for example
12.5 - strings, for example
"abc"
Variables can be used directly by name:
let x = 10;
print(x);Access can also start with $:
let x = 10;
print($x);Properties can be read from objects:
let s = status();
print(s.sessionActive);
print($s.injectReady);Lists, strings, and byte arrays can be indexed:
let cap = guiEndCapture();
print(cap.frames[0]);
print(cap.frames[0].commandHex);Tokens and strings can use placeholders with ${...}.
let i = 5;
print("Block ${i}");
save-text("out/block-${i}.txt", "Hello");Paths/properties are also possible:
let s = status();
print("ready=${s.injectReady}");Blocks can be closed with } (preferred) or end (legacy).
Used by:
iffor
Examples:
if (true) {
print("ok");
}
if true
print "ok"
end
if (x == 1) {
print("one");
} else if (x == 2) {
print("two");
} else {
print("other");
}
if $x == 1
print "one"
else if $x == 2
print "two"
else
print "other"
end
Supported:
==!=>>=<<=
Examples:
if (x == 10) {
print("ok");
}if ($gain > 0)
print "positive"
endSupported:
andornot
Examples:
if ($a == 1 and $b == 2)
print "both match"
endif ($a == 1 or $b == 2)
print "at least one matches"
endif not ($x == 0)
print "not zero"
endConditions may be parenthesized:
if (($a == 1 and $b == 2) or $c == 3) {
print("ok");
}Syntax:
for <variable> in <start>..<end>
...
endModern style:
for (let i in 0..5) {
print(i);
}Legacy style:
for i in 0..5
print $i
endDescending ranges are also supported:
for i in 5..0
print $i
endConnects to the default proxy:
connect
connect()Or with explicit parameters:
connect("127.0.0.1", 19081, "127.0.0.1", 19082);Legacy style:
connect "127.0.0.1" 19081 "127.0.0.1" 19082disconnect
disconnect()Reads the proxy status.
status
let s = status()
print($s.sessionActive)
print($s.injectReady)Attaches to an existing GUI/DSP session without rebuilding it via reset-session.
This is the recommended mode when the original GUI should stay connected.
connect();
attachSession();
handshake();reset-session
resetSession()ensure-session
ensureSession()clear-frames
clearFrames()Sends:
handshake_initdevice_infosystem_info
handshake
handshake()let r = handshakeInit();
print(r.payloadHex);let r = deviceInfo();
print(r.payloadHex);let r = systemInfo();
print(r.payloadHex);login 1234
login("1234")The PIN must be exactly 4 digits.
let b = readBlock(0x00);
print(b.payloadHex);Allowed range:
0x00to0x1C
send-payload "00 01 03 35 04 01"
tx "00 01 03 35 04 01"
write "00 01 03 35 04 01"With expected response:
send-payload "00 01 02 27 00" expect 0x24write("00 01 03 35 04 01")
write("00 01 02 27 00", 0x24)If an expected command is specified, response matching is strict.
Connects the GUI sniffer to the stream.
gui-connect
guiConnect()or:
guiConnect("127.0.0.1", 19081);gui-disconnect
guiDisconnect()Starts capture without interactive input. The engine arms the sniffer, waits a configurable time window for the GUI action, and then automatically collects until the quiet phase.
Preferred modern style:
let cap = guiCapture("Please perform the GUI action", 15000, 1500, 12000);
print(cap);Parameters:
- optional note text
- optional
actionWindowMs - optional
quietMs - optional
maxWaitMs
Examples:
let cap = guiCapture();
let cap = guiCapture(15000);
let cap = guiCapture("Move one slider now");
let cap = guiCapture("Move one slider now", 15000, 1500, 12000);Legacy style is also supported:
let cap = gui-capture "Move one slider now" 15000 1500 12000guiBeginCapture();
sleep(5000);
let cap = guiEndCapture(1500, 12000);let cap = guiEndCapture();
let cap = guiEndCapture(1500, 12000);assert $x == 10
assert not ($resp == null)If the condition fails, the script aborts with an error.
sleep 500
sleep(500)Unit: milliseconds
print("Hello");
print(resp);
print(resp.payloadHex);Syntax:
save-text <path> <expr>
saveText(path, expr)Examples:
saveText("out/result.txt", "Hello World");save-text "out/block.txt" $resp.payloadHexWrites a formatted diff report to a file and returns the absolute path.
saveDiffReport("out/diff.txt", beforeBytes, afterBytes);Legacy style:
save-diff-report "out/diff.txt" $before $afterWrites all detected read_block responses from a capture to files.
Generated files:
block-XX.hex.txtblock-XX.ascii.txt
Example:
saveCaptureReadBlocks(cap, "out/capture-blocks");Legacy style:
save-capture-read-blocks $cap "out/capture-blocks"The script language supports two styles:
- legacy style
- function style
Both can be mixed.
Examples:
status
read-block 0x00
login 1234
len $cap.frames
hex $resp.payloadExamples:
status()
readBlock(0x00)
login("1234")
len(cap.frames)
hex(resp.payload)These helpers are especially useful for fast DSP decoding.
Returns the read-block index if the value is a read-block response, otherwise null.
Works on:
ProxyResponseSniffedFrame- payload
byte[]
let r = readBlock(0x04);
print(readBlockIndex(r));Looks up the last captured read-block response for the given block index and returns its payload bytes.
Input:
GuiCaptureResult- block index
let payload = readBlockPayload(cap, 0x04);
print(hex(payload));Returns null if not found.
Returns the command byte as integer.
Works on:
ProxyResponseSniffedFrame- payload
byte[]
print(cmd(resp));
print(cmd(frame));Returns payload bytes.
Works on:
ProxyResponseSniffedFrame- payload
byte[]passthrough
let p = payload(resp);
print(hex(p));Returns raw frame bytes.
Works on:
ProxyResponseSniffedFrame- payload
byte[]passthrough
let r = raw(resp);
print(hex(r));Shortcut for payload bytes as hex text.
print(payloadHex(resp));
print(payloadHex(frame));Shortcut for payload bytes as ASCII preview.
print(payloadAscii(resp));
print(payloadAscii(frame));Formats byte-level differences between two byte arrays.
print(diffBytes(before, after));Example output:
0x12: 01 -> 02
0x13: 10 -> 11Formats little-endian 16-bit diff candidates between two byte arrays.
print(diffU16le(before, after));Example output:
u16le@0x12: 280 -> 308 (0x0118 -> 0x0134)Builds a full decode-oriented report:
- before/after length
- byte diffs
- u16le candidates
print(diffReport(before, after));Returns a list of integer offsets that changed.
let offsets = changedOffsets(before, after);
print(offsets);
print(len(offsets));len("abc")
len($bytes)
len($cap.frames)contains("abcdef", "cd")
contains($resp.payloadHex, "2C")startsWith("abcdef", "abc")endsWith("abcdef", "def")upper("abc")
lower("ABC")
trim(" test ")join(split("a,b,c", ","), " | ")at("Hello", 1)
at($list, 0)
at($bytes, 3)split("a,b,c", ",")replace("Hello World", "World", "DSP")Converts hex to byte[].
let p = bytes("00 01 03 35 04 01")Outputs bytes as hex.
hex($resp.payload)
hex($resp.payload, 0, 4)slice($resp.payload, 0, 8)ascii($resp.payload, 0, 8)
ascii($resp.payload, 0, 8, true)In legacy style, trimzero is also possible:
ascii $resp.payload 0 8 trimzerou8($resp.payload, 0)
u16le($resp.payload, 4)
u32le($resp.payload, 8)All multi-byte values are little endian.
Properties:
sessionActiveinjectReadyrawResponse
Example:
let s = status()
print $s.sessionActive
print $s.injectReadyProperties:
rawpayloadchecksumOkcommandcommandHexreadBlockIndexrawHexpayloadHexpayloadAsciirawLenpayloadLen
Example:
let r = readBlock(0x00);
print(r.commandHex);
print(r.readBlockIndex);
print(r.payloadHex);
print(r.payloadLen);Properties:
framestotalFrameswriteCountresponseCountisEmptyfirstFramelastFramefirstWritelastWritefirstResponselastResponsewritesresponsesreadBlockResponses
Example:
let cap = guiEndCapture();
print(cap.totalFrames);
print(cap.lastWrite);
print(cap.readBlockResponses);Properties:
directionframepayloadframeHexpayloadHexpayloadAsciicommandcommandHexreadBlockIndexpayloadLenchecksumOk
Example:
let f = cap.lastWrite;
print(f.commandHex);
print(f.payloadHex);
print(f.frameHex);A byte array also supports property access in paths:
hexasciilencommandcommandHexreadBlockIndex
Example:
let p = bytes("00 01 02 24 04");
print(p.hex);
print(p.commandHex);
print(p.readBlockIndex);firstWrite(cap)
lastWrite(cap)firstResponse(cap)
lastResponse(cap)writes(cap)
responses(cap)captureCount(cap)
captureWriteCount(cap)
captureResponseCount(cap)captureFrame(cap, 0)lastWriteExcluding(cap, 0x40)recentWrites(cap, 10)recentWritesExcluding(cap, 10, 0x40)The following values are considered false:
nullfalse- numeric
0 - empty string
- empty byte array
- empty list
Everything else is true.
Example:
if $resp
print "Response present"
endconnect();
resetSession();
ensureSession();
handshake();
let resp = readBlock(0x00);
print(resp);
print(resp.payloadHex);
print(resp.payloadAscii);connect();
resetSession();
ensureSession();
handshake();
login("1234");
let resp = write("00 01 03 35 04 01");
print(resp);guiConnect();
let cap = guiCapture("Please perform the action in the original GUI now", 15000, 1800, 12000);
print(cap);
print(cap.lastWrite);
print(cap.writes);connect();
resetSession();
ensureSession();
handshake();
for (let i in 0..5) {
let r = readBlock(i);
print("Block ${i}");
print(r.payloadHex);
}connect();
let s = status();
if (s.sessionActive and s.injectReady) {
print("Proxy is ready");
} else {
print("Proxy is not ready");
}let before = bytes("00 01 04 34 04 18 01");
let after = bytes("00 01 04 34 04 34 01");
print(diffBytes(before, after));
print(diffU16le(before, after));
print(diffReport(before, after));
print(changedOffsets(before, after));let before = bytes("00 01 04 34 04 18 01");
let after = bytes("00 01 04 34 04 34 01");
let path = saveDiffReport("out/gain-diff.txt", before, after);
print(path);let cap = guiCapture("Trigger one GUI read action now", 15000, 1500, 12000);
let p = readBlockPayload(cap, 0x04);
if (p != null) {
print(hex(p));
print(p.readBlockIndex);
}guiConnect();
let cap = guiActionCapture("Toggle one GUI value now", 45000, 1200, 6000);
print(cap);
print(lastWriteExcluding(cap, 0x40));guiConnect();
let cap = guiActionCapture("Move the InA gain fader now", 45000, 1200, 6000);
let series = writesByCommandAndChannel(cap, 0x34, 0x00);
print(payloadSeries(series));
print(changingOffsetsAcrossWrites(series));
print(u16Series(series, 5));Use the dedicated Gate capture scripts to identify the real GUI write command before readback localization:
script-example/64-auto-capture-ina-gate-threshold-clean.dspd
script-example/65-auto-capture-ina-gate-attack-clean.dspd
script-example/66-auto-capture-ina-gate-hold-clean.dspd
script-example/67-auto-capture-ina-gate-release-clean.dspdRecommended workflow:
- Move exactly one InA gate control once.
- Inspect
out/clean/.../last-write.txt. - Inspect
recent-interesting-writes.txtfor related command series. - If the GUI emitted read-block responses, inspect the saved
blocks/. - After the Gate write command is known, create focused before/after read-diff scripts like the existing mute, phase, and gain decode scripts.
Event-based capture mode for live decoding.
It:
- waits for the first real GUI write
- ignores command
0x40 - auto-stops after the action becomes quiet
let cap = guiActionCapture("Move one slider now", 45000, 1200, 6000);
print(lastWriteExcluding(cap, 0x40));let gainWrites = writesByCommand(cap, 0x34);let inAGain = writesByCommandAndChannel(cap, 0x34, 0x00);print(payloadSeries(series));print(u16Series(series, 5));print(changingOffsetsAcrossWrites(series));if true
print "ok"Error: block not closed.
for i in 0-5
print $i
endCorrect:
for i in 0..5
print $i
endwrite("00 01 0G")Error: 0G is not valid hex.
print $resp.unknownFieldError: the property does not exist.
let <var> = <expr>connectdisconnectgui-connectgui-disconnectstatusattach-sessionreset-sessionensure-sessionclear-frameshandshakeassert <condition>sleep <ms>save-text <path> <expr>save-diff-report <path> <before> <after>save-capture-read-blocks <capture> <dir>print <expr>
if ...else if ...elsefor i in a..bend{ ... }
==!=>>=<<=
andornot
read-block-indexread-block-payloadcmdpayloadrawpayload-hexpayload-asciidiff-bytesdiff-u16lediff-reportchanged-offsetsgui-action-capturewrites-by-commandwrites-by-command-and-channelpayload-seriesu16-serieschanging-offsets-across-writes
byteshexsliceasciiu8u16leu32lelencontainsstarts-withends-withupperlowertrimjoinatsplitreplace