STM32 HELIX解码DAC播放MP3 HAL库版本
工程前置: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标签的,如果需要获取歌名、作者等信息需要自己编程实现。
解码器拿到以上信息后的一帧处理流程
-
读帧头 → 算帧长、采样率、版本。
-
读 side_info → 得每 gr/ch 的 Huffman 表号、region 边界、增益、block_type 等。
-
按 main_data_begin 把主数据从比特池拷到本地缓存。
-
解 scalefactor → 反量化时恢复各子带增益。
-
Huffman 解码 → 得到 576 条频线(每 gr/ch)。
-
反量化 + 立体声处理(MS/Joint)→ 576 频线变 PCM 子带样点。
-
重排序(长窗/短窗/混合窗)→ 576 样点按 18×32 排列。
-
IMDCT → 32 子带各 18 样点 → 32×18 时域样点。
-
频率反转 + 合成多相滤波器组 → 每粒度 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
也不知道博客能写多久,先留一年的连接,过期了记得踢我
更多推荐



所有评论(0)