Pythonで英語CDを分割してみた

蒸し暑くなってきて、もともと少ないやる気がさらに無くなってきたのだけど、
多少は何かしないといけないなぁと思って、なんとなくDUO3.0の例文でも覚えようかと思った。


で、英語CDにありがちな1トラックにいくつかのセンテンスが入っているのが鬱陶しいので、
とりあえず分割ファイルをつくろうと思ってスクリプトを書いてみた。


やり方はいたって単純でレベルの低い部分がしばらく続いたら分割する。

こんな感じでやった

  1. iTunesでwavエンコーダを使ってCDからwavファイルを作る。
  2. 設定ファイルを作る
  3. 実行


設定ファイルはYAMLで書く。
src_path と dst_path は必須で、それ以外はオプション。

src_path:
  wavファイルのあるフォルダ

dst_path:
  分割したファイルを出力するフォルダ

params:
  分割するためのパラメータ

profiles:
  分割するための情報
paramsの説明

分割するためのパラメータをPythonのタプルの書式で書く。

  • 無音の音量レベル(0-32768)
  • 無音の継続時間(秒) … これより短い場合は無音部分とみなされない
  • 音声の継続時間(秒) … これより短い場合は捨てる
params:
  (75, 0.50, 1.0)
profilesの説明

各トラック毎の音声開始時間(秒)と終了時間(秒)をタプルの書式で書く。
profilesが無ければ、paramsの設定でprofilesを作成する。(あらかじめどの区間で区切るかが分かっているときに使う)

profiles:
  -
    - (  3.244,   7.142) #  1
    - (  7.142,  12.462) #  2
    - ( 12.462,  20.028) #  3
    - ( 20.028,  26.871) #  4
    - ( 26.871,  31.658) #  5
    - ( 31.658,  37.434) #  6
    - ( 37.434,  44.912) #  7
    - ( 45.150,  49.823) #  8
    - ( 49.823,  57.293) #  9


paramsだけだと細かくなりすぎてしまう。
妥協すまいとトラックごとにパラメータ調整できるようにとか色々やってみたけど、
半日くらいかけてバシっとキマる設定を探すのは諦めて、profilesを指定できる方法にしてみた。


今はDUO3.0用の profiles を作っています。まだ英文は覚えていない。本末転倒。

ソースコード

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import sys
import math
import wave
import yaml

def bytes_to_shorts(lst):
    it = iter(lst)
    while True:
        a = 0x00ff & ord(it.next())
        b = 0x00ff & ord(it.next())
        x = ((b << 8 ) + a)
        if x & 0x8000:
            x = -(0x10000 - x)
        yield x

def split_2ch(it):
    while True:
        l = it.next()
        r = it.next()
        yield l, r

def pp(id, begin, end):
    print '    - (%7.3f, %7.3f) # %2d' % (begin, end, id)
    
def make_profile(filepath,
                 silent_lv=150, min_silent_interval=0.5, min_voice_interval=2.0):
    
    src = wave.open(filepath, 'rb')
    params = src.getparams()
    nchannles, sampwidth, framerate = params[:3]
    
    def iterator(src, buf_size = 1024):
        read_bytes = buf_size * (nchannles * sampwidth)
        
        while True:
            buf = src.readframes(buf_size)
            
            for l, r in split_2ch(bytes_to_shorts(buf)):
                yield (l + r) / 2
                
            if len(buf) != read_bytes:
                break
    
    print 'make_profile'
    id = 0
    result = []
    beginOfVoice = beginOfSilent = endOfSilent = None
    for i, x in enumerate(iterator(src)):
        if abs(x) <= silent_lv:
            if beginOfSilent is None:
                beginOfSilent = i
            else:
                endOfSilent   = i
        else:
            if beginOfVoice is None:
                beginOfVoice  = i
            else:
                if beginOfSilent is not None and endOfSilent is not None:
                    silent_interval = (endOfSilent - beginOfSilent) / float(framerate)
                    if silent_interval >= min_silent_interval:
                        voice_interval = (beginOfSilent - beginOfVoice) / float(framerate)
                        if voice_interval >= min_voice_interval:
                            begin = beginOfVoice / float(framerate)
                            end   = endOfSilent  / float(framerate)
                            result.append((begin, end))
                            pp(id, begin, end)
                            
                            id += 1
                            beginOfVoice = None
                        else:
                            beginOfVoice = None
                    beginOfSilent = endOfSilent = None
    else:
        if beginOfSilent is not None and endOfSilent is not None:
            silent_interval = (endOfSilent - beginOfSilent) / float(framerate)
            if silent_interval >= min_silent_interval:
                voice_interval = (beginOfSilent - beginOfVoice) / float(framerate)
                if voice_interval >= min_voice_interval:
                    begin = beginOfVoice / float(framerate)
                    end   = endOfSilent  / float(framerate)
                    result.append((begin, end))
                    pp(id, begin, end)
    src.close()
    return result

def write_waves(filepath, profile, dst_path):
    src = wave.open(filepath, 'rb')   
    params = src.getparams()
    nchannles, sampwidth, framerate = params[:3]
    
    result = []
    for i, (begin, end) in enumerate(profile):
        
        beginOfVoice = int(math.ceil(begin * float(framerate)))
        endOfVoice   = int(math.ceil(end   * float(framerate)))
        
        src.setpos(beginOfVoice)
        dat = src.readframes(endOfVoice - beginOfVoice)

        filename = os.path.basename(filepath)
        root, ext = os.path.splitext(filename)
        
        filename = '%s_%02d%s' % (root, i, ext)
        filepath2 = os.path.join(dst_path, filename)
        
        dst = wave.open(filepath2, 'wb')
        dst.setparams(src.getparams())
        dst.writeframes(dat)
        dst.close()
           
        result.append((filepath2, begin, end))
       
    src.close()
    return result

def main(argc, argv):
    
    for f in argv[1:]:
        settings = yaml.load(open(f))
        src_path = settings['src_path']
        dst_path = settings['dst_path']
        
        targets  = settings.get('targets')
        if not isinstance(targets, list):
            targets = None
            
        params = settings.get('params')
        if params:
            params = eval(params)
        else:
            params = (100, 0.50, 2.0)

        profiles = settings.get('profiles')
        
        file_no = 0
        for filename in os.listdir(src_path):
            if not filename.endswith('wav'):
                continue

            file_no += 1
            if targets and filename not in targets:
                continue            
            
            filepath = os.path.join(src_path, filename)
            print '%s' % filepath
            
            if profiles and file_no <= len(profiles):
                profile = [ eval(x) for x in profiles[file_no - 1] ]
            else:
                profile = make_profile(filepath, *params)
                
            file_list = write_waves(filepath, profile, dst_path)
            for output, begin, end in file_list:
                print ' %10s: %7.3f, %7.3f' % (output, begin, end)


if __name__ == '__main__':
    main(len(sys.argv), sys.argv)