0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

USB-MIDIキーボードでpyxelの音を鳴らしてみるテスト その2

Last updated at Posted at 2025-05-20

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()
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?