2026-09-25 19:23:12 +02:00

463 lines
13 KiB
C++

// SPDX-License-Identifier: Apache-2.0
#include <drivers/esp32_dac_speaker.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/audio_codec.h>
#include <tactility/drivers/gpio_controller.h>
#include <tactility/error.h>
#include <tactility/log.h>
#include <cstddef>
#include <cstdint>
#include <new>
#include "freertos/FreeRTOS.h"
#include "soc/soc.h"
#if SOC_DAC_SUPPORTED
#include "driver/dac_continuous.h"
#include "soc/dac_channel.h"
#endif
#define TAG "Esp32DacSpeaker"
namespace {
// The ESP32 DAC is 8-bit.
constexpr size_t CONVERT_CHUNK_SAMPLES = 1024;
constexpr uint32_t DMA_DESC_COUNT = 8;
constexpr size_t DMA_BUF_SIZE = 2048; // 1024 samples per descriptor, 8 descriptors = 512 ms at 16 kHz
// The DAC digital controller on ESP32 can only reach audio rates through the APLL clock source
constexpr uint8_t MID_DAC_LEVEL = 128;
#if SOC_DAC_SUPPORTED
struct Esp32DacSpeakerInternal {
dac_continuous_handle_t handle = nullptr;
struct GpioDescriptor* pin_descriptor = nullptr;
// Open stream state.
bool is_open = false;
uint8_t open_bits_per_sample = 0;
uint32_t open_sample_rate = 0;
// Software volume/mute
int volume_percent = 100;
bool muted = false;
uint8_t scratch[CONVERT_CHUNK_SAMPLES];
};
#endif
#define GET_CONFIG(device) (static_cast<const Esp32DacSpeakerConfig*>((device)->config))
#define GET_DATA(device) (static_cast<Esp32DacSpeakerInternal*>(device_get_driver_data(device)))
#if SOC_DAC_SUPPORTED
dac_channel_mask_t pin_to_channel_mask(uint8_t pin) {
if (pin == DAC_CHAN0_GPIO_NUM) {
return DAC_CHANNEL_MASK_CH0;
}
if (pin == DAC_CHAN1_GPIO_NUM) {
return DAC_CHANNEL_MASK_CH1;
}
return 0;
}
int ticks_to_timeout_ms(TickType_t ticks) {
if (ticks == portMAX_DELAY) {
return -1;
}
return (int) (((uint64_t) ticks) * 1000 / configTICK_RATE_HZ);
}
// Create (or recreate, on a rate change) the DAC continuous channel at the given rate
error_t create_channel(Esp32DacSpeakerInternal* data, const Esp32DacSpeakerConfig* config, uint32_t sample_rate) {
dac_channel_mask_t mask = pin_to_channel_mask(config->pin.pin);
if (mask == 0) {
LOG_E(TAG, "Pin %u is not a DAC channel (ESP32: GPIO25 or GPIO26)", config->pin.pin);
return ERROR_NOT_SUPPORTED;
}
if (data->handle != nullptr) {
dac_continuous_disable(data->handle);
dac_continuous_del_channels(data->handle);
data->handle = nullptr;
}
const dac_continuous_config_t cfg = {
.chan_mask = mask,
.desc_num = DMA_DESC_COUNT,
.buf_size = DMA_BUF_SIZE,
.freq_hz = sample_rate,
.offset = 0,
.clk_src = DAC_DIGI_CLK_SRC_APLL,
.chan_mode = DAC_CHANNEL_MODE_SIMUL,
};
if (dac_continuous_new_channels(&cfg, &data->handle) != ESP_OK) {
LOG_E(TAG, "Failed to create continuous DAC channel at %lu Hz", (unsigned long) sample_rate);
data->handle = nullptr;
return ERROR_RESOURCE;
}
if (dac_continuous_enable(data->handle) != ESP_OK) {
LOG_E(TAG, "Failed to enable continuous DAC channel");
dac_continuous_del_channels(data->handle);
data->handle = nullptr;
return ERROR_RESOURCE;
}
return ERROR_NONE;
}
// Convert a chunk of 16-bit mono PCM into 8-bit DAC samples with software volume/mute applied.
void convert_pcm16_to_dac8(
Esp32DacSpeakerInternal* data,
const int16_t* pcm,
size_t samples,
int volume_percent,
uint8_t* out
) {
for (size_t i = 0; i < samples; i++) {
int32_t scaled = ((int32_t) pcm[i] * volume_percent) / 100;
out[i] = data->muted ? MID_DAC_LEVEL : (uint8_t) ((scaled >> 8) + 128);
}
}
// Same, for already-8-bit mono PCM (values 0..255, centered on 128).
void convert_pcm8_to_dac8(
Esp32DacSpeakerInternal* data,
const uint8_t* pcm,
size_t samples,
int volume_percent,
uint8_t* out
) {
for (size_t i = 0; i < samples; i++) {
int32_t scaled = (((int32_t) pcm[i] - 128) * volume_percent) / 100;
int32_t level = scaled + 128;
out[i] = data->muted ? MID_DAC_LEVEL : (uint8_t) (level < 0 ? 0 : (level > 255 ? 255 : level));
}
}
#endif // SOC_DAC_SUPPORTED
// region AudioCodecApi
error_t open(Device* device, const struct AudioCodecStreamConfig* config) {
#if SOC_DAC_SUPPORTED
auto* data = GET_DATA(device);
const auto* cfg = GET_CONFIG(device);
if (data == nullptr || cfg == nullptr) {
return ERROR_RESOURCE;
}
if (config->direction != AUDIO_CODEC_DIR_OUTPUT && config->direction != AUDIO_CODEC_DIR_BOTH) {
return ERROR_NOT_SUPPORTED;
}
if (config->bits_per_sample != 8 && config->bits_per_sample != 16) {
return ERROR_NOT_SUPPORTED;
}
if (config->channels != 1) {
return ERROR_NOT_SUPPORTED;
}
if (config->sample_rate == 0) {
return ERROR_INVALID_ARGUMENT;
}
if (data->is_open) {
bool same_config = data->open_bits_per_sample == config->bits_per_sample
&& data->open_sample_rate == config->sample_rate;
return same_config ? ERROR_NONE : ERROR_RESOURCE;
}
if (create_channel(data, cfg, config->sample_rate) != ERROR_NONE) {
return ERROR_RESOURCE;
}
data->open_bits_per_sample = config->bits_per_sample;
data->open_sample_rate = config->sample_rate;
data->is_open = true;
return ERROR_NONE;
#else
return ERROR_NOT_SUPPORTED;
#endif
}
error_t close(Device* device) {
#if SOC_DAC_SUPPORTED
auto* data = GET_DATA(device);
if (data == nullptr) {
return ERROR_RESOURCE;
}
if (data->is_open && data->handle != nullptr) {
dac_continuous_disable(data->handle);
dac_continuous_del_channels(data->handle);
data->handle = nullptr;
}
data->is_open = false;
data->open_bits_per_sample = 0;
data->open_sample_rate = 0;
return ERROR_NONE;
#else
return ERROR_NOT_SUPPORTED;
#endif
}
error_t read(Device* device, void* buffer, size_t size, size_t* bytes_read, TickType_t timeout) {
(void) device;
(void) buffer;
(void) size;
(void) bytes_read;
(void) timeout;
return ERROR_NOT_SUPPORTED;
}
error_t write(Device* device, const void* buffer, size_t size, size_t* bytes_written, TickType_t timeout) {
#if SOC_DAC_SUPPORTED
auto* data = GET_DATA(device);
if (data == nullptr || !data->is_open || data->handle == nullptr) {
return ERROR_RESOURCE;
}
const size_t frame_size = data->open_bits_per_sample / 8;
const size_t whole_bytes = size - (size % frame_size); // drop partial trailing frames
const size_t frame_count = whole_bytes / frame_size;
const uint8_t* input = static_cast<const uint8_t*>(buffer);
const int timeout_ms = ticks_to_timeout_ms(timeout);
size_t frames_done = 0;
while (frames_done < frame_count) {
const size_t chunk = frame_count - frames_done < CONVERT_CHUNK_SAMPLES
? frame_count - frames_done
: CONVERT_CHUNK_SAMPLES;
if (data->open_bits_per_sample == 16) {
convert_pcm16_to_dac8(data, reinterpret_cast<const int16_t*>(input + frames_done * frame_size), chunk, data->volume_percent, data->scratch);
} else {
convert_pcm8_to_dac8(data, input + frames_done * frame_size, chunk, data->volume_percent, data->scratch);
}
size_t loaded = 0;
esp_err_t result = dac_continuous_write(data->handle, data->scratch, chunk, &loaded, timeout_ms);
if (result != ESP_OK) {
*bytes_written = (frames_done + loaded) * frame_size;
return result == ESP_ERR_TIMEOUT ? ERROR_TIMEOUT : ERROR_RESOURCE;
}
frames_done += chunk;
}
*bytes_written = frame_count * frame_size;
return ERROR_NONE;
#else
return ERROR_NOT_SUPPORTED;
#endif
}
error_t set_volume(Device* device, AudioCodecDirection direction, float volume_percent) {
#if SOC_DAC_SUPPORTED
auto* data = GET_DATA(device);
if (data == nullptr) {
return ERROR_RESOURCE;
}
if (direction != AUDIO_CODEC_DIR_OUTPUT) {
return ERROR_NOT_SUPPORTED;
}
if (volume_percent < 0.0f || volume_percent > 100.0f) {
return ERROR_INVALID_ARGUMENT;
}
data->volume_percent = (int) (volume_percent + 0.5f);
return ERROR_NONE;
#else
return ERROR_NOT_SUPPORTED;
#endif
}
error_t get_volume(Device* device, AudioCodecDirection direction, float* volume_percent) {
#if SOC_DAC_SUPPORTED
auto* data = GET_DATA(device);
if (data == nullptr) {
return ERROR_RESOURCE;
}
if (direction != AUDIO_CODEC_DIR_OUTPUT) {
return ERROR_NOT_SUPPORTED;
}
*volume_percent = (float) data->volume_percent;
return ERROR_NONE;
#else
return ERROR_NOT_SUPPORTED;
#endif
}
error_t set_mute(Device* device, AudioCodecDirection direction, bool muted) {
#if SOC_DAC_SUPPORTED
auto* data = GET_DATA(device);
if (data == nullptr) {
return ERROR_RESOURCE;
}
if (direction != AUDIO_CODEC_DIR_OUTPUT) {
return ERROR_NOT_SUPPORTED;
}
data->muted = muted;
return ERROR_NONE;
#else
return ERROR_NOT_SUPPORTED;
#endif
}
error_t get_mute(Device* device, AudioCodecDirection direction, bool* muted) {
#if SOC_DAC_SUPPORTED
auto* data = GET_DATA(device);
if (data == nullptr) {
return ERROR_RESOURCE;
}
if (direction != AUDIO_CODEC_DIR_OUTPUT) {
return ERROR_NOT_SUPPORTED;
}
*muted = data->muted;
return ERROR_NONE;
#else
return ERROR_NOT_SUPPORTED;
#endif
}
error_t get_native_channels(Device* device, AudioCodecDirection direction, uint8_t* channels) {
(void) device;
if (direction != AUDIO_CODEC_DIR_OUTPUT) {
return ERROR_NOT_SUPPORTED;
}
*channels = 1;
return ERROR_NONE;
}
error_t get_native_sample_rate(Device* device, AudioCodecDirection direction, uint32_t* rate_hz) {
if (direction != AUDIO_CODEC_DIR_OUTPUT) {
return ERROR_NOT_SUPPORTED;
}
auto* config = GET_CONFIG(device);
if (config != nullptr) {
*rate_hz = config->sample_rate;
return ERROR_NONE;
}
return ERROR_RESOURCE;
}
error_t get_capabilities(Device* device, AudioCodecDirection* supported_directions) {
(void) device;
*supported_directions = AUDIO_CODEC_DIR_OUTPUT;
return ERROR_NONE;
}
static const struct AudioCodecApi API = {
.open = open,
.close = close,
.read = read,
.write = write,
.set_volume = set_volume,
.get_volume = get_volume,
.set_mute = set_mute,
.get_mute = get_mute,
.get_native_sample_rate = get_native_sample_rate,
.get_native_channels = get_native_channels,
.get_capabilities = get_capabilities,
};
// endregion
// region Driver lifecycle
error_t start_device(Device* device) {
#if SOC_DAC_SUPPORTED
const auto* config = GET_CONFIG(device);
if (config == nullptr) {
return ERROR_RESOURCE;
}
if (pin_to_channel_mask(config->pin.pin) == 0) {
LOG_E(TAG, "Pin %u is not an ESP32 DAC channel (GPIO25 = DAC1, GPIO26 = DAC2)", config->pin.pin);
return ERROR_NOT_SUPPORTED;
}
auto* data = new (std::nothrow) Esp32DacSpeakerInternal();
if (data == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
if (config->pin.gpio_controller != nullptr) {
// GPIO_OWNER_PERIPHERAL: the DAC driver configures the pin's mux/function itself, so we
// only reserve ownership to keep it out of other consumers' hands.
data->pin_descriptor = gpio_descriptor_acquire(
config->pin.gpio_controller,
config->pin.pin,
config->pin.flags | GPIO_FLAG_DIRECTION_OUTPUT,
GPIO_OWNER_PERIPHERAL
);
if (data->pin_descriptor == nullptr) {
LOG_E(TAG, "Failed to reserve DAC pin %u", config->pin.pin);
delete data;
return ERROR_RESOURCE;
}
}
device_set_driver_data(device, data);
return ERROR_NONE;
#else
(void) device;
return ERROR_NOT_SUPPORTED;
#endif
}
error_t stop_device(Device* device) {
#if SOC_DAC_SUPPORTED
auto* data = GET_DATA(device);
if (data == nullptr) {
return ERROR_NONE;
}
if (data->handle != nullptr) {
dac_continuous_disable(data->handle);
dac_continuous_del_channels(data->handle);
data->handle = nullptr;
}
if (data->pin_descriptor != nullptr) {
gpio_descriptor_release(data->pin_descriptor);
data->pin_descriptor = nullptr;
}
device_set_driver_data(device, nullptr);
delete data;
return ERROR_NONE;
#else
(void) device;
return ERROR_NONE;
#endif
}
// endregion
} // namespace
extern "C" {
Driver esp32_dac_speaker_driver = {
.name = "esp32_dac_speaker",
.compatible = (const char*[]) { "espressif,esp32-dac-speaker", nullptr },
.start_device = start_device,
.stop_device = stop_device,
.api = &API,
.device_type = &AUDIO_CODEC_TYPE,
.owner = nullptr,
.internal = nullptr,
};
}