#![warn(missing_docs)]
#![doc = include_str!("../README.md")]
use esp_idf_hal as hal;
use awedio::{manager::BackendSource, manager::Manager};
use hal::delay::TickType;
use hal::task::thread;
#[cfg(feature = "report-render-time")]
use std::time::Instant;
pub struct Esp32Backend {
pub driver: hal::i2s::I2sDriver<'static, hal::i2s::I2sTx>,
pub channel_count: u16,
pub sample_rate: u32,
pub num_frames_per_write: usize,
pub stack_size: u32,
pub task_priority: u32,
pub pin_to_core: Option<hal::cpu::Core>,
pub auto_disable_channel: bool,
pub on_channel_enable_change: Option<Box<dyn FnMut(bool) + 'static + Send + Sync>>,
}
impl Esp32Backend {
pub fn with_defaults(
driver: hal::i2s::I2sDriver<'static, hal::i2s::I2sTx>,
channel_count: u16,
sample_rate: u32,
num_frames_per_write: usize,
) -> Self {
Self {
driver,
channel_count,
sample_rate,
num_frames_per_write,
stack_size: 30000,
task_priority: 19,
pin_to_core: None,
auto_disable_channel: true,
on_channel_enable_change: None,
}
}
}
impl Esp32Backend {
pub fn start(self) -> Manager {
let (manager, renderer) = Manager::new();
self.start_with_backend_source(Box::new(renderer));
manager
}
pub fn start_with_backend_source(self, mut backend_source: Box<dyn BackendSource>) {
backend_source
.set_output_channel_count_and_sample_rate(self.channel_count, self.sample_rate);
let awedio::NextSample::MetadataChanged = backend_source
.next_sample()
.expect("backend_source should never return an error")
else {
panic!("MetadataChanged expected but not received.");
};
let stack_size = self.stack_size as usize;
let priority: u8 = self.task_priority.try_into().unwrap();
let pin_to_core = self.pin_to_core;
let orig_spawn_config = thread::ThreadSpawnConfiguration::get().unwrap_or_default();
let new_config = thread::ThreadSpawnConfiguration {
name: Some("AwedioBackend\0".as_bytes()),
stack_size, priority,
inherit: false,
pin_to_core,
stack_alloc_caps: Default::default(),
};
new_config
.set()
.expect("a valid stack size and priority for thread spawn");
std::thread::Builder::new()
.stack_size(stack_size)
.name("AwedioBackend".to_owned())
.spawn(|| audio_task(self, backend_source))
.expect("spawn should succeed");
orig_spawn_config
.set()
.expect("original spawn config is valid");
}
}
fn audio_task(mut backend: Esp32Backend, mut backend_source: Box<dyn BackendSource>) {
let mut driver = backend.driver;
let channel_count = backend.channel_count as usize;
let num_frames_per_write = backend.num_frames_per_write;
let mut buf = vec![0_i16; num_frames_per_write * channel_count];
const SAMPLE_SIZE: usize = std::mem::size_of::<i16>();
assert!(SAMPLE_SIZE == 2);
let pause_time = std::time::Duration::from_millis(20);
let mut stopped = backend.auto_disable_channel;
if !stopped {
driver
.tx_enable()
.expect("tx_enable should always succeed. Was the channel already enabled?");
}
#[cfg(feature = "report-render-time")]
let mut render_time_since_report = std::time::Duration::ZERO;
#[cfg(feature = "report-render-time")]
let mut samples_rendered_since_report = 0;
#[cfg(feature = "report-render-time")]
let mut last_report = Instant::now();
loop {
#[cfg(feature = "report-render-time")]
let start = Instant::now();
backend_source.on_start_of_batch();
#[cfg(feature = "report-render-time")]
let end_start_of_batch = Instant::now();
let mut paused = false;
let mut finished = false;
let mut have_data = true;
for (i, buf_sample) in buf.iter_mut().enumerate() {
let sample = match backend_source
.next_sample()
.expect("backend source should never return an error")
{
awedio::NextSample::Sample(s) => s,
awedio::NextSample::MetadataChanged => {
unreachable!("we do not change the metadata of the renderer")
}
awedio::NextSample::Paused => {
paused = true;
if i == 0 {
have_data = false;
break;
}
0
}
awedio::NextSample::Finished => {
finished = true;
if i == 0 {
have_data = false;
break;
}
0
}
};
*buf_sample = sample;
}
if have_data {
#[cfg(feature = "report-render-time")]
{
let end = Instant::now();
let start_of_batch_time = end_start_of_batch.duration_since(start);
render_time_since_report += end.duration_since(end_start_of_batch);
samples_rendered_since_report += buf.len();
if end.duration_since(last_report) > std::time::Duration::from_secs(1) {
let budget_micros = samples_rendered_since_report as f32 * 1_000_000.0
/ backend.sample_rate as f32
/ channel_count as f32;
let percent_budget =
render_time_since_report.as_micros() as f32 / budget_micros * 100.0;
println!(
"Start of batch took {:4}ms. Rendered {:6} frames in {:4}ms. Total {:.1}% of budget.",
start_of_batch_time.as_millis(),
samples_rendered_since_report,
render_time_since_report.as_millis(),
percent_budget
);
render_time_since_report = std::time::Duration::ZERO;
samples_rendered_since_report = 0;
last_report = end;
}
}
let byte_slice = unsafe {
core::slice::from_raw_parts(buf.as_ptr() as *const u8, buf.len() * SAMPLE_SIZE)
};
if stopped {
stopped = false;
if let Some(on_change) = &mut backend.on_channel_enable_change {
on_change(true);
}
let loaded = driver
.preload_data(byte_slice)
.expect("preload should succeed");
assert_eq!(loaded, byte_slice.len());
driver
.tx_enable()
.expect("tx_enable should always succeed. Was the channel already enabled?");
} else {
driver
.write_all(byte_slice, BLOCK_TIME.into())
.expect("I2sDriver::write_all should succeed");
}
}
if finished {
break;
}
if paused {
if !stopped && backend.auto_disable_channel {
stopped = true;
if let Some(on_change) = &mut backend.on_channel_enable_change {
on_change(false);
}
driver
.tx_disable()
.expect("tx_disable should always succeed");
}
std::thread::sleep(pause_time);
continue;
}
}
if backend.auto_disable_channel {
if let Some(on_change) = &mut backend.on_channel_enable_change {
on_change(false);
}
driver
.tx_disable()
.expect("tx_disable should always succeed");
}
}
const BLOCK_TIME: TickType = TickType::new(100_000_000);