|
|
@@ -0,0 +1,385 @@
|
|
|
+#include <Arduino.h>
|
|
|
+#include <WiFi.h>
|
|
|
+#include <ESPAsyncWebServer.h>
|
|
|
+#include <AsyncTCP.h>
|
|
|
+#include <SPIFFS.h>
|
|
|
+#include <driver/i2s.h>
|
|
|
+
|
|
|
+// WiFi配置
|
|
|
+const char *ssid = "zlsh-office";
|
|
|
+const char *password = "zlsh2018";
|
|
|
+
|
|
|
+// I2S配置
|
|
|
+#define I2S_NUM I2S_NUM_0 // I2S 端口号
|
|
|
+#define I2S_BCK_IO 6 // 位时钟
|
|
|
+#define I2S_WS_IO 4 // 字选择
|
|
|
+#define I2S_DO_IO 5 // 数据输出
|
|
|
+#define I2S_DI_IO I2S_PIN_NO_CHANGE // 不使用数据输入
|
|
|
+
|
|
|
+// I2S配置结构体
|
|
|
+i2s_config_t i2s_config = {
|
|
|
+ .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
|
|
|
+ .sample_rate = 44100,
|
|
|
+ .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
|
|
|
+ .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
|
|
|
+ .communication_format = I2S_COMM_FORMAT_STAND_I2S,
|
|
|
+ .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
|
|
|
+ .dma_buf_count = 16,
|
|
|
+ .dma_buf_len = 512,
|
|
|
+ .use_apll = true,
|
|
|
+ .tx_desc_auto_clear = true,
|
|
|
+ .fixed_mclk = 0};
|
|
|
+
|
|
|
+// I2S引脚配置
|
|
|
+i2s_pin_config_t pin_config = {
|
|
|
+ .mck_io_num = I2S_PIN_NO_CHANGE,
|
|
|
+ .bck_io_num = I2S_BCK_IO,
|
|
|
+ .ws_io_num = I2S_WS_IO,
|
|
|
+ .data_out_num = I2S_DO_IO,
|
|
|
+ .data_in_num = I2S_DI_IO};
|
|
|
+
|
|
|
+// 创建AsyncWebServer对象在80端口
|
|
|
+AsyncWebServer server(80);
|
|
|
+
|
|
|
+// 当前播放文件状态
|
|
|
+File currentFile;
|
|
|
+bool isPlaying = false;
|
|
|
+
|
|
|
+// 初始化I2S
|
|
|
+void initI2S()
|
|
|
+{
|
|
|
+ esp_err_t err;
|
|
|
+
|
|
|
+ // 安装I2S驱动
|
|
|
+ err = i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL);
|
|
|
+ if (err != ESP_OK)
|
|
|
+ {
|
|
|
+ Serial.printf("Failed to install i2s driver: %d\n", err);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置I2S引脚
|
|
|
+ err = i2s_set_pin(I2S_NUM, &pin_config);
|
|
|
+ if (err != ESP_OK)
|
|
|
+ {
|
|
|
+ Serial.printf("Failed to set i2s pins: %d\n", err);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清除DMA缓冲区
|
|
|
+ err = i2s_zero_dma_buffer(I2S_NUM);
|
|
|
+ if (err != ESP_OK)
|
|
|
+ {
|
|
|
+ Serial.printf("Failed to clear DMA buffer: %d\n", err);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 网页HTML
|
|
|
+const char index_html[] PROGMEM = R"rawliteral(
|
|
|
+<!DOCTYPE html>
|
|
|
+<html>
|
|
|
+<head>
|
|
|
+ <title>ESP32音乐播放器</title>
|
|
|
+ <meta charset='UTF-8'>
|
|
|
+ <style>
|
|
|
+ body { font-family: Arial, sans-serif; margin: 20px; max-width: 800px; margin: 0 auto; padding: 20px; }
|
|
|
+ .file-item { margin: 10px 0; padding: 10px; border: 1px solid #ccc; border-radius: 4px; }
|
|
|
+ .controls { margin-top: 10px; }
|
|
|
+ button { padding: 5px 10px; margin-right: 5px; }
|
|
|
+ .upload-form { margin: 20px 0; padding: 10px; border: 1px solid #ccc; border-radius: 4px; }
|
|
|
+ </style>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+ <h1>ESP32音乐播放器</h1>
|
|
|
+ <div class="upload-form">
|
|
|
+ <h2>上传音频文件</h2>
|
|
|
+ <input type="file" id="audioFile" accept="audio/*">
|
|
|
+ <button onclick="uploadFile()">上传</button>
|
|
|
+ <div id="uploadProgress"></div>
|
|
|
+ </div>
|
|
|
+ <div id='fileList'></div>
|
|
|
+ <script>
|
|
|
+ function uploadFile() {
|
|
|
+ const file = document.getElementById('audioFile').files[0];
|
|
|
+ if (!file) {
|
|
|
+ alert('请选择文件');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const formData = new FormData();
|
|
|
+ formData.append('file', file);
|
|
|
+
|
|
|
+ const progressDiv = document.getElementById('uploadProgress');
|
|
|
+ progressDiv.innerHTML = '上传中...';
|
|
|
+
|
|
|
+ fetch('/upload', {
|
|
|
+ method: 'POST',
|
|
|
+ body: formData
|
|
|
+ })
|
|
|
+ .then(response => {
|
|
|
+ if (!response.ok) throw new Error('上传失败');
|
|
|
+ progressDiv.innerHTML = '上传成功';
|
|
|
+ loadFiles(); // 重新加载文件列表
|
|
|
+ })
|
|
|
+ .catch(error => {
|
|
|
+ progressDiv.innerHTML = '上传失败: ' + error.message;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function loadFiles() {
|
|
|
+ fetch('/list')
|
|
|
+ .then(response => response.json())
|
|
|
+ .then(files => {
|
|
|
+ const fileList = document.getElementById('fileList');
|
|
|
+ fileList.innerHTML = '<h2>文件列表</h2>';
|
|
|
+ if (files.length === 0) {
|
|
|
+ fileList.innerHTML += '<p>没有找到音频文件</p>';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ files.forEach(file => {
|
|
|
+ const div = document.createElement('div');
|
|
|
+ div.className = 'file-item';
|
|
|
+ div.innerHTML = `
|
|
|
+ <div>${decodeURIComponent(file)}</div>
|
|
|
+ <div class="controls">
|
|
|
+ <button onclick="playFile('${file}')">播放</button>
|
|
|
+ <button onclick="stopPlay()">停止</button>
|
|
|
+ <button onclick="deleteFile('${file}')">删除</button>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ fileList.appendChild(div);
|
|
|
+ });
|
|
|
+ })
|
|
|
+ .catch(error => {
|
|
|
+ console.error('Error:', error);
|
|
|
+ document.getElementById('fileList').innerHTML = '加载文件列表失败';
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function playFile(filename) {
|
|
|
+ fetch('/play?file=' + encodeURIComponent(filename))
|
|
|
+ .then(response => response.text())
|
|
|
+ .then(text => alert(text))
|
|
|
+ .catch(error => alert('播放失败: ' + error));
|
|
|
+ }
|
|
|
+
|
|
|
+ function stopPlay() {
|
|
|
+ fetch('/stop')
|
|
|
+ .then(response => response.text())
|
|
|
+ .then(text => alert(text))
|
|
|
+ .catch(error => alert('停止失败: ' + error));
|
|
|
+ }
|
|
|
+
|
|
|
+ function deleteFile(filename) {
|
|
|
+ if (!confirm('确定要删除文件 ' + filename + ' 吗?')) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ fetch('/delete?file=' + encodeURIComponent(filename))
|
|
|
+ .then(response => response.text())
|
|
|
+ .then(text => {
|
|
|
+ alert(text);
|
|
|
+ loadFiles(); // 重新加载文件列表
|
|
|
+ })
|
|
|
+ .catch(error => alert('删除失败: ' + error));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 页面加载时获取文件列表
|
|
|
+ window.onload = loadFiles;
|
|
|
+ </script>
|
|
|
+</body>
|
|
|
+</html>
|
|
|
+)rawliteral";
|
|
|
+
|
|
|
+// 发送音频数据到I2S
|
|
|
+void playAudioData(const uint8_t *data, size_t len)
|
|
|
+{
|
|
|
+ size_t bytesWritten;
|
|
|
+ i2s_write(I2S_NUM, data, len, &bytesWritten, portMAX_DELAY);
|
|
|
+}
|
|
|
+
|
|
|
+void setup()
|
|
|
+{
|
|
|
+ Serial.begin(115200);
|
|
|
+ Serial.println("\n启动中...");
|
|
|
+
|
|
|
+ // 初始化SPIFFS
|
|
|
+ if (!SPIFFS.begin(true))
|
|
|
+ {
|
|
|
+ Serial.println("SPIFFS挂载失败");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ Serial.println("SPIFFS挂载成功");
|
|
|
+
|
|
|
+ // 配置WiFi
|
|
|
+ WiFi.mode(WIFI_STA);
|
|
|
+ WiFi.disconnect(true);
|
|
|
+ delay(1000);
|
|
|
+
|
|
|
+ // 连接WiFi
|
|
|
+ Serial.printf("连接到WiFi: %s\n", ssid);
|
|
|
+ WiFi.begin(ssid, password);
|
|
|
+
|
|
|
+ // 等待WiFi连接
|
|
|
+ int attempt = 0;
|
|
|
+ while (WiFi.status() != WL_CONNECTED && attempt < 20)
|
|
|
+ {
|
|
|
+ delay(500);
|
|
|
+ Serial.print(".");
|
|
|
+ attempt++;
|
|
|
+ }
|
|
|
+ Serial.println();
|
|
|
+
|
|
|
+ if (WiFi.status() == WL_CONNECTED)
|
|
|
+ {
|
|
|
+ Serial.println("WiFi连接成功!");
|
|
|
+ Serial.print("IP地址: ");
|
|
|
+ Serial.println(WiFi.localIP());
|
|
|
+ Serial.print("子网掩码: ");
|
|
|
+ Serial.println(WiFi.subnetMask());
|
|
|
+ Serial.print("网关: ");
|
|
|
+ Serial.println(WiFi.gatewayIP());
|
|
|
+ Serial.print("信号强度(RSSI): ");
|
|
|
+ Serial.print(WiFi.RSSI());
|
|
|
+ Serial.println(" dBm");
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ Serial.println("WiFi连接失败!");
|
|
|
+ Serial.println("重启设备...");
|
|
|
+ ESP.restart();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置WiFi事件处理
|
|
|
+ WiFi.onEvent([](WiFiEvent_t event, WiFiEventInfo_t info)
|
|
|
+ {
|
|
|
+ Serial.print("WiFi断开连接. 原因: ");
|
|
|
+ Serial.println(info.wifi_sta_disconnected.reason);
|
|
|
+ Serial.println("尝试重新连接...");
|
|
|
+ WiFi.begin(ssid, password); }, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED);
|
|
|
+
|
|
|
+ // 初始化I2S
|
|
|
+ initI2S();
|
|
|
+
|
|
|
+ // 设置Web服务器路由
|
|
|
+ // 主页
|
|
|
+ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
|
|
|
+ { request->send(200, "text/html", index_html); });
|
|
|
+
|
|
|
+ // 文件列表
|
|
|
+ server.on("/list", HTTP_GET, [](AsyncWebServerRequest *request)
|
|
|
+ {
|
|
|
+ File root = SPIFFS.open("/");
|
|
|
+ String json = "[";
|
|
|
+ if(root) {
|
|
|
+ File file = root.openNextFile();
|
|
|
+ bool first = true;
|
|
|
+ while(file) {
|
|
|
+ if(!file.isDirectory()) {
|
|
|
+ if(!first) json += ",";
|
|
|
+ json += "\"" + String(file.name()) + "\"";
|
|
|
+ first = false;
|
|
|
+ }
|
|
|
+ file = root.openNextFile();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ json += "]";
|
|
|
+ request->send(200, "application/json", json); });
|
|
|
+
|
|
|
+ // 文件上传
|
|
|
+ server.on("/upload", HTTP_POST, [](AsyncWebServerRequest *request)
|
|
|
+ { request->send(200); }, [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final)
|
|
|
+ {
|
|
|
+ if(!index) {
|
|
|
+ // 开始上传新文件
|
|
|
+ request->_tempFile = SPIFFS.open("/" + filename, "w");
|
|
|
+ }
|
|
|
+ if(request->_tempFile) {
|
|
|
+ request->_tempFile.write(data, len);
|
|
|
+ }
|
|
|
+ if(final) {
|
|
|
+ request->_tempFile.close();
|
|
|
+ } });
|
|
|
+
|
|
|
+ // 删除文件
|
|
|
+ server.on("/delete", HTTP_GET, [](AsyncWebServerRequest *request)
|
|
|
+ {
|
|
|
+ if(request->hasParam("file")) {
|
|
|
+ String filename = request->getParam("file")->value();
|
|
|
+ if(SPIFFS.remove("/" + filename)) {
|
|
|
+ request->send(200, "text/plain", "文件已删除");
|
|
|
+ } else {
|
|
|
+ request->send(500, "text/plain", "删除失败");
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ request->send(400, "text/plain", "缺少文件参数");
|
|
|
+ } });
|
|
|
+
|
|
|
+ // 播放控制
|
|
|
+ server.on("/play", HTTP_GET, [](AsyncWebServerRequest *request)
|
|
|
+ {
|
|
|
+ if(request->hasParam("file")) {
|
|
|
+ String filename = request->getParam("file")->value();
|
|
|
+ if(currentFile) currentFile.close();
|
|
|
+
|
|
|
+ currentFile = SPIFFS.open("/" + filename, "r");
|
|
|
+ if(!currentFile) {
|
|
|
+ request->send(404, "text/plain", "文件不存在");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ isPlaying = true;
|
|
|
+ request->send(200, "text/plain", "开始播放");
|
|
|
+ } else {
|
|
|
+ request->send(400, "text/plain", "缺少文件参数");
|
|
|
+ } });
|
|
|
+
|
|
|
+ // 停止播放
|
|
|
+ server.on("/stop", HTTP_GET, [](AsyncWebServerRequest *request)
|
|
|
+ {
|
|
|
+ if(currentFile) currentFile.close();
|
|
|
+ isPlaying = false;
|
|
|
+ request->send(200, "text/plain", "停止播放"); });
|
|
|
+
|
|
|
+ // 处理404错误
|
|
|
+ server.onNotFound([](AsyncWebServerRequest *request)
|
|
|
+ { request->send(404, "text/plain", "找不到页面"); });
|
|
|
+
|
|
|
+ // 启动服务器
|
|
|
+ server.begin();
|
|
|
+ Serial.println("Web服务器已启动");
|
|
|
+}
|
|
|
+
|
|
|
+void loop()
|
|
|
+{
|
|
|
+ if (isPlaying && currentFile)
|
|
|
+ {
|
|
|
+ static uint8_t buffer[2048];
|
|
|
+ size_t bytesRead = currentFile.read(buffer, sizeof(buffer));
|
|
|
+
|
|
|
+ if (bytesRead > 0)
|
|
|
+ {
|
|
|
+ size_t bytesWritten = 0;
|
|
|
+ esp_err_t result = i2s_write(I2S_NUM, buffer, bytesRead, &bytesWritten, portMAX_DELAY);
|
|
|
+
|
|
|
+ if (result != ESP_OK)
|
|
|
+ {
|
|
|
+ Serial.printf("I2S写入错误: %d\n", result);
|
|
|
+ currentFile.close();
|
|
|
+ isPlaying = false;
|
|
|
+ i2s_zero_dma_buffer(I2S_NUM);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // 文件播放完毕
|
|
|
+ currentFile.close();
|
|
|
+ isPlaying = false;
|
|
|
+ i2s_zero_dma_buffer(I2S_NUM);
|
|
|
+ Serial.println("播放完成");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 让出时间给其他任务
|
|
|
+ delay(1);
|
|
|
+}
|