プログラミング

Waveデータ (PCM)

かなり涼しくなってきましたね。涼しいというか日によっては朝寒く感じるようになってきました。私はもともと暑いのが苦手なのでようやく体が楽になってきた感じでしょうか。仕事がら外出も多いのでかなり助かります。夏はなんといってもスケジュールが詰まっていて、時間の関係から早歩きで移動した後などは地獄です。特に霞が関の役所のビルはどこも空調が弱いので本当に辛いです。外部の方々を集めた会議の時だけでも涼しくして欲しい。

ところで、しばらく前にWaveDataの作成プログラムを紹介したのですが、久しぶりにその時に書いたコードを見直して気が付きました。何に気が付いたかというと、窓掛。そのときのポストでは作成した音がブチブチとノイズが入ると書いていましたが、よく考えたら当たり前で窓掛けしてないから。ずいぶんとボケたものです。基本中の基本をそのときに気づけない、思い出せないとは恥ずかしい限りです。趣味のプログラムなので、時間かけてコード書いても仕方ないので、取り合えず修正したものを紹介しておきます。

//=====================================================================================

/// <summary>

///

/// </summary>

/// <param name="bw"></param>

/// <param name="nAmplitude"></param>

/// <param name="nFrequency"></param>

/// <param name="nDuration"></param>

/// <returns></returns>

private int WriteOutSound(int nAmplitude, int nFrequency, int nDuration)

{

        double                   dA;

        double                   dDeltaFT;

        int                              nSamples;


                        

        dA                       = ((nAmplitude * 2 ^ 15) / 1000) -1;

        System.Diagnostics.Debug.WriteLine(dA.ToString());

                        

        if (nFrequency == 0)

        {

                // 周波数ゼロの場合

                dA              = 0;

                dDeltaFT = Math.PI * 2 * (double)0 / (double)SamplingRate;

        }

        else

        {

                dDeltaFT = Math.PI * 2 * (double)nFrequency / (double)SamplingRate;

        }


        // どの程度窓掛するかの計算。単純に両端の10msに対して適用。

        nSamples = (SamplingRate * nDuration / 100) / 10;


        for (int T = 0; T < nSamples; T++)

        {

                short Sample;

                double dADegree;


                // 先頭の窓掛

                if (T < WindowPeriod)

                {

                         dADegree = Math.PI / 2 * (1.0 - (double)T / (double)WindowPeriod);

                }

                // 後尾の窓掛

                // これをやらないと、ブチブチとノイズ

                else if (T > nSamples - WindowPeriod)

                {

                         dADegree = Math.PI / 2 * ((double)(WindowPeriod - (nSamples - T)) / (double)WindowPeriod);

                }

                else

                {

                         dADegree = 0;

                }


                Sample = (short)(dA * Math.Sin(dDeltaFT * T) * Math.Cos(dADegree));

                bwWave.Write((short)Sample);

                bwWave.Write((short)Sample);

        }

                        

        return nSamples;

}

取り敢えず、ちょっとしたモールス信号練習ソフトウェアということで・・
ZIP


CW Viewer?(モールス信号の可視化)

涼しかったり、暑かったりと、これを繰り返して夏に近づいているのを感じます。私は冬生まれだから、秋からクリスマス頃までが一番好きな季節です。私にとって今はその正反対の忍耐の季節。基本スーツの私としては本当に辛い季節が近づいてきているほかありません。

一方で趣味はというと、登山、ロッククライミング、無線と色々ありますが、モールスは日々の練習が重要ですね。とは言ってもこの年で上達できるのり代はあまり大きくありません。そこで少しプログラムの力を借りようかと思い、ちょっとしたツールを作り始めました。

これは、その作りかけの試しで作成したものです。これは何かというと、モールス信号を視覚化できないかと思い、リグのスピーカー、ライン出力をPCのライン入力に取り込み、処理して表示するツールです。ネットワークプロトコルの標準化、実装などを手掛けてきたことはあっても、信号処理は専門外です。そこで、まずはライン入力からの信号を取り込み、FFT解析して、あとは付け焼刃的に信号処理を行うツールを作ってみました。

実際のところ、きっかけはCW Skimmer。これも良いのですが、あれ?これなら自分で作った方がいいような・・・などと思ってしまったのがいけなかった。

自分でも何をやっているのかと思ってしまいます。こんなツールを作るのに半日使ってしまいました。というのも、世の中にフリーで存在するC#で書かれているFFTライブラリで良いものが無い!言ってしまえば、良い実装が無い(遅い、他色々・・・)。もちろん有償のものはあるのでしょうが、貧乏暇なしサラリーマンとしては、無ければ自分で作成する主義でこれまでやってきているので、今回もFFTライブラリから作りました。

それが、コレです。このスクリーンショットは、入力信号をFFT解析し、信号の強度を同時に表示したものです。何も加工はしていません。通常音声信号の場合、FFT解析の際にHamming Windowを使用したものが多いですが、忠実度重視でRectangle(何もしてない)となっています。趣味的ツールなので、設定でHamming、Blackman、Hann、Welch、Bertlett、Nuttall, Blackman Harrisを選べるようにしてあります。それはそうとして、取りあえず信号は可視化できるようになりました。

このコードのポイントの1つは、FFT解析を多重化できるところでしょうか。入力バッファーをスレッド毎に投入し、幾つでもCPUへの負荷とメモリが許す限り多重化できます。デフォルトでは16スレッド作成していますが、全然使いきれていません。(かなり余裕がある)

試してみたところ、2048ビットでも十分解析できました。(2048ビット、結果が1024ビット。)解析は、1024ビットずつずらして2048ビットを1つのChunkとして解析、1024ビットの結果を得て、次の2048ビットを取得といった感じです。CPU負荷を見る限り44.1KHz、16ビット x 2チャンネルで4096でも行けそう。(試してないです。)

RAW

このスクリーンショットは、信号自身のシグナル強度によって掛けたものです。シグナルが強ければ残りますが、弱いと掛けることでより弱い信号になります。0.1 x 0.1 = 0.01、0.9 x 0.9 = 0.91のような感じです。
シグナル毎フィルター

私は信号処理の専門家では無いので、まずは適当に実装してみています。以下の画面は、モールス信号が特定の周波数の限られた帯域で強度を持つことを利用して、時間スライス毎に標準偏差を出し、指定の偏差値以下をカットしたものです。これで、微弱なシグナルは基本消えてなくなります。
標準偏差フィルター

FFT解析では、信号の周波数成分は取り出せますが、絶対強度を把握することが出来ません。そこで、時間毎のAmplitudeを使用して信号が無い、弱いものを取り除いています。
出力によるフィルター

そして、これは上記に加えて信号毎に更にそれぞれを掛けて強調しています。
併用

弱いノイズを全体的により取り除いた状態。
併用2

ここまで来るとモールス信号だけになってきました。
シグナル抽出強

これに、極端に短い信号にアリ、ナシ部分を補正すれば、ほぼデジタルデータとして抽出ができます。また、エレキーを使用している人が多いので、その分デコードは楽ですね。

って、ここまで作って、あとは複数のモールス信号を同時にデコード出来るところまで実装ですね。ただ、これが結構難しそう。1つの信号だけなら簡単なんですけどね。完成させたとして、自分以外に使う人いるのかな。ニーズがあればデコード機能含め実装し、完成度を上げて公開しようかと思います。

自作のHamlogも何人かに共有したら評判が良いので、そのうち公開するかもしれません。ICOM製リグを使用している場合は、細かい設定も一括で記録できるようになっています。ライン入力とも連動していて、開始、終了時間を入力すると、通話もアーカイブ出来るようになっています。(常時録音していて[最大時間は指定可]、時間入力とともに、そこから抽出する仕組みになっています。) って、懲りすぎですね。

WAVEデータの生成


ゴールデンウイークの感覚がまだ抜けきらない週末です。
かなり仕事の積み残しがあるので、週末に少し自宅で仕事をして減らそうと思ったのですが、結局それも叶わず貴重な休日が終わろうとしています。こんなことで良いのか、いつも悩んでしまいます。

先日、WAVEファイル生成のコードをアップロードするとお約束していたので、アップロードしました。
WAVEファイルを作成して保存するだけでは面白くないので、モールス信号を生成してメモリーストリームに入れ、それを再生するクラスにしてみました。
15~30分で作成したものなのでかなり手抜きコードですが、何をしているのかは理解いただけるかと思います。(コメントが少なくてすいません。)

前回の説明とは特に関係はないのですが、データの書き込みにBinaryWriter使用しています。byte[]を使用するよりも、ファイルデータと同じ感覚で書き出しができるので非常に便利です。このプログラムでは音データのサイズが最後までわかりませんので、最初にヘッダーサイズ分だけ書き出しポジションを移動させ、音データを書き出し、その後の書き出したデータサイズを元にヘッダーを更新、書き出しポジションを先頭に戻して書き出すことをしています。

msWave      = new MemoryStream();

bwWave      = new BinaryWriter(msWave);

中略
書き出し位置を先頭も移動し、ヘッダーの書き出し。

//=======================================================

// WaveHeaderをストリームに書き出し。

//=======================================================

bwWave.Seek(0, SeekOrigin.Begin);

bwWave.Write(StructureToByteArray(Header));


と、このような感じです。
昔はC++/アセンブラしか使用していませんでしたしので、デバイス制御、バイナリ操作が必要な場合は、C#よりもC++が私としては楽ですね。自分でメンテナンスしているライブラリもあるので。(並べ替え、メモリ管理からデバイス制御まで)

一方で、ユーザーインターフェースが絡むとC#の方が楽なんですよね。C++のポインタ感覚でデータ処理ができると言うことないのですが。.NET Frameworkの今後のアップデートに期待したいと思います。

ZIP MorsePlayer.cs

音源(正弦波)の生成

私は日記とか、そういう類のものが長続きした試しがありません。Twitterも驚きのツイート1回のみ。Facebookも、私の誕生日に「お友達」からのメッセージとそれに対するお礼。このほか、一緒に飲んだり、講演、他諸々したときの写真にタグ付けされ、それがタイムラインに載る程度。そんな私は、ブログを綴っているとは。自分でも驚いています。

さて、音源データ作成の続きです。細かい説明は後にしてまずは、そのサンプルコードを紹介します。


int
WriteOutSound(int nAmplitude, int nFrequency, int nDuration)

{

    double    dA;

    double    dDeltaFT;

    int       nSamples;

    int       nCycles;

    int       nGap;


   
dA    = ((nAmplitude * 2 ^ 15) / 1000) -1;

   

    if (nFrequency == 0)

    {
        // 周波数ゼロの場合

        dA    = 0;

        dDeltaFT = Math.PI * 2 * (double)0 / (double)SamplingRate;

    }

    else

    {

        dDeltaFT = Math.PI * 2 * (double)nFrequency / (double)SamplingRate;

    }

    nSamples = (SamplingRate * nDuration / 100) / 10;


   
// 正弦波2PIの終わりで止めるための計算

    if (dDeltaFT != 0)

    {

        nCycles = (int)((dDeltaFT * (double)nSamples) / (Math.PI * 2.0));

        nGap    = nSamples % nCycles;

    }

    else

    {

        nGap = 0;

    }

    for (int T = 0; T < nSamples - nGap; T++)

    {

        short Sample;


       
Sample = (
short)(dA * Math.Sin(dDeltaFT * T));

        bwWave.Write((short)Sample);

        bwWave.Write((short)Sample);

    }

   

    return nSamples;

}


このサンプルコードに出てくるbwWaveとは、BinaryWriteのオブジェクトです。私はもともとC++/アセンブラでコードを書いていた人間なのですが、最近ではすっかりC#ばかり。便利ですね。

MemoryStream msWave   = new MemoryStream();

BinaryWriter bwWave   = new BinaryWriter(msWave);


あとは、サンプルコードにもあるように、bwWave.Write()でメモリーストリームのデータを着だすことが出来ます。ヘッダーのような構造化されたものは、以下のようなコードでバイト列に変換することが出来ます。バイト列に変換さえ出来てしまえば、後は簡単ですね。

byte[] StructureToByteArray(object obj)

{

    int        nLen;

    byte[]     rgbArray;

    IntPtr     ptrMem;

   

    nLen        = Marshal.SizeOf(obj);

    rgbArray   = new byte[nLen];

    ptrMem      = Marshal.AllocHGlobal(nLen);



    Marshal.StructureToPtr(obj, ptrMem, true);

    Marshal.Copy(ptrMem, rgbArray, 0, nLen);

    Marshal.FreeHGlobal(ptrMem);


    return rgbArray;

}


次回は、全てをクラスにまとめたサンプルコードをアップロードしたいと思います。

RIFF (Resource Interchange File Format)

気温がかなり上がってきましたね。私の好きな季節は「」なので、だんだんと嫌~な季節と言えます。何が嫌かというと、暑さがです。女性は涼しげな服装が出来て羨ましいのですが、私は仕事上お会いする方々の関係で常にスーツです。霞が関に終日いるような場合は涼しい服装で行きますが、あまり崩したくない私はどうしても人よりは暑く感じる服装になってしまいます。

さて、音データを作成して再生する方法について紹介していきたいと思います。Windowsに限ったことではありませんが、多くのシステムが音データのフォーマットとしてRIFF (Resource Interchange File Format)を使用しています。このフォーマットは、WAVEデータの他に様々なマルチメディアデータのフォーマットとして使用されています。

具体的には、データの先頭にあるヘッダーの中で何のデータをなのかを示す識別子があり、これにより判別します。

以下は、この後に紹介しるWAVEファイルを生成するプログラムの中で定義しているヘッダーの構造体です。

[StructLayout(LayoutKind.Sequential, Pack = 1)]

protected struct WaveHeader

{

        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]

        public byte[]   RiffIdentifier;

        public uint     TotalSize;



        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]

        public byte[]   WaveIdentifier;



        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]

        public byte[]   FormatDefinition;

        public uint     FormatSize;

        public ushort   FormatID;

        public ushort   ChannelCount;

        public uint     SamplingRate;

        public uint     DataRate;

        public ushort   BlockSize;

        public ushort   SamplingSize;



        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]

        public byte[]   DataIdentifier;

        public uint     DataSize;

}


RiffIdentifierには常に"RIFF"をASCII文字のバイトコードでセットします。また、ここで使用するのはWAVEデータなので、WaveIdentifierには、"WAVE"を同じくASCII文字のバイトコードでセットします。この他、把握しておいた方が良いのは、FormatIDでしょうか。FormatIDは、PCMデータのフォーマットの種類を示す数字です。例えば、PCMであれば0x0001、ADPCM(G.723)であれば、0x0014といった具合です。他のIDに興味のある方は、Windows SDK、Visual Studioとともにインストールされるmmreg.hファイルを探してみてください。「WAVE form wFormatTag IDs」として一覧がまとめてあるので参考になると思います。

この他、SamplingRateは44,100が一般的に使用されています。これはCDのサンプリングレートと同じですね。ChannelCountは、モノラルなら1、ステレオであれば2といったように、音源数をセットします。この中で厄介なのが、DataRate、DataSizeでしょうか。

DataRateは、この名前の通り転送レートです。ステレオ、サンプリングレートが44,1KHz、サンプリングサイズが16ビットの場合は、2[チャンネル] x (16 / 8 [ビット]) x 44100となります。8[ビット]で割っているのは、転送レートがバイト単位。一方で、サンプリングサイズはビットなので、8で割ってバイトに変換をしています。

DataSizeは、WaveHeaderのサイズにその後に続くデータサイズを足したものとなります。逆に言えば、データサイズが確定しないことには、この情報は確定しないということになります。リアルタイムで録音するようなアプリの場合は、録音終了後にヘッダー情報を更新しなくてはなりません。

次回は、正弦波を作ってデータとして書き出すコードを見てみたいと思います。