😽

Node.js でマイクから音声を拾いミュートを判定する

2024/06/06に公開

はじめに

この記事では Node.js でマイクから音声を拾いミュートを判定する記事を紹介します。

作業プロジェクトの準備

TypeScript の簡易プロジェクトを作成します。

長いので折りたたんでおきます。

package.json を作成

package.json を作成します。

$ mkdir -p mute-sample
$ cd mute-sample
$ pnpm init

package.json を変更します。

package.json
{
  "name": "mute-sample",
  "version": "1.0.0",
  "description": "",
- "main": "index.js",
- "scripts": {
-   "test": "echo \"Error: no test specified\" && exit 1"
- },
- "keywords": [],
- "author": "",
- "license": "ISC"
+ "main": "index.ts",
+ "scripts": {
+   "typecheck": "tsc --noEmit",
+   "dev": "vite-node index.ts",
+   "build": "tsc"
+ },
+ "keywords": [],
+ "author": "",
}

TypeScript & vite-node をインストール

TypeScript と vite-node をインストールします。補足としてこちらの理由のため ts-node ではなく vite-node を利用します。

$ pnpm install -D typescript vite-node @types/node

TypeScriptの設定ファイルを作成

tsconfig.json を作成します。

$ npx tsc --init

tsconfig.json を上書きします。

tsconfig.json
{
  "compilerOptions": {
    /* Base Options: */
    "esModuleInterop": true,
    "skipLibCheck": true,
    "target": "ES2022",
    "allowJs": true,
    "resolveJsonModule": true,
    "moduleDetection": "force",
    "isolatedModules": true,

    /* Strictness */
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "checkJs": true,

    /* Bundled projects */
    "noEmit": true,
    "outDir": "dist",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "jsx": "preserve",
    "incremental": true,
    "sourceMap": true,
  },
  "include": ["**/*.ts", "**/*.js"],
  "exclude": ["node_modules", "dist"]
}

git を初期化します。

$ git init

.gitignore を作成します。

$ touch .gitignore
.gitignore
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage

# next.js
/.next/
/out/

# production
/build
dist/

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local
.env

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

動作確認コードを作成

動作を確認するためのコードを作成します。

$ touch index.ts
index.ts
console.log('Hello, World');

型チェック

型チェックします。

$ pnpm run typecheck

動作確認

動作確認を実施します。

$ pnpm run dev

Hello, World

コミットします。

$ git add .
$ git commit -m "初回コミット"

Mute を判定

ミュートを判定するためのコードを作成します。

インストール

$ pnpm install node-record-lpcm16

sox が必要なためインストールします。

$ brew install sox

コードの作成

$ touch demo01.ts
demo01.ts
const record = require('node-record-lpcm16');
import { Buffer } from 'buffer';

const THRESHOLD = 20; // 無音と判定する音量レベルの閾値
const MUTE_DURATION = 3; // ミュートと判定する無音状態が続く秒数
const SAMPLE_RATE = 44100; // サンプリングレート
const BUFFER_SIZE = 4096; // チャンクのサイズ
const CHUNKS_PER_SECOND = SAMPLE_RATE / BUFFER_SIZE; // 1秒間に受け取るチャンクの数

// 無音状態が続いているチャンクの数
let silentChunks = 0;

// 録音データを受け取るインスタンス
const mic = record.record({
  sampleRate: SAMPLE_RATE,
  threshold: 0,
  verbose: false
});

// 録音データを受け取るストリーム
const micInputStream = mic.stream();

// 録音データを受け取る
micInputStream.on('data', (data: Buffer) => {
  // チャンクのデータをInt16Arrayに変換
  const intData = new Int16Array(data.buffer);

  // 音量レベルを計算
  const volumeLevel = intData.reduce((sum, value) => sum + Math.abs(value), 0) / intData.length;

  // 無音状態の判定
  // 音量レベルが閾値未満のときは無音と判定
  if (volumeLevel < THRESHOLD) {
    silentChunks += 1;
  } else {
    silentChunks = 0;
  }

  // ミュート状態の判定
  if (silentChunks > CHUNKS_PER_SECOND * MUTE_DURATION) {
    console.log('Muted');
    // ミュート状態が続いているときの処理
  }
});

// エラー処理
micInputStream.on('error', (err: Error) => {
  console.error('Error: ', err);
});

// ストリーム終了時の処理
mic.start();
console.log('Recording...');

// SIGINTシグナル(Ctrl+Cなど)を受け取ったときに録音を停止
process.on('SIGINT', () => {
  mic.stop();
  console.log('Stopping recording...');
});
$ pnpm vite-node demo01.ts

コミットします。

$ git add .
$ git commit -m "Node.js でマイクから音声を拾いミュートを判定する"

Mute を判定

ミュートを判定するためのコードを作成します。

コードの解説

CHUNKS_PER_SECOND は 1 秒間に受け取るチャンクの数を計算しています。

const THRESHOLD = 20; // 無音と判定する音量レベルの閾値
const MUTE_DURATION = 3; // ミュートと判定する無音状態が続く秒数
const SAMPLE_RATE = 44100; // サンプリングレート
const BUFFER_SIZE = 4096; // チャンクのサイズ
const CHUNKS_PER_SECOND = SAMPLE_RATE / BUFFER_SIZE; // 1秒間に受け取るチャンクの数

無音状態が続いているチャンクの数です。

let silentChunks = 0;

録音データを受け取るインスタンスです。

const mic = record.record({
  sampleRate: SAMPLE_RATE,
  threshold: 0,
  verbose: false
});

録音データを受け取るストリームです。

const micInputStream = mic.stream();

ストリームからデータを受け取る処理です。

micInputStream.on('data', (data: Buffer) => {
  ...
});

ボリュームレベルが閾値未満のときは無音と判定します。MUET_DURATION 秒間無音状態が続いたときにミュートと判定します。

// チャンクのデータをInt16Arrayに変換
const intData = new Int16Array(data.buffer);

// 音量レベルを計算
const volumeLevel = intData.reduce((sum, value) => sum + Math.abs(value), 0) / intData.length;

// 無音状態の判定
// 音量レベルが閾値未満のときは無音と判定
if (volumeLevel < THRESHOLD) {
  silentChunks += 1;
} else {
  silentChunks = 0;
}

// ミュート状態の判定
if (silentChunks > CHUNKS_PER_SECOND * MUTE_DURATION) {
  console.log('Muted');
  // ミュート状態が続いているときの処理
}

エラー処理です。

micInputStream.on('error', (err: Error) => {
  console.error('Error: ', err);
});

録音を開始します。

mic.start();
console.log('Recording...');

SIGINT シグナル(Ctrl+C など)を受け取ったときに録音を停止します。

process.on('SIGINT', () => {
  mic.stop();
  console.log('Stopping recording...');
});

さいごに

この記事では Node.js でマイクから音声を拾いミュートを判定する記事を紹介しました。

作業リポジトリ

作業リポジトリはこちらです。

https://github.com/hayato94087/mute-sample

Discussion