pyxelでBGM4音+割り込みSEを実現する
2024.6.25追記
Pyxelバージョン2.1で、play関数にオプションresumeが追加され、割り込みSEは標準機能で実装できるようになりました。
そのため以下の記事のような実装をする必要は今はありません。
pyxelでは4音を同時に鳴らすことができるが、うち1音をSE(効果音)に使うとするとBGM用には残りの3音しか使えない。
と思っていたのだけど、公式ドキュメント(https://github.com/kitao/pyxel/blob/main/docs/README.ja.md)のサウンドクラスを改めて見ると、実は簡単にできるんじゃないかということに気づいた。もしかすると最近のバージョンアップで機能追加されたのかもしれない。
play(ch, snd, [tick], [loop])
チャンネルch (0-3) でサウンドsnd (0-63) を再生します。sndがリストの場合順に再生されます。再生開始位置はtick (1 tick = 1/120秒) で指定できます。loopにTrueを指定するとループ再生します。
play_pos(ch)
チャンネルch (0-3) のサウンド再生位置を(サウンド番号, ノート番号)のタプルとして取得します。再生停止時はNoneを返します。
まず0~3のサウンドを全て使って4音でBGMを鳴らして、SEを鳴らしたいタイミングでチャンネル3あたりを使って効果音と鳴らす。
SEが鳴り終わるとチャンネル3が欠けたままになっているので、「play_pos(3)の状態がNoneになったらplay(3, <snd>, <tick>, True)を呼び出す」ことで、元のBGMのチャンネル3のサウンドを再開時のタイミングに合わせてもう一度鳴らすというやり方である。
おそらく、ファミコンのサウンドなども同様の手法で4音+割り込みSEを実現しているはず。
ポイントは上記の<tick>を正確に指定するところだが、シンプルなやり方としては、BGM再生と同時に経過tick数をプログラム内部でカウントすればBGMの現在の再生位置と合わせることができる。
わたしの場合は、BGMのspeedを1で統一すると(*1)play_pos(ch)が再生開始してからの経過tick数と一致する(speedが1のとき、 1ノートが1/120秒)ことを利用して、play_pos(0)をplayメソッドの<tick>部分に指定することとした。
デモコードは以下のようになった。python初心者なのでコード汚いのと、後に載せるデモ用動画のため余計なコードが増えているが、かなり簡単な記述で実現できる。
import pyxel
import bgm # この中でBGMをセットしています
seChannel = 3 # チャンネル3をSE用にする
class App:
def __init__(self):
pyxel.init(256, 144, title="music")
pyxel.load("sepack.pyxres") # SE読み込み用
self.tick = None
self.se = None
pyxel.run(self.update, self.draw)
def update(self):
pressed = pyxel.btnp(pyxel.KEY_SPACE)
if self.tick is None:
if pressed: # 最初にスペースを押すとBGMが鳴る
bgm.play() # BGMを鳴らす(サウンド番号0, 1, 2, 3)
self.tick = 0
else:
if pressed: # 2回目以降スペースを押すとSEを鳴らす
pyxel.play(seChannel, [38]) # 38はSEの番号
self.se = seChannel
self.tick = pyxel.play_pos(0)[1] # 現在の再生位置
if pyxel.play_pos(seChannel) is None: # SEが鳴り終わったら
pyxel.play(seChannel, [seChannel], self.tick, True) # 再びBGMを鳴らす
self.se = None
def draw(self):
(以下略)実際に試してみると以下の動画のような感じで、ちゃんとBGM4音+割り込みSEになっている。
この動画の曲は「RPGの戦闘シーン」のイメージなんだけど、こういったちょっと激しめの曲を作ろうとするとメロディ・サブ・ベース・ノイズの4音がどうしても欲しくなるところなので、これでだいぶ幅が広がったなーと満足してる。
(*1) ここはサラッと書きましたが、実際にはspeed=1で曲を作ろうとすると膨大な数のノートを打ち込むことになります。たとえばBPM120の8分音符を1つ打ち込む場合、speed=30ならノート1個で済むところ、speed=1だとノート30個が必要です。公式のサウンドエディタではとても打ち込み切れなくなるので、mmlのような記法で楽曲を定義してプログラムで自動的にノートを生成するようにしました。需要があるか不明ですが、これはこれでどこかでまとめようと思ってます。
