レトロゲームエンジンPyxelで作曲してみる

nana music マーケティングチームの @usangit です。
最近話題になった Pyxel を使って遊んでみようと思います。

# Pyxel とは？
Python 向けのレトロゲームエンジンです。

![02_jump_game.gif](https://qiita-image-store.s3.amazonaws.com/0/311439/63e4e254-9d4e-5ae3-3c93-0fb6ddcb2c02.gif)
https://github.com/kitao/pyxel

ファミコン世代には懐かしいあのレトロゲームを Python で作れるということで、本記事では Pyxel の作曲機能に注目してみます。

付属の [Pyxel Editor](https://github.com/kitao/pyxel/blob/master/README.ja.md#pyxel-editor) を使用することで GUI を使用して画像やサウンドを作成することが出来ますが、今回は Python コードのみでレッツチャレンジ！

# まずは出来上がったサウンドを聴いてみる
懐かしのファミコン曲を Pyxel で打ち込んでみました。[^1]

https://nana-music.com/sounds/046ddc00/
<a href="https://nana-music.com/sounds/046ddc00/"
<img src="https://storage.nana-music.com/ogp/post/046ddc00-370de663-5bfe-469e-aec7-5deffe1dbd50.png" width="70%">
</a>

# コードはこんな感じ

サウンドの一部のみ抜粋、引用[^2]

```python

import pyxel


class App:
    def __init__(self):
        pyxel.init(200, 150, caption="Sound API")

        # ch0 前半
        pyxel.sound(0).set(
            "c2re2rg2rc3c3 c3c3b2a2g2f2e2d2",
            "p",
            "6",
            "n",
            20,
        )

        # ch0 後半
        pyxel.sound(1).set(
            "c2re-2rg2rc3c3 c3c3b2c3d3c3b2d3",
            "p",
            "6",
            "nnnnnnnn nnnnnnnn",
            20,
        )

        # ch1 前半
        pyxel.sound(2).set(
            "c1c1rrrrrr g1g1g1g1g0g0g0g0",
            "t",
            "6",
            "n",
            20,
        )

        # ch1 後半
        pyxel.sound(3).set(
            "rrrrrrrrrrrrrrrr",
            "t",
            "6",
            "n",
            20,
        )

        # ch2 前半
        pyxel.sound(4).set(
            "e1e1rrrrrr rrd2c2b1a1g1b1",
            "s",
            "3",
            "n",
            20
        )

        # ch2 後半
        pyxel.sound(5).set(
            "rrrrrrrrrrrrrrrr",
            "p",
            "5",
            "nnnnnnnn nnnnnnnn",
            20
        )

        self.is_playing = [True] * 3
        self.play_music(True, True, True)
        pyxel.run(self.update, self.draw)

    def play_music(self, ch0, ch1, ch2):
        self.is_playing = (ch0, ch1, ch2)

        if ch0:
            pyxel.play(0, [0, 1], loop=True)
        else:
            pyxel.stop(0)

        if ch1:
            pyxel.play(1, [2, 3], loop=True)
        else:
            pyxel.stop(1)

        if ch2:
            pyxel.play(2, [4, 5], loop=True)
        else:
            pyxel.stop(2)

    def update(self):
        if pyxel.btnp(pyxel.KEY_Q):
            pyxel.quit()

        if pyxel.btnp(pyxel.KEY_SPACE):
            if True in self.is_playing:
                self.play_music(False, False, False)
            else:
                self.play_music(True, True, True)

    def draw(self):
        pyxel.cls(0)

App()
```

- 作曲にはオーディオ API のサウンドクラスを使用
- Pyxel で使用できるチャンネルは ch0 〜 ch3 の計4チャンネルでチャンネルそれぞれに対して各値を設定することが可能
- 各チャンネルごとに再生/停止することが可能(今回は使用していません)
- キーイベントとして `Qキー` で終了、`SPACEキー` で再生/停止を定義


----

今回のメイン、音符の打ち込みは`set(note, tone, volume, effect, speed)` を使用します。

### speed
`1音の長さ (120 = 1音1秒)`
今回は 1ノート = 16分音符 になるように 20 を設定

### note
`'CDEFGAB'+'#-'+'0123'または'R'の文字列で音程を設定する。大文字と小文字を区別せず、空白は無視される`

例えば、`C2` は `16 分音符の ド` の音。

`G2G2G2G2` のように連続で打ち込むと `4 分音符の ソ` の音になり、
スタッカートで打ち込みたい場合は `G2RG2R` と間に`休符の R` を入れます。

### tone
`音色 (0:Triangle / 1:Square / 2:Pulse / 3:Noise) のリスト`
頭文字を設定。`p は Pulse`

音色は以下パートで使われることが多いハズ

| 音色 | 主なパート |
| --- | --- |
| Triangle(三角波) | ベース音 |
| Square(矩形波) | 主旋律、副旋律 |
| Pulse(パルス波) | 主旋律、副旋律 |
| Noise(ノイズ) | ドラム、パーカッション |

### effect
`エフェクト (0:None / 1:Slide / 2:Vibrato / 3:FadeOut) のリスト`

'NSVF'の文字列でエフェクトを設定
1文字が1音の長さに対応

[サウンド](https://qiita.com/drafts/d379f8ff29971a8364bd/edit#%E3%82%B5%E3%82%A6%E3%83%B3%E3%83%89%E3%82%92%E8%81%B4%E3%81%84%E3%81%A6%E3%81%BF%E3%82%8B) では以下のポイントでエフェクトを入れてみました
- `0:36` Vivrato 
- `0:48` FadeOut

# まとめ
仕様もシンプルで用意されているクラスを使うことですぐにゲームが作れるので、Python を使ったことがあり、MIDI や DTM に馴染みのある人ならすんなり始めることが出来そうでした。
いつか Pyxel を使ったゲーム公開できればと思います。

[nana music Advent Calendar 2018](https://qiita.com/advent-calendar/2018/nana-music) はまだまだ続きます！
明日は @yamanemur さんです。どうぞお見逃しなく💁

# 番外編
試しにイメージクラスを追加して画面描画してみました。

```python
import math
import pyxel


class App:
    def __init__(self):
        pyxel.init(200, 150, caption="Image API")

        pyxel.image(0).set(
            0,
            0,
            [
                "11011100000011101100",
                "11111110000111111100",
                "11100111001110011100",
                "11000011001100001100",
                "11000011001100001100",
                "11000011001110011100",
                "11000011000111111100",
                "11000011000011110100",
            ],
        )

    def draw(self):
        pyxel.cls(0)
        pyxel.text(58, 60, "nana music", 7)
        pyxel.text(58, 70, "Advent Calendar 2018", 7)
        pyxel.rectb(6, 97, 193, 143, 7)
        pyxel.text(12, 102, "Everyone is a Co-Creator", 7)
        pyxel.text(12, 110, "", 7)

        for i, v in enumerate([0, 1]):
            pyxel.pal(1, 14)
            pyxel.blt(
                156 + i * 21,
                7 + math.sin(pyxel.frame_count * 0.2 + i * 1.1) * 2,
                0,
                0,
                0,
                20,
                8,
                0,
            )
        pyxel.pal()

App()

```

- `pyxel.image`でイメージのデータ (NumPy配列)をセット(よーくみると 01 のビットマップで na の文字が見えますね)
- draw 関数で画面描画
- for文内で 正弦*フレームカウント でイメージに動きを付ける

実行してみると、、、

<img src="https://qiita-image-store.s3.amazonaws.com/0/311439/900bd1ae-01c8-8746-04d7-a9eed344d9e9.gif" width=70%>

それっぽい！

[^1]: C4以上の高さになるとピッチがズレるため、ところどころ1オクターブ下げている
[^2]: 大人の事情