WAVEファイルヘッダのサンプリングレートを変更して音階を演奏する

短いサンプリング音、例えばペットの「ワン」とか「ニャー」とかいう鳴き声や、コップや空き缶を叩いた音で音楽を演奏できないかと思って、 なんとなくWAVEファイルのフォーマットなど調べていたら、ファイルのヘッダを書き換えればピッチを変更できることがわかりました。
それなら音階を作って音楽を演奏できないかと思って作ってみました。
注1)もしかして、WAVEファイルのデータをいじるようなプログラムを期待して来た人がいたら、ごめんなさい。他を当たってください。そんな難しいことはできません。
注2)ちゃんとした演奏をしたい人は専用のソフトがあるはずなので、そちらを使ってください。音楽と言えるレベルのものはできません。

原理

WAVEファイルには1〜44バイト目までのヘッダがあります。25〜28バイトはサンプリングレートを表します。
一般にサンプリングレート44.1kHzのことが多く、44100という値が書き込まれています。
ここを書き換えて、例えば88200とすると2倍速で再生され、22050とすると半分の速度で再生されます。
音の高さは2倍速なら高い音に、半分なら低い音になります。この方法で音階を作ります。
当然ながら2倍速なら再生時間が半分に、半分速度なら2倍になりますが、主旋律の再生なら1オクターブもあれば足りるのでそこは目をつぶります。







作成したもの

VB2010を使用して作成しました。
簡単な使い方
何かレコーダーを使って短いWAVEファイルを作成します。できたらそのファイルを読み込みます。
下にあるキーボード状に並んだボタンを押すと音が出ます。
ボタンに書かれている文字のキーを押しても音が出ます。
サンプリングした音がどの音名に該当するかを設定できます。
WAVEファイルを準備しなくても、空き缶を叩いた音が内蔵されているのでそのままでも音はでます。





遊んでみたい方はこちらからどうぞ
WaveTest.zip

音を鳴らすのには SoundPlayer を使っています。これはWAVEを同時に複数鳴らすことができません。続けて音を鳴らすと前の音が止まってしまいます。
和音や速いフレーズなどをきれいに演奏したければ別の方法が必要です。(DirectXとか使うらしいです)



説明用サンプルコード

見るようなものはないと思いますがコードの一部です。説明のため簡略化してありますが基本は上のものと同じです。興味のある方はご覧ください。
超手抜き演奏ルーチンは2種類あります。
PLAYのコードは最初に書いたコードですがネタレベルです。これではテンポずれが激しいので書き直しました。
PLAY2のコードは無限ループ内で時間を計って音を鳴らしています。これだとだいぶましになります。上ではこのルーチンを使っています。
フォームにテキストボックス1個:名前はTextBox1、ボタン11個:FileButton、PlayButton、Play2Button、Button1〜Button8
OpenFileDialog:OpenFileDialog1、Timer:Timer1を配置します。
Button1〜Button8はキーボードのキーに見立てています。簡略化のために1オクターブのみで半音はなしです。
曲データは2次元配列にして、("音名","長さ")とします。音名はアルファベットA-Gに数字を付けて高さを表します。A=ラ〜G=ソです。長さはミリ秒で指定します。

Imports System.IO

Public Class Form1
    Dim WaveByte() As Byte 'Waveのバイト配列
    Dim SampleRate As Int32 'サンプルレート
    Dim cdtix As Integer '自動演奏データ位置
    Dim ButtonArray() As Button 'キーボタンの配列
    Dim PlayData(,) As String = {{"C2", "500"}, {"C2", "500"}, {"G2", "500"}, {"G2", "500"}, {"A3", "500"}, {"A3", "500"}, {"G2", "1000"}, _
                                 {"F2", "500"}, {"F2", "500"}, {"E2", "500"}, {"E2", "500"}, {"D2", "500"}, {"D2", "500"}, {"C2", "1000"}} '曲データ
    Dim HZ() As Double = {261.63, 293.66, 329.63, 349.23, 392, 440, 493.88, 523.25} 'C3-C4周波数
    Dim CdAr() As String = {"C2", "D2", "E2", "F2", "G2", "A3", "B3", "C3"} 'コード文字と周波数位置を合わせるための配列

    '初期化
    Private Sub Form1_Load(sender As Object, e As System.EventArgs) Handles Me.Load
        ButtonArray = {Button1, Button2, Button3, Button4, Button5, Button6, Button7, Button8}
        For i = 0 To ButtonArray.Length - 1
            AddHandler ButtonArray(i).Click, _
           AddressOf Me.KeyButtons_Click
        Next i

    End Sub

    'ファイル読み込み
    Private Sub FileButton_Click(sender As System.Object, e As System.EventArgs) Handles FileButton.Click
        OpenFileDialog1.Filter = "wave|*.wav"
        If OpenFileDialog1.ShowDialog() = Windows.Forms.DialogResult.OK Then
            Dim waveFile As String = OpenFileDialog1.FileName
            TextBox1.Text = waveFile
            ReadWaveByte(waveFile)
        End If

    End Sub

    'WAVEをバイト配列に保存
    Private Sub ReadWaveByte(ByVal filename As String)
        Dim fs As New System.IO.FileStream(filename, _
         System.IO.FileMode.Open, _
         System.IO.FileAccess.Read)
        'ファイルを読み込むバイト型配列を作成する
        ReDim WaveByte(fs.Length - 1)
        'ファイルの内容をすべて読み込む
        fs.Read(WaveByte, 0, WaveByte.Length)
        '閉じる
        fs.Close()

        Dim wrate(3) As Byte 'サンプルレートを保存する
        For i = 24 To 27
            wrate(i - 24) = WaveByte(i)
        Next
        SampleRate = BitConverter.ToInt32(wrate, 0)

    End Sub

    'KeyButtonのクリックイベントハンドラ
    Private Sub KeyButtons_Click(ByVal sender As Object, _
            ByVal e As EventArgs)
        Dim s As String = CType(sender, System.Windows.Forms.Button).Name
        s = s.Replace("Button", "") 'ボタン名から数字部分のみ取り出す
        Try
            Dim i As Integer = CInt(s) - 1
            MyPlaySound(i)
        Catch ex As Exception

        End Try
    End Sub

    Private Sub MyPlaySound(ByVal KeyNo As Integer)
        Dim r As Double = HZ(KeyNo) / 440 'ここでは音源はラの音として処理している。実際には変数を使用
        'サンプルレートを書き込む
        Dim br As Int32
        br = CInt(SampleRate * r)
        Dim bt() As Byte = BitConverter.GetBytes(br)
        For i = 24 To 27
            WaveByte(i) = bt(i - 24)
        Next
        'Waveファイルを作って鳴らす
        Dim WMemoryStream As New MemoryStream
        WMemoryStream.Write(WaveByte, 0, WaveByte.Length)
        WMemoryStream.Seek(0, SeekOrigin.Begin)
        Dim player1 As System.Media.SoundPlayer
        player1 = New System.Media.SoundPlayer(WMemoryStream)
        'player1.Stop()
        player1.Play()

    End Sub

    'PLAY
    Private Sub PlayButton_Click(sender As System.Object, e As System.EventArgs) Handles PlayButton.Click
        Timer1.Interval = 1
        cdtix = 0
        Timer1.Start()

    End Sub

    '自動演奏 タイマー使用
    Private Sub Timer1_Tick(sender As Object, e As System.EventArgs) Handles Timer1.Tick
        If cdtix < 0 Or cdtix >= PlayData.Length / 2 Then 'データが無いので停止
            Timer1.Stop()
            Exit Sub
        End If
        Dim Code As String = PlayData(cdtix, 0) 'コードを取り出す
        Dim ix = Array.IndexOf(CdAr, Code) 'コードからキー位置を調べる
        If ix >= 0 And ix < ButtonArray.Length Then
            Dim Len As Integer = CInt(PlayData(cdtix, 1)) '音を鳴らす時間を得る
            ButtonArray(ix).PerformClick() '音を鳴らす
            Timer1.Stop()
            Timer1.Interval = Len '音を鳴らす時間が過ぎたら次のコールが来るようにする
            Timer1.Start()
        End If
        cdtix = cdtix + 1
    End Sub

    'PLAY2 自動演奏ループ
    Private Sub Play2Button_Click(sender As System.Object, e As System.EventArgs) Handles Play2Button.Click
        Dim StWt As New System.Diagnostics.Stopwatch()
        cdtix = 0
        Dim CT As Int32 = -1
        Dim LPCT As Integer = 0
        StWt.Reset()
        StWt.Start()
        While True
            If CT = -1 Then CT = StWt.ElapsedMilliseconds
            LPCT = LPCT + 1
            If cdtix < 0 Or cdtix >= PlayData.Length / 2 Then 'データが無いので停止
                StWt.Stop()
                Exit While
            Else
                If StWt.ElapsedMilliseconds > CT Then
                    Try
                        Dim Len As Integer = CInt(PlayData(cdtix, 1)) '音を鳴らす時間を得る
                        CT = CT + Len
                        Dim Code As String = PlayData(cdtix, 0) 'コードを取り出す
                        Dim ix = Array.IndexOf(CdAr, Code) 'コードからキー位置を調べる
                        If ix >= 0 And ix < ButtonArray.Length Then
                            MyPlaySound(ix)
                        End If
                        cdtix = cdtix + 1
                    Catch ex As Exception
                    End Try
                End If
            End If

        End While

    End Sub
End Class






【参考にしたページ】
WAVEファイルの構造
WAVEファイルの構造が書いてあります。

【AquesTalk】ゆっくりボイスのピッチ変更法
WAVEファイルのヘッダ変更で音の高さが変わる事をこのページで知りました。

WAVEファイルを再生する
VB .net でのWAVEファイル再生の参考にしました。

埋め込まれたWAVEのリソースファイルを再生する
VB .net でのWAVEファイル再生の参考にしました。

Visual Studioでリソースを管理する
VB .net でのWAVEファイルをリソースとして埋め込む参考にしました。






TOPに戻る
2016/9/6