Python の wave モジュール例の殴り書き(4)

まぁ(3)の直接的な続きではあるけれど。

  1 import itertools
  2 import wave
  3 import numpy as np
  4 
  5 try:
  6     # for python 2.7
  7     zip = itertools.izip
  8 except AttributeError:
  9     pass
 10 
 11 class _WaveRWBase(object):
 12     def __init__(self, waveobj):
 13         self._wave = waveobj
 14 
 15     def __enter__(self):
 16         return self
 17 
 18     def __exit__(self, type, value, tb):
 19         try:
 20             self._wave.close()
 21         except:
 22             pass
 23 
 24     def __getattr__(self, attrname):
 25         return getattr(self._wave, attrname)
 26 
 27 class _WaveReader(_WaveRWBase):
 28     def __init__(self, fname):
 29         _WaveRWBase.__init__(self, wave.open(fname, "r"))
 30 
 31 class _WaveWriter(_WaveRWBase):
 32     def __init__(self, fname, nchannels, sampwidth, samprate):
 33         _WaveRWBase.__init__(self, wave.open(fname, "w"))
 34         self._wave.setparams(
 35             (nchannels, sampwidth, samprate,
 36              # For seekable output streams, the wave header will automatically
 37              # be updated to reflect the number of frames actually written.
 38              0,
 39              'NONE', 'not compressed'  # you can't change these
 40              )
 41             )
 42 
 43     def _writepcmraw(self, pcm):
 44         import struct
 45 
 46         packed = struct.pack('h' * len(pcm), *pcm)
 47         self._wave.writeframes(packed)
 48 
 49     def writechannels(self, channels):
 50         import struct
 51         CHUNK_SIZE = 4096
 52 
 53         _pcm = []
 54         for d in itertools.chain.from_iterable(zip(*channels)):
 55             _pcm.append(d)
 56             if len(_pcm) == CHUNK_SIZE:
 57                 self._writepcmraw(_pcm)
 58                 _pcm = []
 59         if _pcm:
 60             self._writepcmraw(_pcm)
 61 
 62 
 63 if __name__ == '__main__':
 64     import os
 65     import argparse
 66     parser = argparse.ArgumentParser()
 67     parser.add_argument(
 68         "mode", choices=[
 69             "copy", "reverse", "swaplr", "rdelay",
 70             "left2mono", "right2mono", "negate", "sampx2"
 71             ])
 72     parser.add_argument("--modeintparam", type=int, default=0)
 73     parser.add_argument("target")
 74     args = parser.parse_args()
 75 
 76     with _WaveReader(args.target) as fi:
 77         nchannels, width, rate, nframes, _, _ = fi.getparams()
 78         onchannels = nchannels
 79         raw = np.fromstring(fi.readframes(nframes), dtype=np.int16)
 80         if args.mode == "reverse":
 81             channels = reversed(raw[::2]), reversed(raw[1::2])
 82         elif args.mode == "swaplr":
 83             # if nchannels is 1, actualy this swap odd and even.
 84             channels = raw[1::2], raw[::2]
 85         elif args.mode == "rdelay":
 86             if not args.modeintparam:
 87                 delay = rate // 50
 88             else:
 89                 delay = args.modeintparam
 90             channels = [
 91                 np.hstack((raw[::2], np.zeros(delay, dtype=np.int16))),
 92                 np.hstack((np.zeros(delay, dtype=np.int16), raw[1::2]))]
 93         elif nchannels == 1 and args.mode in ("left2mono", "right2mono"):
 94             channels = (raw,)
 95         elif args.mode == "negate":
 96             channels = (-raw[::2], -raw[1::2])
 97         elif args.mode == "left2mono":
 98             channels = (raw[::2],)
 99             onchannels = 1
100         elif args.mode == "right2mono":
101             channels = (raw[1::2], )
102             onchannels = 1
103         elif args.mode == "sampx2":
104             left, right = raw[::2], raw[1::2]
105             left = itertools.chain.from_iterable(zip(left, left))
106             right = itertools.chain.from_iterable(zip(right, right))
107             channels = left, right
108         else:
109             channels = raw[::2], raw[1::2]
110 
111     ofname = os.path.splitext(args.target)[0] + "_" + args.mode + '.wav'
112     with _WaveWriter(ofname, onchannels, width, rate) as fo:
113         fo.writechannels(channels)

例によってスクリプトの機能は「欲しいから」とか「皆様のお役に立つに違いない」じゃなくて、「本当にやりたいことのための下ごしらえ」で増やしたもの。sampx2 モードは実際には「0.5倍速」に相当することが起こりまする。

(3)同様に「オレ的動機」の一部だけは説明しとく。izip と CHUNK_SIZE だけが意図…だけで伝わるかなぁ? 一般的な曲はだいたい長くて5分程度でしょ。そのくらいのサイズを処理するのに、「リスト化」とか「'h' * len(pcm)」がベラぼうなコストになることがあるのね。sampx2 モードやその他「色々なごにょごにょ」をやり出すと、処理時間だけならともかく PC が限りなくハングアップ状態に近くなることも普通に起こっちゃうわけな。だから出来るだけリストの実体化を避けたい、というわけだ。

まぁここまでするなら、しかも元から numpy は使っている上にこのあと scipy もどうせ使うよな、ってノリなんで、scipy に含まれてる wave ファイルを扱うモジュールを使っちゃえばいいのに、みたいなことにはなってくるんだけれども。ただあたしが根本的にやりたいのは「wave ファイルの扱い」つーよりは PCM データそのもののデータ処理だったりするんで、別に wave ファイルの扱いがダルかろうがそうでなかろうが、あんまし関係ないんだよね。(どっちでもたかが知れておろう。)

あ、コードみればわかると思うんだけれど、(3)までは Python 3.x 前提だったけど、今回のは 2.7 でも動く。わかるよね? コンテクストマネージャが 2.7 版にはないの。

あと一応。正しい理解をしている人には自明と思うけれど、出力の sampwidth、samprate を書き換えると「知らなかった人には面白い」かもしれないんで、興味あったら遊んでみるといいよ。sampwidth を 2倍にするとどうなる? samprate を 2倍にするとどうなる?

ちなみに蛇足。「なんで 44100?」の理由を知ってちょっと感動した。最初の4つの素数の自乗の積(2**2 * 3**2 * 5**2 * 7**2)、なんだね。だから何で割ってもあんまし端数が出ない。考えた人天才。

02:30 追記
一応これは言っておこうと思う。

ワタシの興味が「音声「ファイル」を扱う」ではないので、上の例も、今後の派生も「ハイレゾ」(24ビット)を扱う気はないです。パッキング時の「’h’」は 16ビットだから、だし、読み込み時の int16 も 16ビット前提だから。

それをしたい人は探せば日本語で見つかるよ。

じゃぁあたしの興味範囲はなんなのか? まぁ整理出来始めて来たら何か形にするかもしれんけれど、要するに「音声もその一味」という興味の持ち方…、てことで今のところは許して。しかるに「音声だけ特別に興味を持っているわけでない」てこと。画像、映像、音声、テキスト。これらは「所詮データでしょ」、と言う見方をしたいが、そのためには…、て学び方の最中なの。

ちなみに上の sampwidth を書き換えて遊ぶ、てのは、「真面目な製品品質のものを作るならダメ」よ。まさにその「ハイレゾ」あたりとセットで正しく理解してセットしないと「不当なもの」を作っちゃうことになる。ただ書き換えるとプレイヤーがどんな反応をするのかを知っとくのは理解の助けにはなると思うのでね。