Skip to content

Commit 71e5658

Browse files
authored
Merge pull request #2 from PRX/feat/replace-sox-with-ffmpeg
Replace sox with ffmpeg for audio conversion and filtering
2 parents 2f58d19 + 99ea214 commit 71e5658

File tree

4 files changed

+39
-44
lines changed

4 files changed

+39
-44
lines changed

.github/workflows/node.js.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ jobs:
2020

2121
steps:
2222
- uses: actions/checkout@v3
23-
- name: Install sox and multimon-ng
23+
- name: Install ffmpeg and multimon-ng
2424
run: |
2525
sudo apt-get update
26-
sudo apt-get install -y sox multimon-ng
26+
sudo apt-get install -y ffmpeg multimon-ng
2727
- name: Use Node.js ${{ matrix.node-version }}
2828
uses: actions/setup-node@v4
2929
with:

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ Supports full and **partial** match detection — even if only the attention ton
99
## Requirements
1010

1111
- Node.js >= 18
12-
- [SoX](https://sox.sourceforge.net/)
13-
- [multimon-ng](https://github.com/EliasOenal/multimon-ng)
12+
- [FFmpeg](https://ffmpeg.org/) (`brew install ffmpeg`)
13+
- [multimon-ng](https://github.com/EliasOenal/multimon-ng) (build from source or `apt install multimon-ng`)
1414

1515
## Install
1616

@@ -20,7 +20,7 @@ npm install @prx.org/eas-detect
2020

2121
## CLI Usage
2222

23-
Call on any audio file, it will use sox to analyze the file:
23+
Call on any audio file, it will use ffmpeg to analyze the file:
2424

2525
```bash
2626
npx eas-detect recording.wav
@@ -177,7 +177,7 @@ Three detection methods run on the audio:
177177

178178
1. **Attention tone detection**[Goertzel algorithm](https://en.wikipedia.org/wiki/Goertzel_algorithm) tuned to 853 Hz and 960 Hz (the EAS dual-tone attention signal), using energy-ratio analysis. Also runs on bandpass-filtered audio (800-1000 Hz) to catch tones buried under speech or music.
179179
2. **FSK detection** — Detects the FSK modulation pattern (mark at 2083.3 Hz, space at 1562.5 Hz). The default mode uses Goertzel energy ratios with mark/space alternation validation. The sensitive mode applies narrow bandpass filters around each frequency and detects anti-correlation between the mark and space energy envelopes using Pearson correlation.
180-
3. **SAME header decoding**Pipes audio through [SoX](https://sox.sourceforge.net/) into [multimon-ng](https://github.com/EliasOenal/multimon-ng) for full SAME protocol decoding with error correction.
180+
3. **SAME header decoding**Converts audio via [FFmpeg](https://ffmpeg.org/) and pipes into [multimon-ng](https://github.com/EliasOenal/multimon-ng) for full SAME protocol decoding with error correction.
181181

182182
Decoded SAME headers are parsed and enriched with human-readable lookups from 3,235 FIPS county codes and all standard EAS originator/event codes.
183183

src/audio.js

Lines changed: 32 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ const SAMPLE_RATE = 22050;
77

88
/**
99
* Read an audio file and return mono PCM samples as a Float64Array at 22050 Hz.
10-
* Uses sox for format conversion. Also accepts raw s16le PCM if `raw` option is set.
10+
* Uses ffmpeg for format conversion. Also accepts raw s16le PCM if `raw` option is set.
1111
*
12-
* For non-raw input, sox writes to a temp raw file allowing files of any length
12+
* For non-raw input, ffmpeg writes to a temp raw file allowing files of any length
1313
* to be processed. The temp file path is returned as `rawPath` so downstream
1414
* consumers (e.g., multimon-ng) can reuse it without re-converting.
1515
*
@@ -29,20 +29,20 @@ export function readAudio(filePath, { raw = false, sampleRate = SAMPLE_RATE } =
2929
const dir = mkdtempSync(join(tmpdir(), "eas-"));
3030
rawPath = join(dir, "audio.raw");
3131

32-
execFileSync("sox", [
32+
execFileSync("ffmpeg", [
33+
"-i",
3334
filePath,
34-
"-t",
35-
"raw",
36-
"-e",
37-
"signed-integer",
38-
"-b",
39-
"16",
40-
"-r",
35+
"-f",
36+
"s16le",
37+
"-acodec",
38+
"pcm_s16le",
39+
"-ar",
4140
String(sampleRate),
42-
"-c",
41+
"-ac",
4342
"1",
43+
"-y",
4444
rawPath,
45-
]);
45+
], { stdio: ["pipe", "pipe", "pipe"] });
4646
buf = readFileSync(rawPath);
4747
}
4848

@@ -57,9 +57,9 @@ export function readAudio(filePath, { raw = false, sampleRate = SAMPLE_RATE } =
5757
}
5858

5959
/**
60-
* Apply a sox bandpass filter to a raw PCM file and return filtered samples.
61-
* Used to isolate EAS frequency bands before energy-ratio analysis, improving
62-
* detection of tones buried under speech or music.
60+
* Apply a bandpass filter to a raw PCM file and return filtered samples.
61+
* Uses ffmpeg's sinc FIR filter generator with afir convolution to produce
62+
* a sharp bandpass response equivalent to sox's sinc filter.
6363
*
6464
* @param {string} rawPath - Path to s16le 22050 Hz mono raw PCM file
6565
* @param {number} lowFreq - Lower edge of the bandpass filter in Hz
@@ -71,33 +71,28 @@ export function bandpassFilter(rawPath, lowFreq, highFreq, sampleRate = SAMPLE_R
7171
const dir = mkdtempSync(join(tmpdir(), "eas-bp-"));
7272
const filteredPath = join(dir, "filtered.raw");
7373

74+
// Generate a FIR bandpass kernel with ffmpeg's sinc filter (hp + lp cutoffs),
75+
// then apply it to the audio with afir. This matches sox's sinc filter quality:
76+
// steep rolloff, flat passband, linear phase.
7477
try {
75-
execFileSync("sox", [
76-
"-t",
77-
"raw",
78-
"-e",
79-
"signed-integer",
80-
"-b",
81-
"16",
82-
"-r",
78+
execFileSync("ffmpeg", [
79+
"-f",
80+
"s16le",
81+
"-ar",
8382
String(sampleRate),
84-
"-c",
83+
"-ac",
8584
"1",
85+
"-i",
8686
rawPath,
87-
"-t",
88-
"raw",
89-
"-e",
90-
"signed-integer",
91-
"-b",
92-
"16",
93-
"-r",
94-
String(sampleRate),
95-
"-c",
96-
"1",
87+
"-filter_complex",
88+
`sinc=hp=${lowFreq}:lp=${highFreq}:r=${sampleRate}[ir];[0:a][ir]afir`,
89+
"-f",
90+
"s16le",
91+
"-acodec",
92+
"pcm_s16le",
93+
"-y",
9794
filteredPath,
98-
"sinc",
99-
`${lowFreq}-${highFreq}`,
100-
]);
95+
], { stdio: ["pipe", "pipe", "pipe"] });
10196

10297
const buf = readFileSync(filteredPath);
10398
const sampleCount = buf.length / 2;

src/fsk-detect-sensitive.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* alternate at the baud rate. Music harmonics tend to rise and fall together
88
* (positive correlation).
99
*
10-
* Uses narrow bandpass filters (±50 Hz) via sox to isolate each FSK frequency,
10+
* Uses narrow bandpass filters (±50 Hz) via ffmpeg to isolate each FSK frequency,
1111
* then computes energy envelopes and Pearson correlation over sliding regions.
1212
* The bandpass improves sensitivity by removing competing frequencies that
1313
* would dilute the correlation signal.

0 commit comments

Comments
 (0)