serpentでドラムマシンを作る

Increments × cyma (Ateam Inc.) Advent Calendar 2020 の25日目は
Increments株式会社の @ktroutner が担当します。

最近は大学時代に授業でやったプロジェクトをもう一度見てコードを整理したりしています。その中で面白かったプロジェクトをひとつここで紹介したいと思います。

serpentという言語でスケジューリングを利用してMIDIメッセージを送信する、という内容です。今回は簡単なドラムマシンを作りたいと思いますが自由に拡張するところがありますので色々試してみると面白いです。（最後にいくつか紹介します。）

# serpentとは

[serpent](https://www.cs.cmu.edu/~music/serpent/serpent.htm)はコンピュータミュージックアプリケーションなどリアルタイムシステムのために作成された言語です。（pythonに似ているのでpythonの経験がある方はなんとなく分かるかもしれません。）

メジャーな言語ではないためドキュメンテーションが少ないですが、[ここ](https://www.cs.cmu.edu/~music/serpent/doc/)にあるドキュメンテーションを基にここで説明していきます。

## インストール

1. [このサイト](https://sourceforge.net/projects/serpent/files/)から最新のバージョンをダウンロードします。
2. ファイルをホームディレクトリに移動する
3. バイナリファイルを`bin`ディレクトリに移動する

    ```
    mkdir ~/bin
    cd ~/serpent
    mv serpent64 wxserpent64.app ~/bin
    ```
3. `wxserpent64`を実行するスクリプトを作成する

    ```
    #!/bin/sh
    echo "running wxserpent64 with arguments $*"
    /Users/ktroutner/bin/wxserpent64.app/Contents/MacOS/wxserpent64 $*
    ```
4. スクリプトを実行可能にする

    ```
chmod +x ~/bin/wxserpent64
    ```
5. `PATH`を設定する
:warning: `SERPENTPATH`は`export`しないと起動できない

    ```
PATH=$PATH:/Users/ktroutner/bin
export SERPENTPATH="/Users/ktroutner/serpent/lib:/Users/ktroutner/serpent/programs:/Users/ktroutner/serpent/wxslib
    ```

ここまでできたら、`wxserpent64`のインタプリタを実行できるはずです。

```
wxserpent64 wxs_test.srp
```

参考： https://cs.cmu.edu/~music/serpent/doc/installation.htm

##  スケジューリングについて
以下のように好きなタイミングで処理をスケジューリングできます。
こちらのサンプルコードは1秒に1回メッセージを出力します。

```
require "debug"
require "sched"

def activity(i):
  display "activity", i, sched_rtime, time_get()
  sched_cause(1, nil, 'activity', i+1)

sched_init()
sched_cause(1, nil, 'activity', 101)
sched_run()
```

- `sched_init`でスケジュールを初期化する
- `sched_cause(delay, object, callback, params)`
    - `delay`: 何秒後にスケジュールするか
    - `object`: メソッドを呼び出すオブジェクト
    - `callback`: 呼び出すメソッド
    - `params`: 呼び出すメソッドに渡すパラメータ

参考：https://cs.cmu.edu/~music/serpent/doc/scheduling.htm

## MIDIメッセージを送信する
serpentが提供しているPortMidiのAPIを使ってMIDIメッセージを送信します。

`midi_create()`でMIDIのストリームを作成して、`midi_open_output`でオープンします。

```
midi_open_output(midi, devno, buffer_size, latency)
```

- `midi`: `midi_create`で作成したMIDIストリーム
- `devno`:  MIDIのアウトプットデバイス番号（`midi_out_default()`でデフォルトを取得できる）
- `buffer_size`: バッファーサイズ
- `latency`: レイテンシー（ミリ秒）

このストリームを使って`midi_write`でメッセージを送信します。

```
midi_write(midi, time, msg)
```

- `midi`: `midi_create`で作成したMIDIストリーム
- `time`: タイムスタンプ（今回特に気にしないので`0`を渡す）
- `msg`: MIDIメッセージ
    - MIDIメッセージの構成については[こちら](https://ja.wikipedia.org/wiki/MIDI#:~:text=MIDI%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E3%81%AF%E3%80%81%E8%A4%87%E6%95%B0%E3%81%AE,%E3%81%AE%E5%BE%8C%E3%81%ABH%E3%82%92%E4%BB%98%E3%81%91%E3%82%8B%E3%80%82)

参考：https://cs.cmu.edu/~music/serpent/doc/serpent-midi.htm

# ドラムマシンの実装

ここで流れだけを説明しますが、全てのコードはGitHubにあります。
https://github.com/ktroutner/drum-machine

## 初期化

MIDIのセットアップを行います。

```
mo = midi_create()
result = midi_open_output(mo, MIDI_ID, 100, 0)
if (result != 0)
    print "midi_open_returns "; result
    return
```

スケジュールの設定も行います。

```
sched_init()
rtsched.time_offset = time_get()
vtsched.set_bps(tempo/60.0)
```

## ノートの再生

`drum`メソッドの中で、現在時刻のノートを `midi_note_on`で再生して、停止する `midi_note_off`をスケジュールします。
次時刻でまた `drum`の呼び出しもスケジュールします。

```
def midi_note_on(midi, chan, key, loud)
    chan = chan & 15
    midi_write(midi, 0, chr(midi_status_noteon + chan) + chr(key) + chr(loud))

//play the current notes, schedule them to be turned off, and schedule
//notes for next beat
def drum(id, i, n)
    if id != player_id
        return
    p = int(subseq(sequence, i, i+1)) - 1
    notes = patterns[p]
    for note in notes[n]
        midi_note_on(mo, 9, note, 100)
        sched_cause(intrvl_sec, nil, 'midi_note_on', mo, 9, note, 0)
    if n == len(notes)-1
        if i < len(sequence)-1
            sched_cause(intrvl_sec, nil, 'drum', player_id, i+2, 0)
        else
            return  //nothing more to do
    else
        sched_cause(intrvl_sec, nil, 'drum', player_id, i, n+1)
```

そして、再生ループを初期化後に開始します。

```
vtsched.cause(0.2, nil, 'drum', [player_id, 0, 0])
wxs_timer_start(2, 'timer_callback')
```

## 再生の制御

音声を再生するだけではなく、再生中にピッチベンドやテンポの変更などのエフェクトも実装できます。今回ははテンポを変更するエフェクトを実装してみます。

```
def set_tempo(obj, x)
    vtsched.set_bps(x/60.0)
```

serpentが簡単なUIも提供していますのでこちらを使って再生中に自由にテンポの変更を行えるようにします。

```
tempo_slider = Labeled_slider(0, "Tempo", 5, 30, 250, 20,
                                70, 10, 300, tempo, 'linear')
tempo_slider.method = 'set_tempo'
```

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553103/0bc54c10-adcf-f752-bc7f-d33b004452f3.png">


## ビートファイルについて

どのノートをどのタイミングで再生するかはビートファイルで指定します。なお、同じパターンを簡単に繰り返しできるように作っています。

```
1 1 1 1 2 2 2 2
120

46xxxxxxxx
38  x   x 
36x   x

42xxxxxxxx
38    x
35x x 
```

1行目はパターンを再生順番です。（各パターンは下の方で定義しています。）
2行目ではデフォルトのテンポを指定します。

3行目に空行を入れて、以降は各パターンの定義です。
パターンのフォーマットとしては、1行ごとにノートナンバーとそのノートを再生するビートを指定します。（1文字は八分音符です。「x」は再生する、空白は再生しない、というふうに扱います。）

パターンの間に空行を入れますので、上のサンプルで以下の2パターンを定義することになります。

 **パターン1** 

```
46xxxxxxxx
38  x   x 
36x   x
```

 **パターン2** 

```
42xxxxxxxx
38    x
35x x 
```

# ドラムマシンを実行する

上のプログラムは実行できますが、まだ音は出ない状態になります。

実際に音を出すためにPCでMIDIのドライバーとデバイスを設定する必要があります。

## MIDIバスの設定

Macの場合、`Applications/Utilities`にある「Audio MIDI設定」から設定できます。

`ウインドウ > MIDIスタジオを表示` でMIDIスタジオを開きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553103/854ab1e5-674c-c52e-5790-feee16f5c182.png)

`IACドライバー`をダブルクリックして、「装置はオンライン」にチェックを入れます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553103/e573cd5c-b648-9a46-652d-84b3b5e98f24.png)

## MIDIデバイスの設定

ここまでできたらMIDIメッセージは受信できるので、次はメッセージを音として出力するシンセサイザーを作成します。

シンセサイザーはなんでもいいですが、Macの場合はとりあえず無料で使えるGarageBandが良いでしょう。

GarageBandを開いて「トラックのタイプを選択」で「ソフトウェア音源」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553103/034464c2-4892-5ad2-95c3-d149864fc0dd.png)

ドラムマシンを想定しているので「Electronic Drum Kit」から「After Party」を選択してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553103/a13788eb-3445-fb3c-fae0-e124740f215f.png)

ドラム以外ももちろん使えますので好きなものを試してみると面白いかと思います。（SynthesizerのBassは個人的に好きでした。）シンセサイザーによってノートが変わる場合もありますのでビートファイルの修正も必要かもしれません。

https://www.youtube.com/watch?v=tAbzgQUgzaU

# 次に試してみると面白いこと

- TouchOSCを使ってスマホで操作できるようにする
- ビートファイルでもっと細かいタイミングを可能にする
- pythonで実装する
    - スケジューリング: [sched](https://docs.python.org/3/library/sched.html)
    - MIDIメッセージ送信：[python-rtmidi](https://github.com/SpotlightKid/python-rtmidi) OR [mido](https://github.com/mido/mido)


---

[Increments × cyma (Ateam Inc.) Advent Calendar 2020](https://qiita.com/advent-calendar/2020/increments-cyma) は以上となります！ それでは良いお年を！Happy New Year！