タイトル画像
琴葉茜

今回は、前回作ったリアルタイム音量グラフに「一定の音量を超えたら+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}")
琴葉葵

これでついに「音量が一定値を超えるとカウントされるアプリ」の基本機能が完成したね!

琴葉茜

そうだね!

琴葉茜

次回は最終ステップ!このアプリをゲーム配信などで使えるように、「配信向けグリーンバック演出」にアレンジしていくよ!

琴葉葵

配信画面に重ねて使えるようにするんだね!楽しみ!