タブ区切りの表をスペースを入れて整形する

エクセルなどの表計算ソフトで作った表をテキストファイルにコピーペーストしたいときがあります。

f:id:uenewsar:20151226212636p:plain

この表を全選択してコピーして、テキストエディタにペーストします。

すると、セルの間はタブ区切りになるので、表のフォーマットが崩れてしまいます。

f:id:uenewsar:20151226212638p:plain

そこで、タブ区切りの表から、表示が崩れないように整形するperlスクリプトを書きました。

タブ区切りのテキストファイル(in.txt)を標準入力で取り込むと、スペースを入れて整形して、標準出力から出力します(out.txt)。

perl makeSpaceTable.pl < in.txt > out.txt

出力されたテキストファイルは、整形されているので、表っぽく見えます。

f:id:uenewsar:20151226214100p:plain

スクリプト makeSpaceTable.pl

use strict;
use utf8;

# OSにより標準入出力の文字コードを変える
if ($^O =~ /MSWin/) {
    binmode(STDIN, ":encoding(cp932)");
    binmode(STDOUT, ":encoding(cp932)");
    binmode(STDERR, ":encoding(cp932)");
} else {
    binmode(STDIN, ":utf8");
    binmode(STDOUT, ":utf8");
    binmode(STDERR, ":utf8");
}

my @array = ();
my $maxCol = 0;

# 入力文字列をバッファに取り込む
while (<STDIN>) {
    s/[\r\n]+$//; s/\t+$//;
    if ($_ eq "") {
        next;
    }
    my @tab = split(/\t/, $_, 1000);
    push(@array, \@tab);
    # 最大カラム数を保存
    if (@tab>$maxCol) {
        $maxCol = @tab;
    }
}

# バッファの各行のカラム数を合わせる
for (my $i=0; $i<@array; ++$i) {
    for (my $j=0; $j<$maxCol-@{$array[$i]}; ++$j) {
        push(@{$array[$i]}, "");
    }
}

# 各カラムの最大文字長を求める
my @maxLen = ();
for (my $i=0; $i<$maxCol; ++$i) {
    my $tmp = 0;
    for (my $j=0; $j<@array; ++$j) {
        if (getLenForPrint($array[$j][$i]) > $tmp) {
            $tmp = getLenForPrint($array[$j][$i]);
        }
    }
    push(@maxLen, $tmp);
}

# 表示する
for (my $i=0; $i<@array; ++$i) {
    my $buf = "";
    for (my $j=0; $j<$maxCol; ++$j) {
        # カラム間に挿入するスペースを作る
        my $space = " " x ($maxLen[$j] - getLenForPrint($array[$i][$j]));
        if ($i==0 || $j==0) {
            # 第1行、または第1カラムの場合、左詰め
            $buf .= $array[$i][$j] . $space;
        } else {
            # それ以外は右詰め
            $buf .= $space . $array[$i][$j];
        }
        # カラム間にはスペース3個を入れる
        $buf .= "   ";

    }
    # 行末の空白は消しておく
    $buf =~ s/ +$//;
    # 出力
    print $buf . "\n";
}


# 表示される文字長さを数える
# (半角=1, 全角=2になるように)
sub getLenForPrint {

    my ($in) = @_;

    # 半角文字だけを残す
    my $ascii = $in;
    $ascii =~ s/[^\p{InBasicLatin}]//g;

    # 非半角文字だけを残す(→全角文字とみなす)
    my $nonAscii = $in;
    $nonAscii =~ s/[\p{InBasicLatin}]//g;

    return (length($ascii) + length($nonAscii) * 2);

}

ファミコンのDPCMで再生した音をシミュレートする

ファミコンの音源には、サンプリング音声を再生するDPCMチャンネルがあります。 用意した音声ファイルをサンプリングしてDPCMチャンネルから再生したときに、どのような音になるかをシミュレートするpythonスクリプトを書きました。

DPCMチャンネルの仕様は、こちらの記事を参考にしました。
FC音源とは (ファミコンオンゲンとは) [単語記事] - ニコニコ大百科

動作確認は、Macpython 2.7.6 (homebrew)で行いました。ライブラリにnumpyを使っています。

スクリプト

dpcm.py

# coding: UTF-8

# 入力の音声ファイルから、
# あたかもRP2A03のDPCMで再生したような音声ファイルを作る

import wave
import sys
import struct
import numpy as np
import math

# 音声データをwavファイルとして保存する
# 音声データは、モノラル, 量子化ビット数16とする。
# @param inData 音声データ
# @param inSampFreq サンプリング周波数[Hz]
# @param inFileName 出力するファイルのファイル名
def writeWav(inData, inSampFreq, inFileName):
    data = struct.pack("h" * len(inData), *inData)
    wf = wave.open(inFileName, "w")
    wf.setnchannels(1)
    wf.setsampwidth(2)
    wf.setframerate(inSampFreq)
    wf.writeframes(data)
    wf.close()

# 音声ファイルを読み込む
# 非圧縮, 1サンプル2バイト(量子化ビット数16)のwavファイルを読み込む。
# チャンネル数は1または2。チャンネル数が2の場合には、Lチャンネルのみを読み込む。
# @param inFileName 音声ファイル名(.wav)
# @return [音声データ, サンプリング周波数[Hz]] 
def readWav(inFileName):

    # 音声ファイルを開く
    wr = wave.open(inFileName, "rb")

    # 音声ファイルの検査
    # 1サンプルのバイト数は2であること
    if wr.getsampwidth() != 2:
        print " error : size per one sample must be 2."
        wr.close()
        usage()
        quit()
    # 非圧縮であること
    if wr.getcomptype() != "NONE":
        print " error : input file must be uncompresed format."
        wr.close()
        usage()
        quit()
    # チャンネル数は1か2であること
    channels = wr.getnchannels()
    if channels != 1  and channels != 2 :
        print " error : channel number must be 1 or 2."
        wr.close()
        usage()
        quit()

    # 読み込む
    sampFreq = wr.getframerate()
    data = wr.readframes(wr.getnframes())
    numData = np.frombuffer(data, dtype=np.int16)
    wr.close()

    # 読み込んだデータをintのリストに付け替える
    i=0
    out = []
    while i<len(numData):
        out.append(int(np.asscalar(np.float64(numData[i]))))
        # 音声ファイルが2チャンネルならLだけを取る
        i += channels

    return [out, sampFreq]

# DPCMチャンネルで再生した音を作る
# 参考URL: http://dic.nicovideo.jp/a/fc音源
# ロジック:
# 1. 圧縮後サンプル値を作る
#   1個目のサンプル
#     元のサンプル値(16 bit)を、7 bitに圧縮する
#   2サンプル以降:
#     当該サンプル値(16 bit)を、7 bitに圧縮する
#     直前の圧縮後サンプル値(7 bit)と比較し、
#     ・当該サンプルのほうが大きければ、直前の圧縮後サンプル値+1を追加する
#     ・当該サンプルのほうが小さいまたは等しければ、直前の圧縮後サンプル値-1を追加する
# 2. 音声に変換する
#   サンプル値の下位1 bitを0にする
#   (デルタ初期ボリュームは7bit長だが、再生時は下位1bitを無視した64段階で
#    ボリューム変更される)
#   そのあと、9 bit(=16-9)だけ左シフトし、量子化ビット数を16に戻す。
# @param inData 入力となる音声波形
# @param inSampFreq 入力音声のサンプリング周波数 [Hz]
# @param inRegId DPCM化後のサンプリング周波数を決める値
def convDpcm(inData, inSampFreq, inRegId):

    # DPCM再生のサンプリング周波数を決める
    # 指定された0-15の値に対応する値をテーブルから引く
    # サンプリング周波数[Hz] = 1789772.5 / テーブルから引いた値
    if inRegId < 0 or inRegId > 15:
        print " error : number for determining sampling frequency must be from 0 to 15."
        usage()
        quit()
    dpcmFreqTable = [428,380,340,320,286,254,226,214,190,160,142,128,106,85,72,54]
    dpcmFreq = 1789772.5 / dpcmFreqTable[inRegId]
    print " DPCM-converting sampling frequency[Hz]:", dpcmFreq

    # 音声の総時間
    totalTime = float(len(inData)) / inSampFreq
    
    # 初期値の設定
    # 注目サンプルの時刻
    t = 0.0
    # 直前サンプルの圧縮後の値
    prev = 0.0
    # 出力
    out = []

    while t < totalTime:

        # 注目サンプルの値を取る
        sample = inData[int(math.floor(t * inSampFreq))]

        if t <= 0.0:
            # 第1サンプルのとき
            # 音を7 bitに圧縮する
            # pythonは算術シフトなので、普通に9 bit(=16-7)だけ右シフトさせればよい
            val = sample >> 9
        else:
            # 第2サンプル以降
            # 当該サンプルを圧縮する
            tmpVal = sample >> 9
            # 圧縮後の当該サンプルが直前のサンプルより大きければ、直前の圧縮後サンプルの+1にする。
            # 以下ならば-1する。
            if tmpVal > prev:
                val = prev+1
            else:
                val = prev-1

        # 圧縮後サンプル値が範囲を出ていれば、範囲内に収める
        # 範囲は、-64〜63 (7 bitの符号付き整数)
        if val > 63:
            val = 63
        elif val < -64:
            val = -64

        # 出力のリストに登録
        out.append(val)
        # 当該圧縮後サンプル値は次につかうので、保存
        prev = val

        # 時刻を更新
        t += 1.0 / dpcmFreq

    # 圧縮後サンプル値を、量子化ビット16 bitの波形に戻す
    # 圧縮後サンプル値は7 bitだが、下位1bitはボリュームは再生時のボリュームには反映されない(無視される)ため、
    # 下位1 bitを削除した後に、16 bitに戻す
    for i in xrange(len(out)):
        out[i] = (out[i] >> 1) << 10

    # 出力を返す
    return [out, dpcmFreq]

# usage
def usage():

    print "usage: python dpcm.py (input file) (output file) [options]"
    print " This script converts input speech so as to be played from NES DPCM channel."
    print "  (input file) : input wav file name."
    print "  (output file) : output wav file name."
    print "  [options]"
    print "   -ds (number) : specify DPCM sampling frequency."
    print "     The specified number must be in the range from 0 to 15 [defalut=15]."
    print "     Each number indicates a stored value to a register that determines"
    print "     DPCM sampling frequency."
    print "     The relationship between the specified numbers and the sampling"
    print "     frequencies are as followings:"
    print "       (number) (frequency[Hz])"
    print "             0            4182"
    print "             1            4710"
    print "             2            5264"
    print "             3            5593"
    print "             4            6258"
    print "             5            7046"
    print "             6            7919"
    print "             7            8363"
    print "             8            9420"
    print "             9           11186"
    print "            10           12604"
    print "            11           13983"
    print "            12           16885"
    print "            13           21056"
    print "            14           24858"
    print "            15           33144"


# メインルーチン
if __name__ == "__main__" :

    # 引数を取る
    if len(sys.argv) < 3:
        print " error : less args."
        usage()
        quit()
    
    # 入力ファイル名を受け取る
    inFileName = sys.argv[1]
    # 出力ファイル名を受け取る
    outFileName = sys.argv[2]

    # DPCMのサンプリングレートを決める値を受け取る
    # (デフォルト値)
    dpcmFlag = 15
    i=3
    while i<len(sys.argv):
        if sys.argv[i] == '-ds':
            i+=1
            if i>=len(sys.argv):
                print " error : invalid args."
                usage()
                quit()
            if not sys.argv[i].isdigit():
                print " error : invalid args."
                usage()
                quit()
            dpcmFlag = int(sys.argv[i])
        else:
            print " error : invalid args."
            usage()
            quit()
        i+=1
        
    # 入力音声ファイルを受け取る
    [data, sampFreq] = readWav(inFileName)

    print " input file:", inFileName
    print " output file:", outFileName
    print " input sampling frequency [Hz]:", sampFreq
    print " speech length [s]:", float(len(data)) / sampFreq

    
    # DPCM変換 → 復元を実行
    [out, dpcmFreq] = convDpcm(data, sampFreq, dpcmFlag)

    # 出力時のサンプリング周波数
    # wavの場合、サンプリング周波数は整数になるので、
    # DPCM変換時の周波数の四捨五入にする
    outSampFreq = round(dpcmFreq)
    print " output sampling frequency[Hz]:", outSampFreq

    # できた音声ファイルを出力する
    writeWav(out, outSampFreq, outFileName)

使い方

用意した音声ファイル(in.wav)から、DPCMチャンネルからの再生をシミュレートした音声ファイル(out.wav)を作るには、以下のコマンドを実行します。

$ python dpcm.py in.wav out.wav

ファミコンDPCMチャンネルでは、再生音のサンプリング周波数を16個のなかから選択できます。再生音のサンプリング周波数は、デフォルト設定でもっとも高い33144Hzになります。再生音のサンプリング周波数を変更するには、-dsオプションで値(0-15)を指定します。この値とサンプリング周波数の対応は、スクリプトのusageメッセージに記載しています。

実際のファミコンでは、再生音のサンプリング周波数は小数になるようです。このスクリプトでは、wavファイルとして出力する関係上、再生時のサンプリング周波数は、整数に四捨五入しています。

例として、ドラム音を使って、DPCMチャンネルから再生した音を作ります。再生音のサンプリング周波数の設定は、デフォルト(33144Hz)です。

元のドラム音 Loop01 by uenewsar on SoundCloud - Hear the world’s sounds

DPCMチャンネルからの再生音に変換した音 Converted by uenewsar on SoundCloud - Hear the world’s sounds

ドラム音には、以下のサイトのものを使わせていただきました。
Music is VFR ~BGM・ジングル・効果音のフリー素材~