2021/04/20

Pythonでサウンドプログラミング

標準ライブラリなしでチャレンジ

Pythonではwaveが標準ライブラリに含まれていますが、今回はPythonでゼロから作ってみることにしました。その方がWAVEフォーマットがどのようなものなのかが分かるからです。

学習用に選んだ書籍は「サウンドプログラミング入門 〜音響合成の基本とC言語による実装〜」(著:青木直史 出版社:技術評論社)です。こちらの書籍はC言語で書かれております。サポートサイト(書籍内に書かれています)も設置されており、各章のプログラムもダウンロードできます。また先日、著者の方にメールしたところ、すぐに返信がありました。2013年発売にも関わらず、サポートもしっかりされていて勉強には最適な印象です。

この書籍を参考に、サウンドプログラミングの勉強をするにあたって、まず必要となるコードがwave.hに相当するものですが、今回は1ファイルで完結するようにしました。


from struct import pack
import math

RIFF_CHUNK_ID = 0x52494646     # 'RIFF'
FILE_FORMAT_TYPE = 0x57415645  # 'WAVE'
FMT_CHUNK_ID = 0x666D7420      # 'fmt '
DATA_CHUNK_ID = 0x64617461     # 'data'


class MonoPcm:
    def __init__(self, fs, bits, length=None):
        self.fs = fs
        self.bits = bits
        if length is None:
            self.length = fs * 1
        else:
            self.length = length
        self.s = [0] * self.length

    def wave_write_8bit(self, filename):
        riff_chunk_id = pack(">I", RIFF_CHUNK_ID)
        riff_chunk_size = pack("<I", 36 + self.length)
        file_format_type = pack(">I", FILE_FORMAT_TYPE)
        fmt_chunk_id = pack(">I", FMT_CHUNK_ID)
        fmt_chunk_size = pack("<I", 16)
        wave_format_type = pack("<H", 1)
        channel = pack("<H", 1)
        samples_per_sec = pack("<I", self.fs)
        bytes_per_sec = pack("<I", self.fs * self.bits // 8)
        block_size = pack("<H", self.bits // 8)
        bits_per_sample = pack("<H", self.bits)
        data_chunk_id = pack(">I", DATA_CHUNK_ID)
        data_chunk_size = pack("<I", self.length)
        
        with open(filename, "wb") as f:
            f.write(riff_chunk_id)
            f.write(riff_chunk_size)
            f.write(file_format_type)
            f.write(fmt_chunk_id)
            f.write(fmt_chunk_size)
            f.write(wave_format_type)
            f.write(channel)
            f.write(samples_per_sec)
            f.write(bytes_per_sec)
            f.write(block_size)
            f.write(bits_per_sample)
            f.write(data_chunk_id)
            f.write(data_chunk_size)

            for n in range(0, self.length):
                tmp = (self.s[n] + 1.0) / 2.0 * 256.0

                if tmp > 255.0:
                    tmp = 255.0
                elif tmp < 0.0:
                    tmp = 0.0

                data = pack("<B", round(tmp + 0.5))
                f.write(data)

            if (self.length % 2) == 1:
                data = pack("<B", 0)
                f.write(data)

    def wave_write_16bit(self, filename):
        riff_chunk_id = pack(">I", RIFF_CHUNK_ID)
        riff_chunk_size = pack("<I", 36 + self.length * 2)
        file_format_type = pack(">I", FILE_FORMAT_TYPE)
        fmt_chunk_id = pack(">I", FMT_CHUNK_ID)
        fmt_chunk_size = pack("<I", 16)
        wave_format_type = pack("<H", 1)
        channel = pack("<H", 1)
        samples_per_sec = pack("<I", self.fs)
        bytes_per_sec = pack("<I", self.fs * self.bits // 8)
        block_size = pack("<H", self.bits // 8)
        bits_per_sample = pack("<H", self.bits)
        data_chunk_id = pack(">I", DATA_CHUNK_ID)
        data_chunk_size = pack("<I", self.length * 2)
        
        with open(filename, "wb") as f:
            f.write(riff_chunk_id)
            f.write(riff_chunk_size)
            f.write(file_format_type)
            f.write(fmt_chunk_id)
            f.write(fmt_chunk_size)
            f.write(wave_format_type)
            f.write(channel)
            f.write(samples_per_sec)
            f.write(bytes_per_sec)
            f.write(block_size)
            f.write(bits_per_sample)
            f.write(data_chunk_id)
            f.write(data_chunk_size)

            for n in range(0, self.length):
                tmp = (self.s[n] + 1.0) / 2.0 * 65536.0

                if tmp > 65535.0:
                    tmp = 65535.0
                elif tmp < 0.0:
                    tmp = 0.0

                data = pack("<h", round(tmp + 0.5) - 32768)
                f.write(data)

def sine_wave(pcm, a, f0):
    for n in range(0, pcm.length):
        pcm.s[n] = a * math.sin(2.0 * math.pi * f0 * n / pcm.fs)

def sine_wave_scale(pcm, f0, a, offset, duration):
    dur = int(duration)
    tmp = [0]*dur

    for n in range(0, dur):
        tmp[n] = a * math.sin(2.0 * math.pi * f0 * n / pcm.fs)

    for n in range(0, int(pcm.fs * 0.01)):
        tmp[n] *= n / int(pcm.fs * 0.01)
        tmp[dur - n - 1] *= n / int(pcm.fs * 0.01)

    for n in range(0, dur):
        pcm.s[int(offset + n)] += tmp[n]

def saw_wave(pcm, f0):
    for i in range(1, 45):
        for n in range(0, pcm.length):
            pcm.s[n] += 1.0 / i * math.sin(
                    2.0 * math.pi * i * f0 * n / pcm.fs
                    )
    gain = 0.1
    for n in range(0, pcm.length):
        pcm.s[n] *= gain

def saw_wave_2(pcm, f0):
    for i in range(1, 45):
        for n in range(0, pcm.length):
            pcm.s[n] += 1.0 / i * math.cos(
                    2.0 * math.pi * i * f0 * n / pcm.fs
                    )
    gain = 0.1
    for n in range(0, pcm.length):
        pcm.s[n] *= gain

def square_wave(pcm, f0):
    for i in range(1, 45, 2):
        for n in range(0, pcm.length):
            pcm.s[n] += 1.0 / i * math.sin(
                    2.0 * math.pi * i * f0 * n / pcm.fs
                    )
    gain = 0.1
    for n in range(0, pcm.length):
        pcm.s[n] *= gain

def triangle_wave(pcm, f0):
    for i in range(1, 45, 2):
        for n in range(0, pcm.length):
            pcm.s[n] += 1.0 / i / i * math.sin(
                    math.pi * i / 2.0) * math.sin(
                            2.0 * math.pi * i * f0 * n / pcm.fs)
    gain = 0.1
    for n in range(0, pcm.length):
        pcm.s[n] *= gain


if __name__ == '__main__':

    # 8bit sine wave
    pcm_sine_8bit = MonoPcm(44100, 8)
    sine_wave(pcm_sine_8bit, 0.1, 500.0)
    pcm_sine_8bit.wave_write_8bit("sine8.wave")

    # 16bit sine wave
    pcm_sine_16bit = MonoPcm(44100, 16)
    sine_wave(pcm_sine_16bit, 0.1, 500.0)
    pcm_sine_16bit.wave_write_16bit("sine16.wave")

    # 16bit saw wave
    pcm_saw_16bit = MonoPcm(44100, 16)
    saw_wave(pcm_saw_16bit, 500.0)
    pcm_saw_16bit.wave_write_16bit("saw16.wave")

    # 16bit saw wave 2
    pcm_saw_2_16bit = MonoPcm(44100, 16)
    saw_wave_2(pcm_saw_2_16bit, 500.0)
    pcm_saw_2_16bit.wave_write_16bit("saw2_16.wave")

    # 16bit sine scale
    pcm = MonoPcm(44100, 16, 44100 * 2)
    sine_wave_scale(pcm, 261.63, 0.1, pcm.fs * 0.00, pcm.fs * 0.25)
    sine_wave_scale(pcm, 293.66, 0.1, pcm.fs * 0.25, pcm.fs * 0.25)
    sine_wave_scale(pcm, 329.63, 0.1, pcm.fs * 0.50, pcm.fs * 0.25)
    sine_wave_scale(pcm, 349.23, 0.1, pcm.fs * 0.75, pcm.fs * 0.25)
    sine_wave_scale(pcm, 392.00, 0.1, pcm.fs * 1.00, pcm.fs * 0.25)
    sine_wave_scale(pcm, 440.00, 0.1, pcm.fs * 1.25, pcm.fs * 0.25)
    sine_wave_scale(pcm, 493.88, 0.1, pcm.fs * 1.50, pcm.fs * 0.25)
    sine_wave_scale(pcm, 523.25, 0.1, pcm.fs * 1.75, pcm.fs * 0.25)
    pcm.wave_write_16bit("sine_scale_16.wave")

    # 16bit square wave
    pcm_square = MonoPcm(44100, 16)
    square_wave(pcm_square, 500.0)
    pcm_square.wave_write_16bit("square.wave")

    # 16bit triangle wave
    pcm_tri = MonoPcm(44100, 16)
    triangle_wave(pcm_tri, 500.0)
    pcm_tri.wave_write_16bit("triangle.wave")

このコードでは標準ライブラリのstruct使っていますので、ちょっと説明しようと思います。WAVEフォーマットでは〇〇をビッグorリトルエンディアンで〇バイトというような形でバイトコードを記述します。これは決まりごとなので、それに従わなくてはなりません。structを使うと、ビッグorリトルエンディアンの指定、バイト数の指定ができるようになります。例えばビッグエンディアンで符号なし整数4バイトの場合、>I、リトルエンディアンで符号なし短整数2バイトの場合、<Hと指定します。

今回私はwaveファイルの読み込みは使わないので、wave_read_8bitおよびwave_read_16bit、StereoPcmは今後必要になったら追記しようと思います。

上記のプログラムを実行すると、7つのwaveファイルが作成されます。参考書籍の3章あたりまでに相当します。

実際に音が作れると、なんだかとても楽しい気分になります。ぜひお試しください。また、より詳しく勉強したい方はぜひ参考書籍「サウンドプログラミング入門」を読んでみてください。