1
3

Pythonで作る8ビット風ゲーム音楽ジェネレーター:スペースシューター編

Posted at

はじめに

レトロゲームの魅力の一つである8ビットサウンド。そのノスタルジックな音色は、多くのゲーマーの心を掴んで離しません。今回は、Pythonを使って8ビット風のゲーム音楽を生成する方法を紹介します。特に、スペースシューターゲームをイメージした約45秒の曲を作成していきます。

image.png

要件と仕様

このプロジェクトを始める前に、明確な要件と仕様を定義しました。これにより、目的に沿った実装が可能になり、また読者の皆さんにもプロジェクトの意図をより良く理解していただけると思います。

要件:

image.png

  1. Pythonを使用して、8ビット風の音楽を生成すること。
  2. スペースシューターゲームに適した雰囲気の音楽を作ること。
  3. 楽曲の長さは約45秒とし、ゲームの進行に合わせたストーリー性を持たせること。
  4. 生成した音楽をMP3形式で保存できること。
  5. コードは再利用可能で、カスタマイズしやすい構造にすること。

仕様:

  1. 音声合成:

    • 矩形波を使用して8ビット風の音を生成する。
    • ADSRエンベロープを適用して、音の立ち上がりと減衰を自然にする。
  2. 音楽構成:

    • イントロ(宇宙船の出発):約8秒
    • メイン戦闘シーン:約16秒
    • ボス登場のジングル:約4秒
    • ボス戦:約12秒
    • エンディング(勝利のファンファーレ):約5秒
  3. 使用音階:

    • C3からG6までの音符を使用可能とする。
  4. テンポ:

    • 基本テンポを150 BPMとする。
  5. トラック構成:

    • メロディトラックとベースライントラックの2トラックを使用。
  6. 出力形式:

    • 44.1kHz、16ビット、ステレオのMP3ファイル。
  7. ライブラリ:

    • numpy: 数値計算用
    • scipy: 信号処理用
    • pydub: オーディオセグメント操作用
    • simpleaudio: オーディオ再生用

これらの要件と仕様に基づいて実装を行いました。では、具体的な実装の詳細を見ていきましょう。

必要な環境

まず、以下のライブラリをインストールする必要があります:

pip install numpy scipy pydub simpleaudio

実装の解説

1. 音符と周波数の定義

まず、音符とその周波数を定義します。これにより、音楽理論の知識がなくても、直感的に曲を作ることができます。

NOTES = {
    'C3': 130.81, 'C#3': 138.59, 'D3': 146.83,
    # ... 中略 ...
    'F#6': 1479.98, 'G6': 1567.98
}

2. 波形の生成

8ビット風の音を作るために、矩形波を使用します。

def create_square_wave(frequency, duration, duty_cycle=0.5, sample_rate=44100):
    t = np.linspace(0, duration, int(sample_rate * duration), False)
    wave = signal.square(2 * np.pi * frequency * t, duty=duty_cycle)
    return wave

3. エンベロープの適用

音の立ち上がりと減衰を自然にするために、ADSR(Attack, Decay, Sustain, Release)エンベロープを適用します。

def apply_envelope(wave, attack=0.01, decay=0.1, sustain=0.7, release=0.1):
    # ... エンベロープの計算と適用 ...

4. メロディの作成

メロディは音符と長さのペアのリストとして定義します。ここでは、ゲームの各シーンに合わせてメロディを構成しています。

intro_melody = [
    ('C5', 0.25), ('E5', 0.25), ('G5', 0.25), ('C6', 0.25),
    ('B5', 0.5), ('G5', 0.5),
    # ... 続く ...
]

# メイン戦闘シーン、ボス登場ジングル、ボス戦、エンディングも同様に定義

5. ベースラインの追加

メロディに深みを加えるために、シンプルなベースラインを追加します。

def create_bass_line(tempo=120):
    bass_pattern = [
        ('C3', 0.5), ('G3', 0.5), ('C4', 0.5), ('G3', 0.5),
        # ... 続く ...
    ]
    return create_melody(bass_pattern, tempo)

6. トラックのミキシング

メロディとベースラインを組み合わせて、最終的な音楽トラックを作成します。

def mix_tracks(*tracks):
    mixed = AudioSegment.silent(duration=max(track.duration_seconds for track in tracks) * 1000)
    for track in tracks:
        mixed = mixed.overlay(track)
    return mixed

使用方法

完全な実装を space_shooter_music_generator.py として保存し、以下のように実行します:

python space_shooter_music_generator.py

実行すると、約45秒のスペースシューターゲーム風の音楽が生成され、再生された後、MP3ファイルとして保存されます。

コードを表示
import numpy as np
from scipy import signal
from pydub import AudioSegment
import simpleaudio as sa

# 音符と周波数の定義(E6まで拡張)
NOTES = {
    'C3': 130.81, 'C#3': 138.59, 'D3': 146.83, 'D#3': 155.56, 'E3': 164.81, 'F3': 174.61,
    'F#3': 185.00, 'G3': 196.00, 'G#3': 207.65, 'A3': 220.00, 'A#3': 233.08, 'B3': 246.94,
    'C4': 261.63, 'C#4': 277.18, 'D4': 293.66, 'D#4': 311.13, 'E4': 329.63, 'F4': 349.23,
    'F#4': 369.99, 'G4': 392.00, 'G#4': 415.30, 'A4': 440.00, 'A#4': 466.16, 'B4': 493.88,
    'C5': 523.25, 'C#5': 554.37, 'D5': 587.33, 'D#5': 622.25, 'E5': 659.25, 'F5': 698.46,
    'F#5': 739.99, 'G5': 783.99, 'G#5': 830.61, 'A5': 880.00, 'A#5': 932.33, 'B5': 987.77,
    'C6': 1046.50, 'C#6': 1108.73, 'D6': 1174.66, 'D#6': 1244.51, 'E6': 1318.51, 'F6': 1396.91,
    'F#6': 1479.98, 'G6': 1567.98
}

def create_square_wave(frequency, duration, duty_cycle=0.5, sample_rate=44100):
    t = np.linspace(0, duration, int(sample_rate * duration), False)
    wave = signal.square(2 * np.pi * frequency * t, duty=duty_cycle)
    return wave

def apply_envelope(wave, attack=0.01, decay=0.1, sustain=0.7, release=0.1):
    total_length = len(wave)
    attack_length = int(attack * total_length)
    decay_length = int(decay * total_length)
    release_length = int(release * total_length)
    sustain_length = total_length - attack_length - decay_length - release_length
    
    envelope = np.concatenate([
        np.linspace(0, 1, attack_length),
        np.linspace(1, sustain, decay_length),
        np.ones(sustain_length) * sustain,
        np.linspace(sustain, 0, release_length)
    ])
    
    return wave * envelope

def create_tone(note, duration, volume=-20, duty_cycle=0.5):
    sample_rate = 44100
    try:
        frequency = NOTES[note]
    except KeyError:
        print(f"警告: 未定義の音符 '{note}' です。C4を代わりに使用します。")
        frequency = NOTES['C4']  # デフォルトの音符として C4 を使用
    
    wave = create_square_wave(frequency, duration, duty_cycle, sample_rate)
    wave = apply_envelope(wave)
    wave = np.int16(wave * 32767 * 10**(volume/20))
    return AudioSegment(wave.tobytes(), frame_rate=sample_rate, sample_width=2, channels=1)

def create_melody(notes, tempo=120):
    melody = AudioSegment.silent(duration=0)
    beat_duration = 60 / tempo
    
    for note, duration in notes:
        if note == 'R':
            tone = AudioSegment.silent(duration=int(duration * beat_duration * 1000))
        else:
            tone = create_tone(note, duration * beat_duration)
        melody += tone
    
    return melody

# イントロ部分(宇宙船の出発)
intro_melody = [
    ('C5', 0.25), ('E5', 0.25), ('G5', 0.25), ('C6', 0.25),
    ('B5', 0.5), ('G5', 0.5),
    ('A5', 0.25), ('F5', 0.25), ('D5', 0.25), ('B4', 0.25),
    ('G4', 0.5), ('C5', 0.5),
] * 2

# メイン戦闘シーン
battle_melody = [
    ('D5', 0.25), ('F5', 0.25), ('A5', 0.25), ('C6', 0.25),
    ('B5', 0.5), ('G5', 0.5),
    ('A5', 0.25), ('F5', 0.25), ('D5', 0.25), ('B4', 0.25),
    ('C5', 0.5), ('E5', 0.5),
] * 4

# ボス登場のジングル
boss_jingle = [
    ('C5', 0.25), ('C5', 0.25), ('C5', 0.25), ('R', 0.25),
    ('G4', 0.5), ('G4', 0.5),
    ('C5', 0.25), ('B4', 0.25), ('A4', 0.25), ('G4', 0.25),
    ('F4', 0.5), ('R', 0.25), ('F5', 0.75),
]

# ボス戦
boss_battle = [
    ('C6', 0.25), ('A5', 0.25), ('F5', 0.25), ('C5', 0.25),
    ('D5', 0.5), ('G5', 0.5),
    ('B5', 0.25), ('G5', 0.25), ('E5', 0.25), ('C5', 0.25),
    ('A4', 0.5), ('D5', 0.5),
] * 3

# エンディング(勝利のファンファーレ)
ending = [
    ('C5', 0.5), ('E5', 0.5), ('G5', 0.5), ('C6', 0.5),
    ('G5', 0.5), ('C6', 0.5), ('E6', 1.0),
    ('C6', 0.25), ('C6', 0.25), ('C6', 0.25), ('C6', 0.25),
    ('C6', 1.0), ('R', 1.0)
]

# 完全なメロディ
full_melody = intro_melody + battle_melody + boss_jingle + boss_battle + ending

def create_bass_line(tempo=120):
    bass_pattern = [
        ('C3', 0.5), ('G3', 0.5), ('C4', 0.5), ('G3', 0.5),
        ('A3', 0.5), ('E3', 0.5), ('A3', 0.5), ('E3', 0.5),
        ('F3', 0.5), ('C3', 0.5), ('F3', 0.5), ('C3', 0.5),
        ('G3', 0.5), ('D3', 0.5), ('G3', 0.5), ('D3', 0.5),
    ] * 6  # 6回繰り返して約45秒の長さにする
    return create_melody(bass_pattern, tempo)

def mix_tracks(*tracks):
    mixed = AudioSegment.silent(duration=max(track.duration_seconds for track in tracks) * 1000)
    for track in tracks:
        mixed = mixed.overlay(track)
    return mixed

def play_audio(audio):
    play_obj = sa.play_buffer(
        audio.raw_data,
        num_channels=audio.channels,
        bytes_per_sample=audio.sample_width,
        sample_rate=audio.frame_rate
    )
    play_obj.wait_done()

def save_audio_as_mp3(audio, filename):
    audio.export(filename, format="mp3")

def main():
    print("拡張版スペースシューターゲーム風の音楽を生成します...")
    
    tempo = 150
    
    # メロディの生成
    melody = create_melody(full_melody, tempo=tempo)
    
    # ベースラインの生成
    bass_line = create_bass_line(tempo=tempo)
    
    # メロディとベースラインをミックス
    final_track = mix_tracks(melody, bass_line)
    
    # 音量調整
    final_track = final_track - 3  # 全体の音量を少し下げる
    
    print("スペースシューターゲームの音楽を再生します...")
    play_audio(final_track)
    
    print("音楽をMP3ファイルとして保存します...")
    save_audio_as_mp3(final_track, "extended_space_shooter_music.mp3")
    
    print("処理が完了しました。extended_space_shooter_music.mp3が作成されました。")

if __name__ == "__main__":
    main()

設計

クラス図

クラス概要

クラス名 説明
Note 音符の基本情報を表現
WaveGenerator 異なる種類の波形を生成
Envelope ADSRエンベロープを適用
Melody メロディを作成・管理
Bassline ベースラインを作成・管理
Mixer 複数の音楽トラックを混合

クラス詳細

Note

属性/メソッド 説明
frequency float 音の高さ(周波数)
duration float 音の長さ

WaveGenerator

メソッド 説明
generate_square_wave() 矩形波を生成
generate_triangle_wave() 三角波を生成
generate_sawtooth_wave() のこぎり波を生成

Envelope

属性/メソッド 型/説明
attack float(音の立ち上がり時間)
decay float(最大音量から持続音量までの減衰時間)
sustain float(持続音量のレベル)
release float(キーを離してから音が消えるまでの時間)
apply_envelope(wave) 与えられた波形にエンベロープを適用

Melody

属性/メソッド 型/説明
notes List[Note](メロディを構成する音符のリスト)
create_melody() 音符のリストからメロディを生成

Bassline

属性/メソッド 型/説明
pattern List[Note](ベースラインのパターンを表す音符のリスト)
create_bassline() パターンからベースラインを生成

Mixer

属性/メソッド 型/説明
tracks List[AudioSegment](混合する音楽トラックのリスト)
add_track(track) ミキサーに新しいトラックを追加
mix() すべてのトラックを混合して最終的な音楽を生成

シーケンス図

発展の可能性

このコードをベースに、さまざまな拡張が可能です:

  1. 異なるゲームジャンル向けの音楽生成
  2. UIを追加して、インタラクティブな音楽作成ツールの開発
  3. 機械学習を使用した自動作曲機能の実装
  4. リアルタイムでのゲーム状況に応じた動的な音楽生成

まとめ

image.png

Pythonを使って8ビット風のゲーム音楽を生成する方法を紹介しました。明確な要件と仕様に基づいて実装することで、目的に沿った効果的な音楽生成が可能になりました。この手法を使えば、レトロゲーム風の音楽を手軽に作成できます。ぜひ自分好みにアレンジして、オリジナルの8ビットサウンドを作ってみてください!

作成した音楽

1
3
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
1
3