想定するノイズは、バッファアンダーランのようなノイズ。
検証対象としてはノイズ検出しやすそうな穏やかな曲調のカノン。以下からダウンロード。
classical-sound.seesaa.net
以下のスクリプトで、バッファアンダーランを想定して一部バッファを削除してノイズを想定した音源を作成。
import numpy as np
import librosa
import soundfile as sf
import random
def inject_audio_dropouts(input_path, output_path, num_dropouts=5, dropout_ms=50, dropout_interval_ms=500, random_drop=False, remove=False):
y, sr = librosa.load(input_path, sr=None, mono=True)
samples_per_drop = int(sr * (dropout_ms / 1000.0))
print(f"Loaded audio file: {input_path} with sample rate: {sr} Hz samples_per_drop: {samples_per_drop}")
if random_drop:
for i in range(num_dropouts):
start = random.randint(0, len(y) - samples_per_drop)
if remove:
y = np.concatenate([y[:start], y[start+samples_per_drop:]])
else:
y[start:start+samples_per_drop] = 0
print(f"\rDropout {i+1}/{num_dropouts} at sample {start}", end='')
else:
for i in range(num_dropouts):
start = i * int(sr * (dropout_interval_ms / 1000.0))
if start + samples_per_drop > len(y):
break
if remove:
y = np.concatenate([y[:start], y[start+samples_per_drop:]])
else:
y[start:start+samples_per_drop] = 0
print(f"\rDropout {i+1}/{num_dropouts} at sample {start}", end='')
sf.write(output_path, y, sr)
inject_audio_dropouts("Canon.wav", "Canon_with_dropouts.wav", num_dropouts=10000, dropout_ms=40, dropout_interval_ms=200, random_drop=False, remove=True)
ノイズ検出は以下のコードで行う。スペクトル変化量でノイズ検出する。
import librosa
import numpy as np
import plotly.graph_objects as go
import os
def detect_noise_offline(
input_file,
spectral_threshold=0.02,
frame_length=1024,
hop_length=512
):
"""
WAV/音声ファイルを読み込み、スペクトル変化量でノイズ検出し、
PlotlyでHTMLグラフを保存する。
Parameters
----------
input_file : str
入力音声ファイル(wavなど)
spectral_threshold : float
スペクトル変化量の絶対値閾値
frame_length : int
STFTのフレーム長
hop_length : int
STFTのホップ長
"""
y, sr = librosa.load(input_file, sr=None, mono=True)
S = np.abs(librosa.stft(y, n_fft=frame_length, hop_length=hop_length))
diffs = []
for i in range(1, S.shape[1]):
diff_val = np.mean(np.abs(S[:, i] - S[:, i - 1]))
diffs.append(diff_val)
times = librosa.frames_to_time(np.arange(1, S.shape[1]), sr=sr, hop_length=hop_length)
detections = [t for t, d in zip(times, diffs) if d > spectral_threshold]
fig = go.Figure()
fig.add_trace(go.Scatter(
x=times,
y=diffs,
mode='lines',
name='Spectral Change'
))
fig.add_trace(go.Scatter(
x=[times[0], times[-1]],
y=[spectral_threshold, spectral_threshold],
mode='lines',
line=dict(color='red', dash='dash'),
name='Threshold'
))
fig.add_trace(go.Scatter(
x=detections,
y=[spectral_threshold] * len(detections),
mode='markers',
marker=dict(color='orange', size=8, symbol='x'),
name='Detected Noise'
))
fig.update_layout(
title=f"Spectral Change and Noise Detection - {os.path.basename(input_file)}",
xaxis_title="Time (s)",
yaxis_title="Spectral Difference (abs)",
template="plotly_white"
)
output_html = os.path.splitext(input_file)[0] + ".html"
fig.write_html(output_html)
for det_time in detections:
print(f"Detect! {det_time:.3f}s")
print(f"✅ グラフを保存しました: {output_html}")
return detections
ノイズ有無でのグラフの比較は以下のとおり。ノイズありのほうが値が大きくなっており、0.4あたりを閾値にすればノイズ検出できそう。
ノイズなし オリジナルの音源
ノイズあり
リアルタイムで検出する場合は以下。
import sounddevice as sd
import numpy as np
import librosa
import time
import sys
DEVICE_ID = 1
FRAME_DURATION = 0.05
SPECTRAL_THRESHOLD = 0.13
FRAME_LENGTH = 1024
HOP_LENGTH = 512
prev_spec = None
def audio_callback(indata, frames, time_info, status):
global prev_spec
if status:
print(status, file=sys.stderr)
y = indata[:, 0]
S = np.abs(librosa.stft(y, n_fft=FRAME_LENGTH, hop_length=HOP_LENGTH))
if prev_spec is not None:
diff = np.mean(np.abs(S - prev_spec))
bar = "#" * int(diff * 100)
sys.stdout.write(f"\r[{bar:<50}] {diff:.4f}")
sys.stdout.flush()
if diff > SPECTRAL_THRESHOLD:
print(f"\nDetect! {time.strftime('%H:%M:%S')} diff={diff:.4f}")
prev_spec = S
def main():
print(f"録音開始 (閾値={SPECTRAL_THRESHOLD}, Ctrl+Cで終了)")
with sd.InputStream(
device=DEVICE_ID,
channels=1,
callback=audio_callback,
samplerate=44100,
blocksize=int(44100 * FRAME_DURATION)
):
while True:
time.sleep(0.1)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n終了")