Upload 6 files
Browse files- .gitignore +6 -0
- README.md +96 -5
- app.py +439 -0
- auxiva_online.py +169 -0
- requirements.txt +6 -0
- sample_code_c5_2.py +158 -0
.gitignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*
|
| 2 |
+
!.gitignore
|
| 3 |
+
!REAMDE.md
|
| 4 |
+
!requirements.txt
|
| 5 |
+
!.gitattributes
|
| 6 |
+
!*.py
|
README.md
CHANGED
|
@@ -1,13 +1,104 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 6.
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: mit
|
| 11 |
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
---
|
| 2 |
+
title: iPhoneBlindSourceSeparation
|
| 3 |
+
emoji: 🏢
|
| 4 |
+
colorFrom: pink
|
| 5 |
+
colorTo: yellow
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 6.0.1
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: mit
|
| 11 |
---
|
| 12 |
+
# iPhoneBlindSourceSeparation
|
| 13 |
+
このデモでは、iPhoneで撮影したステレオ録音付きの録画から**Independent Vector Analysis (IVA)**によるブラインド音源分離を体験できます。
|
| 14 |
+
|
| 15 |
+
## ⚠️ 注意
|
| 16 |
+
- 動画アップロードによって生じるトラブルは責任を負いかねます
|
| 17 |
+
|
| 18 |
+
## 📱 対応機種 (要件)
|
| 19 |
+
|
| 20 |
+
ステレオ録音機能に対応しているiOSデバイス:
|
| 21 |
+
- **iPhone**: XS / XR / SE (第2世代 以降) / 11 以降
|
| 22 |
+
- iOS 16以降
|
| 23 |
+
|
| 24 |
+
> ⚠️ **注記**: 対応機種一覧は参考情報です。
|
| 25 |
+
> 対応機種以外でも,サンプル音声を用いてブラインド音源分離を体験できます.
|
| 26 |
+
|
| 27 |
+
## 🎥 ステレオ録音の設定方法 (事前準備)
|
| 28 |
+
|
| 29 |
+
本デモでは標準カメラアプリで録画すると,ステレオ録音が行えることを利用します.
|
| 30 |
+
iPhoneでステレオ音源付きで録画する場合、以下の手順で設定を確認してください:
|
| 31 |
+
|
| 32 |
+
1. **設定** → **カメラ** を開く
|
| 33 |
+
2. **ビデオ録画** セクションで **ステレオ** を有効にする
|
| 34 |
+
|
| 35 |
+
詳細は [Apple公式ガイド](https://support.apple.com/ja-jp/guide/iphone/iph31c1ca6c7/ios#:~:text=%E6%B3%A8%E8%A8%98:%20iPhone%2016%E4%BB%A5%E9%99%8D%E3%81%A7%E3%81%AF,%E3%82%92%E5%8F%82%E7%85%A7%E3%81%97%E3%81%A6%E3%81%8F%E3%81%A0%E3%81%95%E3%81%84%E3%80%82) をご参照ください。
|
| 36 |
+
|
| 37 |
+
## 🎬 録画時の配置と条件
|
| 38 |
+
|
| 39 |
+
このアプリケーションで高精度に音源分離するため、以下の条件で録画することをお勧めします:
|
| 40 |
+
|
| 41 |
+
### 推奨される配置
|
| 42 |
+
```
|
| 43 |
+
話者B(カメラ側) 話者A(撮影者側)
|
| 44 |
+
△ ---- [iPhone (話者Aが持つ)] ---- △
|
| 45 |
+
```
|
| 46 |
+
- 各話者の中心にiPhoneがあることが重要です.
|
| 47 |
+
- ステレオ録音で録画する際, {前面上部のマイク, 背面カメラ付近のマイク}の2つが使われてる可能性が高いです (経験則). なので, 上の図を描き変えるとこのようなセットアップになります.
|
| 48 |
+
```
|
| 49 |
+
話者B(カメラ側) 話者A(撮影者側)
|
| 50 |
+
△ ---- [前面上部のマイク] -- [背面カメラ付近のマイク] ---- △
|
| 51 |
+
```
|
| 52 |
+
- 録画ボタンを押して自由に発話してください. お互いが別々のことを話してる形式でも,対話形式でも構いません.
|
| 53 |
+
|
| 54 |
+
### 音源分離の成功条件
|
| 55 |
+
- ✅ 2つの音源(話者の発話)がステレオの左右チャンネルに比較的均等に分布している.
|
| 56 |
+
- ✅ 話者やiPhone(マイク)の位置がほぼ固定で、大きな動きがない
|
| 57 |
+
- ✅ 背景雑音が少ない環境である(背景雑音があっても音声分離自体は可能ですが,背景雑音自体の抑圧はできてません. マイク数を多くすると雑音抑圧も可能になりますが,デモでは2マイクの場合のみ想定しています)
|
| 58 |
+
|
| 59 |
+
### 分離が上手くいかない場合
|
| 60 |
+
|
| 61 |
+
1. **STFT窓長を変える**
|
| 62 |
+
- 特に,残響時間が長い場合は窓長を例えばサンプリング周波数の0.6倍にすると上手く分離できるかもしれません.ただし,窓長を過剰に長くしすぎると分離性能が劣化する可能性があります.
|
| 63 |
+
|
| 64 |
+
1. **オンライン処理を使用する**
|
| 65 |
+
- 処理モードを「オンライン (Online AuxIVA)」に切り替える
|
| 66 |
+
- これにより、特に話者やiPhoneに大きな動きがある場合を(ある程度)考慮できます.
|
| 67 |
+
- 逆に,時不変(=話者やマイクが動いてない)な状況を想定しているのにオンライン処理で上手く分離ができない場合は,まずオフライン処理で上手く分離できるか確かめると良いでしょう.
|
| 68 |
+
- 話者やマイクの動きが多い場合は忘却係数を調整してみてください. 値が低いほど過去の情報を忘却するようになります.
|
| 69 |
+
|
| 70 |
+
## 🚀 使い方
|
| 71 |
+
|
| 72 |
+
### Webアプリケーション (Hugging Face)
|
| 73 |
+
1. ページを開く
|
| 74 |
+
2. **上段**: 動画ファイルをアップロード、または **サンプル混合音声を生成・再生** ボタンを押す
|
| 75 |
+
3. **中央**: 処理パラメータを設定
|
| 76 |
+
- サンプリング周波数を計算してSTFT窓長を自動設定
|
| 77 |
+
- 処理モード(オフライン/オンライン)
|
| 78 |
+
- (オンライン処理時)忘却係数を調整
|
| 79 |
+
4. **分離開始** ボタンを押す
|
| 80 |
+
5. **結果**: 混合音声と分離音声の波形、抽出された音声ファイルが表示されます
|
| 81 |
+
|
| 82 |
+
### ローカルモード (gradioがinstallされてない環境でも動作可)
|
| 83 |
+
```bash
|
| 84 |
+
python app.py
|
| 85 |
+
```
|
| 86 |
+
実行時に以下のパラメータを対話形式で入力します:
|
| 87 |
+
- ビデオファイルパス(またはサンプル���成)
|
| 88 |
+
- STFT窓長
|
| 89 |
+
- 処理モード(オンライン/オフライン)
|
| 90 |
+
- パラメータ(忘却係数)
|
| 91 |
+
|
| 92 |
+
## License & Credits
|
| 93 |
+
|
| 94 |
+
以下を使わせて頂きました:
|
| 95 |
+
- pyroomacoustics (https://github.com/LCAV/pyroomacoustics)
|
| 96 |
+
- Pythonで学ぶ音源分離 (https://github.com/masahitotogami/python_source_separation)
|
| 97 |
+
- CMU ARCTIC Concatenated 15s (https://zenodo.org/records/3066489)
|
| 98 |
+
- J. Kominek and A. W. Black, “CMU ARCTIC databases for speech synthesis,” CMU-LTI-03-177, 2003.
|
| 99 |
+
|
| 100 |
+
デモに使用したIVA
|
| 101 |
+
1. N. Ono, *Stable and fast update rules for independent vector analysis based on auxiliary function technique,* Proc. IEEE, WASPAA, pp. 189-192, Oct. 2011.
|
| 102 |
+
1. T. Taniguchi, N. Ono, A. Kawamura, and S. Sagayama, *An auxiliary-function approach to online independent vector analysis for real-time blind source separation,* Proc. HSCMA, pp. 107-111, May. 2014.
|
| 103 |
|
| 104 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
app.py
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import subprocess
|
| 3 |
+
import tempfile
|
| 4 |
+
import sample_code_c5_2
|
| 5 |
+
import numpy as np
|
| 6 |
+
import soundfile as sf
|
| 7 |
+
import pyroomacoustics as pra
|
| 8 |
+
from auxiva_online import auxiva_online
|
| 9 |
+
import matplotlib
|
| 10 |
+
matplotlib.use('Agg') # Hugging Face Spaces対応(ディスプレイなし環境)
|
| 11 |
+
import matplotlib.pyplot as plt
|
| 12 |
+
from pdb import set_trace as test
|
| 13 |
+
from scipy import signal
|
| 14 |
+
import uuid
|
| 15 |
+
import shutil
|
| 16 |
+
import atexit
|
| 17 |
+
|
| 18 |
+
# ===== 実行環境判定: Hugging Face / Local =====
|
| 19 |
+
try:
|
| 20 |
+
import gradio as gr
|
| 21 |
+
USE_GRADIO = True
|
| 22 |
+
except ImportError:
|
| 23 |
+
USE_GRADIO = False
|
| 24 |
+
|
| 25 |
+
# ===== デフォルト値(Hugging Faceもローカルも共通) =====
|
| 26 |
+
DEFAULT_WIN_LEN = 1024
|
| 27 |
+
DEFAULT_ALPHA = 0.9
|
| 28 |
+
|
| 29 |
+
# ===== ffmpegで音声抽出(サンプリング周波数は指定しない) =====
|
| 30 |
+
def extract_audio(input_video, output_audio):
|
| 31 |
+
cmd = ["ffmpeg", "-i", input_video, "-vn", output_audio]
|
| 32 |
+
subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
| 33 |
+
|
| 34 |
+
# ===== サンプル混合データ作成 =====
|
| 35 |
+
def create_sample_audio(audio_path):
|
| 36 |
+
sample_code_c5_2.main(audio_output=audio_path)
|
| 37 |
+
|
| 38 |
+
# ===== オフライン IVA音源分離 =====
|
| 39 |
+
def separate_sources(audio_path, win_len, alpha=0.95, use_online=False, session_dir=None):
|
| 40 |
+
x, sr = sf.read(audio_path)
|
| 41 |
+
hop = win_len // 2
|
| 42 |
+
|
| 43 |
+
_, _, X = signal.stft(x.T, sr, nperseg=win_len, noverlap=hop)
|
| 44 |
+
if use_online:
|
| 45 |
+
Y = auxiva_online(X.transpose((2,1,0)), alpha=alpha)
|
| 46 |
+
else:
|
| 47 |
+
Y = pra.bss.auxiva(X.transpose((2,1,0)), n_iter=50)
|
| 48 |
+
_, y = signal.istft(Y.transpose((2,1,0)), sr, nperseg=win_len, noverlap=hop)
|
| 49 |
+
|
| 50 |
+
# セッションディレクトリにファイルを保存
|
| 51 |
+
if session_dir is None:
|
| 52 |
+
session_dir = "."
|
| 53 |
+
|
| 54 |
+
out1 = os.path.join(session_dir, "separated_1.wav")
|
| 55 |
+
out2 = os.path.join(session_dir, "separated_2.wav")
|
| 56 |
+
sf.write(out1, y[0], sr)
|
| 57 |
+
sf.write(out2, y[1], sr)
|
| 58 |
+
|
| 59 |
+
return (out1, out2), y, sr
|
| 60 |
+
|
| 61 |
+
# ===== 波形プロット(混合音声と分離音声を一緒に表示) =====
|
| 62 |
+
def plot_waveform(mixed_data, separated_data):
|
| 63 |
+
fig, ax = plt.subplots(2, 2, figsize=(12, 6))
|
| 64 |
+
|
| 65 |
+
# 混合音声
|
| 66 |
+
for i in range(2):
|
| 67 |
+
ax[0, i].plot(mixed_data[:, i], alpha=0.7, label=f"Mixed Channel {i+1}")
|
| 68 |
+
ax[0, i].set_title(f"Mixed Audio - Channel {i+1}")
|
| 69 |
+
ax[0, i].set_ylabel("Amplitude")
|
| 70 |
+
ax[0, i].legend()
|
| 71 |
+
|
| 72 |
+
# 分離音声
|
| 73 |
+
for i in range(2):
|
| 74 |
+
ax[1, i].plot(separated_data[i], alpha=0.7, label=f"Separated {i+1}", color="orange")
|
| 75 |
+
ax[1, i].set_title(f"Separated Audio {i+1}")
|
| 76 |
+
ax[1, i].set_ylabel("Amplitude")
|
| 77 |
+
ax[1, i].set_xlabel("Time")
|
| 78 |
+
ax[1, i].legend()
|
| 79 |
+
|
| 80 |
+
plt.tight_layout()
|
| 81 |
+
return fig
|
| 82 |
+
|
| 83 |
+
# ===== サンプリング周波数を計算(動画優先、サンプルからフォールバック) =====
|
| 84 |
+
def get_sampling_rate(video_file, sample_file):
|
| 85 |
+
"""動画ファイルまたはサンプル音声からサンプリング周波数を取得(優先度: video > sample)"""
|
| 86 |
+
# 優先順位: video_file > sample_file
|
| 87 |
+
target = video_file or sample_file
|
| 88 |
+
if not target:
|
| 89 |
+
return "**サンプリング周波数**: 動画またはサンプルが必要です", DEFAULT_WIN_LEN
|
| 90 |
+
|
| 91 |
+
try:
|
| 92 |
+
# Gradio Audio/Video コンポーネントの形式を処理
|
| 93 |
+
target_path = None
|
| 94 |
+
|
| 95 |
+
# Audio コンポーネントの場合、タプル (sample_rate, audio_data) を返すことがある
|
| 96 |
+
if isinstance(target, tuple) and len(target) == 2:
|
| 97 |
+
# (sample_rate, audio_data) の形式
|
| 98 |
+
sr = target[0]
|
| 99 |
+
target_path = None # パスは不要、既にサンプリング周波数がある
|
| 100 |
+
elif isinstance(target, dict):
|
| 101 |
+
# Video/Audio コンポーネントの辞書形式
|
| 102 |
+
target_path = target.get("name") or target.get("path")
|
| 103 |
+
else:
|
| 104 |
+
# ファイルパス文字列
|
| 105 |
+
target_path = target
|
| 106 |
+
|
| 107 |
+
# パスから読み込む場合
|
| 108 |
+
if target_path is not None:
|
| 109 |
+
if not os.path.exists(target_path):
|
| 110 |
+
return "**サンプリング周波数**: ファイルが見つかりません", DEFAULT_WIN_LEN
|
| 111 |
+
|
| 112 |
+
# まず soundfile で読み込みを試みる
|
| 113 |
+
try:
|
| 114 |
+
data, sr = sf.read(target_path)
|
| 115 |
+
except Exception:
|
| 116 |
+
# ffmpegで音声抽出して読み直す
|
| 117 |
+
tmp = "__tmp_extracted_audio.wav"
|
| 118 |
+
try:
|
| 119 |
+
subprocess.run(["ffmpeg", "-y", "-i", target_path, "-vn", "-acodec", "pcm_s16le", tmp], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 120 |
+
data, sr = sf.read(tmp)
|
| 121 |
+
except Exception as e:
|
| 122 |
+
print(f"Error extracting audio for sampling rate: {e}")
|
| 123 |
+
return "**サンプリング周波数**: 読み取りに失敗しました", DEFAULT_WIN_LEN
|
| 124 |
+
finally:
|
| 125 |
+
if os.path.exists(tmp):
|
| 126 |
+
try:
|
| 127 |
+
os.remove(tmp)
|
| 128 |
+
except Exception:
|
| 129 |
+
pass
|
| 130 |
+
|
| 131 |
+
sr_text = f"**サンプリング周波数**: {sr} Hz"
|
| 132 |
+
candidates = np.array([256, 512, 1024, 2048, 4096, 8192, 16384])
|
| 133 |
+
target_win = int(sr * 0.2)
|
| 134 |
+
closest = int(candidates[np.argmin(np.abs(candidates - target_win))])
|
| 135 |
+
return sr_text, closest
|
| 136 |
+
except Exception as e:
|
| 137 |
+
return f"**サンプリング周波数**: エラー ({str(e)})", DEFAULT_WIN_LEN
|
| 138 |
+
|
| 139 |
+
# ===== メイン処理 (動画 → 分離) =====
|
| 140 |
+
def process(video_file, win_len=DEFAULT_WIN_LEN, alpha=DEFAULT_ALPHA, use_online=False, session_dir=None):
|
| 141 |
+
if session_dir is None:
|
| 142 |
+
session_dir = "."
|
| 143 |
+
|
| 144 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 145 |
+
audio_path = os.path.join(tmpdir, "audio.wav")
|
| 146 |
+
|
| 147 |
+
# video_file が Gradio Audio コンポーネントからのタプル (sr, audio_data) の場合に対応
|
| 148 |
+
if isinstance(video_file, tuple) and len(video_file) == 2:
|
| 149 |
+
sr_audio, audio_data = video_file
|
| 150 |
+
# タプル形式の場合は直接ファイルに書き込む
|
| 151 |
+
sf.write(audio_path, audio_data, sr_audio)
|
| 152 |
+
elif video_file is not None and os.path.exists(video_file):
|
| 153 |
+
extract_audio(video_file, audio_path)
|
| 154 |
+
else:
|
| 155 |
+
create_sample_audio(audio_path)
|
| 156 |
+
|
| 157 |
+
# 混合音声データを読み込む
|
| 158 |
+
mixed_data, sr = sf.read(audio_path)
|
| 159 |
+
|
| 160 |
+
(file1, file2), sep_data, sr = separate_sources(audio_path, win_len, alpha, use_online, session_dir=session_dir)
|
| 161 |
+
fig = plot_waveform(mixed_data, sep_data)
|
| 162 |
+
|
| 163 |
+
# 混合音声をファイルに保存(セッションディレクトリ)
|
| 164 |
+
# - ステレオで保存するファイルと、チャンネルごとのファイルを作成
|
| 165 |
+
mixed_out = os.path.join(session_dir, "mixed_audio.wav")
|
| 166 |
+
mixed_out_ch1 = os.path.join(session_dir, "mixed_audio_ch1.wav")
|
| 167 |
+
mixed_out_ch2 = os.path.join(session_dir, "mixed_audio_ch2.wav")
|
| 168 |
+
|
| 169 |
+
try:
|
| 170 |
+
if mixed_data.ndim == 1:
|
| 171 |
+
# モノラル入力はそのまま保存
|
| 172 |
+
sf.write(mixed_out, mixed_data, sr)
|
| 173 |
+
sf.write(mixed_out_ch1, mixed_data, sr)
|
| 174 |
+
sf.write(mixed_out_ch2, mixed_data, sr)
|
| 175 |
+
else:
|
| 176 |
+
# 複数チャンネルがある場合はステレオとして保存し、各チャンネルを個別に書き出す
|
| 177 |
+
chans = mixed_data.shape[1]
|
| 178 |
+
if chans >= 2:
|
| 179 |
+
sf.write(mixed_out, mixed_data[:, :2], sr)
|
| 180 |
+
sf.write(mixed_out_ch1, mixed_data[:, 0], sr)
|
| 181 |
+
sf.write(mixed_out_ch2, mixed_data[:, 1], sr)
|
| 182 |
+
else:
|
| 183 |
+
# チャンネル数が1の扱い
|
| 184 |
+
sf.write(mixed_out, mixed_data[:, 0], sr)
|
| 185 |
+
sf.write(mixed_out_ch1, mixed_data[:, 0], sr)
|
| 186 |
+
sf.write(mixed_out_ch2, mixed_data[:, 0], sr)
|
| 187 |
+
except Exception as e:
|
| 188 |
+
# 失敗したら最小限の情報を返す(呼び出し側でエラーハンドリング)
|
| 189 |
+
print(f"Warning: failed to write mixed audio files: {e}")
|
| 190 |
+
mixed_out_ch1 = mixed_out_ch2 = None
|
| 191 |
+
|
| 192 |
+
return sr, fig, file1, file2, mixed_out_ch1, mixed_out_ch2
|
| 193 |
+
|
| 194 |
+
# ===== Gradio UI (Hugging Face上のみ) =====
|
| 195 |
+
if USE_GRADIO:
|
| 196 |
+
# セッション管理: ユーザーごとに隔離されたディレクトリ
|
| 197 |
+
session_dir = None
|
| 198 |
+
|
| 199 |
+
def get_session_dir():
|
| 200 |
+
"""ユーザーセッション用の一時ディレクトリを取得"""
|
| 201 |
+
global session_dir
|
| 202 |
+
if session_dir is None:
|
| 203 |
+
# セッションディレクトリを作成(アプリ起動時に1回だけ)
|
| 204 |
+
session_id = str(uuid.uuid4())[:8]
|
| 205 |
+
session_dir = os.path.join(tempfile.gettempdir(), f"gradio_session_{session_id}")
|
| 206 |
+
os.makedirs(session_dir, exist_ok=True)
|
| 207 |
+
# パーミッション設定:所有者のみアクセス可能 (mode 0o700)
|
| 208 |
+
os.chmod(session_dir, 0o700)
|
| 209 |
+
print(f"🔹 Session directory created: {session_dir}")
|
| 210 |
+
# プログラム終了時にセッションディレクトリを削除するコールバック登録
|
| 211 |
+
atexit.register(cleanup_session_dir, session_dir)
|
| 212 |
+
return session_dir
|
| 213 |
+
|
| 214 |
+
def cleanup_session_dir(session_dir):
|
| 215 |
+
"""セッション終了時にディレクトリを削除"""
|
| 216 |
+
try:
|
| 217 |
+
if os.path.exists(session_dir):
|
| 218 |
+
shutil.rmtree(session_dir)
|
| 219 |
+
print(f"🔹 Session directory cleaned up: {session_dir}")
|
| 220 |
+
except Exception as e:
|
| 221 |
+
print(f"⚠️ Failed to clean up session directory: {e}")
|
| 222 |
+
|
| 223 |
+
with gr.Blocks() as demo:
|
| 224 |
+
gr.Markdown("🎧 **IVAステレオ音源分離デモ (pyroomacoustics)**")
|
| 225 |
+
# 小さい動画プレビューにするCSS(アップロードした大きな動画が邪魔なときに有効)
|
| 226 |
+
# ここで高さを制限し、必要ならブラウザで拡大して見るよう促すUIにできます。
|
| 227 |
+
gr.HTML("<style>video{max-height:240px; max-width:100%;}</style>")
|
| 228 |
+
gr.Markdown(
|
| 229 |
+
"""
|
| 230 |
+
このデモでは、動画ファイルから音声を抽出し、2つの音源に分離します。動画をアップロードするか、サンプル混合音声を生成してから「分離開始」ボタンを押してください。
|
| 231 |
+
注意:
|
| 232 |
+
- 個人情報や機密情報はアップロードしないでください
|
| 233 |
+
- アップロードによって生じるトラブルは責任を負いかねます
|
| 234 |
+
""")
|
| 235 |
+
# 入力ファイル (左:アップローダ, 右:サンプル生成/再生)
|
| 236 |
+
with gr.Row():
|
| 237 |
+
with gr.Column(scale=1):
|
| 238 |
+
gr.Markdown("**📤 入力ファイル**")
|
| 239 |
+
video_input = gr.Video(label="🎥 動画ファイルをアップロード")
|
| 240 |
+
gr.Markdown("**注意:** 動画をアップロードするか、または「🎼 サンプル混合音声を生成・再生」を押してください。どちらかを必ず行ってください。")
|
| 241 |
+
|
| 242 |
+
with gr.Column(scale=1):
|
| 243 |
+
gr.Markdown("**🎵 サンプル生成**")
|
| 244 |
+
gen_sample_btn = gr.Button("🎼 サンプル混合音声を生成・再生", variant="secondary", size="lg")
|
| 245 |
+
sample_audio = gr.Audio(label="生成されたサンプル", interactive=False)
|
| 246 |
+
|
| 247 |
+
# パラメータ / 処理モード
|
| 248 |
+
with gr.Group():
|
| 249 |
+
gr.Markdown("**⚙️ 処理モードとパラメータ**")
|
| 250 |
+
|
| 251 |
+
# 窓長説明(最初)
|
| 252 |
+
gr.Markdown("💡 窓長(サンプル数)は表示されたサンプリング周波数の0.2倍程度を推奨します。下のボタンを押すと自動設定されます。")
|
| 253 |
+
|
| 254 |
+
# サンプリング周波数計算ボタンと表示
|
| 255 |
+
with gr.Row():
|
| 256 |
+
check_sr_btn = gr.Button("▶ サンプリング周波数を計算", variant="secondary", size="sm")
|
| 257 |
+
sr_display = gr.Markdown("**サンプリング周波数**: まだ計算されていません")
|
| 258 |
+
|
| 259 |
+
# STFT 窓長
|
| 260 |
+
win_len = gr.Dropdown(
|
| 261 |
+
[256, 512, 1024, 2048, 4096, 8192, 16384],
|
| 262 |
+
value=DEFAULT_WIN_LEN,
|
| 263 |
+
label="STFT 窓長 (hopは自動で半分)"
|
| 264 |
+
)
|
| 265 |
+
|
| 266 |
+
# 処理モード選択
|
| 267 |
+
use_online = gr.Radio(
|
| 268 |
+
choices=["オフライン (AuxIVA)", "オンライン (Online AuxIVA)"],
|
| 269 |
+
value="オフライン (AuxIVA)",
|
| 270 |
+
label="処理モード",
|
| 271 |
+
interactive=True
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
# オンライン処理専用パラメータ区切り
|
| 275 |
+
gr.Markdown("💡 **以下はオンライン処理の時のみ有効です**")
|
| 276 |
+
|
| 277 |
+
# Online-specific parameters
|
| 278 |
+
forgetting_factor = gr.Slider(0.90, 0.99, value=DEFAULT_ALPHA, step=0.01, label="忘却係数 (高いほど過去の情報を忘れにくくなります)")
|
| 279 |
+
|
| 280 |
+
run_btn = gr.Button("▶ 分離開始", size="lg", variant="primary")
|
| 281 |
+
|
| 282 |
+
# ステータス表示(入力チェック等のメッセージを表示)
|
| 283 |
+
status_output = gr.Markdown("", visible=True)
|
| 284 |
+
# 結果表示 (下部)
|
| 285 |
+
with gr.Group():
|
| 286 |
+
gr.Markdown("**📊 結果**")
|
| 287 |
+
fig_out = gr.Plot(label="混合音声 vs 分離音声の波形比較")
|
| 288 |
+
with gr.Row():
|
| 289 |
+
mixed_audio_ch1 = gr.Audio(label="🎵 混合音 - チャンネル1", type="filepath")
|
| 290 |
+
mixed_audio_ch2 = gr.Audio(label="🎵 混合音 - チャンネル2", type="filepath")
|
| 291 |
+
audio1 = gr.Audio(label="🔊 分離音1", type="filepath")
|
| 292 |
+
audio2 = gr.Audio(label="🔊 分離音2", type="filepath")
|
| 293 |
+
|
| 294 |
+
# コールバック: サンプル生成
|
| 295 |
+
def generate_and_play_sample():
|
| 296 |
+
try:
|
| 297 |
+
out_path = "sample_mixed_ch1.wav"
|
| 298 |
+
# sample_code_c5_2 (若干改変) を使ってサンプルを生成
|
| 299 |
+
sample_code_c5_2.main(out_path)
|
| 300 |
+
data, sr = sf.read(out_path)
|
| 301 |
+
# 生成されたサンプルを保存(分離処理で2chが必要)
|
| 302 |
+
sf.write(out_path, data, sr)
|
| 303 |
+
return out_path
|
| 304 |
+
except Exception as e:
|
| 305 |
+
print(f"Error generating sample audio: {e}")
|
| 306 |
+
return None
|
| 307 |
+
|
| 308 |
+
gen_sample_btn.click(generate_and_play_sample, inputs=[], outputs=[sample_audio])
|
| 309 |
+
|
| 310 |
+
# サンプリング周波数計算コールバック(win_lenを更新)
|
| 311 |
+
check_sr_btn.click(get_sampling_rate, inputs=[video_input, sample_audio], outputs=[sr_display, win_len])
|
| 312 |
+
|
| 313 |
+
# main processing callback
|
| 314 |
+
def process_and_format(video_file, sample_file, win_len_val, forgetting_val, use_online_mode):
|
| 315 |
+
try:
|
| 316 |
+
use_online_flag = use_online_mode == "オンライン (Online AuxIVA)"
|
| 317 |
+
|
| 318 |
+
# 入力チェック: 動画がアップロードされていない && サンプルも生成されていない -> エラー表示
|
| 319 |
+
if not video_file and not sample_file:
|
| 320 |
+
status_msg = "❗ 動画が選択されていません。動画をアップロードするか、サンプルを生成してください。"
|
| 321 |
+
return None, None, None, None, status_msg
|
| 322 |
+
|
| 323 |
+
print(f"DEBUG: video_file={video_file}, sample_file={sample_file}")
|
| 324 |
+
print(f"DEBUG: video_file type={type(video_file)}, sample_file type={type(sample_file)}")
|
| 325 |
+
|
| 326 |
+
target = video_file or sample_file
|
| 327 |
+
print(f"DEBUG: target={target}, target type={type(target)}")
|
| 328 |
+
|
| 329 |
+
# セッションディレクトリを取得してprocess()に渡す
|
| 330 |
+
sess_dir = get_session_dir()
|
| 331 |
+
sr, fig, file1, file2, mixed_out_ch1, mixed_out_ch2 = process(target, win_len=win_len_val, alpha=forgetting_val, use_online=use_online_flag, session_dir=sess_dir)
|
| 332 |
+
status_msg = "✅ 分離が完了しました。下のオーディオを確認してください。"
|
| 333 |
+
return fig, mixed_out_ch1, mixed_out_ch2, file1, file2, status_msg
|
| 334 |
+
except Exception as e:
|
| 335 |
+
import traceback
|
| 336 |
+
error_msg = f"❌ エラーが発生しました: {str(e)}\n{traceback.format_exc()}"
|
| 337 |
+
print(error_msg)
|
| 338 |
+
return None, None, None, None, None, error_msg
|
| 339 |
+
|
| 340 |
+
run_btn.click(
|
| 341 |
+
process_and_format,
|
| 342 |
+
inputs=[video_input, sample_audio, win_len, forgetting_factor, use_online],
|
| 343 |
+
outputs=[fig_out, mixed_audio_ch1, mixed_audio_ch2, audio1, audio2, status_output]
|
| 344 |
+
)
|
| 345 |
+
|
| 346 |
+
gr.Markdown(
|
| 347 |
+
"""
|
| 348 |
+
---
|
| 349 |
+
|
| 350 |
+
**Resources Used for Demo Site**
|
| 351 |
+
- pyroomacoustics (https://github.com/LCAV/pyroomacoustics)
|
| 352 |
+
- Pythonで学ぶ音源分離(機械学習実践シリーズ)のソースコード (https://github.com/masahitotogami/python_source_separation)
|
| 353 |
+
- CMU ARCTIC Concatenated 15s (https://zenodo.org/records/3066489)
|
| 354 |
+
- J. Kominek and A. W. Black, “CMU ARCTIC databases for speech synthesis,” CMU-LTI-03-177, 2003.
|
| 355 |
+
|
| 356 |
+
**references**
|
| 357 |
+
- N. Ono, *Stable and fast update rules for independent vector analysis based on auxiliary function technique,* Proc. WASPAA, pp. 189-192, Oct. 2011.
|
| 358 |
+
- T. Taniguchi, N. Ono, A. Kawamura, and S. Sagayama, *An auxiliary-function approach to online independent vector analysis for real-time blind source separation,* Proc. HSCMA, pp. 107-111, May. 2014.
|
| 359 |
+
|
| 360 |
+
This demo is for educational and research purposes only.
|
| 361 |
+
"""
|
| 362 |
+
)
|
| 363 |
+
|
| 364 |
+
demo.launch(debug=True)
|
| 365 |
+
else:
|
| 366 |
+
# ===== ローカルモード:対話形式でパラメータ入力 =====
|
| 367 |
+
print("🔍 Local mode detected (gradio not available)")
|
| 368 |
+
print("=" * 50)
|
| 369 |
+
|
| 370 |
+
# test_video 入力
|
| 371 |
+
print("\n【ビデオファイル】")
|
| 372 |
+
video_input = input("ビデオファイルを入力してください (Enterで test_input.mp4, sでサンプル, 他は入力値): ").strip()
|
| 373 |
+
if video_input.lower() in [""]:
|
| 374 |
+
test_video = "test_input.mp4"
|
| 375 |
+
elif video_input.lower() in ["s"]:
|
| 376 |
+
test_video = None
|
| 377 |
+
DEFAULT_WIN_LEN = 2048
|
| 378 |
+
else:
|
| 379 |
+
test_video = video_input
|
| 380 |
+
|
| 381 |
+
if test_video is not None and not os.path.exists(test_video):
|
| 382 |
+
raise FileNotFoundError(f"{test_video} が見つかりません。")
|
| 383 |
+
|
| 384 |
+
# win_len 入力
|
| 385 |
+
print("\n【STFT 窓長】")
|
| 386 |
+
win_len_input = input(f"win_lenを入力してください (デフォルト: {DEFAULT_WIN_LEN}): ").strip()
|
| 387 |
+
if win_len_input == "":
|
| 388 |
+
win_len = DEFAULT_WIN_LEN
|
| 389 |
+
else:
|
| 390 |
+
try:
|
| 391 |
+
win_len = int(win_len_input)
|
| 392 |
+
except ValueError:
|
| 393 |
+
print(f"⚠️ 無効な入力。デフォルト値 {DEFAULT_WIN_LEN} を使用します。")
|
| 394 |
+
win_len = DEFAULT_WIN_LEN
|
| 395 |
+
|
| 396 |
+
# use_online 入力
|
| 397 |
+
print("\n【処理モード】")
|
| 398 |
+
use_online_input = input("オンライン処理を使用しますか? (yes/no, デフォルト: yes): ").strip().lower()
|
| 399 |
+
use_online = not use_online_input in ["no", "n"]
|
| 400 |
+
|
| 401 |
+
# use_online が True の場合のみ alpha 入力
|
| 402 |
+
if use_online:
|
| 403 |
+
print("\n【Alpha パラメータ】")
|
| 404 |
+
alpha_input = input(f"alphaを入力してください (0.90-0.99, デフォルト: {DEFAULT_ALPHA}): ").strip()
|
| 405 |
+
if alpha_input == "":
|
| 406 |
+
alpha = DEFAULT_ALPHA
|
| 407 |
+
else:
|
| 408 |
+
try:
|
| 409 |
+
alpha = float(alpha_input)
|
| 410 |
+
if not (0.90 <= alpha <= 0.99):
|
| 411 |
+
print(f"⚠️ alpha は 0.90-0.99 の範囲で指定してください。デフォルト値 {DEFAULT_ALPHA} を使用します。")
|
| 412 |
+
alpha = DEFAULT_ALPHA
|
| 413 |
+
except ValueError:
|
| 414 |
+
print(f"⚠️ 無効な入力。デフォルト値 {DEFAULT_ALPHA} を使用します。")
|
| 415 |
+
alpha = DEFAULT_ALPHA
|
| 416 |
+
|
| 417 |
+
else:
|
| 418 |
+
alpha = DEFAULT_ALPHA
|
| 419 |
+
|
| 420 |
+
# パラメータ表示
|
| 421 |
+
print("\n" + "=" * 50)
|
| 422 |
+
print("【処理パラメータ】")
|
| 423 |
+
print(f" ビデオファイル: {test_video if test_video else 'サンプル音声を生成'}")
|
| 424 |
+
print(f" win_len: {win_len}")
|
| 425 |
+
print(f" use_online: {use_online}")
|
| 426 |
+
if use_online:
|
| 427 |
+
print(f" alpha: {alpha}")
|
| 428 |
+
print("=" * 50)
|
| 429 |
+
print("\n処理を開始します...")
|
| 430 |
+
|
| 431 |
+
# 処理実行
|
| 432 |
+
sr, fig, file1, file2, mixed_ch1, mixed_ch2 = process(test_video, win_len=win_len, alpha=alpha, use_online=use_online)
|
| 433 |
+
|
| 434 |
+
print(f"✅ 処理完了")
|
| 435 |
+
print(f"Sampling Rate: {sr}")
|
| 436 |
+
print(f"Saved separated files: {file1}, {file2}")
|
| 437 |
+
print(f"Saved mixed channel files: {mixed_ch1}, {mixed_ch2}")
|
| 438 |
+
fig.savefig("separated_plot.png")
|
| 439 |
+
print("Waveform plot saved to separated_plot.png")
|
auxiva_online.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 Tetsuya Ueda
|
| 2 |
+
# このコードはPyroomacoutsticsに記載されているAuxIVAを参考にしてそのオンライン拡張版を実装した.
|
| 3 |
+
# そのため,このコードのオリジナルの著作権は以下の通りPyroomacoutsticsの著作権表示に従う.
|
| 4 |
+
# このコード自体の著作権はMITライセンスに従う.
|
| 5 |
+
|
| 6 |
+
# Copyright (c) 2018-2019 Robin Scheibler
|
| 7 |
+
#
|
| 8 |
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 9 |
+
# of this software and associated documentation files (the "Software"), to deal
|
| 10 |
+
# in the Software without restriction, including without limitation the rights
|
| 11 |
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 12 |
+
# copies of the Software, and to permit persons to whom the Software is
|
| 13 |
+
# furnished to do so, subject to the following conditions:
|
| 14 |
+
#
|
| 15 |
+
# The above copyright notice and this permission notice shall be included in all
|
| 16 |
+
# copies or substantial portions of the Software.
|
| 17 |
+
#
|
| 18 |
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 19 |
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 20 |
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 21 |
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 22 |
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 23 |
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 24 |
+
# SOFTWARE.
|
| 25 |
+
"""
|
| 26 |
+
auxiva_online
|
| 27 |
+
======
|
| 28 |
+
|
| 29 |
+
Blind Source Separation using independent vector analysis based on auxiliary function.
|
| 30 |
+
This function will separate the input signal into statistically independent sources
|
| 31 |
+
without using any prior information.
|
| 32 |
+
|
| 33 |
+
Example
|
| 34 |
+
-------
|
| 35 |
+
.. code-block:: python
|
| 36 |
+
|
| 37 |
+
from scipy.io import wavfile
|
| 38 |
+
import pyroomacoustics as pra
|
| 39 |
+
|
| 40 |
+
# read multichannel wav file
|
| 41 |
+
# audio.shape == (nsamples, nchannels)
|
| 42 |
+
fs, audio = wavfile.read("my_multichannel_audio.wav")
|
| 43 |
+
|
| 44 |
+
# STFT analysis parameters
|
| 45 |
+
fft_size = 4096 # `fft_size / fs` should be ~RT60
|
| 46 |
+
hop == fft_size // 2 # half-overlap
|
| 47 |
+
win_a = pra.hann(fft_size) # analysis window
|
| 48 |
+
# optimal synthesis window
|
| 49 |
+
win_s = pra.transform.compute_synthesis_window(win_a, hop)
|
| 50 |
+
|
| 51 |
+
# STFT
|
| 52 |
+
# X.shape == (nframes, nfrequencies, nchannels)
|
| 53 |
+
X = pra.transform.analysis(audio, fft_size, hop, win=win_a)
|
| 54 |
+
|
| 55 |
+
# Separation
|
| 56 |
+
Y = auxiva_online(X)
|
| 57 |
+
|
| 58 |
+
# iSTFT (introduces an offset of `hop` samples)
|
| 59 |
+
# y contains the time domain separated signals
|
| 60 |
+
# y.shape == (new_nsamples, nchannels)
|
| 61 |
+
y = pra.transform.synthesis(Y, fft_size, hop, win=win_s)
|
| 62 |
+
|
| 63 |
+
References
|
| 64 |
+
----------
|
| 65 |
+
|
| 66 |
+
.. [1] T. Taniguchi, N. Ono, A. Kawamura, and S. Sagayama, *An auxiliary-function approach to online independent vector analysis for real-time blind source separation,* Proc. HSCMA, pp. 107-111, May. 2014.
|
| 67 |
+
|
| 68 |
+
"""
|
| 69 |
+
import numpy as np
|
| 70 |
+
|
| 71 |
+
def auxiva_online(
|
| 72 |
+
X,
|
| 73 |
+
model="laplace",
|
| 74 |
+
alpha=0.9,
|
| 75 |
+
):
|
| 76 |
+
"""
|
| 77 |
+
This is an implementation of online AuxIVA that separates the input
|
| 78 |
+
signal into statistically independent sources. The separation is done
|
| 79 |
+
in the time-frequency domain and the FFT length should be approximately
|
| 80 |
+
equal to the reverberation time.
|
| 81 |
+
|
| 82 |
+
Two different statistical models (Laplace or time-varying Gauss) can
|
| 83 |
+
be used by using the keyword argument `model`. The performance of Gauss
|
| 84 |
+
model is higher in good conditions (few sources, low noise), but Laplace
|
| 85 |
+
(the default) is more robust in general.
|
| 86 |
+
|
| 87 |
+
この関数はpyroomacousticsにおけるAuxIVAのonline拡張を実装したものである。
|
| 88 |
+
ただし,デモ作成を目的としており,コードの修正は最小限に留めている.
|
| 89 |
+
|
| 90 |
+
Parameters
|
| 91 |
+
----------
|
| 92 |
+
X: ndarray (nframes, nfrequencies, nchannels)
|
| 93 |
+
STFT representation of the signal
|
| 94 |
+
model: str
|
| 95 |
+
The model of source distribution 'gauss' or 'laplace' (default)
|
| 96 |
+
alpha: float
|
| 97 |
+
Forgetting factor for the online AuxIVA (0 < alpha < 1).
|
| 98 |
+
|
| 99 |
+
Returns
|
| 100 |
+
-------
|
| 101 |
+
Returns an (nframes, nfrequencies, nsources) array.
|
| 102 |
+
"""
|
| 103 |
+
|
| 104 |
+
n_frames, n_freq, n_chan = X.shape
|
| 105 |
+
|
| 106 |
+
# default to determined case
|
| 107 |
+
n_src = n_chan
|
| 108 |
+
|
| 109 |
+
if model not in ["laplace", "gauss"]:
|
| 110 |
+
raise ValueError("Model should be either " "laplace" " or " "gauss" ".")
|
| 111 |
+
|
| 112 |
+
W_hat = np.zeros((n_freq, n_chan, n_chan), dtype=X.dtype)
|
| 113 |
+
W = W_hat[:, :n_src, :]
|
| 114 |
+
|
| 115 |
+
# initialize W
|
| 116 |
+
for f in range(n_freq):
|
| 117 |
+
W[f, :, :n_src] = np.eye(n_src)
|
| 118 |
+
|
| 119 |
+
eps = 1e-15
|
| 120 |
+
cov_init_factor=1e-9
|
| 121 |
+
eyes = np.tile(np.eye(n_chan, n_chan), (n_freq, 1, 1))
|
| 122 |
+
V = np.tile(np.eye(n_chan, n_chan, dtype=X.dtype), (n_freq, n_src, 1, 1))*cov_init_factor
|
| 123 |
+
r_inv = np.zeros((n_src, 1))
|
| 124 |
+
r = np.zeros((n_src, 1))
|
| 125 |
+
|
| 126 |
+
# Things are more efficient when the frequencies are over the first axis
|
| 127 |
+
Y = np.zeros((n_freq, n_src, n_frames), dtype=X.dtype)
|
| 128 |
+
X = X.transpose([1, 2, 0]).copy()
|
| 129 |
+
|
| 130 |
+
Y = np.zeros_like(X, dtype=X.dtype)
|
| 131 |
+
for frame in range(n_frames):
|
| 132 |
+
# W: (n_freq x n_src x n_chan)
|
| 133 |
+
# X: (n_freq x n_chan x n_frames)
|
| 134 |
+
XTmp = X[:, :, frame, None] # (n_freq x n_chan x 1)
|
| 135 |
+
YTmp = np.matmul(W, XTmp) # (n_freq x n_src x 1)
|
| 136 |
+
# shape: (n_frames, n_src)
|
| 137 |
+
if model == "laplace":
|
| 138 |
+
r[:, :] = 2.0 * np.linalg.norm(YTmp, axis=0) # (n_src x 1)
|
| 139 |
+
elif model == "gauss":
|
| 140 |
+
r[:, :] = (np.linalg.norm(YTmp, axis=0) ** 2) / n_freq # (n_src x 1)
|
| 141 |
+
r[r < eps] = eps
|
| 142 |
+
r_inv[:, :] = 1.0 / r
|
| 143 |
+
|
| 144 |
+
# Update now the demixing matrix
|
| 145 |
+
for s in range(n_src):
|
| 146 |
+
# Compute Auxiliary Variable
|
| 147 |
+
VTmp= (
|
| 148 |
+
np.matmul(XTmp*r_inv[None, s, None, :], np.conj(XTmp.swapaxes(1, 2)))
|
| 149 |
+
)
|
| 150 |
+
V[:, s, :, :] = alpha*V[:, s, :, :] + (1-alpha)*VTmp
|
| 151 |
+
|
| 152 |
+
WV = np.matmul(W_hat, V[:, s])
|
| 153 |
+
W[:, s, :] = np.conj(np.linalg.solve(WV, eyes[:, :, [s]]))[..., 0]
|
| 154 |
+
|
| 155 |
+
# normalize
|
| 156 |
+
denom = np.matmul(
|
| 157 |
+
np.matmul(W[:, None, s, :], V[:, s, :, :]), np.conj(W[:, s, :, None])
|
| 158 |
+
)
|
| 159 |
+
W[:, s, :] /= np.sqrt(denom[:, :, 0])
|
| 160 |
+
|
| 161 |
+
YTmp = np.matmul(W, XTmp) # (n_freq x n_src x 1)
|
| 162 |
+
# projection_back
|
| 163 |
+
A = np.linalg.inv(W) # (n_freq x n_chan x n_src)
|
| 164 |
+
# A: (n_freq x n_chan x n_src)
|
| 165 |
+
# YTmp: (n_freq x n_src x 1)
|
| 166 |
+
Y[:, :, frame] = A[:, 0] * YTmp[:, :, 0] # (n_freq x n_src x n_frames)
|
| 167 |
+
Y = Y.transpose([2, 0, 1])
|
| 168 |
+
return Y
|
| 169 |
+
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio==6.0.1
|
| 2 |
+
numpy
|
| 3 |
+
scipy==1.15.3
|
| 4 |
+
soundfile
|
| 5 |
+
matplotlib
|
| 6 |
+
pyroomacoustics==0.8.4
|
sample_code_c5_2.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'''
|
| 2 |
+
このコードはPythonで学ぶ音源分離(第5章)のサンプルコード5-2を改変したものです.
|
| 3 |
+
https://raw.githubusercontent.com/masahitotogami/python_source_separation/master/section5/sample_code_c5_2.py
|
| 4 |
+
'''
|
| 5 |
+
import os
|
| 6 |
+
import requests
|
| 7 |
+
import tarfile
|
| 8 |
+
import wave as wave
|
| 9 |
+
import pyroomacoustics as pa
|
| 10 |
+
import numpy as np
|
| 11 |
+
import random
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def download_and_extract_cmu_arctic():
|
| 15 |
+
folder_name = "cmu_arctic_concat15"
|
| 16 |
+
tar_filename = "cmu_arctic_concat15.tar.gz"
|
| 17 |
+
url = "https://zenodo.org/records/3066489/files/cmu_arctic_concat15.tar.gz?download=1"
|
| 18 |
+
|
| 19 |
+
# 既にフォルダがある場合は処理しない
|
| 20 |
+
if os.path.exists(folder_name):
|
| 21 |
+
print(f"📂 フォルダ '{folder_name}' は既に存在しています。ダウンロードしません。")
|
| 22 |
+
return
|
| 23 |
+
|
| 24 |
+
print(f"⬇️ '{folder_name}' が見つかりません。ダウンロード開始...")
|
| 25 |
+
|
| 26 |
+
# ===== ダウンロード =====
|
| 27 |
+
response = requests.get(url, stream=True)
|
| 28 |
+
response.raise_for_status()
|
| 29 |
+
|
| 30 |
+
with open(tar_filename, "wb") as f:
|
| 31 |
+
for chunk in response.iter_content(chunk_size=8192):
|
| 32 |
+
f.write(chunk)
|
| 33 |
+
|
| 34 |
+
print(f"📦 ダウンロード完了: {tar_filename}")
|
| 35 |
+
|
| 36 |
+
# ===== 展開 =====
|
| 37 |
+
print(f"📂 展開中...")
|
| 38 |
+
with tarfile.open(tar_filename, "r:gz") as tar:
|
| 39 |
+
tar.extractall()
|
| 40 |
+
|
| 41 |
+
print("🎉 展開完了!")
|
| 42 |
+
|
| 43 |
+
# ===== ダウンロードファイル削除 =====
|
| 44 |
+
os.remove(tar_filename)
|
| 45 |
+
print(f"🧹 掃除完了: {tar_filename} を削除しました")
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def main(audio_output="mixed_audio.wav"):
|
| 49 |
+
# CMU Arcticデータセットをダウンロード・展開
|
| 50 |
+
download_and_extract_cmu_arctic()
|
| 51 |
+
|
| 52 |
+
# 乱数の種を初期化
|
| 53 |
+
np.random.seed(0)
|
| 54 |
+
|
| 55 |
+
# ===== ① 使用する音源ファイル =====
|
| 56 |
+
base_dir = "cmu_arctic_concat15/"
|
| 57 |
+
files = os.listdir(base_dir)
|
| 58 |
+
wav_files = [
|
| 59 |
+
base_dir + f for f in files
|
| 60 |
+
if not f.startswith(".") and f.endswith(".wav")
|
| 61 |
+
]
|
| 62 |
+
clean_wave_files = random.sample(wav_files, 2)
|
| 63 |
+
|
| 64 |
+
# 音源数
|
| 65 |
+
n_sources = len(clean_wave_files)
|
| 66 |
+
|
| 67 |
+
# 長さを調べる
|
| 68 |
+
n_samples = 0
|
| 69 |
+
n_samples_min = 1e10
|
| 70 |
+
for clean_wave_file in clean_wave_files:
|
| 71 |
+
wav = wave.open(clean_wave_file)
|
| 72 |
+
if n_samples < wav.getnframes():
|
| 73 |
+
n_samples = wav.getnframes()
|
| 74 |
+
n_samples_min = min(n_samples_min, wav.getnframes())
|
| 75 |
+
wav.close()
|
| 76 |
+
|
| 77 |
+
clean_data = np.zeros([n_sources, n_samples])
|
| 78 |
+
|
| 79 |
+
# ファイルを読み込む
|
| 80 |
+
s = 0
|
| 81 |
+
for clean_wave_file in clean_wave_files:
|
| 82 |
+
wav = wave.open(clean_wave_file)
|
| 83 |
+
data = wav.readframes(wav.getnframes())
|
| 84 |
+
data = np.frombuffer(data, dtype=np.int16)
|
| 85 |
+
data = data / np.iinfo(np.int16).max # -1〜1に正規化
|
| 86 |
+
clean_data[s, :wav.getnframes()] = data
|
| 87 |
+
wav.close()
|
| 88 |
+
s = s + 1
|
| 89 |
+
|
| 90 |
+
# ===== シミュレーションのパラメータ =====
|
| 91 |
+
|
| 92 |
+
sample_rate = 16000 # サンプリング周波数
|
| 93 |
+
SNR = 50. # 音声と雑音の比率 [dB]
|
| 94 |
+
room_dim = np.r_[10.0, 10.0, 10.0] # 部屋の大きさ
|
| 95 |
+
|
| 96 |
+
# マイクロホンアレイを置く部屋の場所
|
| 97 |
+
mic_array_loc = room_dim / 2 + np.random.randn(3) * 0.1
|
| 98 |
+
|
| 99 |
+
# マイクロホンアレイのマイク配置(2ch固定)
|
| 100 |
+
mic_alignments = np.array(
|
| 101 |
+
[
|
| 102 |
+
[-0.01, 0.0, 0.0],
|
| 103 |
+
[0.01, 0.0, 0.0],
|
| 104 |
+
]
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
n_channels = mic_alignments.shape[0]
|
| 108 |
+
R = mic_alignments.T + mic_array_loc[:, None]
|
| 109 |
+
|
| 110 |
+
# 部屋を生成する
|
| 111 |
+
room = pa.ShoeBox(room_dim, fs=sample_rate, max_order=0)
|
| 112 |
+
|
| 113 |
+
# マイクロホンアレイの情報を設定
|
| 114 |
+
room.add_microphone_array(pa.MicrophoneArray(R, fs=room.fs))
|
| 115 |
+
|
| 116 |
+
# 音源の場所
|
| 117 |
+
doas = np.array(
|
| 118 |
+
[[np.pi/2., 0],
|
| 119 |
+
[np.pi/2., np.pi/2.]]
|
| 120 |
+
)
|
| 121 |
+
distance = 1.
|
| 122 |
+
source_locations = np.zeros((3, doas.shape[0]), dtype=doas.dtype)
|
| 123 |
+
source_locations[0, :] = np.cos(doas[:, 1]) * np.sin(doas[:, 0])
|
| 124 |
+
source_locations[1, :] = np.sin(doas[:, 1]) * np.sin(doas[:, 0])
|
| 125 |
+
source_locations[2, :] = np.cos(doas[:, 0])
|
| 126 |
+
source_locations *= distance
|
| 127 |
+
source_locations += mic_array_loc[:, None]
|
| 128 |
+
|
| 129 |
+
# 各音源を部屋に追加
|
| 130 |
+
for s in range(n_sources):
|
| 131 |
+
clean_data[s] /= np.std(clean_data[s])
|
| 132 |
+
room.add_source(source_locations[:, s], signal=clean_data[s])
|
| 133 |
+
|
| 134 |
+
# ===== シミュレーション実行 =====
|
| 135 |
+
room.simulate(snr=SNR)
|
| 136 |
+
|
| 137 |
+
# ===== ステレオ1つのファイルとして保存 =====
|
| 138 |
+
multi_conv_data = room.mic_array.signals # (channels, samples)
|
| 139 |
+
|
| 140 |
+
# int16スケール調整
|
| 141 |
+
data_scaled_L = (multi_conv_data[0] * np.iinfo(np.int16).max / 10).astype(np.int16)
|
| 142 |
+
data_scaled_R = (multi_conv_data[1] * np.iinfo(np.int16).max / 10).astype(np.int16)
|
| 143 |
+
|
| 144 |
+
# ステレオ結合
|
| 145 |
+
stereo_data = np.stack([data_scaled_L, data_scaled_R], axis=1)[:n_samples_min]
|
| 146 |
+
|
| 147 |
+
# ===== 保存 =====
|
| 148 |
+
out = wave.open(audio_output, "w")
|
| 149 |
+
out.setnchannels(2)
|
| 150 |
+
out.setsampwidth(2) # int16
|
| 151 |
+
out.setframerate(sample_rate)
|
| 152 |
+
out.writeframes(stereo_data.tobytes())
|
| 153 |
+
out.close()
|
| 154 |
+
|
| 155 |
+
print(f"🎧 {audio_output} を生成しました(2chステレオ)")
|
| 156 |
+
|
| 157 |
+
if __name__ == "__main__":
|
| 158 |
+
main()
|