Closed5

pydubを改めて試してみた

kun432kun432

以前に音声会話データから無音部分をカットするのに使ったpydubだが、

https://zenn.dev/kun432/scraps/2e3f25fe0fa69d

無音カットだけでなく、いろいろな機能がある。少し触れる機会があったので、改めて一通り触ってみたい。

https://github.com/jiaaro/pydub

pydub: シンプルで使いやすい高機能インターフェースで音声を操作する

Pydubは、オーディオに対して、馬鹿げた方法ではないやり方で加工を行うことができる。

kun432kun432

インストール・オーディオファイルの読み込み

Colaboratoryで。

パッケージインストール。pydubは型ヒントが用意されていないので、他の方が作ったpydubのスタブファイルのパッケージも合わせてインストールしておく。

!pip install pydub pydub-stubs

オーディオファイルの読み込み。これでpydubで扱えるAudioSegmentオブジェクトの形で読み込まれる。

from pydub import AudioSegment

song = AudioSegment.from_mp3("music.mp3")

なお、今回はmp3を使用したので、.from_mp3()メソッドを使用しているが、ffmpegがサポートしているファイルならなんでも読み込めるらしく、例えば.from_wav().from_oggなどファイル形式に合わせて読み込めば良い。

または、以下のようにfrom_file()でファイル形式を指定することもできる。

from pydub import AudioSegment

song = AudioSegment.from_file("music.mp3", "mp3")

読み込んだAudioSegmentについて調べてみる。

print("再生時間(秒):", song.duration_seconds)
print("再生時間(ミリ秒):", len(song))
print("チャネル数(1:モノラル、2:ステレオ):", song.channels)
print("サンプリングレート(Hz):", song.frame_rate)
print("ビット深度:", song.sample_width)
print("平均音量(dB:0が最大):", song.dBFS)
print("ピーク音量(dB:0が最大):", song.max_dBFS)
再生時間(秒): 71.16
再生時間(ミリ秒): 71160
チャネル数(1:モノラル、2:ステレオ): 2
サンプリングレート(Hz): 48000
ビット深度: 2
平均音量(dB:0が最大): -10.048038776889726
ピーク音量(dB:0が最大): 0.0
kun432kun432

オーディオデータの操作

オーディオデータの操作をいろいろやっていく。操作したオーディオの確認用に、Colaboratoryのセルにオーディオ再生インタフェースを表示するヘルパー関数を用意した。

import io
from IPython.display import Audio, display
from pydub import audio_segment

def play_audio(audio_segment: audio_segment.AudioSegment, auto_play: bool = True) ->None:
    """
    ColaboratoryのセルでpydubのAudioSegmentを再生する

    Args:
        audio_segment (audio_segment.AudioSegment): 再生するAudioSegment
        auto_play (bool, optional): オーディオを自動再生するかどうか。デフォルトは True
    """
    buffer = io.BytesIO()
    audio_segment.export(buffer, format="mp3")
    buffer.seek(0)
    display(Audio(buffer.read(), autoplay=auto_play))

こんな感じで確認できる。実際に試しつつ確認すると良い。

ではいろいろやってみる。

Pythonのスライスでオーディオを部分的に切り出すことができる。スライスはミリ秒で指定する。先頭5秒を切り出してみる。

# 5秒 = 5000ミリ秒
five_seconds = 5 * 1000

# 先頭5秒を切り出す
first_five_secs = song[:five_seconds]
print(first_five_secs.duration_seconds)
play_audio(first_five_secs)
5.0

最後5秒

first_five_secs = song[:five_seconds]
print(first_five_secs.duration_seconds)
play_audio(first_five_secs)
5.0

数値を足し引きすれば音量(dB)を調節できる(元ファイルの音量によっては音量上げた場合に音割れするかも)

# 10秒切り出し
ten_secs = 10 * 1000
song_piece = song[:ten_secs]

# 5dB音量を上げる
song_piece_plus_5db = song_piece + 5

# 5dB音量を下げる
song_piece_minus_5db = song_piece - 5

print("Normal:", song_piece.dBFS, "dB")
play_audio(song_piece, False)

print("+5db:", song_piece_plus_5db.dBFS, "dB")
play_audio(song_piece_plus_5db, False)

print("-5db:", song_piece_minus_5db.dBFS, "dB")
play_audio(song_piece_minus_5db, False)]

AudioSegmentオブジェクトを結合すると、続けて再生される1つのオーディオとなる。上で音量を上げ下げした2つのオーディオを結合してみる。

# AudioSegmentオブジェクトを結合
song_concatenated = song_piece_minus_5db + song_piece_plus_5db
print(song_concatenated.duration_seconds)
play_audio(song_concatenated)

10秒+10秒で20病のオーディオになっていることがわかる。実際に再生してみるとわかるが、この例では小さい音量のオーディオに大きい音量のオーディオを結合しているので、途中(開始10秒後)で音量が変わる1つのオーディオになる

ループは回数で掛け算する。

# 10秒切り出し
ten_secs = 10 * 1000
song_piece = song[:ten_secs]

# 3回ループ
song_piece_loop = song_piece * 3
print(song_piece_loop.duration_seconds)
play_audio(song_piece_loop)
30.0

逆再生もできる。ちょっとサイケな感じになるね。

# 10秒切り出し
ten_secs = 10 * 1000
song_piece = song[:ten_secs]

# 逆再生
song_piece_backward = song_piece.reverse()
play_audio(song_piece_backward)

クロスフェード。重なり合う時間を指定する。

# 先頭と最後10秒をそれぞれ切り出し
ten_secs = 10 * 1000
song_start = song[:ten_secs]
song_end = song[-ten_secs:]

# 3秒でクロスフェード
song_crossfade = song_start.append(song_end, crossfade=3000)
print(song_crossfade.duration_seconds)
play_audio(song_crossfade)
17.0

イメージとしてはこんな感じ。

フェードイン・フェードアウト。これもそれぞれの時間を指定する。

# 10秒分切り出し
ten_secs = 10 * 1000
song_piece = song[:ten_secs]

# 5秒間でフェードイン
print("fade in:")
song_piece_fade_in = song_piece.fade_in(5000)
play_audio(song_piece_fade_in, False)

# 5秒間でフェードアウト
print("fade out:")
song_piece_fade_out = song_piece.fade_out(5000)
play_audio(song_piece_fade_out, False)

# フェードイン・フェードアウトをチェーンで書ける。それぞれ3秒。
print("fade in and out:")
song_piece_fade_in_out = song_piece.fade_in(3000).fade_out(3000)
play_audio(song_piece_fade_in_out, False)

操作したオーディオは.export()で保存する。メタデータの付与や、ビットレートの指定、またffmpegやavlibなどのオプションも使用できるっぽい、

song_piece_fade_in_out.export("edit_music.mp3", format="mp3")
kun432kun432

ドキュメントに載ってるメソッドは上の通りだけど、コード読んでたら、他にもメソッドがあった。

.fade_in()/.fade_out()はそれぞれ音量を0から徐々に上げる、0に向かって徐々に下げるという感じだけど、例えば、途中で音量を少しだけ下げたい、みたいな場合に.fade()が使える。

ten_secs = 10 * 1000
song_piece = song[:ten_secs]

# 平均音量を取得
base_volume = song_piece.dBFS
low_volume = base_volume - 20

# 平均音量からスタートし、3秒後に2秒間で-20dBフェード
song_fade_up = song_piece.fade(from_gain=base_volume, to_gain=low_volume, start=3000, end=5000)

# 平均音量-20dBからスタートし、3秒後に2秒間で+20dBフェード
song_fade_down = song_piece.fade(from_gain=low_volume, to_gain=base_volume, start=3000, end=5000)

play_audio(song_fade_up, False)
play_audio(song_fade_down, False)

from_gain=to_gainで音量を絶対値で指定する必要があるってのが違い。

.silent()を使うと、無音のAudioSegmentオブジェクトを作成できる。例えば音声の前後に無音を追加する、というような使い方ができる。

# 10秒切り出し
ten_secs = 10 * 1000
song_piece = song[:ten_secs]

# 2秒間の無音を作成
silence = AudioSegment.silent(duration=2000)

# 前後に無音を追加
song_with_silences = silence + song_piece + silence

print(song_with_silences.duration_seconds)
play_audio(song_with_silences)

前後の無音部分が追加されたので再生時間が長くなっている。

13.999833333333333

.overlay()を使うと、複数のオーディオファイルをミックスすることができる。例えばこんな使い方ができる。

# 複数のオーディオファイルを読み込む、ここでは音楽データと音声データを読み込んでいる
music = AudioSegment.from_file("music.mp3", format="mp3")
voice = AudioSegment.from_file("voice.mp3", format="mp3")

print("音楽の長さ(秒):", music.duration_seconds)
print("音声の長さ(秒):", voice.duration_seconds)

# 音楽の開始5秒後の音声が乗っかる 
mixed_voice_on_music = music.overlay(voice, position=5000)

# 音声の開始5秒後に音楽が乗っかる(上の逆パターン)
mixed_music_on_voice = voice.overlay(music, position=5000)

print("音楽に音声がミックスされたオーディオの長さ(秒):", mixed_voice_on_music.duration_seconds)
print("音声に音楽がミックスされたオーディオの長さ(秒):", mixed_music_on_voice.duration_seconds)

print("mixed voice on music:")
play_audio(mixed_voice_on_music, False)
print("mixed music on voice:")
play_audio(mixed_music_on_voice, False)

上記の通り、overlayメソッドを呼び出すオブジェクトの長さにあわせて結合されることになる。なので、長さなどは事前に計算した上で結合してやると良いと思う。
あと、実際に試してみるとわかるのだが、音量のバランスも事前に調整しておく必要がある。

これは結構活用の幅が広いと思う。

kun432kun432

以下で、PDF2Audioを紹介したが、これを使うと、PDFの内容からポッドキャストっぽい音声データが作成できる。これで生成した音声データに対して、pydubを使ってBGMトミックスして、BGM付きポッドキャスト風音声データにしてみた。

https://zenn.dev/kun432/scraps/9a2bbf3119d3da

神戸市が公開している観光に関する統計・調査資料のうち、「令和5年度 神戸市観光動向調査結果について」のPDFの内容を元に、PDF2Audioでポッドキャスト風音声データを作成した。

(出典)「神戸市: 観光に関する統計・調査」の「令和5年度観光動向調査」
https://www.city.kobe.lg.jp/a64051/shise/toke/sightseeing.html

作成したポッドキャスト風音声データ
https://www.youtube.com/watch?v=g-QiK2xvHcA

BGMは、DOVA-SYNDROMEにてかずち様が公開されている「ビットワークス」という楽曲を使用させていただいた。

https://dova-s.jp/bgm/play7561.html

https://music.youtube.com/watch?v=WT6Vk1oDixo

それぞれのmp3ファイルを用意して、以下のような感じで、ミックス。

import io
from pydub import AudioSegment
from pydub import audio_segment
from IPython.display import Audio, display

def mix_audio(voice_path: str, music_path: str)->audio_segment.AudioSegment:
    """
    音声ファイルと音楽ファイルをミックスして、BGM付きの音声データを作成する。

    Args:
        voice_path (str): 音声ファイルのパス
        music_path (str): 音楽ファイルのパス

    Returns:
        audio_segment.AudioSegment:ミックスされた音声データを含むAudioSegment オブジェクト
    """
    # 音声ファイルと音楽ファイルを読み込む
    voice = AudioSegment.from_mp3(voice_path)
    music = AudioSegment.from_mp3(music_path)

    # パラメータ設定
    intro_length = 5000  # 5秒
    outro_length = 10000  # 10秒
    fade_duration = 1000  # 1秒のフェード
    final_fade_duration = 5000  # 5秒の最終フェードアウト
    volume_reduction = 10  # 音量を下げる量(デシベル)

    # 音声と音楽の音量調整
    voice = voice + 5  # 音声を5dB大きく
    music = music - 10  # 音楽を10dB小さく

    # クリッピング防止
    if music.max_dBFS > -1:
        music = music - (music.max_dBFS + 1)

    # 音声ファイルの作成(intro + voice + outro)
    output = AudioSegment.silent(duration=intro_length) + voice + AudioSegment.silent(duration=outro_length)
    
    # 必要な長さのBGMを用意
    total_length = len(output)
    if len(music) < total_length:
        music = music * (total_length // len(music) + 1)
    music = music[:total_length]

    # BGMの音量調整とフェード処理
    music_lowered = (
        # 1. イントロ部分(フェードアウト開始前)
        music[:intro_length - fade_duration] +

        # 2. フェードアウト(音声開始直前に完了)
        music[intro_length - fade_duration:intro_length].fade(to_gain=-volume_reduction, duration=fade_duration, start=0) +

        # 3. 音声と重なる部分(音量を下げた状態)
        (music[intro_length:intro_length+len(voice)] - volume_reduction) +

        # 4. フェードイン(音声終了後)
        music[intro_length+len(voice):intro_length+len(voice)+fade_duration].fade(from_gain=-volume_reduction, duration=fade_duration, start=0) +

        # 5. アウトロ部分(最終フェードアウト前)
        music[intro_length+len(voice)+fade_duration:-final_fade_duration] +

        # 6. 最終フェードアウト
        music[-final_fade_duration:].fade_out(duration=final_fade_duration)
    )

    # 最終的な音声ファイルの作成
    result = output.overlay(music_lowered)

    return result


def play_audio(audio_segment: audio_segment.AudioSegment, auto_play: bool = True) ->None:
    """
    ColaboratoryのセルでpydubのAudioSegmentを再生する

    Args:
        audio_segment (audio_segment.AudioSegment): 再生するAudioSegment
        auto_play (bool, optional): オーディオを自動再生するかどうか。デフォルトは True
    """
    buffer = io.BytesIO()
    audio_segment.export(buffer, format="mp3")
    buffer.seek(0)
    display(Audio(buffer.read(), autoplay=auto_play))
# ミックス
result = mix_audio("voice.mp3", "music.mp3")

# Colab上で再生
play_audio(result, False)

# ファイルに保存する
result.export("podcast.mp3", format="mp3")

音声の長さに合わせて、BGMはループさせて、会話の前後でボリュームを上げ下げなどをやっている。
こういう感じになる。

https://www.youtube.com/watch?v=-SGIdezN17U

かなりポッドキャストの雰囲気に近づけたのではないだろうか。

このスクラップは2ヶ月前にクローズされました