ラジオっぽく音楽を再生するMP3プレイヤーをPythonで作った

自分の中でのラジオっぽく、というのは曲の途中でフェードアウト・フェードインで切り替わる、という感じです。
単純に曲と曲のつなぎ目がフェードアウト・インで切り替わるMP3プレイヤーは、けっこうあるけど、曲の途中で、というのは意外となさそう。
自分で作っておいてなんですが、かなり気に入っています。
ゆくゆくは、VOICEVOXと組み合わせてニュースとかも途中ではさみたいなぁ、なんて思ってます。
相変わらずのぐちゃぐちゃコードですが誰かの参考になれば。
Arch Linuxですが、見た目はこんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4042331/975fa7d1-06bf-4418-9833-55a693c0eb67.png)
再生中はこんな感じ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4042331/6abcffa9-ca8e-4767-b779-457041e46313.png)


```python:radiomode.py
#!/home/shu/python/radio/radio_venv/bin/python
"""
 機能：MP3をラジオっぽく再生する
 注意：フォルダ内にあるmp3をランダムで再生
"""
import os
import glob
import threading
import subprocess
import time
import random
import atexit

import tkinter as tk
from tkinter import filedialog
import pygame
from mutagen.mp3 import MP3

# mp3のあるフォルダ
FOLDER_PATH = "/home/shu/music/"
# FOLDER_PATH = "/home/shu/ダウンロード/"

global t1
global nowplaying		# 0：再生していない、1：再生中
global playlist			# フォルダ内のMP3リスト
global listnum			# 再生中のリスト番号
global musictitle		# ファイル名（表示用）
global playtime			# 再生時間

global process

"""
 main関数
"""
class RadioPlayer:
	"""
	初期化
	"""
	def __init__(self, root):
		
		global t1, nowplaying, playlist, listnum, musictitle, playtime
		
		t1 = 0
		nowplaying = 0
		playtime = 0
		listnum = 0
		musictitle = FOLDER_PATH		# 初期フォルダを表示。変なコメント出すよりも有効。
		
		# ウィンドウ作成
		self.root = root
		self.root.title("ラジオ風MP3プレイヤー")
		self.root.geometry("400x38")  # ウィンドウのサイズ
		self.root.configure(bg = "white")  # ウィンドウ背景色
		
		# 再生ボタン
		self.playbutton = tk.Label(root, text = "▶️")
		self.playbutton.configure(bg = "white", foreground = "#32cd32", font=("",20))
		self.playbutton.bind("<Button-1>", self.mouse_click)
		self.playbutton.bind("<Motion>", self.mouse_on)
		self.playbutton.bind("<Leave>", self.mouse_leave)
		self.playbutton.place(x = 10, y = 6)
		
		# 曲名表示
		self.label = tk.Label(root, text = musictitle)
		self.label.configure(bg = "white", width = 47)
		self.label.place(x = 35, y = 10)

		# 停止ボタン
		self.stopbutton = tk.Label(root, text = "")
		# btn = tk.Button(root, text="フォルダを選択", command=select_folder)
		self.stopbutton.configure(bg = "white", foreground = "#8b4513", font=("",16))
		self.stopbutton.bind("<Button-1>", self.select_folder)
		self.stopbutton.bind("<Motion>", self.mouse_on_f)
		self.stopbutton.bind("<Leave>", self.mouse_leave_f)
		self.stopbutton.place(x = 365, y = 8)

		# フォルダパスからプレイリスト作成
		make_playlist( FOLDER_PATH )

		# 更新処理（ループ 1sec）
		self.update_music()

	"""
	ループ処理　ある意味メイン処理
	"""
	def update_music(self):
		
		global t1, nowplaying, playlist, listnum, musictitle, playtime
		
		# 指定時間経過で次の曲再生
		if (nowplaying == 1) and (time.time() - t1 > playtime):
			# 次の曲
			listnum = listnum + 1
			
			# 全曲再生したら停止
			if listnum >= len(playlist):
				nowplaying = 0
				musictitle = "ご視聴ありがとうございました！"
			else:
				# 次の音楽を作成
				musicpath = make_music()
				
				# 再生後もGUIは操作したいのでスレッドを分ける
				thread = threading.Thread(target=playmusic,args=(musicpath, playtime)).start()
				
				# 現在時刻取得
				t1 = time.time()
			
		# タイトルラベル更新
		self.label.config(text = musictitle)
		
		# 1秒後に再度コール
		self.root.after(1000, self.update_music)


	"""
	 機能：フォルダ選択
	"""
	def mouse_on_f(self, e):
		self.stopbutton.configure(foreground = "#b8860b")

	def mouse_leave_f(self, e):
		self.stopbutton.configure(foreground = "#8b4513")

	def select_folder(self, e):
		
		global musictitle
		
		# 停止中のみ変更可
		if nowplaying == 0:
			# ダイアログ表示
			folder_path = filedialog.askdirectory()
			
			if folder_path:
				print("選択されたフォルダ:", folder_path)
				musictitle = folder_path
				make_playlist(folder_path)


	"""
	 ボタン表示処理
	"""
	def mouse_on(self, e):
		self.playbutton.configure(foreground = "#808000")

	def mouse_leave(self, e):
		if nowplaying == 0:
			self.playbutton.configure(foreground = "#32cd32")
		else:
			self.playbutton.configure(foreground = "#ff4500")

	def mouse_click(self, e):
		
		global musictitle
		
		if nowplaying == 0:
			if len(playlist) > 0:
				# 再生していないなら流す。ボタンもストップに変える
				self.playbutton.configure(text = "⏸️")
				on_button_click(e)
			else:
				musictitle = "再生できるMP3ファイルがありません"
		else:
			# 再生中なら止める。ボタンも再生に変える。
			self.playbutton.configure(text = "▶️")
			on_button2_click(e)


"""
 機能：プレイリスト作成
 備考：フォルダ内のMP3ファイルを検索してリスト化
"""
def make_playlist(folderpath):
	
	global playlist
	
	# フォルダ内を再帰検索してmp3のリストを作る
	searchpath = folderpath + "/**/*.mp3"
	playlist = glob.glob(searchpath, recursive=True)

"""
 機能：mp3再生
 備考：プロセスをわけているのは、subprocess.runの名残
"""
def playmusic(filepath, playtime):
	
	global process
	
	"""
	subprocess.run([
			"/home/shu/python/radio/radio_venv/bin/python",
			"/home/shu/python/radio/src/mcore.py",
			filepath,
			str(playtime)
			])
	"""
	process = subprocess.Popen([
		"/home/shu/python/radio/radio_venv/bin/python",
		"/home/shu/python/radio/src/mcore.py",
		filepath,
		str(playtime)
	])



"""
 機能：再生する音楽情報の生成
 引数：なし
 戻り値：mp3ファイルパス
 備考：結局グローバル変数使いまくっているので直したい
"""
def make_music():

	global playlist, listnum, musictitle, playtime

	# リスト番号からファイルパス取得
	musicpath = playlist[listnum]
	
	# ファイルパス → ファイル名取得（表示用）
	musictitle, ext = os.path.splitext(os.path.basename(musicpath))
	
	# 曲の長さを取得して、2分〜曲の長さ - 10秒で再生時間確定
	# 10秒はフェードアウトを考慮(適当)
	audio = MP3(musicpath)
	playtime = random.randint(60*2, (int(audio.info.length) - 10))
	
	return musicpath


"""
 機能：再生ボタンクリックイベント
"""
def on_button_click(e):

	global t1, nowplaying, playtime, listnum, musictitle

	# リストをシャッフルすることでランダム再生
	random.shuffle(playlist)
	
	# リストの1曲目から
	listnum = 0
	
	# 音楽情報取得
	musicpath = make_music()
	
	# 再生後もGUIは操作したいのでスレッドを分ける
	thread = threading.Thread(target=playmusic,args=(musicpath, playtime)).start()
	
	# 再生中に設定
	nowplaying = 1
	
	# 現在時刻取得
	t1 = time.time()


"""
 機能：停止ボタンクリックイベント
"""
def on_button2_click(e):
	
	kill_music()


"""
 機能：停止処理
"""
def kill_music():
	
	global nowplaying
	
	# 停止に設定
	nowplaying = 0
	process.kill()
	process.wait()  # ちゃんと終わるまで待つ


"""
 終了処理
"""
# プログラム終了時の処理
atexit.register(kill_music)


"""
 main関数呼び出し
"""
if __name__ == "__main__":
	root = tk.Tk()
	app = RadioPlayer(root)
	root.mainloop()

```

フェードイン・フェードアウトしてMP3再生するコア部分。

```python:mcore.py
#!/home/shu/python/radio/radio_venv/bin/python
"""
 機能：MP3の再生
"""
import pygame
import time
import argparse

STEP_VOL = 0.02		# 1ステップで変化する音量
FPS_TIME = 0.2		# 単位：秒 スリープ時間＝FPS

"""
 音楽再生
"""
def main(filepath, playtime):
	
	vol = 0
	# 現在時刻取得
	t1 = time.time()
	
	#mp3初期化処理
	pygame.mixer.init()
	pygame.mixer.music.load(filepath)
	pygame.mixer.music.play()
	
	# フェードインさせるので0スタート
	pygame.mixer.music.set_volume(0.0)  # 音量設定
	
	while pygame.mixer.music.get_busy():
		# FPS
		time.sleep(FPS_TIME)
		
		# フェードイン 最初だけ
		if (time.time() - t1 <= 5) and (vol <= 0.5):
			vol = vol + STEP_VOL
		
		# 指定時間経過したらフェードアウト開始
		if time.time() - t1 > playtime:
			# フェードアウト
			vol = vol - STEP_VOL
			# ガード
			if vol < 0.0:
				vol = 0.0
				# 0になったら止める
				pygame.mixer.music.stop()
		# 再生
		pygame.mixer.music.set_volume(vol)  # 音量設定


"""
 main関数呼び出し
"""
if __name__ == "__main__":
	
	# 引数でもらいたいので
	parser = argparse.ArgumentParser(description="Fetch RSS feed")
	parser.add_argument("filepath", type=str, help="Path to mp3")
	parser.add_argument("playtime", type=int, help="Play time")
	args = parser.parse_args()

	main(args.filepath, args.playtime)

```