Add PUT /api/stream/phone: raw PCM audio push from a phone app
New endpoint accepts chunked, header-less, 16-bit LE stereo PCM @ 48 kHz in the request body and writes it straight to the shared I2S sink for as long as the connection stays open. Chosen deliberately unencoded (no MP3/AAC/Opus decode) to keep this path as simple and low-risk as possible — the companion app controls the encoding on its side. Extracted the I2S TX channel that used to be owned outright by web_radio_stream.cpp into main/esp32_i2s_sink.cpp, shared by both producers with a simple tryAcquire()/release() exclusivity guard — web radio streaming and a phone PCM stream would otherwise fight over the same physical wire. web_radio_stream.cpp now acquires/releases around each streamWhileEnabled() cycle instead of owning the channel itself. net::PhoneStreamSink is a plain function-pointer struct (not a class hierarchy) threaded through NetBootstrap::start() -> SetupWebServer::start() -> HttpRouteContext, so components/net stays free of I2S driver headers; main/phone_stream.cpp supplies the concrete functions (bound to esp32_i2s_sink) and does the int16->ADAU 32-bit-slot conversion, batched per chunk rather than per sample for the same reason as the web radio stutter fix (6974095/7e65394 lineage). Verified by build only — not confirmed live yet (board disconnected this session); the actual phone app that will exercise this endpoint doesn't exist yet either. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0178rASQ6ZETPMUamvpoR2KR
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* @file esp32_i2s_sink.cpp
|
||||
* @brief Shared ESP32 -> ADAU1701 I2S TX channel (web radio / phone stream).
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include "esp32_i2s_sink.hpp"
|
||||
|
||||
#include "board_pins.hpp"
|
||||
|
||||
#include "driver/i2s_std.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
|
||||
#include <atomic>
|
||||
|
||||
namespace esp32_i2s_sink {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char kTag[] = "i2s_sink";
|
||||
constexpr int kSampleRateHz = 48000;
|
||||
|
||||
i2s_chan_handle_t gTxHandle = nullptr;
|
||||
std::atomic<bool> gInUse{false};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool open()
|
||||
{
|
||||
if (gTxHandle != nullptr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
i2s_chan_config_t chanCfg =
|
||||
I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_SLAVE);
|
||||
// See main/web_radio_stream.cpp history: default (6 x 240 frames =
|
||||
// ~30 ms) leaves almost no headroom against producer jitter (network
|
||||
// stalls, HTTP scheduling); widen it for both producers that share
|
||||
// this channel.
|
||||
chanCfg.dma_desc_num = 12;
|
||||
chanCfg.dma_frame_num = 480;
|
||||
|
||||
if (i2s_new_channel(&chanCfg, &gTxHandle, nullptr) != ESP_OK) {
|
||||
ESP_LOGE(kTag, "i2s_new_channel failed");
|
||||
gTxHandle = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
i2s_std_config_t stdCfg = {
|
||||
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(kSampleRateHz),
|
||||
// 32-bit slots match ADAU1701 SerialOutRegister1 (64 BCLKs/frame).
|
||||
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(
|
||||
I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_STEREO),
|
||||
.gpio_cfg = {
|
||||
.mclk = I2S_GPIO_UNUSED,
|
||||
.bclk = static_cast<gpio_num_t>(board::pins::I2sBclk),
|
||||
.ws = static_cast<gpio_num_t>(board::pins::I2sLrclk),
|
||||
.dout = static_cast<gpio_num_t>(board::pins::I2sDataOut),
|
||||
.din = I2S_GPIO_UNUSED,
|
||||
.invert_flags = {
|
||||
.mclk_inv = false,
|
||||
.bclk_inv = false,
|
||||
.ws_inv = false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (i2s_channel_init_std_mode(gTxHandle, &stdCfg) != ESP_OK
|
||||
|| i2s_channel_enable(gTxHandle) != ESP_OK) {
|
||||
ESP_LOGE(kTag, "I2S TX channel init/enable failed");
|
||||
i2s_del_channel(gTxHandle);
|
||||
gTxHandle = nullptr;
|
||||
return false;
|
||||
}
|
||||
ESP_LOGI(kTag, "I2S slave TX started (BCLK=%d WS=%d DOUT=%d)",
|
||||
board::pins::I2sBclk, board::pins::I2sLrclk,
|
||||
board::pins::I2sDataOut);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool tryAcquire()
|
||||
{
|
||||
bool expected = false;
|
||||
return gInUse.compare_exchange_strong(expected, true);
|
||||
}
|
||||
|
||||
void release()
|
||||
{
|
||||
gInUse.store(false);
|
||||
}
|
||||
|
||||
bool writeSamples(const std::int32_t* samples, std::size_t sampleCount)
|
||||
{
|
||||
if (gTxHandle == nullptr) {
|
||||
return false;
|
||||
}
|
||||
std::size_t written = 0U;
|
||||
const esp_err_t err =
|
||||
i2s_channel_write(gTxHandle, samples, sampleCount * sizeof(std::int32_t),
|
||||
&written, portMAX_DELAY);
|
||||
return err == ESP_OK;
|
||||
}
|
||||
|
||||
} // namespace esp32_i2s_sink
|
||||
Reference in New Issue
Block a user