工程前置:STM32 HAL SD卡+DAC音乐播放WAV(有视频展示)-CSDN博客

参考文章:STM32F407使用Helix库软解MP3并通过DAC输出,最精简的STM32+SD卡实现MP3播放器_stm32 mp3-CSDN博客


MP3解码是一个高度优化的数字信号处理过程,涉及复杂的数学变换和算法,HELIX库将这些复杂的处理过程封装成简单的API调用,使得在嵌入式系统上实现MP3播放成为可能。

MP3 文件解码过程大致为:比特流分析、霍夫曼编码、逆量化处理、立体声处理、频谱重排列、抗锯齿处理、IMDCT 处理、子带合成、PCM 输出。

Helix 解码库是用来解码 MP3 数据帧,一次解码一帧,它是不能用来检索ID3V1和ID3V2标签的,如果需要获取歌名、作者等信息需要自己编程实现。

解码器拿到以上信息后的一帧处理流程

  1. 读帧头 → 算帧长、采样率、版本。

  2. 读 side_info → 得每 gr/ch 的 Huffman 表号、region 边界、增益、block_type 等。

  3. 按 main_data_begin 把主数据从比特池拷到本地缓存。

  4. 解 scalefactor → 反量化时恢复各子带增益。

  5. Huffman 解码 → 得到 576 条频线(每 gr/ch)。

  6. 反量化 + 立体声处理(MS/Joint)→ 576 频线变 PCM 子带样点。

  7. 重排序(长窗/短窗/混合窗)→ 576 样点按 18×32 排列。

  8. IMDCT → 32 子带各 18 样点 → 32×18 时域样点。

  9. 频率反转 + 合成多相滤波器组 → 每粒度 576 样点,最终输出 1152 样点/帧(44.1 kHz 下约 26 ms)。

如果 MP3 文件完全没有 ID3 等标签内容,那么从文件的第一个字节开始就是有效的 MPEG 音频帧(frame),可以直接开始解码,不需要跳过任何前缀数据。我们参考的工程就是直接开始解码的逻辑,但是,从各大音乐APP(如网易云音乐下载的歌曲都含有TAG(标签),导致直接使用程序会解码失败,作者也是尝试了几天,尝试跳过,并借助了各大AI的能力,但是最终效果并不理想。

所以编者放弃了,编者的最终目的是想从MP3中提取歌词来进行显示,于是去学习了一下,发现大多数MP3都没有内置歌词,好吧好吧,那换一种思路,学习BOOFAR软件,从网络API提取lrc歌词,然后再给MP3处理一下把标签删了,完美。


随着工程越来复杂,我们需要将前置工程修改为比较规范的格式(没有前置工程的也不用着急,文末会附上完整工程ZIP文件)其实就是CUBEMX配置文件管理系统+SD卡挂载+USB+DAC+TIM,代码什么的我们这次从头开始。

为了方便DEBUG调试,在CUBEMX我们打开串口,并开启中断和DMA,配置保持默认即可

我们本次MP3解码的DAC输出支持双声道,勾选上,配置和之前一样,开启DMA,循环模式,半字传输,TIM6触发。

首先在begin4加入串口重定向,此处无需勾选USE MICROLIB

/* USER CODE BEGIN 4 */
#pragma import(__use_no_semihosting)
 
struct __FILE
{
    int handle;
};                                                         // 标准库需要的支持函数
 
FILE __stdout;                                             // FILE 在stdio.h文件
void _sys_exit(int x)
{
    x = x;                                                 // 定义_sys_exit()以避免使用半主机模式
}
 
int fputc(int ch, FILE *f)                                 // 重写fputc函数,使printf的输出由UART1实现,  这里使用USART1
{
    // 注意,不能使用HAL_UART_Transmit_IT(), 机制上会冲突; 因为调用中断发送函数后,如果上次发送还在进行,就会直接返回!它不会继续等待,也不会数据填入队列排队发送
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0x02);   // 使用HAL_UART_Transmit,相等于USART1->DR = ch, 函数内部加了简单的超时判断(ms),防止卡死
    return ch;
}
/* USER CODE END 4 */

然后BEGIN0放一下中断回调函数,这里其实是伪双缓冲,但是能跑,就不改了。

/* USER CODE BEGIN 0 */
void HAL_DAC_ConvHalfCpltCallbackCh1(DAC_HandleTypeDef *hdac)
{
  (void)hdac; 
  // 支持MP3播放的逻辑
  extern volatile uint8_t dac_ht;
  extern volatile uint8_t Isread;
  dac_ht = 1;
  Isread = 1;
}

void HAL_DAC_ConvCpltCallbackCh1(DAC_HandleTypeDef *hdac)
{
  (void)hdac;
  // 支持MP3播放的逻辑
  extern volatile uint8_t dac_ht;
  extern volatile uint8_t Isread;
  dac_ht = 0;
  Isread = 1;
}
/* USER CODE END 0 */

main.c的头文件包含

/* USER CODE BEGIN Includes */
#include <string.h>
#include <stdbool.h>
#include <stdio.h>
#include "mp3player.h"
/* USER CODE END Includes */

然后上大头,两个文件,这个代码还可以调节音量

#ifndef __MP3PLAYER_H
#define __MP3PLAYER_H

#include "main.h"
#include "ff.h" 
#include "mp3dec.h"
#include "dac.h"
#include "gpio.h"
#include "tim.h"
#include "usart.h"
#include "fatfs.h"
#include <stdio.h>
#include <string.h>
#include <stdbool.h>

/* MP3播放器相关定义 */
#define CNT_FREQ          84000000      // TIM6 counter clock (prescaled APB1)
#define MP3BUFFER_SIZE    2304          // MP3解码输出缓冲区大小
#define INPUTBUF_SIZE     3000          // MP3解码输入缓冲区大小
#define DAC_BUF_LEN       2500          // DAC缓冲区长度

/* MP3播放器状态定义 */
#define STA_IDLE     0
#define STA_PLAYING  1
#define STA_PAUSED   2

/* MP3播放器结构体 */
typedef struct {
    uint8_t ucStatus;    /* 播放状态 */
    uint8_t ucVolume;    /* 音量 */
} MP3_TYPE;

/* 外部变量声明 */
extern DAC_HandleTypeDef hdac;
extern DMA_HandleTypeDef hdma_dac1;
extern DMA_HandleTypeDef hdma_dac2;
extern TIM_HandleTypeDef htim6;
extern UART_HandleTypeDef huart1;

/* DAC缓冲区定义 - 双通道 */
extern uint16_t DAC_buff[2][DAC_BUF_LEN]; //DAC1、DAC2输出缓冲

/* 全局变量声明 */
extern volatile uint8_t Isread;          /* DMA传输完成标志 */
extern volatile uint8_t dac_ht;          //DAC dma 半传输标志

/* MP3播放器函数声明 */
bool MP3Player_Init(void);
int MP3DataDecoder(uint8_t **read_ptr, int *bytes_left);
uint8_t read_file(const char *mp3file, uint8_t **read_ptr, int *bytes_left);
void mp3PlayerDemo(const char *mp3file);
void DAC_Config(void);
void DAC_DMA_Start(uint32_t freq, uint16_t len);
void DAC_DMA_Stop(void);

#endif /* __MP3PLAYER_H */
#include <stdio.h>
#include <string.h>
#include "ff.h" 
#include "MP3player.h"
#include "mp3dec.h"
#include "dac.h"
#include "gpio.h"
#include "tim.h"
#include "usart.h"

#pragma diag_suppress 870
#define CNT_FREQ          84000000      // TIM6 counter clock (prescaled APB1)

/* DAC缓冲区定义 - 双通道思路 */
uint16_t DAC_buff[2][DAC_BUF_LEN]; //DAC1、DAC2输出缓冲

/* 外部变量声明 */
extern DAC_HandleTypeDef hdac;
extern DMA_HandleTypeDef hdma_dac1;
extern DMA_HandleTypeDef hdma_dac2;
extern TIM_HandleTypeDef htim6;
extern UART_HandleTypeDef huart1;
 
/* 处理立体声音频数据时,输出缓冲区需要的最大大小为2304*16/8字节(16为PCM数据为16位),
 * 这里我们定义MP3BUFFER_SIZE为2304
 */
#define MP3BUFFER_SIZE  2304
#define INPUTBUF_SIZE   3000
 
static HMP3Decoder		Mp3Decoder;			/* mp3解码器指针	*/
static MP3FrameInfo		Mp3FrameInfo;		/* mP3帧信息  */
static MP3_TYPE mp3player;            /* mp3播放设备 */
volatile uint8_t Isread = 0;          /* DMA传输完成标志 */
volatile uint8_t dac_ht = 0;          //DAC dma 半传输标志
 
 
uint8_t inputbuf[INPUTBUF_SIZE]={0};     /* 解码输入缓冲区,1940字节为最大MP3帧大小  */
static short outbuffer[MP3BUFFER_SIZE];  /* 解码输出缓冲区*/
 
static FIL file;			/* file objects */
static UINT bw;       /* File R/W count */
FRESULT result;

/* 工作缓冲区 */
BYTE work[_MAX_SS]; 

/**
 * @brief 初始化MP3播放器
 * @return 初始化成功返回true,失败返回false
 */
bool MP3Player_Init(void)
{
  printf("\rSystem Clock: %d MHz\r", SystemCoreClock/1000000);
  
  /* 挂载 SD 卡 */
  retSD = f_mount(&SDFatFS, SDPath, 1);
  if (retSD == FR_NO_FILESYSTEM)
  {
    if (f_mkfs(SDPath, FM_FAT32, 512, work, sizeof(work)) != FR_OK)
    {
      printf("SD卡格式化失败\r\n");
      return false;
    }
    retSD = f_mount(&SDFatFS, SDPath, 1);
  }
  if (retSD != FR_OK)
  {
    printf("SD卡挂载失败: %d\r\n", retSD);
    return false;
  }
  
  printf("SD卡挂载成功\r\n");
  return true;
}

//从SD卡读取MP3源文件进行解码,并传入DAC缓冲区
int MP3DataDecoder(uint8_t **read_ptr, int *bytes_left)
{
	int err = 0, i = 0, outputSamps = 0;
 
	//bufflag开始解码 参数:mp3解码结构体、输入流指针、输入流大小、输出流指针、数据格式
	err = MP3Decode(Mp3Decoder, read_ptr, bytes_left, outbuffer, 0);
	
	if (err != ERR_MP3_NONE)	//错误处理
	{
		switch (err)
		{
			case ERR_MP3_INDATA_UNDERFLOW:
							printf("ERR_MP3_INDATA_UNDERFLOW\r\n");
							result = f_read(&file, inputbuf, INPUTBUF_SIZE, &bw);
							*read_ptr = inputbuf;
							*bytes_left = bw;
				break;		
			case ERR_MP3_MAINDATA_UNDERFLOW:
							/* do nothing - next call to decode will provide more mainData */
							printf("ERR_MP3_MAINDATA_UNDERFLOW\r\n");
				break;		
			default:
							printf("UNKNOWN ERROR:%d\r\n", err);		
							// 跳过此帧
							if (*bytes_left > 0)
							{
								(*bytes_left) --;
								read_ptr ++;
							}
				break;
		}
		return 0;
	}
	else		//解码无错误,准备把数据输出到PCM
	{
		MP3GetLastFrameInfo(Mp3Decoder, &Mp3FrameInfo);		//获取解码信息				
		/* 输出到DAC */
		outputSamps = Mp3FrameInfo.outputSamps;						//PCM数据个数
		if (outputSamps > 0)
		{
			if (Mp3FrameInfo.nChans == 1)	//单声道
			{
				//单声道数据需要复制一份到另一个声道
				for (i = outputSamps - 1; i >= 0; i--)
				{
					outbuffer[i * 2] = outbuffer[i];
					outbuffer[i * 2 + 1] = outbuffer[i];
				}
				outputSamps *= 2;
			}//if (Mp3FrameInfo.nChans == 1)	//单声道
		}//if (outputSamps > 0)
					
		//将数据传送至DMA DAC缓冲区
		for (i = 0; i < outputSamps/2; i++)
		{
			if(dac_ht == 1)
			{
				DAC_buff[0][i] = outbuffer[2*i] * mp3player.ucVolume /100 + 32768;
				DAC_buff[1][i] = outbuffer[2*i+1] * mp3player.ucVolume /100 + 32768;
			}
			else
			{
				DAC_buff[0][i+outputSamps/2] = outbuffer[2*i] * mp3player.ucVolume /100 + 32768;
				DAC_buff[1][i+outputSamps/2] = outbuffer[2*i+1] * mp3player.ucVolume /100 + 32768;
			}
		}
		
		return 1;
	}//else 解码正常
}
 
//读取一段MP3数据,并把读取的指针赋值read_ptr,长度赋值bytes_left
uint8_t read_file(const char *mp3file, uint8_t **read_ptr, int *bytes_left)
{
	result = f_read(&file, inputbuf, INPUTBUF_SIZE, &bw);
	
	if(result != FR_OK)
	{
		printf("读取%s失败 -> %d\r\n", mp3file, result);
		return 0;
	}
	else
	{
		*read_ptr = inputbuf;
		*bytes_left = bw;
		
		return 1;
	}
}
 
/**
  * @brief  MP3格式音频播放主程序
  * @param  mp3file MP3文件路径
  * @retval 无
  */
void mp3PlayerDemo(const char *mp3file)
{
	uint8_t *read_ptr = inputbuf;
	int	read_offset = 0;				/* 读偏移指针 */
	int	bytes_left = 0;					/* 剩余字节数 */	
	
	mp3player.ucStatus = STA_IDLE;
	mp3player.ucVolume = 100; //音量值,100满
	
	//初始化DAC
	DAC_Config();
	
	//尝试打开MP3文件
	result = f_open(&file, mp3file, FA_READ);
	if(result != FR_OK)
	{
		printf("Open mp3file :%s fail!!!->%d\r\n", mp3file, result);
		result = f_close (&file);
		return;	/* 停止播放 */
	}
	printf("当前播放文件 -> %s\n", mp3file);
	
	//初始化MP3解码器
	Mp3Decoder = MP3InitDecoder();	
	if(Mp3Decoder == 0)
	{
		printf("初始化helix解码库设备失败!\r\n");
		return;	/* 停止播放 */
	}
	else
	{
		printf("初始化helix解码库完成\r\n");
	}
	
	//尝试读取一段MP3数据,并把读取的指针赋值read_ptr,长度赋值bytes_left
	if(!read_file(mp3file, &read_ptr, &bytes_left))
	{
		MP3FreeDecoder(Mp3Decoder);
		return;	/* 停止播放 */
	}
	
	//尝试解码成功
	if(MP3DataDecoder(&read_ptr, &bytes_left))
	{
		//打印MP3信息
		printf(" \r\n Bitrate       %dKbps", Mp3FrameInfo.bitrate/1000);
		printf(" \r\n Samprate      %dHz",   Mp3FrameInfo.samprate);
		printf(" \r\n BitsPerSample %db",    Mp3FrameInfo.bitsPerSample);
		printf(" \r\n nChans        %d",     Mp3FrameInfo.nChans);
		printf(" \r\n Layer         %d",     Mp3FrameInfo.layer);
		printf(" \r\n Version       %d",     Mp3FrameInfo.version);
		printf(" \r\n OutputSamps   %d",     Mp3FrameInfo.outputSamps);
		printf("\r\n");
		
		//启动DAC,开始发声
		if (Mp3FrameInfo.nChans == 1)	//单声道要将outputSamps*2
		{
			DAC_DMA_Start(Mp3FrameInfo.samprate, 2 * Mp3FrameInfo.outputSamps);
		}
		else//双声道直接用Mp3FrameInfo.outputSamps
		{
			DAC_DMA_Start(Mp3FrameInfo.samprate, Mp3FrameInfo.outputSamps);
		}
	}
	else //解码失败
	{
		MP3FreeDecoder(Mp3Decoder);
		return;
	}
	
	/* 放音状态 */
	mp3player.ucStatus = STA_PLAYING;
	
	/* 进入主程序循环体 */
	while(mp3player.ucStatus == STA_PLAYING)
	{
			//寻找帧同步,返回第一个同步字的位置
			read_offset = MP3FindSyncWord(read_ptr, bytes_left);
			if(read_offset < 0)					//没有找到同步字
			{
				if(!read_file(mp3file, &read_ptr, &bytes_left))//重新读取一次文件再找
				{
					continue;//回到while(mp3player.ucStatus == STA_PLAYING)后面
				}
			}
			else//找到同步字
			{			
				read_ptr   += read_offset;	//偏移至同步字的位置
				bytes_left -= read_offset;	//同步字之后的数据大小	
				
				if(bytes_left < 1024)				//如果剩余的数据小于1024字节,补充数据
				{
					/* 注意这个地方因为采用的是DMA读取,所以一定要4字节对齐  */
					uint16_t i = (uint32_t)(bytes_left)&3;	//判断多余的字节
					if(i) i=4-i;						//需要补充的字节
					memcpy(inputbuf+i, read_ptr, bytes_left);	//从对齐位置开始复制
					read_ptr = inputbuf+i;										//指向数据对齐位置
					result = f_read(&file, inputbuf+bytes_left+i, INPUTBUF_SIZE-bytes_left-i, &bw);//补充数据
					if(result != FR_OK)
					{
						printf("读取%s失败 -> %d\r\n",mp3file,result);
						break;
					}
					bytes_left += bw;		//有效数据流大小
				}
			}
			
			//MP3数据解码并送入DAC缓存
			if(!MP3DataDecoder(&read_ptr, &bytes_left))
			{//如果播放出错,Isread置1,避免卡住死循环
				Isread = 1;
			}
			
			//mp3文件读取完成,退出
      if(file.fptr == file.obj.objsize)
      {
				printf("单曲播放完毕\r\n");
				break;
      }
       
			//等待DAC发送一半或全部中断
			while(Isread == 0)
			{
				//Input_scan();		//等待DMA传输完成,此间可以运行按键扫描及处理事件
			}
			Isread = 0;
	}
 
	//运行到此处,说明单曲播放完成,收尾工作
	DAC_DMA_Stop();//停止喂DAC数据	
	mp3player.ucStatus = STA_IDLE;
	MP3FreeDecoder(Mp3Decoder);//清理缓存
	f_close(&file);	
}
 
/**
 * @brief DAC配置 - 外设已在main.c中初始化,这里只做配置
 */
void DAC_Config(void)
{
    // 外设已在main.c中初始化,这里只需要配置定时器频率
    // 设置默认频率为44.1kHz
    uint32_t period = (CNT_FREQ / 44100) - 1;
    htim6.Init.Prescaler = 0;  // 预分频器 = 1
    htim6.Init.Period = period;
    HAL_TIM_Base_Init(&htim6);
    
    printf("DAC配置完成 - 双通道立体声输出\r\n");
}

//配置DAC采样率和DMA数据长度,并启动DMA DAC
void DAC_DMA_Start(uint32_t freq, uint16_t len)
{
    // 停止之前的DMA
    DAC_DMA_Stop();
    
    // 设置定时器频率
    uint32_t period = (CNT_FREQ / freq) - 1;
    htim6.Init.Prescaler = 0;  // 预分频器 = 1
    htim6.Init.Period = period;
    HAL_TIM_Base_Init(&htim6);
    
    // 启动DAC DMA - 双通道(12位左对齐)
    HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t *)DAC_buff[0], len, DAC_ALIGN_12B_L);
    HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_2, (uint32_t *)DAC_buff[1], len, DAC_ALIGN_12B_L);
    
    // 启动定时器
    HAL_TIM_Base_Start(&htim6);
    
    printf("DAC DMA启动 - 采样率: %d Hz, 缓冲区长度: %d\r\n", freq, len);
}

//停止DMA DAC
void DAC_DMA_Stop(void)
{
    HAL_TIM_Base_Stop(&htim6);
    HAL_DAC_Stop_DMA(&hdac, DAC_CHANNEL_1);
    HAL_DAC_Stop_DMA(&hdac, DAC_CHANNEL_2);
}

最后在主循环前加入几行代码,播放开始(主循环没代码),编译结束没有报错,这四个warning都是HELIX库自带的,无需在意。

  /* USER CODE BEGIN 2 */
  /* 初始化音频播放器 */
  if (!MP3Player_Init())
  {
    while (1);
  }

  /* 播放MP3文件 */
  mp3PlayerDemo("ZAVEREIGH - All For Love.mp3");

  /* USER CODE END 2 */


这里使用python脚本来处理MP3,去掉TAG并从网上抓取歌词,将MP3和main.py放在同一个文件夹运行即可。

import os
import requests
from mutagen.id3 import ID3, ID3NoHeaderError
from mutagen.mp3 import MP3, HeaderNotFoundError

# 网易云音乐歌词API
LYRICS_API = "http://music.163.com/api/song/lyric?id={song_id}&lv=-1&kv=-1&tv=-1"


def get_song_id_from_title(title, artist):
    """
    通过歌曲标题和艺术家名称从网易云音乐获取歌曲ID
    :param title: 歌曲标题
    :param artist: 艺术家名称
    :return: 歌曲ID或None
    """
    search_url = "http://music.163.com/api/search/get/web"
    params = {
        "s": f"{title} {artist}",
        "type": 1,  # 1表示歌曲
        "offset": 0,
        "total": "true",
        "limit": 1
    }
    response = requests.get(search_url, params=params)
    if response.status_code == 200:
        data = response.json()
        if data["result"]["songCount"] > 0:
            return data["result"]["songs"][0]["id"]
    return None


def get_lyrics_from_song_id(song_id):
    """
    通过歌曲ID从网易云音乐获取歌词
    :param song_id: 歌曲ID
    :return: 歌词内容或None
    """
    url = LYRICS_API.format(song_id=song_id)
    response = requests.get(url)
    if response.status_code == 200:
        data = response.json()
        if 'lrc' in data and 'lyric' in data['lrc']:
            return data['lrc']['lyric']
    return None


def extract_song_info_from_mp3(mp3_file_path):
    """
    从MP3文件中提取歌曲标题和艺术家名称
    :param mp3_file_path: MP3文件路径
    :return: 歌曲标题和艺术家名称
    """
    try:
        tags = ID3(mp3_file_path)
    except ID3NoHeaderError:
        print(f"文件 {mp3_file_path} 中没有ID3标签。")
        return None, None

    title = tags.get("TIT2", [None])[0]
    artist = tags.get("TPE1", [None])[0]
    return title, artist


def save_lyrics_to_file(lyrics, lrc_file_path):
    """
    将歌词保存到.lrc文件中
    :param lyrics: 歌词内容
    :param lrc_file_path: 要保存的.lrc文件路径
    """
    try:
        with open(lrc_file_path, 'w', encoding='utf-8') as file:
            file.write(lyrics)
        print(f"歌词已保存到 {lrc_file_path}")
    except Exception as e:
        print(f"保存歌词到 {lrc_file_path} 时出错: {e}")


def remove_id3_tags(mp3_file_path, output_file_path):
    """
    使用mutagen删除MP3文件的ID3标签,并保留音频数据
    :param mp3_file_path: 原始MP3文件路径
    :param output_file_path: 输出文件路径
    """
    try:
        # 读取MP3文件
        audio = MP3(mp3_file_path)

        # 删除所有ID3标签
        if audio.tags:
            audio.clear()
            audio.save()

        # 读取清理后的音频数据
        with open(mp3_file_path, 'rb') as src:
            data = src.read()

        # 找到第一个音频数据帧的起始位置
        start_pos = data.find(b'\xFF\xFB')  # MP3 音频数据帧的起始标志
        if start_pos == -1:
            print(f"文件 {mp3_file_path} 中没有找到音频数据帧。")
            return

        # 保留音频数据帧之后的内容
        audio_data = data[start_pos:]

        # 保存清理后的文件
        with open(output_file_path, 'wb') as dst:
            dst.write(audio_data)

        print(f"已删除ID3标签并保存到 {output_file_path}")
    except HeaderNotFoundError:
        print(f"文件 {mp3_file_path} 不是一个有效的MP3文件。")
    except Exception as e:
        print(f"处理文件 {mp3_file_path} 时出错: {e}")


def main():
    # 获取当前目录
    current_dir = os.getcwd()
    print(f"当前目录: {current_dir}")

    # 创建文件夹
    music_dir = os.path.join(current_dir, "music")
    lyric_dir = os.path.join(current_dir, "lyric")
    os.makedirs(music_dir, exist_ok=True)
    os.makedirs(lyric_dir, exist_ok=True)

    # 遍历当前目录下的所有MP3文件
    for file_name in os.listdir(current_dir):
        if file_name.lower().endswith('.mp3'):
            mp3_file_path = os.path.join(current_dir, file_name)
            print(f"处理文件: {mp3_file_path}")

            # 构造目标文件路径
            output_mp3_file_path = os.path.join(music_dir, file_name)
            lrc_file_path = os.path.join(lyric_dir, file_name[:-4] + '.lrc')

            # 检查目标文件是否已经存在
            if os.path.exists(output_mp3_file_path):
                print(f"文件 {output_mp3_file_path} 已存在,跳过处理。")
                continue

            # 提取歌曲信息
            title, artist = extract_song_info_from_mp3(mp3_file_path)
            if not title or not artist:
                print(f"文件 {mp3_file_path} 中没有足够的信息来搜索歌词。")
                # 即使没有歌词,也处理并保存到music文件夹
                remove_id3_tags(mp3_file_path, output_mp3_file_path)
                continue

            # 获取歌曲ID
            song_id = get_song_id_from_title(title, artist)
            if not song_id:
                print(f"无法找到歌曲 {title} - {artist} 的ID。")
                # 即使没有歌词,也处理并保存到music文件夹
                remove_id3_tags(mp3_file_path, output_mp3_file_path)
                continue

            # 获取歌词
            lyrics = get_lyrics_from_song_id(song_id)
            if lyrics:
                # 保存歌词到.lrc文件
                save_lyrics_to_file(lyrics, lrc_file_path)

            # 使用mutagen删除ID3标签
            remove_id3_tags(mp3_file_path, output_mp3_file_path)


if __name__ == "__main__":
    main()

通过网盘分享的文件:SD_MP3_DAC.zip
链接: https://pan.baidu.com/s/1lYjajDqM62m-OTjo0NzrWg?pwd=4v9b 提取码: 4v9b 

也不知道博客能写多久,先留一年的连接,过期了记得踢我

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐