无线的音乐播放有蓝牙音频了,为何还要折腾这个呢?
对蓝牙音频有过一些了解的人会知道,要支持 AptX 、 LDAC 等才算是好的蓝牙音频,但不管是 AptX 还是 LDAC ,亦或者是所有蓝牙音频都必须支持的 SCB ,它们都是一种有损编码技术,区别只在于有损程度与延迟时间,而且对于 Windows 系统来说,如果播放器的音频输出不设置 WASAPI 独占,解码出来的PCM音频数据还要经过系统的 SRC (采样率转换),才到达蓝牙驱动再编码。这些对 HIFI 爱好者都是难以接受的。
这里就探索一种绕过这些问题的方法:解码出的 PCM 数据(如果是 *.wav 无需解码)通 WIFI 的 TCP 连接传到 ESP32 ,再通过 I2S 接口直通 DAC 芯片,也就是说从音频文件到 DAC 之间都是完全无损的(请忽略 I2S 的时钟抖动 └(^o^)┘ ,毕竟 DAC 对时钟抖动也是有一定容忍的)。
先看硬件
I2S 接口需要 3 条信号线,加上电源和地,一共 5 条。
3 条信号线分别是:
BCK:位时钟
LCK:声道选择
DIN:数据
SCK 系统时钟未用到(一般情况下, PCM5102 内部通过PLL电路自动生成。如果需要严格控制时钟抖动,可由主控端提供一个精准的系统时钟信号),可以悬空,但最好接地避免噪声。
另外 PCM5102 还有 4 个配置引脚:
FLT:滤波器选择,接低为正常延迟,接高为低延迟
DEMP:采样率取重控制
XSMT:静音控制,接低为静音,接高为解除静音
FMT:格式选择,接低为标准格式(飞利浦格式),接高为左对齐
除了XSMT必须接高,其他三个选择接低。
所幸模块预留了配置点,无需插线,用一坨锡连上即可
由于 PCM5102 输出只有 2V 左右电平,只能推耳塞,要推喇叭,必须接功放。
接下来是软件部分
ESP32 是作为服务端的,内部实现了两个服务器:
一个 UDP 服务器,用于响应 IP 查询的服务,电脑端通过 UDP 广播向该服务查询 ESP32 的 IP 地址。
另一个是 TCP 服务,用于接收电脑端的指令及音频数据。
电脑端从音频文件解析 / 解码出PCM数据,然后封装成数据指令推送到 ESP32 。
ESP32程序:
// i2s_music.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_netif.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_check.h"
#include "lwip/sockets.h"
#include "driver/i2s_std.h"
#include "driver/gpio.h"
#define DEFAULT_WIFI_SSID "xxxxxx"
#define DEFAULT_WIFI_PASSWORD "******"
static const char *TAG_WIFI = "wifi_station";
static const char *TAG_UDP = "udp_server";
static const char *TAG_TCP = "tcp_server";
static const char *TAG_I2S = "i2s";
void print_hex(const char *array, size_t length) {
for (size_t i = 0; i < length; i++) {
printf("%02x ", (unsigned char)array[i]);
}
printf("\n");
}
static int i2s_status = 0;
//I2S发送通道
static i2s_chan_handle_t i2s_ch_tx;
static void i2s_output_init(uint32_t sample_rate, i2s_data_bit_width_t bit_depth, i2s_slot_mode_t slot_mode)
{
ESP_LOGI(TAG_I2S, "i2s_output_init: %lu, %u, %u", sample_rate, bit_depth, slot_mode);
// 1、创建通道
i2s_chan_config_t chcfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
ESP_ERROR_CHECK(i2s_new_channel(&chcfg, &i2s_ch_tx, NULL));
// 2、配置通道
i2s_std_config_t stdcfg = {
.clk_cfg = {
.sample_rate_hz = sample_rate,
.clk_src = I2S_CLK_SRC_DEFAULT,
.mclk_multiple = I2S_MCLK_MULTIPLE_384, //默认256在采样位宽为24时没法用,384在采样位宽为16、24、32时都能用。
},
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(bit_depth, slot_mode),
.gpio_cfg = {
.mclk = GPIO_NUM_NC, //主时钟MCLK,也叫系统时钟SCK,未使用
.bclk = GPIO_NUM_12, //位时钟BCLK(BCK),也叫串行时钟SCLK(SCK),数据位同步信号
.ws = GPIO_NUM_13, //字时钟WS,也叫声道时钟LRCLK(LRCK),声道选择信号
.dout = GPIO_NUM_14, //数据输出
.din = GPIO_NUM_NC, //数据输入,未使用
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false
}
}
};
ESP_ERROR_CHECK(i2s_channel_init_std_mode(i2s_ch_tx, &stdcfg));
// 3、使能通道,不然通不了
ESP_ERROR_CHECK(i2s_channel_enable(i2s_ch_tx));
}
static size_t i2s_output_write(char *data, int numData)
{
size_t sendSize;
ESP_ERROR_CHECK(i2s_channel_write(i2s_ch_tx, (void*)data, numData, &sendSize, portMAX_DELAY));
//ESP_LOGI("i2s_output_write", ":");
//print_hex(data, numData);
return sendSize;
}
static void i2s_output_end()
{
ESP_ERROR_CHECK(i2s_channel_disable(i2s_ch_tx));
ESP_ERROR_CHECK(i2s_del_channel(i2s_ch_tx));
ESP_LOGI(TAG_I2S, "i2s_output_end");
}
static int on_command(char *rx_buffer, uint16_t data_len, int sock)
{
//ESP_LOGI(TAG_TCP, "On command.");
//准备播放
if(rx_buffer[0] == 0xa0 && rx_buffer[1] == 0x5f)
{
//print_hex(rx_buffer, data_len + 4);
//解析音频属性
uint32_t sample_rate = 44100;
uint8_t bit_depth = 16;
uint8_t slot_mode = 2;
memcpy(&sample_rate, rx_buffer + 4, 4);
memcpy(&bit_depth, rx_buffer + 8, 1);
memcpy(&slot_mode, rx_buffer + 9, 1);
i2s_data_bit_width_t i2s_data_bit_width = I2S_DATA_BIT_WIDTH_16BIT;
i2s_slot_mode_t i2s_slot_mode = I2S_SLOT_MODE_STEREO;
switch(bit_depth){
case 16:
i2s_data_bit_width = I2S_DATA_BIT_WIDTH_16BIT;
break;
case 24:
i2s_data_bit_width = I2S_DATA_BIT_WIDTH_24BIT;
break;
case 32:
i2s_data_bit_width = I2S_DATA_BIT_WIDTH_32BIT;
break;
}
switch(slot_mode){
case 1:
i2s_slot_mode = I2S_SLOT_MODE_MONO;
break;
case 2:
i2s_slot_mode = I2S_SLOT_MODE_STEREO;
break;
}
//初始化I2S
i2s_output_init(sample_rate, i2s_data_bit_width, i2s_slot_mode);
i2s_status = 1;
//响应
char tx_buffer[4] = { 0xb0, 0x4f, 0x00, 0x00 };
int send_len = send(sock, tx_buffer, sizeof(tx_buffer), 0);
if (send_len < 0) {
ESP_LOGE(TAG_TCP, "Error occurred during sending: errno %d", errno);
return 1;
}
return 0;
}
//播放
if(rx_buffer[0] == 0xa1 && rx_buffer[1] == 0x5e)
{
i2s_output_write(rx_buffer + 4, data_len);
return 0;
}
//终止播放
if(rx_buffer[0] == 0xa2 && rx_buffer[1] == 0x5d)
{
//print_hex(rx_buffer, data_len + 4);
//响应
char tx_buffer[4] = { 0xb1, 0x4e, 0x00, 0x00 };
int send_len = send(sock, tx_buffer, sizeof(tx_buffer), 0);
if (send_len < 0) {
ESP_LOGE(TAG_TCP, "Error occurred during sending: errno %d", errno);
}
return 1;
}
ESP_LOGI(TAG_TCP, "Unexpected data.");
return 0;
}
static void tcp_server_task(void *pvParameters)
{
ESP_LOGI(TAG_TCP, "TCP server start.");
//创建套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
if (listen_sock < 0) {
ESP_LOGE(TAG_TCP, "Unable to create socket: errno %d", errno);
vTaskDelete(NULL);
return;
}
//配置套接字选项
int reuse_addr = 1;
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &reuse_addr, sizeof(int));
//绑定地址
struct sockaddr_in server_addr;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(9902);
if (bind(listen_sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) != 0) {
ESP_LOGE(TAG_TCP, "Socket unable to bind: errno %d", errno);
close(listen_sock);
vTaskDelete(NULL);
return;
}
//配置为服务端(被动连接)
if (listen(listen_sock, 1) != 0) {
ESP_LOGE(TAG_TCP, "Socket unable to listen: errno %d", errno);
close(listen_sock);
vTaskDelete(NULL);
return;
}
while(1){
//接受客户端连接
ESP_LOGI(TAG_TCP, "Accept new tcp connections.");
struct sockaddr_storage client_addr;
socklen_t addr_len = sizeof(client_addr);
int sock = accept(listen_sock, (struct sockaddr *)&client_addr, &addr_len);
if (sock < 0) {
ESP_LOGE(TAG_TCP, "Unable to accept connection: errno %d", errno);
continue;
}
int socket_status = 1;
//配置连接属性
int keep_alive = 1;
int keep_idle = 180;
int keep_interval = 60;
int keep_count = 3;
setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &keep_alive, sizeof(int));
setsockopt(sock, IPPROTO_TCP, TCP_KEEPIDLE, &keep_idle, sizeof(int));
setsockopt(sock, IPPROTO_TCP, TCP_KEEPINTVL, &keep_interval, sizeof(int));
setsockopt(sock, IPPROTO_TCP, TCP_KEEPCNT, &keep_count, sizeof(int));
char rx_buffer[1500] = {0x00};
int buffer_len = 0;
uint16_t data_len = 0;
int over_len = -1;
while(1)
{
//接收数据
int recv_len = recv(sock, rx_buffer + buffer_len, sizeof(rx_buffer) - buffer_len, 0);
if (recv_len == 0) {
ESP_LOGI(TAG_TCP, "Sockt has been closed.");
socket_status = 0;
break;
}
if (recv_len < 0) {
ESP_LOGE(TAG_TCP, "Error occurred during receiving: errno %d", errno);
break;
}
buffer_len += recv_len;
//print_hex(rx_buffer, buffer_len);
int err = 0;
while(buffer_len >= 4)
{
memcpy(&data_len, rx_buffer + 2, 2);
if(data_len + 4 > sizeof(rx_buffer))
{
ESP_LOGI(TAG_TCP, "TCP command length error.");
err = 1;
break;
}
over_len = buffer_len - (data_len + 4);
if(over_len < 0)
{
//命令还没接收完整
break;
}
err = on_command(rx_buffer, data_len, sock);
if(err)
{
break;
}
if(over_len == 0)
{
buffer_len = 0;
break;
}
memmove(rx_buffer, rx_buffer + data_len + 4, over_len);
buffer_len = over_len;
}
if(err)
{
break;
}
}
if(i2s_status == 1)
{
i2s_output_end();
i2s_status = 0;
}
if(socket_status == 1)
{
shutdown(sock, 0);
close(sock);
}
}
}
static void udp_server_task(void *pvParameters)
{
while(1)
{
vTaskDelay(1);
ESP_LOGI(TAG_UDP, "UDP server start.");
//创建套接字
int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
if (sock < 0) {
ESP_LOGE(TAG_UDP, "Unable to create socket: errno %d", errno);
}
//绑定地址
struct sockaddr_in server_addr;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(9901);
if (bind(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) != 0) {
ESP_LOGE(TAG_UDP, "Socket unable to bind: errno %d", errno);
shutdown(sock, 0);
close(sock);
continue;
}
//接收数据
char rx_buffer[128] = { 0x00 };
struct sockaddr_storage client_addr;
socklen_t addr_len = sizeof(client_addr);
int len = recvfrom(sock, rx_buffer, sizeof(rx_buffer) - 1, 0, (struct sockaddr *)&client_addr, &addr_len);
if (len < 0) {
ESP_LOGE(TAG_UDP, "Recvfrom failed: errno %d", errno);
shutdown(sock, 0);
close(sock);
continue;
}
//控制端通过UDP广播查询本设备IP,本设备以定向UDP响应,控制端获得IP后,通过TCP连接到本设备进行后续交互。
if(rx_buffer[0] == 0xf0 && rx_buffer[1] == 0x0f)
{
esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (netif) {
esp_netif_ip_info_t ip_info;
esp_netif_get_ip_info(netif, &ip_info);
ESP_LOGI(TAG_UDP, "IP Address: %d.%d.%d.%d", IP2STR(&(ip_info.ip)));
char tx_buffer[6] = { 0x00 };
tx_buffer[0] = 0xf1;
tx_buffer[1] = 0x0e;
memcpy(tx_buffer + 2, &(ip_info.ip.addr), 4);
int err = sendto(sock, tx_buffer, sizeof(tx_buffer), 0, (struct sockaddr *)&client_addr, addr_len);
if (err < 0) {
ESP_LOGE(TAG_UDP, "Sendto failed: errno %d", errno);
}
}else{
ESP_LOGI(TAG_UDP, "Got IP failed");
}
shutdown(sock, 0);
close(sock);
continue;
}
ESP_LOGI(TAG_UDP, "Unexpected data.");
shutdown(sock, 0);
close(sock);
}
}
static void event_handler(void* arg, esp_event_base_t event_base,int32_t event_id, void* event_data)
{
if(event_base == WIFI_EVENT)
{
switch (event_id)
{
case WIFI_EVENT_STA_START: //WIFI以STA模式启动后触发此事件
esp_wifi_connect(); //启动WIFI连接
break;
case WIFI_EVENT_STA_CONNECTED: //WIFI连上路由器后触发此事件
ESP_LOGI(TAG_WIFI, "Connected to AP.");
break;
case WIFI_EVENT_STA_DISCONNECTED: //WIFI从路由器断开连接后触发此事件
esp_wifi_connect(); //继续重连
ESP_LOGI(TAG_WIFI, "Connect to the AP fail, retry now.");
break;
default:
break;
}
}
if(event_base == IP_EVENT)
{
switch(event_id)
{
case IP_EVENT_STA_GOT_IP: //只有获取到路由器分配的IP,才认为是连上了路由器
ESP_LOGI(TAG_WIFI, "Got ip address.");
xTaskCreatePinnedToCore(udp_server_task, "udp_server", 4096, NULL, 5, NULL, 1);
xTaskCreatePinnedToCore(tcp_server_task, "tcp_server", 4096, NULL, 5, NULL, 1);
break;
}
}
}
//WIFI STA初始化
esp_err_t wifi_sta_init()
{
ESP_ERROR_CHECK(esp_netif_init()); //用于初始化协议栈
ESP_ERROR_CHECK(esp_event_loop_create_default()); //创建一个默认系统事件调度循环,之后可以注册回调函数来处理系统的一些事件
esp_netif_create_default_wifi_sta(); //使用默认配置创建STA对象
//初始化WIFI
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
//注册事件
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT,ESP_EVENT_ANY_ID,&event_handler,NULL));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT,IP_EVENT_STA_GOT_IP,&event_handler,NULL));
//WIFI配置
wifi_config_t wifi_config =
{
.sta =
{
.ssid = DEFAULT_WIFI_SSID, //WIFI的SSID
.password = DEFAULT_WIFI_PASSWORD, //WIFI密码
.threshold.authmode = WIFI_AUTH_WPA2_PSK, //加密方式
.pmf_cfg =
{
.capable = true,
.required = false
},
},
};
//启动WIFI
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) ); //设置工作模式为STA
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config) ); //设置WIFI的配置
ESP_ERROR_CHECK(esp_wifi_start() ); //启动WIFI
ESP_LOGI(TAG_WIFI, "Function wifi_init_sta finished.");
return ESP_OK;
}
void app_main()
{
nvs_flash_init();
wifi_sta_init();
while(1)
{
vTaskDelay(pdMS_TO_TICKS(500));
}
}
电脑端程序比较简陋,仅适合测试
// Form1.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading;
using System.IO;
namespace MusicPusher
{
public partial class Form1 : Form
{
Thread pushThread;
MusicPusher musicPusher = new MusicPusher();
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
if (openFileDialog1.ShowDialog() == DialogResult.OK)
{
if (pushThread != null && pushThread.IsAlive)
{
musicPusher.stop();
pushThread.Join();
}
string extension = Path.GetExtension(openFileDialog1.FileName);
if (extension.ToLower() == ".wav")
{
pushThread = new Thread(new ParameterizedThreadStart(musicPusher.pushPCM));
pushThread.Name = "pushPCM";
pushThread.IsBackground = true;
pushThread.Start(openFileDialog1.FileName);
}
else if (Array.Exists(new string[] { ".mp3"}, element => element.Equals(extension.ToLower())))
{
pushThread = new Thread(new ParameterizedThreadStart(musicPusher.pushCompressed));
pushThread.Name = "pushCompressed";
pushThread.IsBackground = true;
pushThread.Start(openFileDialog1.FileName);
}
else
{
//暂不支持{, ".flac", ".ape", ".wma", ".ogg", ".aac", ".mid"} 等常见音频格式
MessageBox.Show("格式暂不支持");
}
}
}
private void button2_Click(object sender, EventArgs e)
{
musicPusher.stop();
pushThread.Join();
}
}
}
// MusicPusher.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Net;
using System.Net.Sockets;
using NAudio.Wave;
namespace MusicPusher
{
class MusicPusher
{
private volatile bool status;
private long ip = 0;
public void pushPCM(object parammeter)
{
status = true;
byte[] file = File.ReadAllBytes((string)parammeter);
UInt32 sample_rate = BitConverter.ToUInt32(file, 24);
byte bit_depth = (byte)BitConverter.ToUInt16(file, 34);
byte slot_mode = (byte)BitConverter.ToUInt32(file, 22);
if (ip == 0)
{
got_ip();
}
IPEndPoint server = new IPEndPoint(ip, 9902);
TcpClient tcpClient = new TcpClient(server.Address.ToString(), server.Port);
NetworkStream stream = tcpClient.GetStream();
stream.Write(new byte[] { 0xa0, 0x5f }, 0, 2);
stream.Write(BitConverter.GetBytes((UInt16)6), 0, 2);
stream.Write(BitConverter.GetBytes(sample_rate), 0, 4);
stream.Write(BitConverter.GetBytes(bit_depth), 0, 1);
stream.Write(BitConverter.GetBytes(slot_mode), 0, 1);
byte[] buffer = new byte[4];
stream.ReadTimeout = 1000; //设置1s超时,默认-1永不超时。
int c = stream.Read(buffer, 0, 4);
if (buffer[0] != 0xb0 || buffer[1] != 0x4f)
{
throw new Exception("Device busy!");
}
int blockSize = 1400;
int start = 44;
UInt16 dataLen;
while (status)
{
dataLen = file.Length >= start + blockSize ? (UInt16)blockSize : (UInt16)(file.Length - start);
stream.Write(new byte[] { 0xa1, 0x5e }, 0, 2);
stream.Write(BitConverter.GetBytes(dataLen), 0, 2);
stream.Write(file, start, dataLen);
start += dataLen;
if (start >= file.Length)
{
break;
}
}
stream.Write(new byte[] { 0xa2, 0x5d, 0x00, 0x00 }, 0, 4);
stream.Read(buffer, 0, 4);
stream.Close();
tcpClient.Close();
}
public void pushCompressed(object parammeter)
{
status = true;
using (var reader = new Mp3FileReader((string)parammeter))
{
UInt32 sample_rate = (UInt32)reader.WaveFormat.SampleRate;
byte bit_depth = (byte)reader.WaveFormat.BitsPerSample;
byte slot_mode = (byte)reader.WaveFormat.Channels;
if (ip == 0)
{
got_ip();
}
IPEndPoint server = new IPEndPoint(ip, 9902);
TcpClient tcpClient = new TcpClient(server.Address.ToString(), server.Port);
NetworkStream stream = tcpClient.GetStream();
stream.Write(new byte[] { 0xa0, 0x5f }, 0, 2);
stream.Write(BitConverter.GetBytes((UInt16)6), 0, 2);
stream.Write(BitConverter.GetBytes(sample_rate), 0, 4);
stream.Write(BitConverter.GetBytes(bit_depth), 0, 1);
stream.Write(BitConverter.GetBytes(slot_mode), 0, 1);
byte[] buffer = new byte[4];
stream.ReadTimeout = 1000; //设置1s超时,默认-1永不超时。
int c = stream.Read(buffer, 0, 4);
if (buffer[0] != 0xb0 || buffer[1] != 0x4f)
{
throw new Exception("Device busy!");
}
int blockSize = 1400;
byte[] data = new byte[blockSize];
int readLen;
while (status && (readLen = reader.Read(data, 0, data.Length)) > 0)
{
stream.Write(new byte[] { 0xa1, 0x5e }, 0, 2);
stream.Write(BitConverter.GetBytes(readLen), 0, 2);
stream.Write(data, 0, readLen);
}
stream.Write(new byte[] { 0xa2, 0x5d, 0x00, 0x00 }, 0, 4);
stream.Read(buffer, 0, 4);
stream.Close();
tcpClient.Close();
}
}
public void stop()
{
status = false;
}
private void got_ip()
{
UdpClient udpClient = new UdpClient();
IPEndPoint receiver = new IPEndPoint(IPAddress.Broadcast, 9901);
udpClient.Send(new byte[] { 0xf0, 0x0f }, 2, receiver);
IPEndPoint remote = new IPEndPoint(IPAddress.Any, 0);
byte[] data = udpClient.Receive(ref remote);
Console.WriteLine(BitConverter.ToString(data));
if (data.Length < 6 || data[0] != 0xf1 || data[1] != 0x0e)
{
throw new Exception("Got IP failed!");
}
ip = BitConverter.ToUInt32(data, 2);
udpClient.Close();
}
}
}
关于 PCM
PCM 只是一堆纯纯的采样数据,还要知道采样率、采样位宽、声道数才能正确播放,这些信息包含在 WAV 文件头部。
事实上,仅上述 3 个附加信息还是不够的,还有 3 个很重要的信息:
采样值的大小端:当采样位宽 > 8 时,采样出的数据必定是多字节的,那么是 MSB 放前面(低地址),还是 LSB 放前面呢
采样值的范围:虽然定义了采样位宽,比如 16,那么采样值的范围是从 -32768 ~ +32767 呢,还是从 0 ~ 65535 ,也就是说采样值是有符号还是无符号的,亦或者浮点数
声道顺序:比如 2 声道时,采样值的排列顺序是先左后右,还是先右后左
很遗憾的是这三个信息并不包含在 WAV 文件头部,只能认为它们有一个默认约定,谁不按约定来那么他的文件只有他自己能播放,别人播放不了。
这里给出绝大多数适用的约定:大端、有符号整数、先左后右。
I2S 的飞利浦模式对数据的要求也刚好与此是对应的,也就是 MSB 先发送,且先发送左声道,数据值类型也是有符号整数。
总结
这次试验是比较成功的,中间虽然也经历了些曲折,比如一开始也是用 UDP 收数据的,结果就是播放有时会中断,一开始也不是电脑端推数据,而是 ESP32 发指令来拉数据,结果就是时不时就卡顿一下。最终改到此方案时,音质就相当哇塞了。
电脑端功能还很不完善,要完全自己打造一个支持所有格式音频的播放器不太现实,后续可以考虑做一个 footbar2000 的插件,只需要接管它的音频输出即可,这样无线也可以听高品质的音乐了。