音を作ってみる!?

こんにちは、きうちです。

だいぶあたたかくなってきましたね!桜も見ごろを迎え、各地でにぎやかになってますね~。

弊社も今年は花見イベントやりました!

…ということと関係するわけではないですが、今回は

「音を作ってみる!?」

というタイトルでお届けしようかと思います。

音を作ってみる…いってもそんなたいそうなものではなく、サインカーブ(正弦波)…つまり、曲線の波。

あれを音にしてみようと思います。

聞いたことあるかも!?

★ ★ ★

言語は例によってC#にします。Windowsフォームアプリとして作成します。

フォームのサイズは 640×480 にして、そこにPictureBoxを配置します。PictureBoxの背景は黒にします。
とりあえずPictureBoxは画面いっぱいにしちゃいます。Dock=Fill。

フォームのメンバ変数として、フィールドのサイズを定義する定数 width,heightを定義し、それぞれコンストラクタの中でセットします。
また、波データを保持するための変数 waveData を int 型の配列として定義します。

waveDataの中にはSin関数を使ってデータを作って入れます。そのためのメソッド、MakeWaveを用意します。

/// <summary>フォームクラス</summary>
public partial class Form1 : Form
{
    /// <summary>WAVEデータ</summary>
    private int[] waveData;

    /// <summary>ピクチャボックスの横幅</summary>
    private readonly int width = 0;

    /// <summary>ピクチャボックスの高さの半分</summary>
    private readonly float height = 0;

    /// <summary>コンストラクタ</summary>
    public Form1()
    {
        InitializeComponent();

        width = pictureBox1.Width;
        height = pictureBox1.Height / 2;

        waveData = MakeWave(128000, 2000, 0.1);
    }

    /// <summary>
    /// 波データを作る
    /// </summary>
    /// <param name="dataSize">サイズ</param>
    /// <param name="r">波の半径</param>
    /// <param name="dt">角度増分</param>
    /// <returns></returns>
    private int[] MakeWave(int dataSize, int r, double dt)
    {
        var list = new List<int>();

        double theta = 0;
        for (int i = 0; i < dataSize; i++)
        {
            int a = (int)(Math.Sin(theta) * r);
            list.Add(a);
            theta += dt;
        }

        return list.ToArray();
    }

PictureBoxにはこの波データを表示するためのイベントメソッドを紐づけます。

    /// <summary>
    /// Y座標の計算
    /// </summary>
    /// <param name="v"></param>
    /// <returns></returns>
    private float CalcY(int v)
    {
        return v * (height - 20) / 12000 + height;
    }

    /// <summary>
    /// pictureBox1 描画イベント
    /// </summary>
    /// <param name="sender">イベント発生オブジェクト</param>
    /// <param name="e">イベント引数</param>
    private void pictureBox1_Paint(object sender, PaintEventArgs e)
    {
        if (waveData != null)
        {
            float x = 0;
            float y = CalcY(waveData[0]);
            int max = waveData.Length;
            if (max > width) max = width;
            for (int i = 1; i < max; i++)
            {
                float x2 = x + 1;
                float y2 = CalcY(waveData[i]);
                e.Graphics.DrawLine(Pens.Yellow, x, y, x2, y2);
                x = x2;
                y = y2;
            }
        }
    }
}

実行すると、こんな感じ。

黄色い波線が描かれました!

★ ★ ★

今度は、この波線を音声にするため、WAVファイルのデータにしていきます。

WAVファイル形式のフォーマットについては、Webで「WAVファイル データ構造」などで検索すると知ることができます。

それを基に、まずはヘッダの情報をWaveHeaderクラスを作って定義。

/// <summary>Waveヘッダ情報</summary>
class WaveHeader
{
    /// <summary>RIFF識別子</summary>
    public const string IDENTIFIER = "RIFF";

    /// <summary>チャンクサイズ</summary>
    public int ChankSize { get; set; } = 0;

    /// <summary>フォーマット</summary>
    public const string FORMAT = "WAVE";

    /// <summary>fmt識別子</summary>
    public const string FMT_IDENTIFER = "fmt ";

    /// <summary>fmtチャンクのバイト数</summary>
    public int FmtChank = 16;

    /// <summary>音声フォーマット 1:非圧縮のリニアPCM</summary>
    public int SoundFormat { get; set; } = 1;

    /// <summary>チャンネル数 1:モノラル</summary>
    public int ChannelCount { get; set; } = 1;

    /// <summary>サンプリング周波数(Hz) 44.1kHz=44100Hz</summary>
    public int SamplingRate { get; set; } = 44100;

    /// <summary>1秒あたりバイト数平均 44.1kHz、16bit、モノラル=44100x2x1=88,200</summary>
    public int BytesPerSecond { get; set; } = 88200;

    /// <summary>ブロックサイズ</summary>
    public int BlockSize { get; set; } = 2;

    /// <summary>サンプルごとのビット数</summary>
    public int BitPerSample { get; set; } = 16;

    /// <summary>拡張パラメータのサイズ</summary>
    public int ExtensionParameterSize { get; set; } = 0;

    /// <summary>拡張パラメータ</summary>
    public byte[] ExtensionParameter { get; set; } = null;

    /// <summary>サブチャンク識別子</summary>
    public const string SUBCHANK = "data";

    /// <summary>サブチャンクサイズ</summary>
    public int SubchankSize { get; set; } = 0;
}

あとは、FileStream でファイルをオープンして、書き込んでいきます。
WaveWriterクラスを作って、Executeというメソッドでそれを実装します。
文字列を書くのと、数値を書くの両方が必要なので、それぞれのメソッドを用意します。

/// <summary>
/// WAVEファイルの出力
/// </summary>
class WaveWriter
{
    /// <summary>
    /// WAVEファイルの出力
    /// </summary>
    /// <param name="filepath">ファイルパス</param>
    /// <param name="arr">データ配列</param>
    /// <returns>処理成否</returns>
    public bool Execute(string filepath, int[] arr)
    {
        try
        {
            using (FileStream s = new FileStream(filepath, FileMode.OpenOrCreate))
            {
                WaveHeader h = new WaveHeader();
                int headerSize = 44;
                int totalFileSize = headerSize + arr.Length;
                h.ChankSize = totalFileSize - 8;
                h.SubchankSize = arr.Length;

                // ヘッダを書く
                int size = WriteHeader(s, h);

                // データを書く
                for (int i = 0; i < arr.Length; i++)
                {
                    WriteInt(s, arr[i], 2);
                    size += 2;
                }
                size += arr.Length;

            }
            return true;
        }
        catch(IOException ex)
        {
            Console.WriteLine(ex.Message);
            MessageBox.Show(ex.ToString());
            return false;
        }
    }

    /// <summary>
    /// ヘッダ書き込み
    /// </summary>
    /// <param name="s">ファイルストリームオブジェクト</param>
    /// <param name="h">ヘッダ情報</param>
    /// <returns>書き込んだサイズ</returns>
    private int WriteHeader(FileStream s, WaveHeader h)
    {
        int size = 0;
        size += WriteString(s, WaveHeader.IDENTIFIER);
        size += WriteInt(s, h.ChankSize, 4);

        size += WriteString(s, WaveHeader.FORMAT);
        size += WriteString(s, WaveHeader.FMT_IDENTIFER);

        size += WriteInt(s, h.FmtChank, 4);
        size += WriteInt(s, h.SoundFormat, 2);
        size += WriteInt(s, h.ChannelCount, 2);
        size += WriteInt(s, h.SamplingRate, 4);
        size += WriteInt(s, h.BytesPerSecond, 4);
        size += WriteInt(s, h.BlockSize, 2);
        size += WriteInt(s, h.BitPerSample, 2);
        size += WriteString(s, WaveHeader.SUBCHANK);
        size += WriteInt(s, h.SubchankSize, 4);

        return size;
    }

    /// <summary>
    /// 文字列の書き込み
    /// </summary>
    /// <param name="s">ファイルストリームオブジェクト</param>
    /// <param name="str">書き込む文字列</param>
    /// <returns>書き込んだサイズ</returns>
    private int WriteString(FileStream s, string str)
    {
        byte[] b = Encoding.ASCII.GetBytes(str);
        s.Write(b, 0, b.Length);
        return b.Length;
    }

    /// <summary>
    /// 数値の書き込み
    /// </summary>
    /// <param name="s">ファイルストリームオブジェクト</param>
    /// <param name="num">書き込む数値</param>
    /// <param name="size">書き込みサイズ</param>
    /// <returns>書き込んだサイズ</returns>
    private int WriteInt(FileStream s, int num, int size)
    {
        byte[] b = new byte[size];
        Array.Copy(BitConverter.GetBytes(num), b, size);
        s.Write(b, 0, b.Length);
        return b.Length;
    }
}

なんかもうちょっと効率のいい書き方はないのか!
…とか思いますが、今回はこれでいきます。

Form1クラスのコンストラクタを次のように変更(2行追加)します。

public Form1()
{
    InitializeComponent();

    width = pictureBox1.Width;
    height = pictureBox1.Height / 2;

    waveData = MakeWave(128000, 2000, 0.1);

    WaveWriter w = new WaveWriter(); // 追加
    w.Execute("test.wav", waveData); // 追加
}

実行すると、test.wav というファイルができます。
これを、Windowsメディアプレイヤーで再生してみましょう。
再生する前に「視覚エフェクト」を変更して
「バーとウェーブ」の中の「スコープ」にしておきます。
(Windows10の場合、右クリックメニューから)

再生してみると…!!音量注意!!

一度はお聞きになったことがあるかもしれない、あの「ぽー…」という音が流れます!

これ、パラメータを調整すれば「ピー」という音にもなります。

★ ★ ★

というわけで、今回は「音を作ってみる!?」でした。

Windowsメディアプレイヤーの「スコープ」で表示させると波形が見えて面白いですね。データで作ったのと同じ波形が!…ってまあそりゃそうですよね…

さて、今回のは正弦波、つまり曲線の波でしたが、これをかくかくした線にしたりしたらどうなるでしょうか?

あるいは、数値をランダムにしてみたらどうなるでしょうか?

そのあたりを次回、やってみたいと思います。

それではまた!

追伸

ソースあります!
が、ちょっとまだ準備中で雑な感じなので、ご参考までに…
次回までに整えます!

こちら。