Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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=irnorm=-1`,
"-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