見出し画像

PythonでMP3プレイヤーを作ってみる

こんにちは!今回は、Pythonを使って自分専用のMP3プレイヤーを作った経験を共有したいと思います。

きっかけ

私は普段、ブログを書くときにBGMを聴きながら作業することが多いのですが、MacのVOX Playerのような使いやすいプレイヤーがLinux環境で見つからず、「それなら作ってしまおう!」と思い立ちました。

開発環境の準備

今回の開発環境として、ZorinOS上でPython 3.10.15を使用することにしました。GUIフレームワークにはCustomTkinterを採用し、音声エンジンにはpython-vlcを選択しました。これらは安定性が高く、必要な機能を十分にカバーできる組み合わせです。

プレイヤーの設計

プレイヤーの全体像

理想のプレイヤーを作るにあたり、まずはVOX Playerの優れた点を分析することから始めました。洗練されたデザインと必要十分な機能性を目標に、全体像を描きました。シンプルさを追求しながらも、音楽プレイヤーとして必要な機能は確実に実装する。これを基本コンセプトとしました。デザインはダークグレーを基調とし、操作性を重視した配置を心がけています。

必要な機能の整理

開発前に必要な機能を整理しました。音楽プレイヤーとして外せない機能を見極め、それらを実装することにしました。

基本的な再生機能

再生、一時停止、停止の基本操作に加え、次の曲、前の曲への移動機能を実装します。再生位置を示すプログレスバーは、クリックによる位置変更も可能にしました。

プレイリスト管理

音楽ファイルの追加、削除、並び替えの基本機能に加え、プレイリストの保存と読み込み機能を実装します。前回の続きから再生できる機能も追加し、使い勝手を向上させました。音声制御
音量調整はスライダー形式で実装し、直感的な操作を可能にしました。

技術要件の検討

実装に向けて、技術面での要件を整理しました。

パフォーマンス面

大容量MP3ファイルを扱うため、ストリーミング再生を採用し、必要な部分だけをメモリに読み込む方式としました。

インターフェース設計

CustomTkinterを使用し、モダンでクリーンなUIを実現します。ウィンドウサイズは必要最小限に抑え、デスクトップの邪魔にならない設計としました。

開発の優先順位

機能の重要度と実装の難易度を考慮し、開発の優先順位を決めました。

  1. 基本的な再生機能とUIの実装

  2. プレイリスト管理機能の実装

  3. 追加機能の実装

デザインの具体化

ユーザーインターフェースは、以下の要素で構成しました:

  • ウィンドウ上部にプレイリスト表示領域

  • 中央部に再生情報とプログレスバー

  • 下部に直感的な操作ボタンと音量調整

  • 全体的にフラットでモダンなデザイン

このように、全体像を明確にした上で具体的な実装に移ることで、開発の方向性がぶれることなく、効率的に作業を進めることができました。次回は、これらの設計に基づいた実際の実装について解説していきます。

開発のポイント

メモリ管理には特に注意を払いました。大容量のMP3ファイル(100MB程度)でも快適に再生できるよう、ストリーミング再生を実装し、効率的なメモリ使用とシステムリソースの最適化を図りました。使いやすさを重視して、プレイリストの保存と読み込み機能を実装し、直感的な操作性とスムーズな再生制御を実現しました。

AIの活用

AIへの指示の工夫

開発を始めるにあたり、AIへの指示の出し方を工夫しました。まず、プロジェクトの全体像を伝え、その後で具体的な実装方法について質問していく形式を採用しました。

段階的な開発アプローチ

最初に基本設計の相談をし、その後は機能ごとに詳細な実装方法を確認していきました。例えば、プログレスバーの実装では、「CustomTkinterでプログレスバーを実装する際の最適な方法」という具体的な質問をしました。

技術的な詳細の確認

python-vlcの使用方法やCustomTkinterの実装について、特に以下の点を重点的に確認しました:

音声処理の最適化
大容量ファイルを扱う際のメモリ管理や、効率的な再生方法について、具体的なコード例を含めて確認しました。
UIデザインの実装
ダークテーマの実装方法や、ボタンのカスタマイズについて、実際のコードを見ながら改善点を探りました。

エラー対応とデバッグ

開発中に発生した問題については、エラーメッセージと実行環境の詳細を添えて質問することで、的確な解決方法を得ることができました。

コードの最適化

完成したコードについても、パフォーマンスやメモリ使用量の観点から見直しを依頼し、より効率的な実装方法の提案を受けました。

完成したプレイヤーの特徴

出来上がったプレイヤーは、VOX Playerライクなミニマルデザインを持ち、効率的な音声処理と快適な操作性を実現しています。安定した動作も特徴の一つです。

import customtkinter as ctk
import vlc
import os
import json
from tkinter import filedialog
from datetime import timedelta

class VOXPlayerApp(ctk.CTk):
    def __init__(self):
        super().__init__()
        
        self.title("VOX Player")
        self.geometry("800x600")
        self.configure(fg_color="#2E2E2E")
        
        self.playlist = []
        self.current_index = 0
        self.is_playing = False
        self.instance = vlc.Instance()
        self.player = self.instance.media_player_new()
        
        self.create_ui()
        self.setup_bindings()
        
    def create_ui(self):
        # メインフレーム
        self.main_frame = ctk.CTkFrame(self, fg_color="#2E2E2E")
        self.main_frame.pack(fill="both", expand=True, padx=20, pady=20)
        
        # プレイリスト表示
        self.playlist_frame = ctk.CTkFrame(self.main_frame, fg_color="#363636")
        self.playlist_frame.pack(fill="both", expand=True, pady=(0, 20))
        
        self.playlist_box = ctk.CTkTextbox(
            self.playlist_frame,
            fg_color="#363636",
            text_color="#FFFFFF",
            font=("Helvetica", 12)
        )
        self.playlist_box.pack(fill="both", expand=True, padx=10, pady=10)
        
        # コントロールフレーム
        self.control_frame = ctk.CTkFrame(self.main_frame, fg_color="#2E2E2E")
        self.control_frame.pack(fill="x")
        
        # 時間表示
        self.time_label = ctk.CTkLabel(
            self.control_frame,
            text="00:00 / 00:00",
            text_color="#FFFFFF"
        )
        self.time_label.pack(pady=(0, 10))
        
        # プログレスバー
        self.progress_bar = ctk.CTkProgressBar(
            self.control_frame,
            fg_color="#4F4F4F",
            progress_color="#666666",
            height=10
        )
        self.progress_bar.pack(fill="x", pady=(0, 20))
        self.progress_bar.set(0)
        
        # 再生コントロール
        self.button_frame = ctk.CTkFrame(self.control_frame, fg_color="#2E2E2E")
        self.button_frame.pack(fill="x")
        
        buttons = [
            ("Previous", self.previous_track),
            ("Play/Pause", self.toggle_play),
            ("Stop", self.stop),
            ("Next", self.next_track),
            ("Add MP3", self.add_mp3),
            ("Clear", self.clear_playlist),
            ("Save Playlist", self.save_playlist),
            ("Load Playlist", self.load_playlist)
        ]
        
        for text, command in buttons:
            ctk.CTkButton(
                self.button_frame,
                text=text,
                command=command,
                fg_color="#4F4F4F",
                hover_color="#666666",
                corner_radius=10,
                width=100
            ).pack(side="left", padx=5)
        
        # 音量スライダー
        self.volume_frame = ctk.CTkFrame(self.control_frame, fg_color="#2E2E2E")
        self.volume_frame.pack(fill="x", pady=(20, 0))
        
        self.volume_slider = ctk.CTkSlider(
            self.volume_frame,
            from_=0,
            to=100,
            command=self.set_volume,
            fg_color="#4F4F4F",
            progress_color="#666666"
        )
        self.volume_slider.pack(fill="x")
        self.volume_slider.set(50)
        
    def setup_bindings(self):
        self.progress_bar.bind("<Button-1>", self.seek)
        self.after(1000, self.update_progress)
        
    def add_mp3(self):
        files = filedialog.askopenfilenames(filetypes=[("MP3 Files", "*.mp3")])
        for file in files:
            self.playlist.append(file)
        self.update_playlist_display()
        
    def update_playlist_display(self):
        self.playlist_box.delete("1.0", "end")
        for i, track in enumerate(self.playlist, 1):
            self.playlist_box.insert("end", f"{i}. {os.path.basename(track)}\n")
            
    def toggle_play(self):
        if not self.playlist:
            return
            
        if not self.is_playing:
            if not self.player.get_media():
                media = self.instance.media_new(self.playlist[self.current_index])
                self.player.set_media(media)
            self.player.play()
            self.is_playing = True
        else:
            self.player.pause()
            self.is_playing = False
            
    def stop(self):
        self.player.stop()
        self.is_playing = False
        self.progress_bar.set(0)
        
    def next_track(self):
        if self.playlist:
            self.current_index = (self.current_index + 1) % len(self.playlist)
            self.play_current_track()
            
    def previous_track(self):
        if self.playlist:
            self.current_index = (self.current_index - 1) % len(self.playlist)
            self.play_current_track()
            
    def play_current_track(self):
        self.stop()
        media = self.instance.media_new(self.playlist[self.current_index])
        self.player.set_media(media)
        self.player.play()
        self.is_playing = True
        
    def set_volume(self, value):
        self.player.audio_set_volume(int(value))
        
    def seek(self, event):
        if not self.player.get_media():
            return
        width = self.progress_bar.winfo_width()
        ratio = event.x / width
        length = self.player.get_length()
        self.player.set_time(int(length * ratio))
        
    def update_progress(self):
        if self.player.get_media():
            length = self.player.get_length()
            current = self.player.get_time()
            
            if length > 0:
                self.progress_bar.set(current / length)
                current_time = str(timedelta(milliseconds=current))[:-7]
                total_time = str(timedelta(milliseconds=length))[:-7]
                self.time_label.configure(text=f"{current_time} / {total_time}")
                
        self.after(1000, self.update_progress)
        
    def clear_playlist(self):
        self.stop()
        self.playlist = []
        self.current_index = 0
        self.update_playlist_display()
        
    def save_playlist(self):
        if not self.playlist:
            return
            
        file_path = filedialog.asksaveasfilename(
            defaultextension=".json",
            filetypes=[("JSON files", "*.json")]
        )
        if file_path:
            with open(file_path, 'w', encoding='utf-8') as f:
                json.dump(self.playlist, f)
                
    def load_playlist(self):
        file_path = filedialog.askopenfilename(
            filetypes=[("JSON files", "*.json")]
        )
        if file_path:
            with open(file_path, 'r', encoding='utf-8') as f:
                self.playlist = json.load(f)
            self.current_index = 0
            self.update_playlist_display()

if __name__ == "__main__":
    app = VOXPlayerApp()
    app.mainloop()

なお、MP3プレイヤーの実行環境を整えるには、まずPythonパッケージのインストールから始めます。

必要なパッケージはcustomtkinterとpython-vlcです。
これらはpipコマンド(Poetry add)でインストールできます。

次に、システム側の設定として、VLCメディアプレイヤーのインストールが必要です。
ZorinOSを使用している場合は、aptパッケージマネージャーを使用してVLCをインストールします。また、CustomTkinterはTkinterに依存しているため、python3-tkパッケージもインストールする必要があります

これもaptを使用してインストールできます。すべてのインストールが完了したら、簡単なテストコードを実行して環境が正しく設定されているか確認することをお勧めします。CustomTkinterとVLCのバージョン情報を表示させることで、ライブラリが正しく認識されているかを確認できます。これらの設定が完了すれば、MP3プレイヤーを実行する準備が整います。

今後の展望

現在も開発は継続中で、プレイリストの自動保存機能やUIのさらなる改善、音声フォーマットの対応拡大などを検討しています。

まとめ

このプロジェクトを通じて、実用的なアプリケーション開発の面白さを実感できました。特に、AIを活用することで、効率的な開発が可能になることを学びました。プログラミングの学習では、実際に使えるものを作ることが重要です。今回のように、自分が必要としているものを作ることで、モチベーションを保ちながら技術力を向上させることができます。

いいなと思ったら応援しよう!