用 ESP32 + PCM5102 打造一个无线的HIFI音乐播放器

无线的音乐播放有蓝牙音频了,为何还要折腾这个呢?

对蓝牙音频有过一些了解的人会知道,要支持 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 的插件,只需要接管它的音频输出即可,这样无线也可以听高品质的音乐了。
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值