こんにちは、きうちです。
だいぶあたたかくなってきましたね!桜も見ごろを迎え、各地でにぎやかになってますね~。
弊社も今年は花見イベントやりました!
…ということと関係するわけではないですが、今回は
「音を作ってみる!?」
というタイトルでお届けしようかと思います。
音を作ってみる…いってもそんなたいそうなものではなく、サインカーブ(正弦波)…つまり、曲線の波。
あれを音にしてみようと思います。
聞いたことあるかも!?
★ ★ ★
言語は例によって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メディアプレイヤーの「スコープ」で表示させると波形が見えて面白いですね。データで作ったのと同じ波形が!…ってまあそりゃそうですよね…
さて、今回のは正弦波、つまり曲線の波でしたが、これをかくかくした線にしたりしたらどうなるでしょうか?
あるいは、数値をランダムにしてみたらどうなるでしょうか?
そのあたりを次回、やってみたいと思います。
それではまた!
追伸
ソースあります!
が、ちょっとまだ準備中で雑な感じなので、ご参考までに…
次回までに整えます!