今回は、前回作ったリアルタイム音量グラフに「一定の音量を超えたら+1カウントする」機能を追加していくよ!
ついにアプリのコア機能「音量カウント」の実装だね!
さっそく、新しく機能を追加したプログラムを動かしてみよう!
はーい!
import numpy as np
import sounddevice as sd
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import japanize_matplotlib
SAMPLE_RATE = 44100
BLOCK_SIZE = 1024
HISTORY_LEN = 100
THRESHOLD = 0.15
COOLDOWN_FRAMES = 15
class ThresholdCounter:
"""マイクの音量を監視し、閾値超えを数えるクラス"""
def __init__(self, ax, threshold=THRESHOLD, cooldown_frames=COOLDOWN_FRAMES,
history_len=HISTORY_LEN):
self.threshold = threshold
self.cooldown_frames = cooldown_frames
self.cooldown = 0
self.count = 0
self.history_len = history_len
self.volume_history = [0.0] * history_len
self.current_volume = 0.0
self.line, = ax.plot(self.volume_history)
ax.axhline(threshold, color="red", linestyle="--", label="閾値")
ax.set_ylim(0, 1.0)
ax.set_title("Step5: 閾値を超えたらカウント")
ax.legend(loc="upper right")
self.count_text = ax.text(
0.02, 0.92, "", transform=ax.transAxes,
fontsize=16, color="darkred", fontweight="bold"
)
def audio_callback(self, indata, frames, time, status):
self.current_volume = float(np.sqrt(np.mean(indata ** 2)))
def update(self, frame):
self.volume_history.append(self.current_volume)
self.volume_history.pop(0)
self.line.set_ydata(self.volume_history)
if self.cooldown > 0:
self.cooldown -= 1
elif self.current_volume > self.threshold:
self.count += 1
self.cooldown = self.cooldown_frames
self.count_text.set_text(f"カウント: {self.count}")
return self.line, self.count_text
fig, ax = plt.subplots()
counter = ThresholdCounter(ax)
stream = sd.InputStream(
channels=1, samplerate=SAMPLE_RATE, blocksize=BLOCK_SIZE,
callback=counter.audio_callback,
)
with stream:
ani = animation.FuncAnimation(fig, counter.update, interval=50)
plt.show()
おおー!赤い点線(閾値)が表示されてて、大きな声を出すと左上の「カウント」が1ずつ増えてる!
...ん?でもおねーちゃん、プログラムの中に「cooldown」っていう処理(47行目)があるけど、これって何をしてるの?
お、良いところに気づいたね!
実は、この「cooldown」こそが今回の超重要ポイントなんだ!
そうなんだ
前回も説明した通り、アニメーションの更新処理「update()」(42行目)は1秒間に約20回(50ミリ秒ごと)呼ばれてるでしょ?
うんうん
もし「音量が閾値を超えていたら+1」っていう単純な条件文だけにしたらどうなると思う?
あっ!
「あー!」って1秒間声を伸ばしただけで、1秒間に20回判定されて、一気にカウントが+20になっちゃう...!?
大正解!
1回の発声なのに連打されたみたいにカウントされちゃうんだよね
だから「一度カウントしたら、指定したフレーム数(今回は15フレーム=約0.75秒)の間は次のカウントを判定しない」という冷却期間(cooldown)が必要になるわけだね
if self.cooldown > 0:
self.cooldown -= 1
elif self.current_volume > self.threshold:
self.count += 1
self.cooldown = self.cooldown_frames
なるほど!
47行目の「if self.cooldown > 0:」で冷却時間を1ずつ減らしていって、0になるまでは次のカウント判定をスキップしてるんだね!
そういうこと
この冷却時間のおかげで、一回の発声で一回だけ正しくカウントできるようになるんだよ
ゲームの攻撃のクールタイムみたいな感じだね!すごくわかりやすい!
あともう一つのポイントは、画面上に文字を表示する「ax.text()」(34行目)と「set_text()」(53行目)
34行目で「transform=ax.transAxes」を指定してるのは、グラフのデータ数値(0〜100等)に関係なく、「画面の左上を(0,0)、右上を(1,1)」とする相対的な位置でテキストを配置するためなんだ
self.count_text = ax.text(
0.02, 0.92, "", transform=ax.transAxes,
fontsize=16, color="darkred", fontweight="bold"
)
へぇー!だから「0.02, 0.92」を指定すると、常に画面の左上に文字が表示されるんだね!
そして、53行目の「self.count_text.set_text(...)」で、更新のたびに文字列を書き換えているよ
self.count_text.set_text(f"カウント: {self.count}")
グラフの線(line)だけじゃなくて、テキスト(text)も同じようにアニメーションの中で動的に更新できるんだね!
まとめると、今回のポイントは
1. 赤い破線で閾値を視覚化する(「ax.axhline()」, 29行目)
2. 音量超過時の多重カウントを防ぐために冷却時間(「self.cooldown」)を設ける(47-51行目)
3. カウント結果を画面上に動的テキストとして表示・更新する(「ax.text()」&「.set_text()」, 34, 53行目)
の3つだね!
ax.axhline(threshold, color="red", linestyle="--", label="閾値")
if self.cooldown > 0:
self.cooldown -= 1
elif self.current_volume > self.threshold:
self.count += 1
self.cooldown = self.cooldown_frames
self.count_text.set_text(f"カウント: {self.count}")
これでついに「音量が一定値を超えるとカウントされるアプリ」の基本機能が完成したね!
そうだね!
次回は最終ステップ!このアプリをゲーム配信などで使えるように、「配信向けグリーンバック演出」にアレンジしていくよ!
配信画面に重ねて使えるようにするんだね!楽しみ!
