| @@ -24,5 +24,11 @@ web/openai/ | |||||
| web/*-web.zip | web/*-web.zip | ||||
| sdr-visual-suite.zip | sdr-visual-suite.zip | ||||
| # downloaded decoder binaries | |||||
| tools/downloads/ | |||||
| tools/wsjtx/ | |||||
| tools/fldigi/ | |||||
| tools/dsd-neo/ | |||||
| # temp binaries | # temp binaries | ||||
| sdrd.exe~ | sdrd.exe~ | ||||
| @@ -67,7 +67,7 @@ Edit `config.yaml`: | |||||
| - `detector.threshold_db`: power threshold in dB | - `detector.threshold_db`: power threshold in dB | ||||
| - `detector.min_duration_ms`, `detector.hold_ms`: debounce/merge | - `detector.min_duration_ms`, `detector.hold_ms`: debounce/merge | ||||
| - `recorder.*`: enable IQ/audio recording, preroll, output_dir, max_disk_mb | - `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 | ## APIs | ||||
| ### Config API | ### Config API | ||||
| @@ -570,7 +570,11 @@ func main() { | |||||
| http.Error(w, "meta read failed", http.StatusInternalServerError) | http.Error(w, "meta read failed", http.StatusInternalServerError) | ||||
| return | 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 { | if err != nil { | ||||
| http.Error(w, res.Stderr, http.StatusInternalServerError) | http.Error(w, res.Stderr, http.StatusInternalServerError) | ||||
| return | return | ||||
| @@ -32,8 +32,8 @@ recorder: | |||||
| decoder: | decoder: | ||||
| ft8_cmd: "tools/ft8/ft8_decoder --iq {iq} --sample-rate {sr}" | ft8_cmd: "tools/ft8/ft8_decoder --iq {iq} --sample-rate {sr}" | ||||
| wspr_cmd: "tools/wspr/wspr_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}" | fsk_cmd: "tools/fsk/fsk_decoder --iq {iq} --sample-rate {sr}" | ||||
| psk_cmd: "tools/psk/psk_decoder --iq {iq} --sample-rate {sr}" | psk_cmd: "tools/psk/psk_decoder --iq {iq} --sample-rate {sr}" | ||||
| web_addr: ":8080" | web_addr: ":8080" | ||||
| @@ -3,6 +3,7 @@ package decoder | |||||
| import ( | import ( | ||||
| "errors" | "errors" | ||||
| "os/exec" | "os/exec" | ||||
| "strconv" | |||||
| "strings" | "strings" | ||||
| ) | ) | ||||
| @@ -12,14 +13,20 @@ type Result struct { | |||||
| Code int `json:"code"` | 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. | // 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 == "" { | if cmdTemplate == "" { | ||||
| return Result{}, errors.New("decoder command empty") | return Result{}, errors.New("decoder command empty") | ||||
| } | } | ||||
| cmdStr := strings.ReplaceAll(cmdTemplate, "{iq}", iqPath) | 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) | parts := strings.Fields(cmdStr) | ||||
| if len(parts) == 0 { | if len(parts) == 0 { | ||||
| return Result{}, errors.New("invalid decoder command") | 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) | cmd.Args = append(cmd.Args, "--iq", iqPath) | ||||
| } | } | ||||
| if !strings.Contains(cmdTemplate, "{sr}") { | 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() | out, err := cmd.CombinedOutput() | ||||
| res := Result{Stdout: string(out), Code: 0} | res := Result{Stdout: string(out), Code: 0} | ||||
| @@ -40,29 +47,3 @@ func Run(cmdTemplate string, iqPath string, sampleRate int) (Result, error) { | |||||
| } | } | ||||
| return res, nil | 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) | |||||
| } | |||||
| @@ -18,7 +18,13 @@ func (m *Manager) runDecodeIfConfigured(mod string, iqPath string, sampleRate in | |||||
| if cmd == "" { | if cmd == "" { | ||||
| return | 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 { | if err != nil { | ||||
| return | return | ||||
| } | } | ||||
| @@ -6,9 +6,9 @@ import ( | |||||
| "sdr-visual-suite/internal/decoder" | "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 == "" { | if cmd == "" { | ||||
| return decoder.Result{}, errors.New("decoder command empty") | return decoder.Result{}, errors.New("decoder command empty") | ||||
| } | } | ||||
| return decoder.Run(cmd, iqPath, sampleRate) | |||||
| return decoder.Run(cmd, iqPath, sampleRate, audioPath) | |||||
| } | } | ||||
| @@ -10,9 +10,18 @@ Examples (Windows): | |||||
| - `tools/fsk/fsk_decoder.bat` | - `tools/fsk/fsk_decoder.bat` | ||||
| - `tools/psk/psk_decoder.bat` | - `tools/psk/psk_decoder.bat` | ||||
| Each script should accept: | |||||
| Each script should accept either IQ or audio: | |||||
| ``` | ``` | ||||
| --iq <path> --sample-rate <sr> | --iq <path> --sample-rate <sr> | ||||
| ``` | ``` | ||||
| Or: | |||||
| ``` | |||||
| --audio <path> --sample-rate <sr> | |||||
| ``` | |||||
| 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 | |||||