というわけで、コントローラーとキー入力で音色とエフェクトを変更できるようになりました。
自分はコードを書くときにコメントつけながら進める派ではありません
(手直しが多いので最初につけたコメントと最終的なコードに齟齬が生じることが多いので😂)
コードが完成した後にAIにコメントを付与してもらうのですが、ビブラートを「バイブレーション」としているのをみて、音楽に対する知見の深さが滲み出ていて好きw
import pyxel
import mido
import time
# Pyxel の初期化
pyxel.init(160, 160, fps=30)
pyxel.NUM_SOUNDS = 4 # 同時発音数を 4 に制限 (チャンネル0〜3を使用)
# 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まで
is_valid_note = True
# Pyxelで有効な範囲外かどうかをチェック
if not (min_pyxel_octave <= pyxel_octave <= max_pyxel_octave):
is_valid_note = False
# Pyxelの有効なオクターブ範囲にクリップ (表示用。実際の再生はis_valid_for_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_note
# Pyxel のサウンドバンクに初期設定 (矩形波、固定スピード)
# speed値は整数である必要がある
# DEFAULT_SPEEDを22050に設定
DEFAULT_SPEED = 22050
INITIAL_VOLUME = "7" # 最大ボリューム
# トーンの種類とエフェクトの種類をリストで定義
TONES = ["0", "1", "2", "3"] # 0:矩形波, 1:疑似三角波, 2:疑似鋸波, 3:正弦波
EFFECTS = ["n", "f", "v"] # n:なし, f:フェードアウト, v:バイブレーション
# フェードアウトのスピード選択肢と名前
# Pyxelのspeedは値が小さいほど「速い」ことに注意
FADE_OUT_SPEEDS = [
{"name": "NORMAL", "speed": 512}, # NORMALを512に設定
{"name": "FAST", "speed": 256} # FASTを256に設定 (SLOWを廃止)
]
class App:
def __init__(self):
self.inport = None
self.playing_notes = {}
self.available_channels = list(range(pyxel.NUM_SOUNDS))
self.channel_to_midi_note = {}
self.current_tone_index = 0
self.current_effect_index = 0
self.current_fade_out_speed_index = 0
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 pyxel.btnp(pyxel.KEY_ESCAPE) or pyxel.btnp(pyxel.GAMEPAD1_BUTTON_START):
pyxel.quit()
# AキーまたはコントローラーのAボタンでトーン変更 (表示はBボタンに)
if pyxel.btnp(pyxel.KEY_A) or pyxel.btnp(pyxel.GAMEPAD1_BUTTON_A):
self.current_tone_index = (self.current_tone_index + 1) % len(TONES)
print(f"Tone changed to: {TONES[self.current_tone_index]}")
# ZキーまたはコントローラーのBボタンでエフェクト変更 (表示はAボタンに)
if pyxel.btnp(pyxel.KEY_Z) or pyxel.btnp(pyxel.GAMEPAD1_BUTTON_B):
self.current_effect_index = (self.current_effect_index + 1) % len(EFFECTS)
print(f"Effect changed to: {EFFECTS[self.current_effect_index]}")
if EFFECTS[self.current_effect_index] != "f":
self.current_fade_out_speed_index = 0
# CキーまたはコントローラーのXボタンでフェードアウトスピード変更 (表示はYボタンに)
if (pyxel.btnp(pyxel.KEY_C) or pyxel.btnp(pyxel.GAMEPAD1_BUTTON_X)) \
and EFFECTS[self.current_effect_index] == "f":
self.current_fade_out_speed_index = (self.current_fade_out_speed_index + 1) % len(FADE_OUT_SPEEDS)
print(f"Fade Out Speed changed to: {FADE_OUT_SPEEDS[self.current_fade_out_speed_index]['name']}")
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_note, is_valid_note = midi_to_pyxel_note(midi_note)
if not is_valid_note:
return
if self.available_channels:
channel_id = self.available_channels.pop(0)
sound_id = channel_id
current_speed_val = DEFAULT_SPEED
if EFFECTS[self.current_effect_index] == "f":
current_speed_val = FADE_OUT_SPEEDS[self.current_fade_out_speed_index]["speed"]
try:
pyxel.sounds[sound_id].set(
notes=pyxel_note,
tones=TONES[self.current_tone_index],
volumes=INITIAL_VOLUME,
effects=EFFECTS[self.current_effect_index],
speed=current_speed_val
)
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, f"Playing MIDI Chords (Up to {pyxel.NUM_SOUNDS} notes)", 7)
current_tone_name = {
"0": "SQUARE", "1": "TRIANGLE", "2": "SAW", "3": "SINE"
}.get(TONES[self.current_tone_index], TONES[self.current_tone_index])
current_effect_name = {
"n": "NONE", "f": "FADE_OUT", "v": "VIBRATO"
}.get(EFFECTS[self.current_effect_index], EFFECTS[self.current_effect_index])
# 色指定
pyxel.text(10, 20, f"Tone: {current_tone_name} (A/GAMEPAD_B)", 10) # 黄色
pyxel.text(10, 30, f"Effect: {current_effect_name} (Z/GAMEPAD_A)", 3) # 緑色
if EFFECTS[self.current_effect_index] == "f":
fade_speed_name = FADE_OUT_SPEEDS[self.current_fade_out_speed_index]["name"]
pyxel.text(10, 40, f"Fade Speed: {fade_speed_name} (C/GAMEPAD_Y)", 3) # 緑色
y_offset = 60
else:
y_offset = 50
for midi_note, info in self.playing_notes.items():
pyxel_note, _ = midi_to_pyxel_note(midi_note)
pyxel.text(10, y_offset, f"Note: {pyxel_note} (MIDI: {midi_note}) on Channel {info['channel']}", 6)
y_offset += 8
if __name__ == "__main__":
App()