Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ jobs:

steps:
- uses: actions/checkout@v3
- name: Install sox and multimon-ng
- name: Install ffmpeg and multimon-ng
run: |
sudo apt-get update
sudo apt-get install -y sox multimon-ng
sudo apt-get install -y ffmpeg multimon-ng
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ Supports full and **partial** match detection — even if only the attention ton
## Requirements

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

## Install

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

## CLI Usage

Call on any audio file, it will use sox to analyze the file:
Call on any audio file, it will use ffmpeg to analyze the file:

```bash
npx eas-detect recording.wav
Expand Down Expand Up @@ -177,7 +177,7 @@ Three detection methods run on the audio:

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.
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.
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.
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.

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

Expand Down
69 changes: 32 additions & 37 deletions src/audio.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ const SAMPLE_RATE = 22050;

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

execFileSync("sox", [
execFileSync("ffmpeg", [
"-i",
filePath,
"-t",
"raw",
"-e",
"signed-integer",
"-b",
"16",
"-r",
"-f",
"s16le",
"-acodec",
"pcm_s16le",
"-ar",
String(sampleRate),
"-c",
"-ac",
"1",
"-y",
rawPath,
]);
], { stdio: ["pipe", "pipe", "pipe"] });
buf = readFileSync(rawPath);
}

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

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

// Generate a FIR bandpass kernel with ffmpeg's sinc filter (hp + lp cutoffs),
// then apply it to the audio with afir. This matches sox's sinc filter quality:
// steep rolloff, flat passband, linear phase.
try {
execFileSync("sox", [
"-t",
"raw",
"-e",
"signed-integer",
"-b",
"16",
"-r",
execFileSync("ffmpeg", [
"-f",
"s16le",
"-ar",
String(sampleRate),
"-c",
"-ac",
"1",
"-i",
rawPath,
"-t",
"raw",
"-e",
"signed-integer",
"-b",
"16",
"-r",
String(sampleRate),
"-c",
"1",
"-filter_complex",
`sinc=hp=${lowFreq}:lp=${highFreq}:r=${sampleRate}[ir];[0:a][ir]afir`,
"-f",
"s16le",
"-acodec",
"pcm_s16le",
"-y",
filteredPath,
"sinc",
`${lowFreq}-${highFreq}`,
]);
], { stdio: ["pipe", "pipe", "pipe"] });

const buf = readFileSync(filteredPath);
const sampleCount = buf.length / 2;
Expand Down
2 changes: 1 addition & 1 deletion src/fsk-detect-sensitive.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* alternate at the baud rate. Music harmonics tend to rise and fall together
* (positive correlation).
*
* Uses narrow bandpass filters (±50 Hz) via sox to isolate each FSK frequency,
* Uses narrow bandpass filters (±50 Hz) via ffmpeg to isolate each FSK frequency,
* then computes energy envelopes and Pearson correlation over sliding regions.
* The bandpass improves sensitivity by removing competing frequencies that
* would dilute the correlation signal.
Expand Down
Loading