その昔、デジタルオーディオプレイヤーというものが存在した。もはや化石とも言えるこの機器を人類で利用しているのは地球上でも数わずか。そんな貴重な旧人類の一人がここにいる。そう、私だ。

体を動かしたりする際にデジタルオーディオプレイヤーを使って音楽はもちろん、ラジオやポッドキャストを聴いたりするのだが、シークがとにかく面倒くさい。物理ボタンを使うのだが数秒単位なので長時間シークすると手がつりそうになる。なにより時間がかかる。そのため、数時間あるポッドキャストの途中でプレイヤーの電池が切れるとげんなりする。

そこで音声ファイルを一定時間ごとに分割することを思いついた。トラックのスキップは一瞬なので特定のポジションまで移動するのに時間がかからない。

分割にはffmpegを使うことにした。ffmpegだと再エンコードを回避できるし(-codec copy)、指定時間だけ出力対象とすることも可能(-t)だと知っていた。おそらく開始位置をずらすこともできるだろうと思って調べると、やっぱり出来た。-ssオプションがそれにあたる

つまり、ある音声ファイルを先頭10秒後から30秒間切り出すにはこうなる。

ffmepg -ss 10 -i /path/to/input -acodec copy -t 30 /path/to/output

また、分割には総再生時間を知る必要があるが、たいていffmpegと一緒にインストールされているffprobeを使って調べることができる。

ffprobe -print_format json -show_format /path/to/input

これで情報がjson形式でわかる。便利!あとは利用しやすいようにpythonで簡単なスクリプトを書いた。

pythonの実行環境とffmpegがインストールされていてデジタルオーディオプレイヤーを使っている人間は全世界で数人しか居なさそうだが、せっかくなのでcc0で公開しておく。 -dオプションで分割する秒数を指定する。分割後オリジナルファイルを削除するので注意が必要だ。

#!/usr/bin/env python3
# license: cc0 (https://creativecommons.org/publicdomain/zero/1.0/deed.ja) 

from pathlib import Path
import subprocess
import json
import argparse
import platform
import time

class AudioSplitter:
    """音声ファイルを秒数で分割する"""

    def __init__(self, win, duration, path):
        self.media_length = 0
        self.win = win
        self.duration = float(duration)
        self.path = Path(path)

        if not self.path.is_file():
            raise Exception('file not found')

    def get_media_length(self):
        """mediaの再生時間を取得する"""

        if self.media_length:
            return self.media_length
        else:
            media_length = self._get_media_length()

            if media_length:
                self.media_length = media_length

            return media_length

    def _get_media_length(self):
        if self.win:
            bin = "ffprobe.exe"
        else:
            bin = "ffprobe"

        args = [bin, '-hide_banner', '-print_format', 'json', '-show_format', '-loglevel', 'quiet', self.path]

        comp = subprocess.run(args, stdout=subprocess.PIPE, check=True)
        output = comp.stdout
        parsed = json.loads(output)

        return float(parsed["format"]["duration"])


    def real_split(self, duration, start, output):
        if self.win:
            bin = 'ffmpeg.exe'
        else:
            bin = 'ffmpeg'

        args = [bin, '-hide_banner', '-y', '-ss', str(start), '-i', str(self.path), '-map', '0:a', '-acodec', 'copy', '-vn', '-map_metadata', '-1', '-t', str(duration), str(output)]
        comp = subprocess.run(args, check=True)

    def split(self):
        length = self.get_media_length()
        current = 0
        index = 0
        original = self.path.name
        duration = self.duration

        while length > current:
            start = current
            index = index + 1
            filename = f'{index}_{original}'
            output = self.path.with_name(filename)

            self.real_split(self.duration, start, output)
            current = current + duration

        else:
            self.path.unlink()

def argparser():
    parser = argparse.ArgumentParser()
    parser.add_argument('files', nargs='+', help='input file path')
    parser.add_argument('-d', '--duration', default='900', help='ファイルあたりの最大時間')
    #parser.add_argument('-w', '--windows', action='store_true')

    args = parser.parse_args()

    return args


def main():
    args = argparser()

    if platform.system() == 'Windows':
        win = True
    elif str(Path.cwd()).startswith('/mnt/c/'):
        win = True
    else:
        win = False

    for f in args.files:
        audio = AudioSplitter(win=win, duration=args.duration, path=f)
        audio.split()
        time.sleep(1)


if __name__ == '__main__':
    main()