Skip to content

Commit 7a6d158

Browse files
committed
Replace sox with ffmpeg for audio conversion and filtering
Use ffmpeg for converting audio to raw PCM and for bandpass filtering. The bandpass filter uses ffmpeg's sinc FIR kernel generator with afir convolution, which closely matches sox's sinc filter quality (steep rolloff, flat passband, linear phase). Updates README and CI workflow to reference ffmpeg instead of sox.
1 parent 2f58d19 commit 7a6d158

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=irnorm=-1`,
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)