中級・GUIアプリ

Pythonで作る録音アプリ「AudioRecorderApp」

tkinter+PyAudio で、マイクデバイスの選択・リアルタイム波形表示・音量メーター・WAV保存までを備えた本格的な録音アプリを作ります。完全なソースコード付きで、1つずつ仕組みを解説します。

⏱️ 所要時間: 約30分 🎯 難易度: 中級 📦 PyAudio / NumPy / Matplotlib

1. このアプリでできること

今回作る AudioRecorderApp は、Python の標準GUIライブラリ tkinter に、録音ライブラリ PyAudio、数値計算の NumPy、グラフ描画の Matplotlib を組み合わせた録音ツールです。「Python だけでこんな本格的な録音アプリが作れるの?」と思うかもしれませんが、各ライブラリの役割を分けて考えれば、意外とシンプルに組み立てられます。

🎙️

マイク選択

接続中の入力デバイスを自動検出し、プルダウンから選択できます。

📈

リアルタイム波形

録音中の音声をMatplotlibで波形表示。視覚的に状態を確認できます。

🔊

音量メーター

入力レベルをプログレスバーに反映。録れているか一目で分かります。

💾

WAV保存

録音した音声を標準の wave モジュールでWAVファイルに出力します。

ℹ️
このページの位置づけ

本記事は「マイク選択+波形表示」まで踏み込んだ拡張版です。もっとシンプルな録音・再生から始めたい場合は、録音・再生アプリの基本版も参考にしてください。

2. 必要なライブラリとインストール

tkinterwavethreadingtime は標準ライブラリなのでインストール不要です。追加で必要なのは次の3つです。

pip install pyaudio numpy matplotlib
⚠️
Windowsで PyAudio のインストールに失敗するとき

環境によっては pip install pyaudio がビルドエラーになることがあります。その場合は、まず pip を最新化(python -m pip install --upgrade pip)してから再実行してください。それでも失敗する場合は pip install pipwin の後に pipwin install pyaudio を試すと通ることがあります。インストール周りでつまずいたらよくあるエラー解決ページもどうぞ。

各ライブラリの用途を一言でまとめると、次のようになります。役割を意識すると、コードのどの部分が何を担当しているか追いやすくなります。

ライブラリ役割
PyAudioマイクからの音声入力(ストリーム録音)
wave録音データをWAVファイルとして保存(標準)
NumPyバイト列を数値配列に変換し、音量・波形を計算
Matplotlib波形のリアルタイム描画
tkinter / ttkGUI(ボタン・プルダウン・プログレスバー)

NumPy や Matplotlib の基本を先に押さえたい人は、よく使うPythonライブラリ50選で用途別に整理しています。

3. 完成版ソースコード(全文)

まずは全体像です。次の章で各メソッドを分けて解説するので、ここでは「クラスにまとまっているんだな」くらいの気持ちで眺めてください。コードはコピーボタンからそのまま貼り付けて動かせます。

audio_recorder_app.py
import tkinter as tk
from tkinter import ttk, filedialog
import threading
import time
import wave

import pyaudio
import numpy as np
import matplotlib
matplotlib.rcParams['font.family'] = 'Yu Gothic'  # 日本語フォント指定(波形タイトルの文字化け防止)
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg


class EnhancedAudioApp:
    def __init__(self, root):
        self.root = root
        self.root.title("AudioRecorderApp(拡張版)")

        # PyAudio 初期化
        self.p = pyaudio.PyAudio()

        # 録音設定
        self.channels = 1       # モノラル
        self.rate = 44100       # サンプリングレート(Hz)
        self.chunk = 1024       # 1回に読み込むフレーム数

        # 状態管理
        self.recording = False
        self.frames = []
        self.start_time = 0

        self.create_widgets()

    # マイク(入力デバイス)の一覧を取得
    def get_input_devices(self):
        device_list = []
        for i in range(self.p.get_device_count()):
            info = self.p.get_device_info_by_index(i)
            if info["maxInputChannels"] > 0:        # 入力チャンネルがあるものだけ
                name = str(info["name"])             # 文字化け回避のため str 化
                device_list.append((i, name))
        return device_list

    # UI 構築
    def create_widgets(self):
        # --- マイクデバイス選択 ---
        device_frame = ttk.LabelFrame(self.root, text="マイクデバイス選択")
        device_frame.pack(fill="x", padx=10, pady=5)
        ttk.Label(device_frame, text="入力デバイス: ").pack(side="left")

        self.device_list = self.get_input_devices()
        device_names = [name for idx, name in self.device_list]

        self.device_var = tk.StringVar()
        self.device_box = ttk.Combobox(
            device_frame, textvariable=self.device_var,
            values=device_names, state="readonly", width=50,
        )
        self.device_box.pack(side="left", padx=5)
        if device_names:
            self.device_box.current(0)

        # --- 録音ボタン ---
        btn_frame = tk.Frame(self.root)
        btn_frame.pack(pady=10)
        ttk.Button(btn_frame, text="● 録音開始", command=self.start_recording).grid(row=0, column=0, padx=10)
        ttk.Button(btn_frame, text="■ 停止", command=self.stop_recording).grid(row=0, column=1, padx=10)
        ttk.Button(btn_frame, text="💾 保存", command=self.save_audio).grid(row=0, column=2, padx=10)

        # --- 録音時間ラベル ---
        self.time_label = ttk.Label(self.root, text="録音時間: 0.0 秒", font=("Yu Gothic", 12))
        self.time_label.pack()

        # --- 音量レベルバー ---
        volume_frame = ttk.LabelFrame(self.root, text="音量レベル")
        volume_frame.pack(fill="x", padx=10, pady=5)
        self.volume_bar = ttk.Progressbar(volume_frame, orient="horizontal", mode="determinate", length=300)
        self.volume_bar.pack(padx=10, pady=5)

        # --- 波形表示(Matplotlib) ---
        fig = plt.Figure(figsize=(6, 3), dpi=100)
        self.ax = fig.add_subplot(111)
        self.ax.set_ylim([-1, 1])
        self.ax.set_title("リアルタイム波形")
        self.line, = self.ax.plot([], [])
        self.canvas = FigureCanvasTkAgg(fig, master=self.root)
        self.canvas.get_tk_widget().pack()

    # 録音開始
    def start_recording(self):
        if self.recording:
            return
        self.recording = True
        self.frames = []
        self.start_time = time.time()

        # 選択中デバイスの index を名前から逆引き
        selected_name = self.device_var.get()
        device_index = next(idx for idx, nm in self.device_list if nm == selected_name)

        self.stream = self.p.open(
            format=pyaudio.paInt16,
            channels=self.channels,
            rate=self.rate,
            input=True,
            frames_per_buffer=self.chunk,
            input_device_index=device_index,
        )
        # 録音は別スレッドで(GUIを固めないため)
        threading.Thread(target=self.record, daemon=True).start()
        self.update_time()

    # 録音時間の更新(after でメインループをブロックしない)
    def update_time(self):
        if self.recording:
            elapsed = time.time() - self.start_time
            self.time_label.config(text=f"録音時間: {elapsed:.1f} 秒")
            self.root.after(100, self.update_time)

    # リアルタイム録音処理(別スレッドで実行)
    def record(self):
        while self.recording:
            data = self.stream.read(self.chunk)
            self.frames.append(data)

            # int16 のバイト列を -1.0〜1.0 の float に変換
            audio_np = np.frombuffer(data, dtype=np.int16).astype(np.float32) / 32768.0
            audio_np[np.abs(audio_np) < 0.01] = 0       # 簡易ノイズゲート(絶対値で判定)

            # 音量メーター更新
            volume = min(int(np.max(np.abs(audio_np)) * 100), 100)
            self.volume_bar["value"] = volume

            # 波形更新
            self.line.set_data(range(len(audio_np)), audio_np)
            self.ax.set_xlim([0, len(audio_np)])
            self.canvas.draw()

    # 録音停止
    def stop_recording(self):
        if not self.recording:
            return
        self.recording = False
        self.stream.stop_stream()
        self.stream.close()

    # WAV 保存
    def save_audio(self):
        if not self.frames:
            return
        file_path = filedialog.asksaveasfilename(
            defaultextension=".wav",
            filetypes=[("WAVファイル", "*.wav")],
        )
        if not file_path:
            return
        wf = wave.open(file_path, "wb")
        wf.setnchannels(self.channels)
        wf.setsampwidth(self.p.get_sample_size(pyaudio.paInt16))
        wf.setframerate(self.rate)
        wf.writeframes(b"".join(self.frames))
        wf.close()
        print("保存しました:", file_path)


if __name__ == "__main__":
    root = tk.Tk()
    app = EnhancedAudioApp(root)
    root.mainloop()
💡
元コードからの改善点(ノイズゲートのバグ修正)

ノイズゲートは audio_np[np.abs(audio_np) < 0.01] = 0 のように絶対値で判定するのがポイントです。単純に audio_np < 0.01 と書くと、負の値(音声波形の下半分)がすべて条件に当てはまってしまい、波形がつぶれてしまいます。小さな違いですが、音声処理では符号の扱いが結果を大きく左右します。

4. コードのポイント解説

4-1. マイクデバイスの取得と選択

PCに複数のマイクやオーディオインターフェースがつながっていることはよくあります。get_input_devices() は、PyAudio が認識しているデバイスを全部見て、入力チャンネルを持つもの(=録音できるもの)だけを取り出します。

def get_input_devices(self):
    device_list = []
    for i in range(self.p.get_device_count()):
        info = self.p.get_device_info_by_index(i)
        if info["maxInputChannels"] > 0:
            name = str(info["name"])     # 文字化け回避
            device_list.append((i, name))
    return device_list

デバイス名はそのまま使うと文字化けすることがあるため、str() で明示的に文字列化しています。取得した (index, name) のうち名前だけをプルダウン(ttk.Combobox)に並べ、録音開始時に名前から index を逆引きする、という流れです。

4-2. 別スレッドでの録音(GUIを固めない)

録音は「マイクから少しずつ音を読み続ける」処理なので、メインスレッドで実行するとウィンドウが固まって(フリーズして)しまいます。そこで threading.Thread で録音ループを別スレッドに逃がします。

threading.Thread(target=self.record, daemon=True).start()
self.update_time()

daemon=True を付けておくと、メインウィンドウを閉じたときに録音スレッドも一緒に終了するので、プロセスが残り続ける心配がありません。経過時間の表示は、別スレッドではなく root.after(100, ...) を使い、GUIのメインループの中で100msごとに更新しています。

4-3. 波形と音量メーターのリアルタイム更新

録音ループの中では、読み込んだバイト列を NumPy で数値配列に変換しています。int1632768.0 で割ることで、扱いやすい -1.0〜1.0 の範囲に正規化しています。

audio_np = np.frombuffer(data, dtype=np.int16).astype(np.float32) / 32768.0
audio_np[np.abs(audio_np) < 0.01] = 0           # 簡易ノイズゲート

volume = min(int(np.max(np.abs(audio_np)) * 100), 100)
self.volume_bar["value"] = volume

self.line.set_data(range(len(audio_np)), audio_np)
self.ax.set_xlim([0, len(audio_np)])
self.canvas.draw()

音量メーターは「その瞬間の最大振幅」を0〜100に換算してプログレスバーに渡しているだけ。波形は line.set_data() で点を更新し、canvas.draw() で再描画します。冒頭で matplotlib.rcParams['font.family'] = 'Yu Gothic' と日本語フォントを指定しているので、グラフタイトルも文字化けしません。

4-4. WAVファイルへの保存

保存は標準の wave モジュールだけで完結します。チャンネル数・サンプル幅・サンプリングレートを録音時と揃えて、ためておいた frames を書き込むだけです。

wf = wave.open(file_path, "wb")
wf.setnchannels(self.channels)
wf.setsampwidth(self.p.get_sample_size(pyaudio.paInt16))
wf.setframerate(self.rate)
wf.writeframes(b"".join(self.frames))
wf.close()

filedialog.asksaveasfilename() で保存先をユーザーに選んでもらえるので、実用的なデスクトップアプリらしい操作感になります。

5. つまずきやすいポイント

  • PyAudio が入らない:本記事の「2. インストール」のWindows向け手順を参照。pip更新 → pipwin の順で解決することが多いです。
  • 録音できない/無音になる:OSのプライバシー設定で「マイクへのアクセス」が許可されているか、プルダウンで正しいデバイスを選んでいるかを確認しましょう。
  • 波形描画が重いcanvas.draw() は毎チャンク呼ぶと負荷が高めです。動作が重い場合は chunk を大きく(例: 2048)するか、描画を数回に1回へ間引くと軽くなります。
  • 停止時にエラーが出る:録音していない状態で停止・保存を押したときのために、if not self.recording: returnif not self.frames: return のガードを入れてあります。

6. 発展・関連アプリ

動かせたら、次のような拡張に挑戦すると理解が一段深まります。

  • 録音した音声をその場で再生する(基本は録音・再生アプリが参考になります)
  • 無音区間の自動カットや、簡単なフェードイン/フェードアウト
  • MP3など他形式への変換(pydub などの追加ライブラリ)

tkinter のGUIづくりをもっと練習したい人は、中級者向けアプリ100本から興味のあるものを写経していくのがおすすめです。使ったライブラリの用途を整理したいときはライブラリ一覧もどうぞ。

7. 快適に開発するためのPC環境

リアルタイムの波形描画や音声処理は、地味にCPUとメモリを使います。録音しながらMatplotlibを毎フレーム再描画するため、非力なPCだと波形がカクついたり、録音が途切れたりすることがあります。

これからPythonでGUIアプリ開発を本格的に進めるなら、CPUは Core i5 以上、メモリは 16GB 以上、起動と読み込みが速い SSD 搭載のPCがあると安心です。学習用PCの選び方はPython学習におすすめのPCで用途別にまとめています。

ℹ️
お知らせ

当サイトは現在、特定の製品へのアフィリエイトリンクは掲載していません。PC紹介は学習のしやすさという観点での一般的な目安です。

8. よくある質問(FAQ)

Q. PyAudio のインストールに失敗します。

まず pip を最新化(python -m pip install --upgrade pip)してから再実行します。Windowsでビルドエラーになる場合は、pip install pipwin の後に pipwin install pyaudio を試すと通ることが多いです。詳しくはよくあるエラー解決ページもどうぞ。

Q. 録音すると無音になります。

OSのマイク権限(プライバシー設定)が許可されているか、プルダウンで正しい入力デバイスを選んでいるかを確認してください。複数のマイクやオーディオインターフェースがある環境では、デバイスの選択ミスが原因のことが多いです。

Q. リアルタイム波形がカクついて重いです。

波形の再描画(canvas.draw())は1チャンクごとに呼ぶと負荷が高めです。chunk を 2048 などに大きくするか、描画を数回に1回へ間引くと軽くなります。録音データの保存自体は続くので、見た目の更新頻度だけを落とすイメージです。

Q. macOS や Linux でも動きますか。

動きます。ただし日本語フォント指定の Yu Gothic はWindows向けなので、Macは Hiragino Sans、Linuxは IPAexGothic など、環境にインストールされているフォント名へ変えると波形タイトルが正しく表示されます。

Q. 音質(サンプリングレート)やチャンネル数は変えられますか。

__init__self.rate(既定 44100Hz)や self.channels(既定 1=モノラル)を変更します。ステレオで録りたい場合は channels=2 にし、ステレオ入力に対応したマイクを使ってください。

Q. WAV 以外(MP3 など)で保存できますか。

標準の wave モジュールはWAV形式のみ対応です。MP3で保存したい場合は pydub などの追加ライブラリと ffmpeg を組み合わせると、録音後にWAV→MP3へ変換できます。

9. 参考リンク(公式ドキュメント)

本記事で使ったライブラリの一次情報です。仕様の正確な確認や、さらに踏み込んだ使い方は公式ドキュメントを参照してください。

10. まとめ

AudioRecorderApp は、tkinter(GUI)+PyAudio(録音)+NumPy(数値変換)+Matplotlib(波形)という役割分担で組み立てられた録音アプリです。マイク選択・リアルタイム波形・音量メーター・WAV保存と、デスクトップアプリらしい機能がひと通り詰まっています。

ポイントは、(1) 録音ループを別スレッドに逃がしてGUIを固めないこと、(2) バイト列を NumPy で扱いやすい数値に直すこと、(3) ノイズゲートは絶対値で判定すること、の3つ。ここを押さえれば、自分なりに機能を足していくのも難しくありません。ぜひ手元で動かして、自分だけの録音アプリに育ててみてください。

掲載コードについて

本記事のコードはMITライセンスで自由に利用できます。記事は実際に動作するコードをもとに構成・解説しています。環境によって挙動が異なる場合があるため、お手元での動作確認をおすすめします。なお本記事は、生成AIを活用して下書きし、運営者が内容を確認・編集したうえで公開しています。