diff --git a/.gitignore b/.gitignore index 773257b..1ff733b 100644 --- a/.gitignore +++ b/.gitignore @@ -24,5 +24,11 @@ web/openai/ web/*-web.zip sdr-visual-suite.zip +# downloaded decoder binaries +tools/downloads/ +tools/wsjtx/ +tools/fldigi/ +tools/dsd-neo/ + # temp binaries sdrd.exe~ diff --git a/README.md b/README.md index 76c93e4..49ab1ed 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ Edit `config.yaml`: - `detector.threshold_db`: power threshold in dB - `detector.min_duration_ms`, `detector.hold_ms`: debounce/merge - `recorder.*`: enable IQ/audio recording, preroll, output_dir, max_disk_mb -- `decoder.*`: external decode commands (use `{iq}` and `{sr}` placeholders) +- `decoder.*`: external decode commands (use `{iq}`, `{audio}`, `{sr}` placeholders) ## APIs ### Config API diff --git a/cmd/sdrd/main.go b/cmd/sdrd/main.go index be82f64..280b458 100644 --- a/cmd/sdrd/main.go +++ b/cmd/sdrd/main.go @@ -570,7 +570,11 @@ func main() { http.Error(w, "meta read failed", http.StatusInternalServerError) return } - res, err := recorder.DecodeOnDemand(cmd, filepath.Join(base, "signal.cf32"), meta.SampleRate) + audioPath := filepath.Join(base, "audio.wav") + if _, errStat := os.Stat(audioPath); errStat != nil { + audioPath = "" + } + res, err := recorder.DecodeOnDemand(cmd, filepath.Join(base, "signal.cf32"), meta.SampleRate, audioPath) if err != nil { http.Error(w, res.Stderr, http.StatusInternalServerError) return diff --git a/config.yaml b/config.yaml index 69d1757..972fb68 100644 --- a/config.yaml +++ b/config.yaml @@ -32,8 +32,8 @@ recorder: decoder: ft8_cmd: "tools/ft8/ft8_decoder --iq {iq} --sample-rate {sr}" wspr_cmd: "tools/wspr/wspr_decoder --iq {iq} --sample-rate {sr}" - dmr_cmd: "tools/dmr/dmr_decoder --iq {iq} --sample-rate {sr}" - dstar_cmd: "tools/dstar/dstar_decoder --iq {iq} --sample-rate {sr}" + dmr_cmd: "tools/dsd-neo/bin/dsd-neo.exe -fs -i {audio} -s {sr} -o null" + dstar_cmd: "tools/dsd-neo/bin/dsd-neo.exe -fd -i {audio} -s {sr} -o null" fsk_cmd: "tools/fsk/fsk_decoder --iq {iq} --sample-rate {sr}" psk_cmd: "tools/psk/psk_decoder --iq {iq} --sample-rate {sr}" web_addr: ":8080" diff --git a/internal/decoder/decoder.go b/internal/decoder/decoder.go index 8bb15e5..33b3a2d 100644 --- a/internal/decoder/decoder.go +++ b/internal/decoder/decoder.go @@ -3,6 +3,7 @@ package decoder import ( "errors" "os/exec" + "strconv" "strings" ) @@ -12,14 +13,20 @@ type Result struct { Code int `json:"code"` } -// Run executes an external decoder command. If cmdTemplate contains {iq} or {sr}, they are replaced. +// Run executes an external decoder command. If cmdTemplate contains {iq}/{sr}/{audio}, they are replaced. // Otherwise, --iq and --sample-rate args are appended. -func Run(cmdTemplate string, iqPath string, sampleRate int) (Result, error) { +func Run(cmdTemplate string, iqPath string, sampleRate int, audioPath string) (Result, error) { if cmdTemplate == "" { return Result{}, errors.New("decoder command empty") } cmdStr := strings.ReplaceAll(cmdTemplate, "{iq}", iqPath) - cmdStr = strings.ReplaceAll(cmdStr, "{sr}", intToString(sampleRate)) + cmdStr = strings.ReplaceAll(cmdStr, "{sr}", strconv.Itoa(sampleRate)) + if strings.Contains(cmdTemplate, "{audio}") { + if audioPath == "" { + return Result{}, errors.New("audio path required for decoder") + } + cmdStr = strings.ReplaceAll(cmdStr, "{audio}", audioPath) + } parts := strings.Fields(cmdStr) if len(parts) == 0 { return Result{}, errors.New("invalid decoder command") @@ -29,7 +36,7 @@ func Run(cmdTemplate string, iqPath string, sampleRate int) (Result, error) { cmd.Args = append(cmd.Args, "--iq", iqPath) } if !strings.Contains(cmdTemplate, "{sr}") { - cmd.Args = append(cmd.Args, "--sample-rate", intToString(sampleRate)) + cmd.Args = append(cmd.Args, "--sample-rate", strconv.Itoa(sampleRate)) } out, err := cmd.CombinedOutput() res := Result{Stdout: string(out), Code: 0} @@ -40,29 +47,3 @@ func Run(cmdTemplate string, iqPath string, sampleRate int) (Result, error) { } return res, nil } - -func intToString(v int) string { - return strings.TrimSpace(strings.TrimPrefix(strings.TrimPrefix(strings.TrimPrefix(strings.TrimSpace(strings.TrimSpace(strings.TrimSpace((func() string { return string(rune('0')) })()))), ""), ""), "")) + itoa(v) -} - -func itoa(v int) string { - if v == 0 { - return "0" - } - n := v - if n < 0 { - n = -n - } - buf := make([]byte, 0, 12) - for n > 0 { - buf = append(buf, byte('0'+n%10)) - n /= 10 - } - if v < 0 { - buf = append(buf, '-') - } - for i, j := 0, len(buf)-1; i < j; i, j = i+1, j-1 { - buf[i], buf[j] = buf[j], buf[i] - } - return string(buf) -} diff --git a/internal/recorder/decode.go b/internal/recorder/decode.go index acc1063..f35562b 100644 --- a/internal/recorder/decode.go +++ b/internal/recorder/decode.go @@ -18,7 +18,13 @@ func (m *Manager) runDecodeIfConfigured(mod string, iqPath string, sampleRate in if cmd == "" { return } - res, err := decoder.Run(cmd, iqPath, sampleRate) + audioPath := "" + if v, ok := files["audio"]; ok { + if name, ok := v.(string); ok { + audioPath = filepath.Join(dir, name) + } + } + res, err := decoder.Run(cmd, iqPath, sampleRate, audioPath) if err != nil { return } diff --git a/internal/recorder/decode_on_demand.go b/internal/recorder/decode_on_demand.go index 96fdaa2..ff26cb5 100644 --- a/internal/recorder/decode_on_demand.go +++ b/internal/recorder/decode_on_demand.go @@ -6,9 +6,9 @@ import ( "sdr-visual-suite/internal/decoder" ) -func DecodeOnDemand(cmd string, iqPath string, sampleRate int) (decoder.Result, error) { +func DecodeOnDemand(cmd string, iqPath string, sampleRate int, audioPath string) (decoder.Result, error) { if cmd == "" { return decoder.Result{}, errors.New("decoder command empty") } - return decoder.Run(cmd, iqPath, sampleRate) + return decoder.Run(cmd, iqPath, sampleRate, audioPath) } diff --git a/tools/README.md b/tools/README.md index d9c920f..c2d8d6a 100644 --- a/tools/README.md +++ b/tools/README.md @@ -10,9 +10,18 @@ Examples (Windows): - `tools/fsk/fsk_decoder.bat` - `tools/psk/psk_decoder.bat` -Each script should accept: +Each script should accept either IQ or audio: ``` --iq --sample-rate ``` +Or: +``` +--audio --sample-rate +``` + +The app replaces `{iq}`, `{sr}`, and `{audio}` placeholders in config commands. -The app replaces `{iq}` and `{sr}` placeholders in config commands. +Downloaded: +- WSJT-X installer: tools/wsjtx/wsjtx-2.7.0-win64.exe +- fldigi installer: tools/fldigi/fldigi-latest.exe +- dsd-neo binary: tools/dsd-neo/bin/dsd-neo.exe