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?

【アプリ開発】pythonで音楽生成する

Posted at

はじめに

身内に作曲をしている人がいるので、その作曲のたたき台として音楽生成アプリがあったらおもしろそうだなと思い、アプリの開発をしてみました。

要件定義~設計等について

個人開発の一環としてやりたいので、完全無料で生成できるアプリを作成します。
ボタンを押したら適当な音楽が生成されて、ファイルに保管されていくようなイメージです。
基本的にはpythonとVue.jsでの作成を行うようにしました。
フロントのイメージは最近よく見るネオン系の看板や広告のイメージで作成しています。

内容

requirements.txt

fastapi
uvicorn[standard]
midiutil
music21
pydub

今回はこのフレームワーク等をもとにアプリを作成しています。
fastapi-uvicorn間でバックエンド大本を作成・起動させて、midiutil-music21で音楽生成の主機能を担当しているようなイメージになります。
音楽生成については、もともとはmingusを使用して作成予定でしたが、音楽生成時のエラー(module error)が何を行っても解消できず、最終的にmidoというライブラリへ切り替えをしていった形になります。

backend

main.py
# 各種import項目
import os
import time
import random
import mido 

from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse, FileResponse
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles

from pydantic import BaseModel
from typing import Dict, List, Any

# 各トラックの設定を保持するモデル
class TrackConfig(BaseModel):
    instrument: int = 0  # MIDIプログラム番号 (0-127)
    volume: int = 80     # ボリューム (0-127)
    is_melody: bool = False # このトラックでメロディを生成するか
    is_chord: bool = False  # このトラックでコードを生成するか
    is_percussion: bool = False # このトラックでパーカッションを生成するか

# 全体の音楽設定を保持するモデル
class MusicConfig(BaseModel):
    tempo: int = 80
    duration_seconds: int = 30
    key: str = "C"
    scale: str = "major"
    tracks: List[TrackConfig] # 複数トラックの設定リスト (必須)

app = FastAPI()

# CORS ミドルウェアの設定
origins = [
    "http://localhost",
    "http://localhost:8000",
    "http://127.0.0.1",
    "http://127.0.0.1:8000",
    "http://localhost:5173", # UI
    "http://127.0.0.1:5173", 
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# MIDIファイルを配信するための静的ファイル設定
app.mount("/audio", StaticFiles(directory="audio"), name="audio")

# audioディレクトリの作成
@app.on_event("startup")
async def startup_event():
    os.makedirs("audio", exist_ok=True)

@app.get("/")
async def read_root():
    return {"message": "Hello from FastAPI backend!"}

@app.post("/api/generate")
async def generate_music(config: MusicConfig):
    try:
        tempo = config.tempo
        duration_seconds = config.duration_seconds
        key = config.key.upper()
        scale_type = config.scale.lower()
        
        # ログに受け取ったトラック設定を出力
        print(f"DEBUG: Received request: Key={key}, Scale={scale_type}, Tempo={tempo}, Duration={duration_seconds}, Tracks={len(config.tracks)}")
        for i, track_cfg in enumerate(config.tracks):
            print(f"DEBUG:   Track {i}: Instrument={track_cfg.instrument}, Volume={track_cfg.volume}, Melody={track_cfg.is_melody}, Chord={track_cfg.is_chord}, Percussion={track_cfg.is_percussion}")

        # --- 音楽的定義データ ---
        key_root_midi_map = {
            "C": 60, "C#": 61, "D": 62, "D#": 63, "E": 64, "F": 65,
            "F#": 66, "G": 67, "G#": 68, "A": 69, "A#": 70, "B": 71
        }
        base_root_midi = key_root_midi_map.get(key, 60)
        print(f"DEBUG: Key: {key}, Base Root MIDI: {base_root_midi}")

        scales: Dict[str, List[int]] = {
            "major": [0, 2, 4, 5, 7, 9, 11],
            "minor": [0, 2, 3, 5, 7, 8, 10],
            "pentatonic": [0, 2, 4, 7, 9],
        }
        current_scale_intervals = scales.get(scale_type, scales["major"])
        if not current_scale_intervals:
            print(f"ERROR: Scale intervals list is empty for scale_type: {scale_type}")
            raise ValueError(f"Selected scale '{scale_type}' has no defined intervals.")
        print(f"DEBUG: Scale Type: {scale_type}, Intervals: {current_scale_intervals}")

        chord_patterns: Dict[str, Dict[str, List[int]]] = {
            "major": {
                "I": [0, 4, 7],      # C-E-G (C major)
                "II": [2, 5, 9],     # D-F-A (D minor)
                "III": [4, 7, 11],   # E-G-B (E minor)
                "IV": [5, 9, 12],    # F-A-C (F major) - 12はオクターブ上のC
                "V": [7, 11, 14],    # G-B-D (G major) - 14はオクターブ上のD
                "VI": [9, 12, 16],   # A-C-E (A minor) - 12はオクターブ上のC, 16はオクターブ上のE
                "VII": [11, 14, 17]  # B-D-F (B diminished)
            },
            "minor": { # 簡易的なマイナーコードパターン
                "I": [0, 3, 7],      # C-Eb-G (C minor)
                "IV": [5, 8, 12],    # F-Ab-C (F minor)
                "V": [7, 10, 14],    # G-Bb-D (G minor)
            }
        }
        current_chord_patterns = chord_patterns.get(scale_type, chord_patterns["major"])
        print(f"DEBUG: Chord Patterns for {scale_type}: {current_chord_patterns}")

        desired_chord_names_for_progression = ["I", "IV", "V"]
        available_chord_progression_names = []
        for chord_name in desired_chord_names_for_progression:
            if chord_name in current_chord_patterns:
                available_chord_progression_names.append(chord_name)

        if not available_chord_progression_names:
            print(f"ERROR: No valid chords found in patterns for scale '{scale_type}' among {desired_chord_names_for_progression}.")
            if "I" in current_chord_patterns:
                available_chord_progression_names = ["I"]
                print("DEBUG: Falling back to only 'I' chord for progression.")
            else:
                raise ValueError(f"No valid chords found for the selected scale '{scale_type}'. Cannot form a progression.")
        print(f"DEBUG: Available Chord Progression Names: {available_chord_progression_names}")

        # --- MIDIファイルの準備 (mido版) ---
        mid = mido.MidiFile(type=1) # type=1 はマルチトラック用

        # 各トラックを準備
        midi_tracks: List[mido.MidiTrack] = []
        for i, track_config in enumerate(config.tracks):
            track = mido.MidiTrack()
            mid.tracks.append(track)
            midi_tracks.append(track)
            
            # MIDIチャンネルはトラックのインデックスを基本とする (0-indexed for mido messages)
            # ただし、パーカッションは慣例的にチャンネル10 (0-indexedでは9)
            channel = i # デフォルトはトラックのインデックス
            if track_config.is_percussion:
                channel = 9 # MIDIチャンネル10 (ドラムチャンネル)
            
            # 楽器のセット (Program Change)
            track.append(mido.Message('program_change', program=track_config.instrument, channel=channel, time=0))
            # ボリューム設定 (Control Changeメッセージ)
            track.append(mido.Message('control_change', control=7, value=track_config.volume, channel=channel, time=0))
            
            print(f"DEBUG: Track {i} setup: Instrument={track_config.instrument}, Channel={channel+1}, Volume={track_config.volume}, Melody={track_config.is_melody}, Chord={track_config.is_chord}, Percussion={track_config.is_percussion}")

        # テンポの設定 (最初のトラックにのみ設定 - これはメタメッセージなのでどのトラックでも良いが、通常は最初のトラック)
        mpqn = mido.bpm2tempo(tempo)
        # mid.tracks[0] が存在することを確認
        if mid.tracks:
            mid.tracks[0].append(mido.MetaMessage('set_tempo', tempo=mpqn, time=0))
        else:
            print("WARNING: No tracks initialized to set tempo. This should not happen.")

        total_beats = duration_seconds * (tempo / 60)
        beats_per_bar = 4 # 1小節あたりの拍数 (4/4拍子を想定)

        if total_beats <= 0:
            print(f"ERROR: total_beats is non-positive: {total_beats}. Adjusting to minimum.")
            total_beats = 1 

        # midoでの時間管理のための変数
        # time_pos_beats: 現在の論理的な拍数(float)
        # last_event_abs_tick: 最後にMIDIメッセージが追加された時点での絶対tick数 (全トラックで共有)
        time_pos_beats = 0.0  
        last_event_abs_tick = 0 

        # active_notes: 現在オンになっているノートとそのオフ予定時刻を管理 { (midi_note, channel): (on_time_beats, duration_beats) }
        active_notes: Dict[tuple[int, int], tuple[float, float]] = {}

        # ヘルパー関数:拍数を mido のtick数に変換
        def beats_to_ticks(beats, ticks_per_beat=mid.ticks_per_beat):
            return int(round(beats * ticks_per_beat))

        # パーカッションのMIDIノートマッピング
        percussion_map = {
            "kick": 36,          # バスドラム (Bass Drum 1)
            "snare_acoustic": 38, # アコースティックスネア (Acoustic Snare)
            "hihat_closed": 42,  # クローズドハイハット (Closed Hi-Hat)
            "hihat_open": 46,    # オープンハイハット (Open Hi-Hat)
            "cymbal_crash": 49,  # クラッシュシンバル (Crash Cymbal 1)
            "ride_cymbal": 51,   # ライドシンバル (Ride Cymbal 1)
        }

        # 音楽生成ループ
        # 最小の時間単位でループを進める (例: 16分音符 = 0.25拍)
        # これにより、複数のトラックでイベントが同時に発生しても適切に処理できる
        time_step_beats = 0.25 # 16分音符の単位で時間を進める

        while time_pos_beats < total_beats + time_step_beats: 
            # --- ノートオフの処理 ---
            current_abs_tick = beats_to_ticks(time_pos_beats)
            
            notes_to_turn_off_with_channel = []
            # active_notes のキーをタプル (note, channel) に変更したので、それでループ
            for (note, channel), (on_time, duration) in active_notes.items():
                if on_time + duration <= time_pos_beats + 0.0001: # わずかな誤差を許容
                    notes_to_turn_off_with_channel.append(((note, channel), on_time + duration)) # オフ予定時刻も記録

            # オフ予定時刻が早いものから順に処理するためにソート (同時間の場合は順不同)
            notes_to_turn_off_with_channel.sort(key=lambda x: x[1]) 

            for (note, channel), _ in notes_to_turn_off_with_channel:
                delta_ticks = current_abs_tick - last_event_abs_tick
                delta_ticks = max(0, delta_ticks)

                # 該当するトラックを見つけてメッセージを追加
                # MIDIチャンネルは 0-15 の範囲
                if 0 <= channel < len(midi_tracks):
                    midi_tracks[channel].append(mido.Message('note_off', note=note, velocity=64, channel=channel, time=delta_ticks))
                    print(f"DEBUG: Turning off note: {note} on channel {channel+1} at time_pos_beats: {time_pos_beats:.2f} (delta_ticks: {delta_ticks})")
                    del active_notes[(note, channel)] # タプルキーで削除
                    last_event_abs_tick = current_abs_tick # 最後のイベント時刻を更新
                else:
                    print(f"WARNING: Invalid channel {channel+1} for note_off message. Note {note} not turned off.")
            
            # --- 音楽生成ロジック (コード、メロディ、パーカッション) ---
            # 各トラックの役割に応じてノートを生成
            for i, track_config in enumerate(config.tracks):
                # MIDIチャンネルはトラックのインデックスを基本とするが、パーカッションはチャンネル9に固定
                channel = i # 通常の楽器トラックのチャンネル
                if track_config.is_percussion:
                    channel = 9 # ドラムチャンネル

                # 各イベント生成前の絶対tickを再計算 (time_pos_beatsが更新されている可能性のため)
                current_abs_tick = beats_to_ticks(time_pos_beats) 

                # コード生成
                if track_config.is_chord and abs(time_pos_beats % beats_per_bar) < 0.001:
                    print(f"DEBUG: Triggering Chord Generation for Track {i} at time_pos: {time_pos_beats:.2f}")
                    chord_root_name = random.choice(available_chord_progression_names)
                    chord_intervals = current_chord_patterns.get(chord_root_name)
                    
                    if chord_intervals is None:
                        print(f"ERROR: Chord pattern for '{chord_root_name}' not found for scale '{scale_type}'.")
                        continue # このトラックのコード生成はスキップ

                    chord_base_midi = base_root_midi - 12 # 1オクターブ下げる
                    chord_duration_beats = beats_per_bar 

                    for interval in chord_intervals:
                        note = chord_base_midi + interval
                        if 0 <= note <= 127:
                            # デルタタイム計算は、イベントが追加される直前の last_event_abs_tick との差
                            # 同じ time_pos_beats で複数のノートオンがある場合、最初のノートオンにのみデルタタイムが適用されるように
                            delta_ticks = current_abs_tick - last_event_abs_tick
                            delta_ticks = max(0, delta_ticks) 

                            midi_tracks[i].append(mido.Message('note_on', note=note, velocity=track_config.volume, channel=channel, time=delta_ticks))
                            active_notes[(note, channel)] = (time_pos_beats, chord_duration_beats)
                            print(f"DEBUG: Adding Chord Note ON (Track {i}, Channel {channel+1}): note={note}, time_pos_beats={time_pos_beats:.2f}, duration={chord_duration_beats} (delta_ticks: {delta_ticks})")
                            last_event_abs_tick = current_abs_tick # 最後のイベント時刻を更新
                        else:
                            print(f"WARNING: Skipping chord note {note} out of range on Track {i} at time {time_pos_beats:.2f}.")
                
                # メロディ生成
                if track_config.is_melody:
                    melody_note_duration_beats = random.choice([0.25, 0.5, 1.0]) # メロディの長さにバリエーション
                    
                    if random.random() < 0.2: # 20%の確率で休符
                        pass 
                    else:
                        random_interval = random.choice(current_scale_intervals)
                        octave_offset_choices = [-12, 0, 12]
                        random_octave_offset = random.choice(octave_offset_choices)

                        melody_note = base_root_midi + random_interval + random_octave_offset
                        melody_note = max(36, min(84, melody_note)) # MIDIノートの範囲を制限
                        
                        delta_ticks = current_abs_tick - last_event_abs_tick
                        delta_ticks = max(0, delta_ticks) 

                        midi_tracks[i].append(mido.Message('note_on', note=melody_note, velocity=track_config.volume, channel=channel, time=delta_ticks))
                        active_notes[(melody_note, channel)] = (time_pos_beats, melody_note_duration_beats)
                        print(f"DEBUG: Adding Melody Note ON (Track {i}, Channel {channel+1}): note={melody_note}, time_pos_beats={time_pos_beats:.2f}, duration={melody_note_duration_beats} (delta_ticks: {delta_ticks})")
                        last_event_abs_tick = current_abs_tick # 最後のイベント時刻を更新

                # パーカッション生成
                if track_config.is_percussion:
                    # シンプルなランダムパーカッションパターン(時間単位で確率的に発生)
                    # time_step_beats と連動して調整すると良い
                    if time_step_beats == 0.25: # 16分音符単位で処理している場合
                        if random.random() < 0.6: # キック
                            note = percussion_map["kick"]
                            duration = 0.25 
                            delta_ticks = current_abs_tick - last_event_abs_tick
                            delta_ticks = max(0, delta_ticks)
                            midi_tracks[i].append(mido.Message('note_on', note=note, velocity=track_config.volume, channel=channel, time=delta_ticks))
                            active_notes[(note, channel)] = (time_pos_beats, duration)
                            print(f"DEBUG: Adding Percussion (Kick) (Track {i}, Channel {channel+1}): note={note}, time_pos_beats={time_pos_beats:.2f} (delta_ticks: {delta_ticks})")
                            last_event_abs_tick = current_abs_tick

                        # 2拍目、4拍目でスネアを鳴らす(簡易的な2/4拍子感)
                        if abs(time_pos_beats % beats_per_bar - 1.0) < 0.001 or abs(time_pos_beats % beats_per_bar - 3.0) < 0.001:
                            if random.random() < 0.8: # 高い確率でスネア
                                note = percussion_map["snare_acoustic"]
                                duration = 0.25
                                delta_ticks = current_abs_tick - last_event_abs_tick
                                delta_ticks = max(0, delta_ticks)
                                midi_tracks[i].append(mido.Message('note_on', note=note, velocity=track_config.volume, channel=channel, time=delta_ticks))
                                active_notes[(note, channel)] = (time_pos_beats, duration)
                                print(f"DEBUG: Adding Percussion (Snare) (Track {i}, Channel {channel+1}): note={note}, time_pos_beats={time_pos_beats:.2f} (delta_ticks: {delta_ticks})")
                                last_event_abs_tick = current_abs_tick

                        # ハイハットは常に鳴らすようなイメージ
                        if random.random() < 0.9: 
                            note = percussion_map["hihat_closed"]
                            duration = 0.125 # 32分音符
                            delta_ticks = current_abs_tick - last_event_abs_tick
                            delta_ticks = max(0, delta_ticks)
                            midi_tracks[i].append(mido.Message('note_on', note=note, velocity=track_config.volume, channel=channel, time=delta_ticks))
                            active_notes[(note, channel)] = (time_pos_beats, duration)
                            print(f"DEBUG: Adding Percussion (HiHat) (Track {i}, Channel {channel+1}): note={note}, time_pos_beats={time_pos_beats:.2f} (delta_ticks: {delta_ticks})")
                            last_event_abs_tick = current_abs_tick

            # 時間を進める (全トラック共通で進める)
            time_pos_beats += time_step_beats

        # ループ終了後、まだオンになっているすべてのノートをオフにする
        final_abs_tick = beats_to_ticks(time_pos_beats)
        for (note, channel) in list(active_notes.keys()): # active_notesをイテレート中に変更されないようlist()でコピー
            delta_ticks = final_abs_tick - last_event_abs_tick
            delta_ticks = max(0, delta_ticks) # デルタタイムが負になる場合は0にクランプ

            if 0 <= channel < len(midi_tracks):
                midi_tracks[channel].append(mido.Message('note_off', note=note, velocity=64, channel=channel, time=delta_ticks))
                print(f"DEBUG: Turning off final note: {note} on channel {channel+1} (delta_ticks: {delta_ticks})")
                del active_notes[(note, channel)] 
                last_event_abs_tick = final_abs_tick # 最後のイベント時刻を更新
            else:
                print(f"WARNING: Invalid channel {channel+1} for final note_off message. Note {note} not turned off.")

        # --- ファイル保存 ---
        print(f"DEBUG: Loop finished. Final time_pos_beats: {time_pos_beats:.2f}, total_beats: {total_beats:.2f}. Attempting to write MIDI file.")
        
        timestamp = int(time.time())
        midi_filename = f"music_{timestamp}.mid"

        output_dir = "audio"
        os.makedirs(output_dir, exist_ok=True)
        midi_filepath = os.path.join(output_dir, midi_filename)

        mid.save(midi_filepath) # midoのsaveメソッドで直接ファイルに書き込む

        print(f"DEBUG: MIDI file successfully written to {midi_filepath}")

        generation_params = {
            "key": key,
            "scale": scale_type,
            "tempo": tempo,
            "duration_seconds": duration_seconds,
            "filename": midi_filename,
            "tracks": [t.dict() for t in config.tracks] # 生成パラメータにトラック設定も含む
        }

        final_audio_url = f"/audio/{midi_filename}"

        print(f"MIDIファイルが生成されました: {midi_filepath}")
        print(f"生成パラメータ: {generation_params}")

        return JSONResponse(
            content={"audio_url": final_audio_url, "message": "音楽が正常に生成されました。"},
            status_code=200
        )

    except Exception as e:
        import traceback
        traceback.print_exc() # エラーのスタックトレースも表示
        print(f"音楽生成中にエラーが発生しました: {e}")
        raise HTTPException(
            status_code=500,
            detail=f"音楽生成に失敗しました: {str(e)}"
        )

# audioファイルを配信するためのエンドポイント
@app.get("/audio/{filename}")
async def get_audio_file(filename: str):
    file_path = os.path.join("audio", filename)
    if os.path.exists(file_path):
        return FileResponse(path=file_path, media_type="audio/midi", filename=filename)
    raise HTTPException(status_code=404, detail="Audio file not found")

途中のテストですが、F12 開発者ページでのエラーテストの際に、コードをconsoleへ入力する際は、コマンドでのコピペ(Ctrl+V)での実行はできないので、直接入力か、マウスでの貼り付けのみが可能でした。
今後の使用時は注意していきます。

frontend

App.vue
<template>
  <div class="container">
    <h1>Neon Ambient Music Generator</h1>

    <div class="parameters">
      <div class="input-group">
        <label for="tempo">テンポ (BPM):</label>
        <input type="number" id="tempo" v-model.number="tempo" min="40" max="218">
      </div>
      <div class="input-group">
        <label for="duration">生成時間 (秒):</label>
        <input type="number" id="duration" v-model.number="duration_seconds" min="10" max="600">
      </div>
    </div>

    <button id="generateButton" class="neon-button" @click="generateMusic" :disabled="isLoading">
      {{ isLoading ? '生成中...' : '音楽を生成' }}
    </button>

    <p v-if="isLoading" id="loadingMessage">生成中...しばらくお待ちください</p>

    <div v-if="audioUrl" class="results" id="resultsArea">
      <p><strong>生成された音楽:</strong></p>
      <p><a :href="audioUrl" download="generated_music.mid">ダウンロード MIDI ファイル</a></p>
    </div>

    <p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const tempo = ref(60);
const duration_seconds = ref(60);
const isLoading = ref(false);
const audioUrl = ref(null);
const errorMessage = ref(null);

const selectedKey = ref('C'); // 例: デフォルトはC
const selectedScale = ref('major'); // 例: デフォルトはmajor

// バックエンドの TrackConfig モデルに対応するデータ構造
const tracks = ref([
  {
    instrument: 0,       // デフォルトのピアノ (MIDIプログラム番号0)
    volume: 80,
    is_melody: true,     // このトラックでメロディを生成
    is_chord: true,      // このトラックでコードも生成 (または false にして別のトラックにする)
    is_percussion: false // このトラックはパーカッションではない
  }

]);

const generateMusic = async () => {
  isLoading.value = true;
  audioUrl.value = null;
  errorMessage.value = null;

  if (tempo.value < 40 || tempo.value > 218) {
    errorMessage.value = "テンポは40から218の範囲で入力してください。";
    isLoading.value = false;
    return;
  }
  if (duration_seconds.value < 10 || duration_seconds.value > 600) {
    errorMessage.value = "生成時間は10秒から600秒(10分)の範囲で入力してください。";
    isLoading.value = false;
    return;
  }

  try {
    const response = await fetch('http://127.0.0.1:8000/api/generate', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        tempo: tempo.value,
        duration_seconds: duration_seconds.value,
        key: selectedKey.value,   
        scale: selectedScale.value, 
        tracks: tracks.value      
      }),
    });

    if (!response.ok) {
      const errorData = await response.json();
      throw new Error(errorData.detail ? JSON.stringify(errorData.detail) : 'APIリクエストが失敗しました。');
    }

    const data = await response.json();
    console.log('API Response:', data);

    if (data.audio_url) {
      audioUrl.value = `http://127.0.0.1:8000${data.audio_url}`;
    }

  } catch (error) {
    console.error('Error generating music:', error);
    errorMessage.value = `音楽生成中にエラーが発生しました: ${error.message}`;
  } finally {
    isLoading.value = false;
  }
};
</script>

<style scoped>
.container {
    background: rgba(0, 0, 0, 0.7);
    padding: 40px 60px;
    border-radius: 20px;
    box-shadow:
        0 0 15px rgba(0, 255, 255, 0.6),
        0 0 30px rgba(0, 255, 255, 0.4),
        0 0 45px rgba(0, 255, 255, 0.2);
    text-align: center;
    width: 90%;
    max-width: 600px;
    border: 2px solid #0ff;
    animation: pulse-border 2s infinite alternate;
    margin-left: auto;
    margin-right: auto;
}

h1 {
    color: #0ff;
    text-shadow:
        0 0 5px #0ff,
        0 0 10px #0ff,
        0 0 20px #0ff;
    margin-bottom: 30px;
    font-size: 2.5em;
}

.parameters {
    margin-bottom: 30px;
    text-align: center;
}

.input-group {
    margin-bottom: 20px;
    text-align: center; 
}

.input-group label {
    display: block;
    margin-bottom: 8px;
    color: #aaffea;
    font-family: 'Share Tech Mono', monospace;
    text-shadow: 0 0 3px #aaffea;
    text-align: center;
}

.input-group input[type="number"],
.input-group select {
    width: calc(100% - 20px);
    padding: 12px;
    border: 2px solid #0dd;
    background: rgba(10, 10, 10, 0.8);
    color: #e0f2f7;
    border-radius: 8px;
    font-size: 1.1em;
    font-family: 'Share Tech Mono', monospace;
    box-shadow: 0 0 5px rgba(0, 221, 221, 0.5);
    transition: all 0.3s ease;
    /* display: block; 
    /* margin-left: auto; */
    /* margin-right: auto; */
}

.neon-button {
    background: none;
    border: 2px solid #ff00ff;
    color: #fff;
    padding: 15px 30px;
    font-size: 1.3em;
    font-family: 'Orbitron', sans-serif;
    border-radius: 10px;
    cursor: pointer;
    text-transform: uppercase;
    letter-spacing: 2px;
    position: relative;
    overflow: hidden;
    transition: all 0.4s ease;
    box-shadow:
        0 0 10px rgba(255, 0, 255, 0.6),
        0 0 20px rgba(255, 0, 255, 0.4);
}

.neon-button:before {
    content: '';
    position: absolute;
    top: 0;
    left: -100%;
    width: 100%;
    height: 100%;
    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
    transition: all 0.6s ease;
}

.neon-button:hover:not(:disabled) {
    color: #000;
    background: #ff00ff;
    box-shadow:
        0 0 20px rgba(255, 0, 255, 0.8),
        0 0 40px rgba(255, 0, 255, 0.6),
        0 0 60px rgba(255, 0, 255, 0.4);
    transform: scale(1.05);
    border-color: #ff00ff;
}

.neon-button:hover:not(:disabled):before {
    left: 100%;
}

.neon-button:active:not(:disabled) {
    transform: scale(0.98);
}

.neon-button:disabled {
    opacity: 0.5;
    cursor: not-allowed;
    box-shadow: none;
    background: none;
    color: #888;
    border-color: #888;
}

.results {
    margin-top: 40px;
    padding-top: 30px;
    border-top: 2px dashed #0af;
    color: #e0f2f7;
    font-family: 'Share Tech Mono', monospace;
    text-align: left;
}

.results p {
    margin-bottom: 10px;
}

.results a {
    color: #0ff;
    text-decoration: none;
    text-shadow: 0 0 5px #0ff;
    transition: all 0.3s ease;
}

.results a:hover {
    color: #fff;
    text-shadow: 0 0 8px #fff, 0 0 15px #0ff;
}

#loadingMessage {
    color: #f0f8ff;
    font-size: 1.2em;
    margin-top: 20px;
    text-shadow: 0 0 5px #f0f8ff;
}

.error-message {
  color: #ff4d4d;
    margin-top: 20px;
    font-family: 'Share Tech Mono', monospace;
    text-shadow: 0 0 5px #ff4d4d;
}

@keyframes pulse-border {
    0% { border-color: #0ff; box-shadow: 0 0 15px rgba(0, 255, 255, 0.6); }
    100% { border-color: #0af; box-shadow: 0 0 25px rgba(0, 170, 255, 0.8); }
}
</style>

ネオン系デザインの採用。
生成する音楽ファイルにつき、楽器の種類の選択などもう少しカスタマイズできるUIの実装を今後予定しています。

おわりに

現在はUIからの音楽生成は可能だが、ピアノ音のみでシンプルな生成になっているので、今後はパーカッションやほかの楽器音などの追加により、華やかさを増していきたいと思います。
また、現状はMIDIファイルとしてデータをダウンロードしている状況ではあるが、DBとの連携がまだできていないので、DB接続は今後の課題にしていきます。
FastAPIについて、主として扱うことはこれまでなかったですが、backendの状態が複数ページに分けて把握できるので、直感的にも開発がしやすかったという印象です。

他、もともとdockerを利用してデータベース代わりに使用したいと思っていたのですが、起動などを繰り返すうちにエラーが多くなり、docker側での処理がおかしいのか、backend側での処理がおかしいのか、複合した問題なのか、などの切り分けが難しいことがわかり、dockerについては使用しない方向にしました。
前回の書籍レビュー投稿アプリの開発の際も同様にAPIのエンドポイントが見つからないエラーが発生しました。
現状として、なぜそのエラーが発生するのかは不明なので、今後の課題とします。

参考

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?