esp32+max98357做网络音箱
1.从github下载源码用zip方式,https://github.com/pschatzmann/arduino-audio-tools
2.导入到Arduino IDE,项目->导入库->添加.zip库
3.根据需要解码的音频下载安装对应的库,参考文档https://github.com/pschatzmann/arduino-audio-tools/wiki/Encoding-and-Decoding-of-Audiohttps://github.com/pschatzmann/arduino-audio-tools/wiki/Encoding-and-Decoding-of-Audio
4.需要的材料: esp32开发板、Max98357、喇叭
5.接线
6.将代码烧录到esp32,这里我用Mp3解码,因为直接读mp3文件不用转码,如果是从麦克风之类的采集音频数据的话一般是PCM数据,PCM数据建议使用Opus编码
#include "AudioTools.h" #include <WiFi.h> #include <WiFiUdp.h> #include "AudioTools/AudioCodecs/CodecMP3Helix.h" WiFiUDP udp; const int localPort = 12345; const int BUFFER_SIZE = 1024; uint8_t buffer[BUFFER_SIZE]; I2SStream i2s; //I2S流 VolumeStream volume(i2s); //音量流 EncodedAudioStream dec(&volume, new MP3DecoderHelix()); //Opus解码流 void setup() { Serial.begin(115200); WiFi.mode(WIFI_AP); //WIFI热点模式 WiFi.softAP("test", "123456789"); //账号密码 auto cfg = i2s.defaultConfig(TX_MODE); //输出模式 cfg.sample_rate = 48000; //采样率 cfg.channels = 2; //通道 cfg.bits_per_sample = 16; // cfg.pin_bck = 26; //esp32的26引脚接max98357的BCLK cfg.pin_ws = 25; //esp32的25引脚接max98357的LRC cfg.pin_data = 22; //esp32的22引脚接max98357的DIN i2s.begin(cfg); auto dcfg = i2s.defaultConfig(); dcfg.copyFrom(cfg); dec.begin(dcfg); auto vcfg = volume.defaultConfig(); vcfg.copyFrom(cfg); volume.begin(vcfg); volume.setVolume(0.7); //设置音量 udp.begin(localPort); IPAddress IP = WiFi.softAPIP(); Serial.print("AP IP address: "); Serial.println(IP); //从串口中打印本机IP } void loop() { int packetSize = udp.parsePacket(); //检查是否有udp数据包并返回数据包大小 if (packetSize) { int len = udp.read(buffer, BUFFER_SIZE); //从udp数据包读取数据 dec.write(buffer, len);//将数据写入到Opus解码流中 } }
7.这里esp32使用了热点模式,电脑wifi连接热点,然后打开串口看下esp32的ip
8.下载ffmpeg,使用命令推流mp3,记得将ip改为你esp32的ip,还有2.mp3也要改成你电脑上的mp3文件:
ffmpeg -re -i 2.mp3 -c:a libmp3lame -vn -ar 48000 -ac 2 -f mp3 udp://192.168.4.1:12345
9.这里给出安卓代码参考,代码只是简单作为参考,不能实际使用,其中原因之一是线程休眠不精准,需要使用高精度定时器或者多媒体定时器,代码里的mp3文件我先存http服务,可以选择先放sd卡再读取,一共2个ip,一个是本机读取mp3文件的ip,一个是esp32的ip,本机ip不能使用127.0.0.1
package com.example.myapplication import android.app.Activity import android.media.MediaExtractor import android.media.MediaFormat import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button import androidx.compose.material3.Scaffold import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import com.example.myapplication.ui.theme.MyApplicationTheme import java.io.IOException import java.net.DatagramPacket import java.net.DatagramSocket import java.net.InetAddress import java.nio.ByteBuffer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.setValue import androidx.compose.ui.unit.dp class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { MyApplicationTheme { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> Greeting( name = "Android", modifier = Modifier.padding(innerPadding) ) } } } } } @Composable fun Greeting(name: String, modifier: Modifier = Modifier) { val context = LocalContext.current val activity = context as Activity val isButtonEnabled = remember { mutableStateOf(true) } var sliderPosition by remember { mutableStateOf(0f) } var isSeek = remember { mutableStateOf(false) } Box(modifier = Modifier.fillMaxSize().padding(10.dp), contentAlignment = Alignment.Center){ Column { Slider(value = sliderPosition, onValueChange = { sliderPosition = it }, valueRange = 0f..1f, onValueChangeFinished = { synchronized(isSeek){ isSeek.value = true } } ) Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center){ Button(enabled = isButtonEnabled.value, onClick = { isButtonEnabled.value = false; Thread { val socket = DatagramSocket() val IPAddress = InetAddress.getByName("192.168.4.1") var port = 12345; var previousPresentationTimeUs = 0L try { val extractor = MediaExtractor() extractor.setDataSource("http://192.168.4.2:8012/2.mp3") val buffer = ByteBuffer.allocate(500 * 1024) var sampleSize = 0; extractor.selectTrack(0); val format: MediaFormat = extractor.getTrackFormat(0) var duration = format.getLong(MediaFormat.KEY_DURATION); while(true){ if(isSeek.value){ isSeek.value = false extractor.seekTo((duration.toFloat() * sliderPosition.toFloat()).toLong(), MediaExtractor.SEEK_TO_CLOSEST_SYNC); previousPresentationTimeUs = 0 } sampleSize = extractor.readSampleData(buffer, 0) if (sampleSize <= 0) { break } val mp3Frame = ByteArray(sampleSize.toInt()) buffer.rewind() buffer.get(mp3Frame) val presentationTimeUs = extractor.sampleTime activity.runOnUiThread { sliderPosition = (extractor.sampleTime.toFloat() / duration.toFloat()).coerceIn(0F, 100F) } var timeDiffMs = 0L if (previousPresentationTimeUs > 0) { timeDiffMs = (presentationTimeUs - previousPresentationTimeUs) / 1000 Thread.sleep(timeDiffMs-1) } previousPresentationTimeUs = presentationTimeUs val sendPacket = DatagramPacket(mp3Frame, mp3Frame.size, IPAddress, port) socket.send(sendPacket) extractor.advance() } } catch (e: IOException) { e.printStackTrace() } activity.runOnUiThread { isButtonEnabled.value = true; } }.start() }) { Text("esp32播放mp3") } } } } } @Preview(showBackground = true) @Composable fun GreetingPreview() { MyApplicationTheme { Greeting("Android") } }
以下是安卓运行效果:
为什么从麦克风读取PCM数据用Opus而不用Mp3,原因之一就是网络传输,并且个人感觉解码opsu播放比解码mp3播放要快一些,当然这是在esp32测试的,电脑上编解码音频耗时几乎忽略不计,视频编解码则能用GPU编解码就用
对比项 | Opus | MP3 |
压缩效率 | 高(低码率音质更好) | 中(低码率易失真) |
音质 | 宽频表现佳,语音 / 音乐混合更自然 | 中高码率音质稳定,低码率差 |
码率范围 | 6kbps – 510kbps(灵活可调) | 32kbps – 320kbps(常见 128kbps+) |
专利 | 开源(免费) | 受专利保护(需付费) |
兼容性 | 现代设备支持好,旧设备可能受限 | 全平台兼容(几乎所有设备支持) |
应用场景 | 实时通信(视频会议、直播、VoIP)、网络音频流(如游戏语音) | 音乐存储播放、音频文件共享、传统多媒体内容 |
文件大小 | 同音质下更小 | 同音质下更大 |