1. はじめに
前回の記事の「英語学習のため、自作のExcel英単語・英例文リストから英日音声ファイルを生成してみた」では、英単語帳のExcelファイルから音声付きの語彙学習素材を作成し、ウォークマンなどのデバイスで再生できるようにしました。
ただ、この方法ですと、英単語の数が増えると 数千個のMP3ファイル が出力されてしまい、ウォークマンなど一部のプレイヤーでは再生可能数の上限を超えてしまいます。そこで今回は、これらの分割MP3ファイルを 1つの長い音声ファイルに統合するためにPythonでスクリプトを作成した、というのが今回の記事の内容です。
2. 実施環境
- CPU: CORE i5 8th Gen
- メモリ: 8GB
- OS: Windows 11 22H2
3. 使用ライブラリ
必要なライブラリをインストールします。
pip install pydub
また、音声処理のために ffmpeg が必要です。
4. 作成したプログラム:merge_vocab_audio.py
以下が実際に作成したプログラムです。
from pathlib import Path
import re
import csv
import os
import subprocess
from pydub import AudioSegment
from pydub.utils import which
# === 設定 ======================================================
IN_DIR = Path("分割mp3のフォルダを指定") # 分割mp3のフォルダ
OUT_WAV = Path("merged_vocab.wav")
OUT_MP3 = Path("merged_vocab.mp3")
TIMELINE_CSV = Path("merged_timeline.csv")
GAP_MS = 300 # 同一行の各パート間の無音
ROW_GAP_MS = 700 # 行と行の間の無音
PAT = re.compile(r"^(?P<num>\d{4})_.*_(?P<order>[1-4])_(?P<tag>en_word|mean|ja_example|en_example)\.mp3$", re.IGNORECASE)
# 必要なら ffmpeg のフルパスを指定(例:r"C:\ffmpeg\bin\ffmpeg.exe")
# 未設定なら PATH から自動検出
FFMPEG_PATH = os.environ.get("FFMPEG_PATH", "").strip()
# ===============================================================
def resolve_ffmpeg_path() -> str:
if FFMPEG_PATH:
if Path(FFMPEG_PATH).exists():
return FFMPEG_PATH
raise SystemExit(f"FFMPEG_PATH に指定した ffmpeg が見つかりません: {FFMPEG_PATH}")
p = which("ffmpeg")
if p is None:
raise SystemExit("ffmpeg が見つかりません。インストールし PATH を通すか、FFMPEG_PATH を設定してください。")
return p
def require_ffmpeg():
ff = resolve_ffmpeg_path()
AudioSegment.converter = ff
return ff
def export_mp3_with_lame(merged: AudioSegment, out_mp3: Path, tmp_wav: Path, ffmpeg_path: str):
# まずは pydub で直接 MP3 出力(libmp3lame 明示)
try:
merged.export(
out_mp3,
format="mp3",
codec="libmp3lame",
bitrate="48k",
parameters=["-ac", "1", "-ar", "24000", "-write_xing", "0"]
)
return # 成功
except Exception as e:
print(f"[WARN] pydub 直接MP3出力に失敗: {e}")
# フォールバック:WAV に一旦書き出してから ffmpeg CLI で MP3 化
print("フォールバック: WAV に書き出し → ffmpeg CLI で MP3 化 …")
merged.export(tmp_wav, format="wav")
cmd = [
ffmpeg_path, "-y", "-hide_banner", "-loglevel", "error",
"-i", str(tmp_wav),
"-ac", "1", "-ar", "24000",
"-b:a", "48k", "-codec:a", "libmp3lame",
str(out_mp3)
]
try:
subprocess.run(cmd, check=True)
except subprocess.CalledProcessError as ee:
raise SystemExit(f"ffmpeg による MP3 変換に失敗しました。詳細: {ee}") from ee
finally:
try:
tmp_wav.unlink()
except Exception:
pass
def main():
ffmpeg_path = require_ffmpeg()
if not IN_DIR.exists():
raise SystemExit(f"入力フォルダがありません: {IN_DIR.resolve()}")
files = list(IN_DIR.glob("*.mp3"))
if not files:
raise SystemExit(f"{IN_DIR.resolve()} に mp3 が見つかりません。")
# 行ごとにグループ化
rows = {} # num -> [(order, tag, Path)]
skipped = []
for p in files:
m = PAT.match(p.name)
if not m:
skipped.append(p.name)
continue
num = m.group("num")
order = int(m.group("order"))
tag = m.group("tag").lower()
rows.setdefault(num, []).append((order, tag, p))
if not rows:
raise SystemExit("命名規則に合致するファイルがありません。ファイル名のフォーマットを確認してください。")
# 行番号で昇順
sorted_nums = sorted(rows.keys())
merged = AudioSegment.silent(duration=0)
timeline = []
current_ms = 0
print(f"対象行数: {len(sorted_nums)}")
if skipped:
print(f"※命名規則に合わずスキップ: {len(skipped)} 件(例:{skipped[:5]})")
for idx, num in enumerate(sorted_nums, start=1):
items = sorted(rows[num], key=lambda x: x[0])
parts = []
print(f"[{idx}/{len(sorted_nums)}] 行 {num} を結合中...", end="")
for order, tag, path in items:
try:
seg = AudioSegment.from_file(path, format="mp3")
parts.append(seg)
except Exception as e:
print(f"\n [WARN] 読み込み失敗: {path.name} ({e})")
if not parts:
print(" スキップ(有効なセグメントなし)")
continue
# 行の開始時刻を記録
timeline.append((num, current_ms))
row_audio = parts[0]
for s in parts[1:]:
row_audio += AudioSegment.silent(duration=GAP_MS)
row_audio += s
merged += row_audio
current_ms += len(row_audio)
# 行間の無音
if idx < len(sorted_nums):
merged += AudioSegment.silent(duration=ROW_GAP_MS)
current_ms += ROW_GAP_MS
print(f" 完了({len(row_audio)} ms)")
if len(merged) == 0:
raise SystemExit("統合結果が空でした。")
# WAV も必要なら出力
print("WAV に書き出し中…")
merged.export(OUT_WAV, format="wav")
# MP3 へ(libmp3lame 明示+フォールバック付き)
print("MP3 に変換中…")
tmp_wav = OUT_WAV.with_suffix(".tmp.wav")
export_mp3_with_lame(merged, OUT_MP3, tmp_wav, ffmpeg_path)
print(f"OK: {OUT_MP3.resolve()} 総時間: {len(merged)} ms")
# 目次CSV
with open(TIMELINE_CSV, "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["row_num(先頭4桁)", "start_ms"])
for row_num, start_ms in timeline:
w.writerow([row_num, start_ms])
print(f"目次: {TIMELINE_CSV.resolve()}")
if __name__ == "__main__":
main()
5. 実行方法
以下のコマンドで実行します。
python merge_vocab_audio.py
6. 実行結果
スクリプトを実行すると、指定フォルダ内のMP3ファイルが自動的に読み込まれ、
1つのファイルにまとめられます。
生成されるファイルは次の3つです。
| ファイル | 内容 |
|---|---|
merged_vocab.wav |
中間生成物(WAV形式) |
merged_vocab.mp3 |
完成した音声ファイル(ウォークマン対応) |
merged_timeline.csv |
各英単語(行)の再生開始時刻(ms)を記録 |
7. 結果とまとめ
このスクリプトを使うことで、数百〜数千に分割されていた英単語音声ファイルを1つの長いMP3としてまとめることができました。ウォークマンに転送すると、リスト全体をスムーズに再生できることも確認できました。現在は、ExcelからKindle対応の英語単語帳PDFを自動生成してみたの記事で作成したPDFを見つつ、この英単語音声ファイルを聞いて勉強を進めています。 ちなみに問題点としては、やはり発音やイントネーションにやや難があるところです。これの解決に今は取り組むつもりはありませんが、何か新しい手法を見つけたら取り組みたいと思います。