音を1サンプル毎に扱う音響合成環境をPythonでつくる

目的

音を扱うアプリケーションはアナログ機器の真似事ばかり、ほとんどはインターフェイスを模したサンプルプレーヤーになっていてリソースばかり食って全然うれしくない。
たとえばRaspberry Piだとか、Arduinoとかの8bit MCUとかで音を生成するにはどうしたらいいかと考えたときに障害となるのは主に速度とメモリです。
どうにかしたい、っていうかする!

速度について

非リアルタイム合成とすることで解決できそうです。リアルタイムで鳴らさなきゃいけないケースがもうほとんどないわけで、ネットで音源配信とかいう場合むしろ非リアルタイムの方が都合が良いでしょう。速度を気にしなくて良くなるわけなのでどんなに重い演算をしても大丈夫ってことになります。メリットの方が多い。

メモリについて

DAWなどは速度のこともあってほぼオンメモリで音を扱っています。(トラックなどのレンダリング済み音源はストリーミングしたりもしているけれど)

もしトラックを完全にオンメモリで扱った場合、5分のステレオトラックで、サンプリング周波数を44100、ビットレートを16bitとしたら、5*60*44100*2*2、つまり52920000byteのメモリを消費するということになります。

とりあえず速度に関しては非リアルタイムで音を扱うことで気にしなくてよくなっていますのでディスクをシークしながら読み書きすればいい。ついでに指定の1サンプルだけに注視して操作出来るようにすれば、ステレオで16bitだとすれば、ある一瞬で必要となるバッファのサイズは32bitだけですみます。つまり4byte。 これでいきましょう。

使う言語

なんでもよかったのですが書き換えたりいろいろ試したりする場合、CSoundやSupercolliderみたいな専用の言語系を使うよりも既存の言語のライブラリとして動作したほうが都合が良さそうじゃないですか?
ということでとりあえずPythonで作ることにしました。
いちいちコンパイルしなくて済む、音楽系が大変貧弱なLinuxで使えることなどが決め手。

今回のパーツ。

模索しつつ作りつつ、になるので少しずつ環境を整えてゆきます。
実はこのプロジェクト3年くらい前に考えて作っていたものの続きだったりします。
当時はまだPythonにも慣れていなくて何をどうしたらいいのか解って無かった部分もあったりして。

とりあえず今回はOSC、EG、それと汎用のファンクションをひとつのBareAudioというクラスで参照出来るようにしてみます。

OSCクラス

オシレータです。ひとまずサイン波だけ。

osc.py
import math

class OSC:
    def __init__(self):
        self.smpl = 44100.0

    def sinOsc(self, t, freq, phase=0 ):
        return math.sin( t * math.pi*2 / self.smpl * freq + math.pi/180.0 * phase )

サンプリングレートをここにもつのはどうなのかと思うんだけど今はとりあえずこのままにしておきます。

ある時間 t で周波数 freqのサイン波が出力する値は何なのかってのが取得できます。
フェーズも指定すればそのように動くようになってます。

EGクラス。

AとRだけ。しかしこれも過去に作ったものなのでなんか思うところはあり。
とりあえずこのまま。

eg.py
class EG:
    def __init__(self):
        pass

    # EG Attack and Release
    def ar(self, t, a=1.0, r=1000.0):
        if t > a * 100.0:
            return release(t,r)
        else:
            return attack(t,a)

    def release(self, t, r=1000.0):
        return 1.0 / (t/r+1)

    def attack(self, t, a=1.0):
        return 1 - 1.0 / (t/a+1)

時間 t で出力は何倍になるのかってのをAttackとReleaseそれぞれ出せるようになっているんですが見ての通りライナーですし、ハードコーディングしてある値が何か違和感。
昔つくったものですし、でもしっかり動作はしていたので。まあ、とりあえずね。

BareAudioクラス

これが親になります、こいつに大体を集約して使うことにします。
ついでに汎用ファンクションもここにいれてゆきます。

bareaudio.py
import math
import osc
import eg


class BareAudio:
    def __init__(self):
        self.osc = osc.OSC()
        self.eg  = eg.EG()

    def mtof(self, midiNoteNo):
        freq = 8.1758125
        octave = int( midiNoteNo / 12 )
        freq = freq*(2**octave)*(1.0594628987669812**(midiNoteNo-octave*12))
        return freq

OSCクラスとEGクラスもBareAudioクラスの中で初期化してまとめて扱えるようにしました。
ついでに midiノート番号から周波数を得られる mtofファンクションも定義して。今回はとりあえずここまで。

動作確認

test.py
import bareaudio

b = bareaudio.BareAudio()

for fr in range(200):
    a = b.osc.sinOsc(fr,b.mtof(69))
    print( fr , a)

midiノート番号69っていうのは周波数で言うと440Hz、440Hzは44100のサンプリングレートで約100サンプルなので200サンプル分の出力をすると2周期分ってことになります。

出力されるデータを見るかぎりは正しく動いてるような気もします。
以前動いてたものをクラスに直しただけですからね。

次はファイルをシークしながら1サンプル毎に読み書きするってあたりを整備してゆきましょう。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.