initial commit
This commit is contained in:
5
lib/audio_output/library.json
Normal file
5
lib/audio_output/library.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"build": {
|
||||
"flags": "-Ofast"
|
||||
}
|
||||
}
|
||||
30
lib/audio_output/src/DACOutput.cpp
Normal file
30
lib/audio_output/src/DACOutput.cpp
Normal file
@@ -0,0 +1,30 @@
|
||||
#include "DACOutput.h"
|
||||
|
||||
#if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3
|
||||
|
||||
void DACOutput::start(int sample_rate)
|
||||
{
|
||||
// i2s config for writing both channels of I2S
|
||||
i2s_config_t i2s_config = {
|
||||
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_DAC_BUILT_IN),
|
||||
.sample_rate = sample_rate,
|
||||
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
|
||||
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
|
||||
.communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_I2S_MSB),
|
||||
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
|
||||
.dma_buf_count = 2,
|
||||
.dma_buf_len = 1024,
|
||||
.use_apll = false,
|
||||
.tx_desc_auto_clear = true,
|
||||
.fixed_mclk = 0};
|
||||
//install and start i2s driver
|
||||
i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
|
||||
// enable the DAC channels
|
||||
i2s_set_dac_mode(I2S_DAC_CHANNEL_BOTH_EN);
|
||||
// clear the DMA buffers
|
||||
i2s_zero_dma_buffer(I2S_NUM_0);
|
||||
|
||||
i2s_start(I2S_NUM_0);
|
||||
}
|
||||
|
||||
#endif
|
||||
20
lib/audio_output/src/DACOutput.h
Normal file
20
lib/audio_output/src/DACOutput.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <driver/i2s.h>
|
||||
|
||||
#include "Output.h"
|
||||
|
||||
/**
|
||||
* Base Class for both the ADC and I2S sampler
|
||||
**/
|
||||
class DACOutput : public Output
|
||||
{
|
||||
public:
|
||||
DACOutput(i2s_port_t i2s_port) : Output(i2s_port) {}
|
||||
void start(int sample_rate);
|
||||
virtual int16_t process_sample(int16_t sample)
|
||||
{
|
||||
// DAC needs unsigned 16 bit samples
|
||||
return sample + 32768;
|
||||
}
|
||||
};
|
||||
35
lib/audio_output/src/I2SOutput.cpp
Normal file
35
lib/audio_output/src/I2SOutput.cpp
Normal file
@@ -0,0 +1,35 @@
|
||||
|
||||
#include "I2SOutput.h"
|
||||
|
||||
I2SOutput::I2SOutput(i2s_port_t i2s_port, i2s_pin_config_t &i2s_pins) : Output(i2s_port), m_i2s_pins(i2s_pins)
|
||||
{
|
||||
}
|
||||
|
||||
void I2SOutput::start(int sample_rate)
|
||||
{
|
||||
// i2s config for writing both channels of I2S
|
||||
i2s_config_t i2s_config = {
|
||||
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
|
||||
.sample_rate = sample_rate,
|
||||
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
|
||||
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0)
|
||||
.communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_STAND_I2S),
|
||||
#else
|
||||
.communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_I2S),
|
||||
#endif
|
||||
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
|
||||
.dma_buf_count = 2,
|
||||
.dma_buf_len = 1024,
|
||||
.use_apll = false,
|
||||
.tx_desc_auto_clear = true,
|
||||
.fixed_mclk = 0};
|
||||
//install and start i2s driver
|
||||
i2s_driver_install(m_i2s_port, &i2s_config, 0, NULL);
|
||||
// set up the i2s pins
|
||||
i2s_set_pin(m_i2s_port, &m_i2s_pins);
|
||||
// clear the DMA buffers
|
||||
i2s_zero_dma_buffer(m_i2s_port);
|
||||
|
||||
i2s_start(m_i2s_port);
|
||||
}
|
||||
16
lib/audio_output/src/I2SOutput.h
Normal file
16
lib/audio_output/src/I2SOutput.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "Output.h"
|
||||
|
||||
/**
|
||||
* Base Class for both the ADC and I2S sampler
|
||||
**/
|
||||
class I2SOutput : public Output
|
||||
{
|
||||
private:
|
||||
i2s_pin_config_t m_i2s_pins;
|
||||
|
||||
public:
|
||||
I2SOutput(i2s_port_t i2s_port, i2s_pin_config_t &i2s_pins);
|
||||
void start(int sample_rate);
|
||||
};
|
||||
51
lib/audio_output/src/Output.cpp
Normal file
51
lib/audio_output/src/Output.cpp
Normal file
@@ -0,0 +1,51 @@
|
||||
|
||||
#include "Output.h"
|
||||
#include <esp_log.h>
|
||||
#include <driver/i2s.h>
|
||||
|
||||
static const char *TAG = "OUT";
|
||||
|
||||
// number of frames to try and send at once (a frame is a left and right sample)
|
||||
const int NUM_FRAMES_TO_SEND = 256;
|
||||
|
||||
Output::Output(i2s_port_t i2s_port) : m_i2s_port(i2s_port)
|
||||
{
|
||||
// this will contain the prepared samples for sending to the I2S device
|
||||
m_frames = (int16_t *)malloc(2 * sizeof(int16_t) * NUM_FRAMES_TO_SEND);
|
||||
}
|
||||
|
||||
Output::~Output()
|
||||
{
|
||||
free(m_frames);
|
||||
}
|
||||
|
||||
void Output::stop()
|
||||
{
|
||||
// stop the i2S driver
|
||||
i2s_stop(m_i2s_port);
|
||||
i2s_driver_uninstall(m_i2s_port);
|
||||
}
|
||||
|
||||
void Output::write(int16_t *samples, int count)
|
||||
{
|
||||
int sample_index = 0;
|
||||
while (sample_index < count)
|
||||
{
|
||||
int samples_to_send = 0;
|
||||
for (int i = 0; i < NUM_FRAMES_TO_SEND && sample_index < count; i++)
|
||||
{
|
||||
int sample = process_sample(samples[sample_index]);
|
||||
m_frames[i * 2] = sample; // left channel
|
||||
m_frames[i * 2 + 1] = sample; // right channel
|
||||
samples_to_send++;
|
||||
sample_index++;
|
||||
}
|
||||
// write data to the i2s peripheral
|
||||
size_t bytes_written = 0;
|
||||
i2s_write(m_i2s_port, m_frames, samples_to_send * sizeof(int16_t) * 2, &bytes_written, portMAX_DELAY);
|
||||
if (bytes_written != samples_to_send * sizeof(int16_t) * 2)
|
||||
{
|
||||
ESP_LOGE(TAG, "Did not write all bytes");
|
||||
}
|
||||
}
|
||||
}
|
||||
27
lib/audio_output/src/Output.h
Normal file
27
lib/audio_output/src/Output.h
Normal file
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <driver/i2s.h>
|
||||
|
||||
/**
|
||||
* Base Class for both the DAC and I2S output
|
||||
**/
|
||||
class Output
|
||||
{
|
||||
private:
|
||||
int16_t *m_frames;
|
||||
|
||||
protected:
|
||||
i2s_port_t m_i2s_port = I2S_NUM_0;
|
||||
|
||||
public:
|
||||
Output(i2s_port_t i2s_port);
|
||||
~Output();
|
||||
virtual void start(int sample_rate) = 0;
|
||||
void stop();
|
||||
// override this in derived classes to turn the sample into
|
||||
// something the output device expects - for the default case
|
||||
// this is simply a pass through
|
||||
virtual int16_t process_sample(int16_t sample) { return sample; }
|
||||
void write(int16_t *samples, int count);
|
||||
};
|
||||
99
lib/audio_output/src/OutputBuffer.h
Normal file
99
lib/audio_output/src/OutputBuffer.h
Normal file
@@ -0,0 +1,99 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
|
||||
/**
|
||||
* @brief Circular buffer for 8 bit unsigned PCM samples
|
||||
*
|
||||
*/
|
||||
class OutputBuffer
|
||||
{
|
||||
private:
|
||||
// how many samples should we buffer before outputting data?
|
||||
int m_number_samples_to_buffer;
|
||||
// where are we reading from
|
||||
int m_read_head;
|
||||
// where are we writing to
|
||||
int m_write_head;
|
||||
// keep track of how many samples we have
|
||||
int m_available_samples;
|
||||
// the total size of the buffer
|
||||
int m_buffer_size;
|
||||
// are we currently buffering samples?
|
||||
bool m_buffering;
|
||||
// the sample buffer
|
||||
uint8_t *m_buffer;
|
||||
// thread safety
|
||||
SemaphoreHandle_t m_semaphore;
|
||||
|
||||
public:
|
||||
OutputBuffer(int number_samples_to_buffer) : m_number_samples_to_buffer(number_samples_to_buffer)
|
||||
{
|
||||
// create a semaphore and make it available for locking
|
||||
m_semaphore = xSemaphoreCreateBinary();
|
||||
xSemaphoreGive(m_semaphore);
|
||||
// set reading and writing to the beginning of the buffer
|
||||
m_read_head = 0;
|
||||
m_write_head = 0;
|
||||
m_available_samples = 0;
|
||||
// we'll start off buffering data as we have no samples yet
|
||||
m_buffering = true;
|
||||
// make sufficient space for the bufferring and incoming data
|
||||
m_buffer_size = 3 * number_samples_to_buffer;
|
||||
m_buffer = (uint8_t *)malloc(m_buffer_size);
|
||||
memset(m_buffer, 0, m_buffer_size);
|
||||
if (!m_buffer)
|
||||
{
|
||||
Serial.println("Failed to allocate buffer");
|
||||
}
|
||||
}
|
||||
|
||||
// we're adding samples that are 8 bit as they are coming from the transport
|
||||
void add_samples(const uint8_t *samples, int count)
|
||||
{
|
||||
xSemaphoreTake(m_semaphore, portMAX_DELAY);
|
||||
// copy the samples into the buffer wrapping around as needed
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
m_buffer[m_write_head] = samples[i];
|
||||
m_write_head = (m_write_head + 1) % m_buffer_size;
|
||||
}
|
||||
m_available_samples += count;
|
||||
xSemaphoreGive(m_semaphore);
|
||||
}
|
||||
|
||||
// convert the samples to 16 bit as they are going to the output
|
||||
void remove_samples(int16_t *samples, int count)
|
||||
{
|
||||
xSemaphoreTake(m_semaphore, portMAX_DELAY);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
samples[i] = 0;
|
||||
// if we have no samples and we aren't already buffering then we need to start buffering
|
||||
if (m_available_samples == 0 && !m_buffering)
|
||||
{
|
||||
Serial.println("Buffering");
|
||||
m_buffering = true;
|
||||
samples[i] = 0;
|
||||
}
|
||||
// are we buffering?
|
||||
if (m_buffering && m_available_samples < m_number_samples_to_buffer)
|
||||
{
|
||||
// just return 0 as we don't have enough samples yet
|
||||
samples[i] = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// we've buffered enough samples so no need to buffer anymore
|
||||
m_buffering = false;
|
||||
// just send back the samples we've got and move the read head forward
|
||||
int16_t sample = m_buffer[m_read_head];
|
||||
samples[i] = (sample - 128) << 5;
|
||||
m_read_head = (m_read_head + 1) % m_buffer_size;
|
||||
m_available_samples--;
|
||||
}
|
||||
}
|
||||
xSemaphoreGive(m_semaphore);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user