🧑‍🎤音乐MCP,听歌走起

2,667 阅读7分钟

引言

在当今AI技术飞速发展的时代呢,如何将传统应用程序与自然语言交互相结合成为一个非常有趣的技术方向呀。嗯嗯,本文将详细介绍一个基于FastMCP框架开发的智能音乐播放器呢,它能够通过自然语言指令实现音乐播放控制,为用户提供全新的交互体验哦。啊,这个项目最初支持在线音乐播放功能来着,但是呢,出于版权考虑嘛,开源版本就仅保留了本地音乐播放功能啦。

项目概述

这个音乐播放器项目采用Python语言开发呢,核心功能包括:

  1. 嗯~ 本地音乐文件的扫描与加载
  2. 多种播放模式单曲循环呀、列表循环啦、随机播放这样子)
  3. 啊~ 播放控制播放/暂停/停止/上一首/下一首
  4. 嗯嗯,播放列表管理功能
  5. 通过FastMCP框架提供自然语言接口

项目采用模块化设计哦,主要依赖pygame处理音频播放,FastMCP提供AI交互接口,整体架构非常清晰呢,易于扩展和维护的啦。

技术架构解析

1. 核心组件

项目主要包含以下几个关键组件哦:

import os.path
import requests
import re
import json
import pygame
import threading
import queue
import random
  • pygame.mixer:负责音频文件的加载播放
  • threading:实现后台播放线程呀,避免阻塞主程序
  • queue:用于线程间通信(虽然最终版本没直接使用队列啦)
  • random:支持随机播放模式嘛
  • FastMCP:提供AI工具调用接口哦

2. 全局状态管理

播放器通过一组全局变量线程事件来管理播放状态呢:

current_play_list = []  # 当前播放列表呀
current_play_mode = "single"  # 播放模式啦
current_song_index = -1  # 当前歌曲索引哦

# 线程控制事件
is_playing = threading.Event()  # 播放状态标志呢
is_paused = threading.Event()  # 暂停状态标志呀
should_load_new_song = threading.Event()  # 加载新歌曲标志啦

playback_thread = None  # 播放线程句柄哦

这种设计实现了播放状态UI/控制逻辑的分离呢,使得系统更加健壮可维护呀。

核心实现细节

1. 音乐播放线程

播放器的核心是一个独立的后台线程呢,负责实际的音乐播放逻辑哦:

def music_playback_thread():
    global current_song_index, current_play_list, current_play_mode

    # 确保mixer在线程中初始化呀
    if not pygame.mixer.get_init():
        pygame.mixer.init()

    while True:
        # 检查是否需要加载新歌曲啦
        if should_load_new_song.is_set():
            pygame.mixer.music.stop()
            should_load_new_song.clear()
            
            # 处理歌曲加载逻辑哦
            if not current_play_list:
                print("播放列表为空,无法加载新歌曲呢~")
                is_playing.clear()
                is_paused.clear()
                continue
            
            # 验证歌曲索引有效性呀
            if not (0 <= current_song_index < len(current_play_list)):
                current_song_index = 0
            
            # 加载并播放歌曲啦
            song_file_name = current_play_list[current_song_index]
            song_to_play_path = os.path.join("music_file", song_file_name)
            
            if not os.path.exists(song_to_play_path):
                print(f"错误: 歌曲文件 '{song_file_name}' 未找到,跳过啦~")
                continue
            
            try:
                pygame.mixer.music.load(song_to_play_path)
                if not is_paused.is_set():
                    pygame.mixer.music.play()
                print(f"正在播放 (后台): {song_file_name}哦~")
                is_playing.set()
            except pygame.error as e:
                print(f"Pygame加载/播放错误: {e}. 可能音频文件损坏或格式不支持呢。跳过啦~")
                continue
        
        # 播放状态管理呀
        if is_playing.is_set():
            if pygame.mixer.music.get_busy() and not is_paused.is_set():
                pygame.time.Clock().tick(10)
            elif not pygame.mixer.music.get_busy() and not is_paused.is_set():
                # 歌曲自然结束啦,根据模式处理下一首哦
                if current_play_list:
                    if current_play_mode == "single":
                        should_load_new_song.set()
                    elif current_play_mode == "list":
                        current_song_index = (current_song_index + 1) % len(current_play_list)
                        should_load_new_song.set()
                    elif current_play_mode == "random":
                        current_song_index = random.randint(0, len(current_play_list) - 1)
                        should_load_new_song.set()
                else:
                    is_playing.clear()
                    is_paused.clear()
                    pygame.mixer.music.stop()
            elif is_paused.is_set():
                pygame.time.Clock().tick(10)
        else:
            pygame.time.Clock().tick(100)

这个线程实现了完整的播放状态机呢,能够处理各种播放场景哦,包括正常播放呀、暂停啦、歌曲切换等等呢。

2. FastMCP工具函数

项目通过FastMCP提供了一系列可被AI调用的工具函数呢:

播放本地音乐

@mcp.tool()
def play_musics_local(song_name: str = "", play_mode: str = "single") -> str:
    """播放本地音乐呀
    :param song_name: 要播放的音乐名称呢,可以留空哦,留空表示加载进来的歌曲列表为本地文件夹中的所有音乐啦
    :param play_mode: 播放模式呀,可选single(单曲循环),list(列表循环),random(随机播放)哦
    :return: 播放结果呢
    """
    global current_play_list, current_play_mode, current_song_index, playback_thread
    
    # 确保音乐文件夹存在哦
    if not os.path.exists("music_file"):
        os.makedirs("music_file")
        return "本地文件夹中没有音乐文件呢,已创建文件夹 'music_file'啦~"

    # 扫描音乐文件呀
    music_files = [f for f in os.listdir("music_file") if f.endswith(".mp3")]
    if not music_files:
        return "本地文件夹中没有音乐文件呢~"

    # 构建播放列表啦
    play_list_temp = []
    if not song_name:
        play_list_temp = music_files
    else:
        for music_file in music_files:
            if song_name.lower() in music_file.lower():
                play_list_temp.append(music_file)

    if not play_list_temp:
        return f"未找到匹配 '{song_name}' 的本地音乐文件呢~"

    current_play_list = play_list_temp
    current_play_mode = play_mode

    # 设置初始播放索引哦
    if play_mode == "random":
        current_song_index = random.randint(0, len(current_play_list) - 1)
    else:
        if song_name:
            try:
                current_song_index = next(i for i, f in enumerate(current_play_list) if song_name.lower() in f.lower())
            except StopIteration:
                current_song_index = 0
        else:
            current_song_index = 0

    # 确保播放线程运行呀
    if playback_thread is None or not playback_thread.is_alive():
        playback_thread = threading.Thread(target=music_playback_thread, daemon=True)
        playback_thread.start()
        print("后台播放线程已启动啦~")

    # 触发播放哦
    pygame.mixer.music.stop()
    is_paused.clear()
    is_playing.set()
    should_load_new_song.set()

    return f"已加载 {len(current_play_list)} 首音乐到播放列表呢。当前播放模式:{play_mode}哦。即将播放:{current_play_list[current_song_index]}呀~"

播放控制函数

@mcp.tool()
def pause_music(placeholder: str = ""):
    """暂停当前播放的音乐呀"""
    global is_paused, is_playing
    if pygame.mixer.music.get_busy():
        pygame.mixer.music.pause()
        is_paused.set()
        return "音乐已暂停啦~"
    elif is_paused.is_set():
        return "音乐已处于暂停状态呢"
    else:
        return "音乐未在播放中哦,无法暂停呀"

@mcp.tool()
def unpause_music(placeholder: str = ""):
    """恢复暂停的音乐呢"""
    global is_paused, is_playing
    if not pygame.mixer.music.get_busy() and pygame.mixer.music.get_pos() != -1 and is_paused.is_set():
        pygame.mixer.music.unpause()
        is_paused.clear()
        is_playing.set()
        return "音乐已恢复播放啦~"
    elif pygame.mixer.music.get_busy() and not is_paused.is_set():
        return "音乐正在播放中呢,无需恢复哦"
    else:
        return "音乐未在暂停中呀,无法恢复呢"

@mcp.tool()
def stop_music(placeholder: str = ""):
    """停止音乐播放并清理资源哦"""
    global is_playing, is_paused, current_song_index, should_load_new_song
    pygame.mixer.music.stop()
    is_playing.clear()
    is_paused.clear()
    should_load_new_song.clear()
    current_song_index = -1
    return "音乐已停止啦,程序准备好接收新的播放指令哦~"

歌曲导航函数

@mcp.tool()
def next_song(placeholder: str = "") -> str:
    """播放下一首歌曲呀"""
    global current_song_index, current_play_list, is_playing, is_paused, current_play_mode, should_load_new_song
    
    if not current_play_list:
        return "播放列表为空呢,无法播放下一首哦~"

    is_playing.set()
    is_paused.clear()

    # 从单曲循环切换到列表循环啦
    if current_play_mode == "single":
        current_play_mode = "list"
        print("已从单曲循环模式切换到列表循环模式啦~")

    # 计算下一首索引哦
    if current_play_mode == "list":
        current_song_index = (current_song_index + 1) % len(current_play_list)
    elif current_play_mode == "random":
        current_song_index = random.randint(0, len(current_play_list) - 1)

    should_load_new_song.set()
    return f"正在播放下一首: {current_play_list[current_song_index]}呢~"

@mcp.tool()
def previous_song(placeholder: str = "") -> str:
    """播放上一首歌曲呀"""
    global current_song_index, current_play_list, is_playing, is_paused, current_play_mode, should_load_new_song
    
    if not current_play_list:
        return "播放列表为空呢,无法播放上一首哦~"

    is_playing.set()
    is_paused.clear()

    if current_play_mode == "single":
        current_play_mode = "list"
        print("已从单曲循环模式切换到列表循环模式啦~")

    if current_play_mode == "list":
        current_song_index = (current_song_index - 1 + len(current_play_list)) % len(current_play_list)
    elif current_play_mode == "random":
        current_song_index = random.randint(0, len(current_play_list) - 1)

    should_load_new_song.set()
    return f"正在播放上一首: {current_play_list[current_song_index]}呢~"

播放列表查询

@mcp.tool()
def get_playlist(placeholder: str = "") -> str:
    """获取当前播放列表呀"""
    global current_play_list, current_song_index
    
    if not current_play_list:
        return "播放列表当前为空呢~"

    response_lines = ["当前播放列表中的歌曲哦:"]
    for i, song_name in enumerate(current_play_list):
        prefix = "-> " if i == current_song_index else "   "
        response_lines.append(f"{prefix}{i + 1}. {song_name}")

    return "\n".join(response_lines)

部署与使用

1. 环境准备

项目依赖较少呢,只需安装以下库哦:

pip install pygame requests fastmcp

// 或者 指定阿里云的镜像源去加速下载(阿里源提供的PyPI镜像源地址)
pip install pygame requests fastmcp -i https://mirrors.aliyun.com/pypi/simple/

2. 运行程序

python play_music.py

image.png

3. 与AI助手集成

  1. 在支持AI助手的客户端中配置SSE MCP
  2. 添加MCP地址http://localhost:4567/sse
  3. 启用所有工具函数
  4. 设置工具为自动执行以获得更好体验呢

配置,模型服务我选的是大模型openRouter:

image.png

image.png

然后去配置mcp服务器,类型一定要选sse

image.png

image.png

然后保存。

image.png

4. 使用示例

image.png

  • "播放本地歌曲呀,使用随机播放模式哦"
  • "下一首啦"
  • "暂停一下嘛"
  • "继续播放呀"
  • "停止播放呢"
  • "播放歌曲xxx哦,使用单曲循环模式啦"
  • "查看当前音乐播放列表呀"

image.png

JJ的歌真好听。

image.png