This commit is contained in:
Patrick Moessler 2025-03-20 03:58:08 +01:00
parent bd18905a96
commit 385bd830ad
5 changed files with 203 additions and 85 deletions

View file

@ -24,9 +24,14 @@ experimental = ["esp-idf-svc/experimental"]
[dependencies]
log = "0.4"
esp-idf-svc = { version = "0.51", features = ["critical-section", "embassy-time-driver", "embassy-sync"] }
esp-idf-svc = { version = "0.51", features = ["embassy-time-driver", "embassy-sync"] }
anyhow = "1.0.97"
bytemuck = { version="1.22.0", features = ["derive", "min_const_generics"] }
embassy-executor = {version ="0.7.0" , features = ["arch-std", "executor-thread", "log"] }
embassy-sync = "0.6.2"
static_cell = "2.1.0"
embassy-time = "0.4.0"
assign-resources = "0.4.1"
[build-dependencies]
embuild = "0.33"

View file

@ -1,41 +1,51 @@
use esp_idf_svc::sys::{esp_dsp, esp_nofail};
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
use crate::config::{AUDIO_BANDS, AUDIO_BUFFERS, AUDIO_SAMPLES_PER_BUF};
use embassy_sync::zerocopy_channel::{Receiver, Sender};
use esp_idf_svc::sys::{esp_dsp, esp_nofail};
use static_cell::StaticCell;
use crate::config::{AUDIO_BANDS, AUDIO_SAMPLES_PER_BUF};
use crate::helpers::{falloff, falloff_f};
pub type AudioBuffer = [i32; AUDIO_SAMPLES_PER_BUF];
pub type DspBuffer = [f32; AUDIO_SAMPLES_PER_BUF];
pub struct AudioProcessor {
static FFT_WINDOW_CELL: StaticCell<DspBuffer> = StaticCell::new();
static FFT_WINDOW: Option<&DspBuffer> = None;
#[derive(Clone, Copy, Default)]
pub struct AudioStats {
pub floating_max: i32,
pub current_powers: [f32; AUDIO_BANDS],
pub avg_powers: [f32; AUDIO_BANDS],
pub fft_buffer: [DspBuffer; AUDIO_BUFFERS],
pub next_fft_buf: usize,
fft_window: DspBuffer,
}
impl AudioProcessor {
pub fn new() -> Self {
pub struct AudioProcessor {
pub stats: AudioStats,
}
impl Default for AudioProcessor {
fn default() -> Self {
let mut buf: DspBuffer = [0f32; AUDIO_SAMPLES_PER_BUF];
unsafe {
esp_dsp::dsps_wind_hann_f32(buf.as_mut_ptr(), AUDIO_SAMPLES_PER_BUF as i32);
}
FFT_WINDOW_CELL.init(buf);
AudioProcessor {
floating_max: 0i32,
current_powers: [0f32; AUDIO_BANDS],
avg_powers: [0f32; AUDIO_BANDS],
fft_buffer: [[0f32; AUDIO_SAMPLES_PER_BUF]; AUDIO_BUFFERS],
next_fft_buf: 0,
fft_window: buf,
stats: AudioStats {
floating_max: 0i32,
current_powers: [0f32; AUDIO_BANDS],
avg_powers: [0f32; AUDIO_BANDS],
},
}
}
}
pub fn process(&mut self, audio: &AudioBuffer) -> usize {
let &(mut proc_fft_buffer) = &self.fft_buffer[self.next_fft_buf];
impl AudioProcessor {
pub fn process(&mut self, audio: &AudioBuffer, fft: &mut DspBuffer) {
/* calculate floating max */
let mut new_max = 0i32;
for value in audio {
@ -43,17 +53,20 @@ impl AudioProcessor {
}
/* get maximum */
self.floating_max = std::cmp::max(
self.stats.floating_max = std::cmp::max(
10000000,
if new_max > self.floating_max {
if new_max > self.stats.floating_max {
new_max
} else {
falloff(self.floating_max, falloff(self.floating_max, new_max))
falloff(
self.stats.floating_max,
falloff(self.stats.floating_max, new_max),
)
},
);
/* convert to floats for input to fft */
for it in audio.iter().zip(proc_fft_buffer.iter_mut()) {
for it in audio.iter().zip(fft.iter_mut()) {
let (audio_it, fft_it) = it;
*fft_it = (*audio_it as f32) / (i32::MAX as f32);
}
@ -62,9 +75,9 @@ impl AudioProcessor {
let half_sample_count = (AUDIO_SAMPLES_PER_BUF / 2) as i32;
unsafe {
esp_nofail!(esp_dsp::dsps_mul_f32_ae32(
proc_fft_buffer.as_ptr(),
self.fft_window.as_ptr(),
proc_fft_buffer.as_mut_ptr(),
fft.as_ptr(),
FFT_WINDOW.expect("windows not initialized!").as_ptr(),
fft.as_mut_ptr(),
AUDIO_SAMPLES_PER_BUF as i32,
1,
1,
@ -72,40 +85,57 @@ impl AudioProcessor {
));
esp_nofail!(esp_dsp::dsps_fft2r_fc32_aes3_(
proc_fft_buffer.as_mut_ptr(),
fft.as_mut_ptr(),
half_sample_count,
esp_dsp::dsps_fft_w_table_fc32,
)); // operating on half length but complex
esp_nofail!(esp_dsp::dsps_bit_rev2r_fc32(
proc_fft_buffer.as_mut_ptr(),
fft.as_mut_ptr(),
half_sample_count
)); // operating on half length but complex
esp_nofail!(esp_dsp::dsps_cplx2real_fc32_ae32_(
proc_fft_buffer.as_mut_ptr(),
fft.as_mut_ptr(),
half_sample_count,
esp_dsp::dsps_fft_w_table_fc32,
esp_dsp::dsps_fft_w_table_size,
)); // operating on half length but complex
for i in 0..half_sample_count as usize {
proc_fft_buffer[i] = (proc_fft_buffer[i * 2] * proc_fft_buffer[i * 2]
+ proc_fft_buffer[i * 2 + 1] * proc_fft_buffer[i * 2 + 1])
.sqrt();
fft[i] = (fft[i * 2] * fft[i * 2] + fft[i * 2 + 1] * fft[i * 2 + 1]).sqrt();
}
}
/* do band stats */
self.current_powers[0] = proc_fft_buffer[1..8].iter().sum::<f32>() / 8f32;
self.current_powers[1] = proc_fft_buffer[9..86].iter().sum::<f32>() / 78f32;
self.current_powers[2] = proc_fft_buffer[87..470].iter().sum::<f32>() / 384f32;
self.stats.current_powers[0] = fft[1..8].iter().sum::<f32>() / 8f32;
self.stats.current_powers[1] = fft[9..86].iter().sum::<f32>() / 78f32;
self.stats.current_powers[2] = fft[87..470].iter().sum::<f32>() / 384f32;
for it in self.current_powers.iter().zip(self.avg_powers.iter_mut()) {
for it in self
.stats
.current_powers
.iter()
.zip(self.stats.avg_powers.iter_mut())
{
let (current, avg) = it;
*avg = falloff_f(*avg, *current);
}
let last_fft_buf = self.next_fft_buf;
self.next_fft_buf = (self.next_fft_buf + 1) % AUDIO_BUFFERS;
last_fft_buf
}
}
#[embassy_executor::task]
pub async fn process_audio(
mut input: Receiver<'static, NoopRawMutex, AudioBuffer>,
mut output: Sender<'static, NoopRawMutex, (DspBuffer, AudioStats)>,
) {
let mut processor = AudioProcessor::default();
loop {
let in_buf = input.receive().await;
let (out_buf, stats) = output.send().await;
processor.process(in_buf, out_buf);
*stats = processor.stats;
output.send_done();
}
}

View file

@ -1,25 +1,27 @@
use esp_idf_svc::hal::units::{MilliSeconds,FromValueType};
use embassy_sync::zerocopy_channel::Sender;
use esp_idf_svc::hal::units::{FromValueType, MilliSeconds};
use crate::audio::AudioProcessor;
use crate::effects::led_effect::{LedColors, LedEffect, Rgbv};
use embassy_sync::{blocking_mutex::raw::NoopRawMutex, zerocopy_channel::Receiver};
use embassy_time::{Duration, Ticker};
use crate::audio::{AudioStats, DspBuffer};
use crate::effects::led_effect::{LedColors, LedData, LedEffect, Rgbv};
use crate::helpers::random_at_most;
use crate::LED_COUNT;
pub struct LedEffectBassSparks {
bass_color: Rgbv,
}
impl LedEffectBassSparks {
pub fn new() -> Self {
impl Default for LedEffectBassSparks {
fn default() -> Self {
Self {
bass_color: Rgbv::black(0),
}
}
}
impl LedEffect for LedEffectBassSparks {
fn render(&mut self, processed: &AudioProcessor, _fft_buf: usize, leds: &mut LedColors) -> MilliSeconds {
if processed.floating_max > 10100000
&& (processed.current_powers[0] > 1.25 * processed.avg_powers[0])
{
fn render(&mut self, _: &DspBuffer, stats: &AudioStats, leds: &mut LedColors) -> MilliSeconds {
if stats.floating_max > 10100000 && (stats.current_powers[0] > 1.25 * stats.avg_powers[0]) {
self.bass_color = Rgbv::new(127, 0, 255, 4)
}
@ -27,9 +29,9 @@ impl LedEffect for LedEffectBassSparks {
self.bass_color.decrease(3, 5, 5, 0);
if processed.floating_max > 10100000
&& (processed.current_powers[1] > 1.35 * processed.avg_powers[1])
&& (processed.current_powers[2] > 1.35 * processed.avg_powers[2])
if stats.floating_max > 10100000
&& (stats.current_powers[1] > 1.35 * stats.avg_powers[1])
&& (stats.current_powers[2] > 1.35 * stats.avg_powers[2])
{
for _ in 0..10 {
let led_index = random_at_most(LED_COUNT as u32 - 1) as usize;
@ -39,4 +41,19 @@ impl LedEffect for LedEffectBassSparks {
10.ms()
}
async fn process_led_effect(
&mut self,
mut input: Receiver<'static, NoopRawMutex, (DspBuffer, AudioStats)>,
mut output: Sender<'static, NoopRawMutex, LedData>,
) {
let mut ticker = Ticker::every(Duration::from_hz(20));
loop {
let (fft, stats) = input.receive().await;
let leds = output.send().await;
self.render(fft, stats, &mut leds.leds);
output.send_done();
ticker.next().await;
}
}
}

View file

@ -1,11 +1,22 @@
use anyhow::{bail, Result};
use bytemuck::{Pod, Zeroable};
use embassy_sync::{
blocking_mutex::raw::NoopRawMutex,
zerocopy_channel::{Receiver, Sender},
};
use esp_idf_svc::hal::units::MilliSeconds;
use crate::{config::LED_COUNT, AudioProcessor};
use crate::{audio::DspBuffer, config::LED_COUNT, AudioStats};
pub trait LedEffect {
fn render(&mut self, processed: &AudioProcessor, fft_buf: usize, leds: &mut LedColors) -> MilliSeconds;
fn render(&mut self, fft: &DspBuffer, stats: &AudioStats, leds: &mut LedColors)
-> MilliSeconds;
async fn process_led_effect(
&mut self,
input: Receiver<'static, NoopRawMutex, (DspBuffer, AudioStats)>,
output: Sender<'static, NoopRawMutex, LedData>,
);
}
#[derive(Clone, Copy, Eq, PartialEq, Pod, Zeroable)]
@ -112,8 +123,8 @@ pub struct LedData {
ones: u32,
}
impl LedData {
pub fn new() -> Self {
impl Default for LedData {
fn default() -> Self {
Self {
zeros: 0,
leds: [Rgbv::new(0, 0, 0, 0); LED_COUNT],

View file

@ -3,27 +3,31 @@ pub mod config;
pub mod effects;
pub mod helpers;
use audio::{AudioBuffer, AudioProcessor};
use bytemuck::{bytes_of, bytes_of_mut};
use esp_idf_svc::{
hal::{
delay::FreeRtos,
gpio::AnyIOPin,
i2s,
peripherals::Peripherals,
spi,
units::{FromValueType, MilliSeconds},
},
hal::{gpio::AnyIOPin, i2s, peripherals::Peripherals, spi, units::FromValueType},
sys::{esp_dsp, esp_nofail, TickType_t},
};
use bytemuck::{bytes_of, bytes_of_mut};
use embassy_executor::Spawner;
use embassy_sync::{
blocking_mutex::raw::NoopRawMutex,
zerocopy_channel::{Channel, Receiver, Sender},
};
use static_cell::StaticCell;
use audio::{process_audio, AudioBuffer, AudioStats, DspBuffer};
use config::{AUDIO_BUFFERS, AUDIO_SAMPLES_PER_BUF, LED_COUNT};
use effects::{
bass_sparks::LedEffectBassSparks,
led_effect::{LedData, LedEffect},
};
fn main() -> anyhow::Result<()> {
#[embassy_executor::task]
async fn output_leds() {}
#[embassy_executor::main]
async fn main(spawner: Spawner) {
// It is necessary to call this function once. Otherwise some patches to the runtime
// implemented by esp-idf-sys might not link properly. See https://github.com/esp-rs/esp-idf-template/issues/71
esp_idf_svc::sys::link_patches();
@ -31,15 +35,36 @@ fn main() -> anyhow::Result<()> {
// Bind the log crate to the ESP Logging facilities
esp_idf_svc::log::EspLogger::initialize_default();
// static PERIPHERALS: StaticCell<Peripherals> = StaticCell::new();
// let peripherals = PERIPHERALS.init(Peripherals::take().unwrap());
let peripherals = Peripherals::take().unwrap();
// mic buffers
static AUDIO_BUFFER_CELL: StaticCell<[AudioBuffer; AUDIO_BUFFERS]> = StaticCell::new();
let audio_buffer = AUDIO_BUFFER_CELL.init([[0; AUDIO_SAMPLES_PER_BUF]; AUDIO_BUFFERS]);
static AUDIO_CHANNEL_CELL: StaticCell<Channel<'_, NoopRawMutex, AudioBuffer>> =
StaticCell::new();
let audio_channel = AUDIO_CHANNEL_CELL.init(Channel::new(audio_buffer));
let (mut audio_sender, audio_receiver) = audio_channel.split();
// fft buffers
static FFT_BUFFER_CELL: StaticCell<[(DspBuffer, AudioStats); AUDIO_BUFFERS]> =
StaticCell::new();
let fft_buffer = FFT_BUFFER_CELL
.init([([0f32; AUDIO_SAMPLES_PER_BUF], AudioStats::default()); AUDIO_BUFFERS]);
static FFT_CHANNEL_CELL: StaticCell<Channel<'_, NoopRawMutex, (DspBuffer, AudioStats)>> =
StaticCell::new();
let fft_channel = FFT_CHANNEL_CELL.init(Channel::new(fft_buffer));
let (fft_sender, fft_receiver) = fft_channel.split();
// leds
let mut leds = LedData::new();
// audio buffers
let mut audio: [AudioBuffer; AUDIO_BUFFERS] = [[0; AUDIO_SAMPLES_PER_BUF]; AUDIO_BUFFERS];
let mut next_audio_buf: usize = 0;
static LED_DATA: StaticCell<[LedData; 1]> = StaticCell::new();
let leds = LED_DATA.init([LedData::default(); 1]);
static LED_CHANNEL: StaticCell<Channel<'_, NoopRawMutex, LedData>> = StaticCell::new();
let led_channel = LED_CHANNEL.init(Channel::new(leds));
let (led_sender, mut led_receiver) = led_channel.split();
// i2s config
let mic_i2s_per = peripherals.i2s0;
@ -79,7 +104,8 @@ fn main() -> anyhow::Result<()> {
mic_i2s_sd,
AnyIOPin::none(),
mic_i2s_ws,
)?;
)
.unwrap();
// spi config
let led_spi_per = peripherals.spi2;
@ -96,7 +122,8 @@ fn main() -> anyhow::Result<()> {
&spi::config::Config::new()
.baudrate(1.MHz().into())
.data_mode(spi::config::MODE_3),
)?;
)
.unwrap();
unsafe {
esp_nofail!(esp_dsp::dsps_fft2r_init_fc32(
@ -109,28 +136,56 @@ fn main() -> anyhow::Result<()> {
));
}
let mut processor = AudioProcessor::new();
let mut effect = LedEffectBassSparks::new();
spawner
.spawn(process_audio(audio_receiver, fft_sender))
.expect("spawn failed");
mic_drv.rx_enable()?;
#[embassy_executor::task]
async fn effect_task(
mut led_effect: LedEffectBassSparks,
input: Receiver<'static, NoopRawMutex, (DspBuffer, AudioStats)>,
output: Sender<'static, NoopRawMutex, LedData>,
) {
led_effect.process_led_effect(input, output).await;
}
spawner
.spawn(effect_task(
LedEffectBassSparks::default(),
fft_receiver,
led_sender,
))
.expect("spawn failed");
#[embassy_executor::task]
async fn output_leds() {}
mic_drv.rx_enable().expect("rx not enabled");
loop {
let buffer = bytes_of_mut(&mut audio[next_audio_buf]);
let num_bytes_read = mic_drv.read(buffer, TickType_t::MAX)?;
let audio_in_buf: &mut AudioBuffer = audio_sender.send().await;
let buffer = bytes_of_mut(audio_in_buf);
let num_bytes_read = mic_drv.read(buffer, TickType_t::MAX).unwrap();
if num_bytes_read != AUDIO_SAMPLES_PER_BUF * 4 {
log::error!("buffer underflow");
}
audio_sender.send_done();
let current_fft_buf = processor.process(&audio[next_audio_buf]);
let led_data = led_receiver.receive().await;
let delay_ms: MilliSeconds = effect.render(&processor, current_fft_buf, &mut leds.leds);
let output_buffer = bytes_of(&led_data.leds);
led_drv.write(output_buffer).expect("spi write failed");
let output_buffer = bytes_of(&leds);
led_drv.write(output_buffer)?;
// let current_fft_buf = processor.process(&audio[next_audio_buf]);
next_audio_buf = (next_audio_buf + 1) % AUDIO_BUFFERS;
// let delay_ms: MilliSeconds = effect.render(&processor, current_fft_buf, &mut leds.leds);
FreeRtos::delay_ms(delay_ms.into());
// let output_buffer = bytes_of(&leds);
// led_drv.write(output_buffer)?;
// next_audio_buf = (next_audio_buf + 1) % AUDIO_BUFFERS;
// FreeRtos::delay_ms(delay_ms.into());
}
}