4和音まで鳴らせるようになり、B4以上の音程を出そうとするとバグっていたので、B4以上の音程は無視するようにしました。
鍵盤を離しても他の音が切れないようにもしてみました。
これで基本的な音鳴らしはできたかな と思います。
import pyxel
import mido
import time
# Pyxel の初期化
pyxel.init(228, 64, fps=30)
# 同時発音数を 4 に設定 (チャンネル0〜3を使用)
pyxel.NUM_SOUNDS = 4
# MIDI ノート番号から Pyxel のノート文字列を生成する関数
# 返り値: (pyxel_note_string, is_valid_note_for_pyxel)
def midi_to_pyxel_note(midi_note):
note_names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
# MIDIノート60 (C4) が PyxelのC2 になるように調整
# MIDIノート36 (C2) が PyxelのC0 となるようにオフセットを調整する
pyxel_octave = (midi_note - 36) // 12
# ノート名 (C, C#, D, ...) のインデックス
note_index = midi_note % 12
# Pyxelの有効なオクターブ範囲 (0-4)
min_pyxel_octave = 0
max_pyxel_octave = 4 # B4が最高音なので、オクターブ4まで
# Pyxelで有効な範囲外かどうかをチェック
is_valid_for_pyxel = True
if not (min_pyxel_octave <= pyxel_octave <= max_pyxel_octave):
is_valid_for_pyxel = False
# Pyxelの有効なオクターブ範囲にクリップ (表示用。実際の再生はis_valid_for_pyxelで制御)
# これにより、有効範囲外の音が来てもエラーにならず、最も近いPyxelの有効音にマッピングされる
pyxel_octave = max(min_pyxel_octave, min(max_pyxel_octave, pyxel_octave))
result_note = f"{note_names[note_index]}{pyxel_octave}"
return result_note, is_valid_for_pyxel
# Pyxel のサウンドバンクに初期設定 (矩形波、固定スピード)
fixed_speed = 22050 // 10 # 適当な固定値
initial_tones = "0" # 矩形波
initial_effects = "n" # エフェクトなし
initial_volume = "7" # 最大ボリューム
class App:
def __init__(self):
self.inport = None
self.playing_notes = {}
# available_channelsはNUM_SOUNDSまで確保
self.available_channels = list(range(pyxel.NUM_SOUNDS))
self.channel_to_midi_note = {}
try:
input_ports = mido.get_input_names()
if input_ports:
self.inport = mido.open_input(input_ports[0])
print(f"MIDIデバイス '{self.inport.name}' に接続しました。")
else:
print("利用可能なMIDI入力デバイスが見つかりませんでした。")
pyxel.run(self.update, self.draw)
except OSError as e:
print(f"MIDIデバイスのオープン中にエラーが発生しました: {e}\n利用可能なデバイス: {mido.get_input_names()}")
except Exception as e:
print(f"エラーが発生しました: {e}")
finally:
if self.inport:
self.inport.close()
def update(self):
if self.inport:
received_message = self.inport.poll()
if received_message:
if received_message.type == 'note_on':
if received_message.velocity > 0:
midi_note = received_message.note
if midi_note not in self.playing_notes: # 既に再生中のノートでなければ
# Pyxelノート文字列と有効性フラグを取得
pyxel_note, is_valid_note = midi_to_pyxel_note(midi_note)
# Pyxelの有効なノート範囲外なら無視して処理を中断
if not is_valid_note:
return
if self.available_channels: # 空いているチャンネルがあるか
channel_id = self.available_channels.pop(0) # 最初の空いているチャンネルを取得
sound_id = channel_id # サウンドIDはチャンネルIDと同じにする
try:
# Pyxelサウンドにノート情報をセット
pyxel.sounds[sound_id].set(
notes=pyxel_note,
tones=initial_tones,
volumes=initial_volume,
effects=initial_effects,
speed=fixed_speed
)
pyxel.play(channel_id, sound_id) # 指定チャンネルで再生
self.playing_notes[midi_note] = {'channel': channel_id, 'sound_id': sound_id}
self.channel_to_midi_note[channel_id] = midi_note
except ValueError as e:
print(f"ValueError during sound set: {e}, pyxel_note: {pyxel_note}, midi_note: {midi_note}")
except Exception as e:
print(f"Unexpected error during sound set/play: {e}")
else:
print(f"No available channels for MIDI note {midi_note}. Max {pyxel.NUM_SOUNDS} notes playing.")
elif received_message.type == 'note_off':
midi_note = received_message.note
if midi_note in self.playing_notes:
channel_id = self.playing_notes[midi_note]['channel']
pyxel.stop(channel_id) # 対応するチャンネルの再生を停止
# チャンネルを空きリストに戻す
self.available_channels.append(channel_id)
self.available_channels.sort() # 昇順にソートして再利用
del self.playing_notes[midi_note]
del self.channel_to_midi_note[channel_id]
def draw(self):
pyxel.cls(0)
pyxel.text(10, 10, "Playing MIDI Chords (Up to {} notes)".format(pyxel.NUM_SOUNDS), 7)
# 現在再生中のノートとチャンネルを表示
y_offset = 30
for midi_note, info in self.playing_notes.items():
pyxel_note, _ = midi_to_pyxel_note(midi_note) # drawでは有効性チェックは不要なので捨てる
pyxel.text(10, y_offset, f"Note: {pyxel_note} (MIDI: {midi_note}) on Channel {info['channel']}", 6)
y_offset += 8
if __name__ == "__main__":
App()