C#でWAVにサムネを埋め込む

低レベル(ハードウェア寄り)なアナログ信号を扱っていると、サクッと使えるアナログ信号のファイルフォーマットが欲しくなります。そういうときに、WAV(RIFF/PCM)は結構便利です。サンプリングレートを32bitで記録でき、チャンネル数も16bitで記録でき、ビット数もいくつか選択でき、その他にも色々と便利な点が多いです。が、「どんなデータが入っているか」を示す手段がファイル名くらいしかないのが少し困ったところです。
せっかくの音楽ファイルフォーマットなんだから、サムネにデータの中身のアイコンなりを埋め込んだら便利だと思いませんか?

ということで、C#でRIFF/PCMの生成とID3v2.3の埋め込みを行ってみました。

```c#
class Program
{
    static void Main(string[] args)
    {
        using (FileStream fs = new FileStream("test.wav", FileMode.Create, FileAccess.Write))
        {
            ChunkSizeStack sizeStack = new ChunkSizeStack(fs);

            Encoding encoding = Encoding.ASCII;

            fs.Write("RIFF", encoding);
            sizeStack.Push();

            fs.Write("WAVE", encoding);

            fs.Write("fmt ", encoding); // id
            fs.WriteU32LE(16); // size
            fs.WriteU16LE(1); // format: PCM
            fs.WriteU16LE(1); // channels
            fs.WriteU32LE(44100); // samplingrate
            fs.WriteU32LE(0); // bytepersec
            fs.WriteU16LE(0); // blockalign
            fs.WriteU16LE(8); // bitwidth

            //***

            fs.Write("id3 ", encoding);
            sizeStack.Push();
            long a = fs.Position;

            fs.Write("ID3", encoding);
            fs.WriteByte(0x03); // major ver
            fs.WriteByte(0x00); // revision
            fs.WriteByte(0x00); // header flag
            sizeStack.Push();

            fs.Write("APIC", encoding);
            sizeStack.Push();
            fs.WriteU16LE(0); // tag flag
            fs.WriteByte(0); // text encoding
            fs.Write("image/jpeg\0", encoding);
            fs.WriteByte(0x03); // picture type (0x03: front cover)
            fs.WriteByte(0); // description (null terminal)

            using (Bitmap bmp = new Bitmap(128, 128))
            using (Graphics g = Graphics.FromImage(bmp))
            using (Font font = new Font("ＭＳ ゴシック", 60, GraphicsUnit.Pixel))
            using (StringFormat sf = new StringFormat())
            {
                sf.Alignment = sf.LineAlignment = StringAlignment.Center;

                g.Clear(Color.Orange);
                g.DrawString("TEST", font, Brushes.White, 64, 64, sf);

                bmp.Save(fs, System.Drawing.Imaging.ImageFormat.Jpeg);
            }

            sizeStack.Pop(ToID3TagSize); // APIC tag size

            sizeStack.Pop(ToID3size); // ID3v2 tags size

            sizeStack.Pop(); // "id3 " chunk size

            //***

            fs.Write("data", encoding);
            sizeStack.Push();

            byte[] buff = Enumerable.Range(0, 44100).Select(i => (byte)(Math.Sin(i / 44100.0 * 800 * Math.PI * 2) * 127.5 + 127.5)).ToArray();
            fs.Write(buff, 0, buff.Length);

            sizeStack.Pop(); // "data" chunk size

            sizeStack.Pop(); // RIFF size
        }
    }

    static byte[] ToID3size(UInt32 size)
    {
        byte[] tmp = new byte[4]
        {
            (byte)((size >> 21) & 0x7F),
            (byte)((size >> 14) & 0x7F),
            (byte)((size >> 7) & 0x7F),
            (byte)((size >> 0) & 0x7F)
        };
        return (tmp);
    }

    static byte[] ToID3TagSize(UInt32 size)
    {
        byte[] v = BitConverter.GetBytes((UInt32)(size - 2));
        byte w;
        w = v[0]; v[0] = v[3]; v[3] = w;
        w = v[1]; v[1] = v[2]; v[2] = w;
        return (v);
    }

    class ChunkSizeStack
    {
        private readonly Stream stream;
        private readonly Stack<long> stack;

        public ChunkSizeStack(Stream stream)
        {
            this.stream = stream;
            stack = new Stack<long>();
        }

        public void Push()
        {
            stream.Write(new byte[4], 0, 4);
            stack.Push(stream.Position);
        }

        public void Pop()
        {
            long a = stack.Pop();
            if ((stream.Position - a) % 2 != 0) { stream.WriteByte(0); }
            long b = stream.Position;

            stream.Position = a - 4;
            stream.Write(BitConverter.GetBytes((UInt32)(b - a)), 0, 4);
            stream.Position = b;
        }

        public void Pop(Func<UInt32, byte[]> func)
        {
            long a = stack.Pop();
            long b = stream.Position;

            stream.Position = a - 4;
            stream.Write(func((UInt32)(b - a)), 0, 4);
            stream.Position = b;
        }
    }
}

static class MyExt
{
    public static void Write(this Stream stream, string text, Encoding encoding)
    {
        byte[] tmp = encoding.GetBytes(text);
        stream.Write(tmp, 0, tmp.Length);
    }

    public static void WriteU32LE(this Stream stream, UInt32 a)
    {
        stream.Write(BitConverter.GetBytes(a), 0, 4);
    }

    public static void WriteU16LE(this Stream stream, UInt16 a)
    {
        stream.Write(BitConverter.GetBytes(a), 0, 2);
    }
}
```

で、うまく表示できず、いよいよわけがわからなくなってググってみたところ、Win10だとWAVのID3は表示できないみたいです。ﾅﾝﾃｺｯﾀ!!!
ちなみに、上記コードで作ったWAVファイルをTagKaeruで開くとちゃんとサムネが表示されるので、いちおう動いているはずです。

アナログ波形、例えばオシロスコープで取得した波形にオシロのキャプチャをサムネにして保存するとか、NTSCの波形データにNTSCをデコードした画像をサムネにして保存するとか、できると便利だなーと思っていただけに、アテが外れて残念。

***

某サイトで買ったMP3のサムネは600x600が入っていました。上記コードで1024x1024のJPEGを突っ込んでTagKaeruで開くとちゃんと表示できるので、常識的な範囲であれば、画像の大きさの制限は特になさそうです。他のソフトでも動くかはわかりませんが。ID3v2で16MBの制限があるみたいですが、JPEGやPNGで16MBになるような画像はかなりの大きさなので、実質無制限かな、という感じです。

// そういえば、PNGって互換性を保ったままアニメーションを設定できるけど、MP3やWAVのサムネにAPNGを入れて音楽再生中に(非同期で?)アニメーションが表示される、みたいな音楽プレイヤーとかあったりするんだろうか?

***

参考にしたページ

[ID3タグ - Wikipedia](https://ja.wikipedia.org/wiki/ID3%E3%82%BF%E3%82%B0)
[ID3v2 - Informal standard](http://takaaki.info/wp-content/uploads/2013/01/ID3v2.3.0J.html)
[ID3タグ、アートワークの仕様とiTunes4の仕様について](http://silhouette.studio.mu/artwork.shtml)