270 lines
6.3 KiB
C++
270 lines
6.3 KiB
C++
/****
|
|
* Sming Framework Project - Open Source framework for high efficiency native ESP8266 development.
|
|
* Created 2015 by Skurydin Alexey
|
|
* http://github.com/SmingHub/Sming
|
|
* All files of the Sming Core are provided under the LGPL v3 license.
|
|
*
|
|
* application.cpp
|
|
*
|
|
* Basic audio sample
|
|
*
|
|
****/
|
|
|
|
#include <SmingCore.h>
|
|
#include <driver/i2s.h>
|
|
#include <ToneGenerator/DeltaSigma.h>
|
|
#include <Services/Profiling/MinMaxTimes.h>
|
|
#include <SignalGenerator.h>
|
|
|
|
// If using an external DAC, set this to 0 - you'll probably need to change other settings as well
|
|
#define ENABLE_DELTA_SIGMA
|
|
|
|
namespace
|
|
{
|
|
// Set this to 0 to output fixed values for easier checking on a scope or signal analyzer
|
|
//#define GENERATE_FIXED_VALUES
|
|
constexpr i2s_sample_t fixedSampleValue = {0xF55F0AA0};
|
|
|
|
// I2S sample rate to request from hardware
|
|
constexpr unsigned targetSampleRate = 44100;
|
|
|
|
// Target frequency to generate
|
|
constexpr float sineWaveFrequency = 440;
|
|
|
|
// Measure time taken to fill the I2S DMA buffers
|
|
Profiling::MicroTimes fillTime("Fill Time");
|
|
|
|
// Measure time taken between I2S callback (interrupt) and our task callback getting executed
|
|
Profiling::MicroTimes callbackLatency("Callback latency");
|
|
|
|
// Report status periodically
|
|
SimpleTimer statusTimer;
|
|
constexpr unsigned statusIntervalMs = 5000;
|
|
|
|
// One full sine-wave cycle
|
|
struct SineWaveTable {
|
|
std::unique_ptr<uint16_t[]> samples;
|
|
unsigned sampleCount = 0;
|
|
unsigned readPos = 0;
|
|
|
|
bool generate(float sampleRate, float frequency)
|
|
{
|
|
samples.reset();
|
|
readPos = 0;
|
|
|
|
sampleCount = round(sampleRate / frequency);
|
|
// Modify frequency to fit in exact number of samples
|
|
frequency = sampleRate / sampleCount;
|
|
|
|
Serial << _F("Generating sine wave table @ ") << frequency << _F(" Hz, ") << sampleCount << _F(" samples")
|
|
<< endl;
|
|
|
|
samples = std::make_unique<uint16_t[]>(sampleCount);
|
|
if(!samples) {
|
|
debug_e("Memory allocation failed");
|
|
return false;
|
|
}
|
|
|
|
SignalGenerator gen(eST_Sine, frequency);
|
|
for(unsigned i = 0; i < sampleCount; ++i) {
|
|
float t = i / (sampleCount * frequency);
|
|
float value = gen.getValue(t);
|
|
samples[i] = round(value * 32767);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
uint16_t read()
|
|
{
|
|
uint16_t value = samples[readPos++];
|
|
if(readPos == sampleCount) {
|
|
readPos = 0;
|
|
}
|
|
return value;
|
|
}
|
|
};
|
|
|
|
SineWaveTable sineWaveTable;
|
|
|
|
#ifdef GENERATE_FIXED_VALUES
|
|
|
|
void writeFixedValues()
|
|
{
|
|
i2s_buffer_info_t info;
|
|
while(i2s_dma_write(&info, UINT_MAX)) {
|
|
memset(info.samples, 0, info.size);
|
|
// for(unsigned i = 0; i < info.size / sizeof(i2s_sample_t); ++i) {
|
|
// info.samples[i] = fixedSampleValue;
|
|
// }
|
|
}
|
|
}
|
|
|
|
#else
|
|
|
|
/*
|
|
* Outputs a 172.266Hz sine wave (256 samples at 44100 samples/sec)
|
|
*/
|
|
void writeSine()
|
|
{
|
|
unsigned frames_written = 0;
|
|
i2s_buffer_info_t info;
|
|
while(i2s_dma_write(&info, UINT_MAX)) {
|
|
unsigned frame_count = info.size / sizeof(i2s_sample_t);
|
|
for(unsigned i = 0; i < frame_count; ++i) {
|
|
uint32_t sample = sineWaveTable.read();
|
|
#ifdef ENABLE_DELTA_SIGMA
|
|
static DeltaSigma mod;
|
|
sample = mod.update(sample);
|
|
#endif
|
|
info.samples[i].u32 = sample;
|
|
}
|
|
|
|
frames_written += frame_count;
|
|
}
|
|
}
|
|
|
|
#endif // GENERATE_FIXED_VALUES
|
|
|
|
void fillBuffers()
|
|
{
|
|
callbackLatency.update();
|
|
fillTime.start();
|
|
#ifdef GENERATE_FIXED_VALUES
|
|
writeFixedValues();
|
|
#else
|
|
writeSine();
|
|
#endif
|
|
fillTime.update();
|
|
}
|
|
|
|
void checkReceive()
|
|
{
|
|
unsigned total = 0;
|
|
i2s_buffer_info_t info;
|
|
while(i2s_dma_read(&info, UINT_MAX)) {
|
|
total += info.size;
|
|
m_printHex("DATA", info.buffer, info.size);
|
|
}
|
|
debug_i("RX: %u bytes", total);
|
|
}
|
|
|
|
void IRAM_ATTR i2sCallback(void*, i2s_event_type_t event)
|
|
{
|
|
// For this sample, process the data in task context
|
|
switch(event) {
|
|
case I2S_EVENT_TX_DONE:
|
|
System.queueCallback(fillBuffers);
|
|
callbackLatency.start();
|
|
break;
|
|
case I2S_EVENT_RX_DONE:
|
|
System.queueCallback(checkReceive);
|
|
break;
|
|
default:; // ignore
|
|
}
|
|
}
|
|
|
|
void initialiseI2S()
|
|
{
|
|
i2s_config_t config;
|
|
memset(&config, 0, sizeof(config));
|
|
const i2s_module_config_t modcfg = {
|
|
.mode = I2S_MODE_MASTER,
|
|
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
|
|
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
|
|
.communication_format = I2S_COMM_FORMAT_I2S_MSB,
|
|
.dma_buf_len = 128,
|
|
.dma_buf_count = 4,
|
|
.callback_threshold = 1,
|
|
};
|
|
|
|
/*
|
|
* Not clear exactly what this setting does.
|
|
* I think, additional bits written out.
|
|
*
|
|
* For example, 16-bit channel data + bits_mod=8 produces 24-bits per channel.
|
|
* Where do additional 8 bits come from? Scope says 0.
|
|
*/
|
|
config.bits_mod = 16;
|
|
|
|
config.tx = modcfg;
|
|
config.sample_rate = targetSampleRate;
|
|
// config.rx = modcfg;
|
|
// config.rx.mode = I2S_MODE_SLAVE;
|
|
// config.tx_desc_auto_clear = true;
|
|
// config.auto_start = true;
|
|
config.callback = i2sCallback;
|
|
config.param = nullptr; // If you want to pass a parameter to the callback routine
|
|
if(!i2s_driver_install(&config)) {
|
|
debug_e("i2s_driver_install failed");
|
|
return;
|
|
}
|
|
|
|
#ifdef ENABLE_DELTA_SIGMA
|
|
// We're doing delta mod so only need one pin
|
|
i2s_set_pins(I2S_PIN_DATA_OUT, true);
|
|
#else
|
|
// Real I2S DACs require channel select (assuming its stereo) and clock lines
|
|
i2s_set_pins(I2S_PIN_DATA_OUT | I2S_PIN_WS_OUT | I2S_PIN_BCK_OUT, true);
|
|
#endif
|
|
|
|
auto realSampleRate = i2s_get_real_rate();
|
|
Serial << _F("I2S initialised, rate = ") << realSampleRate << endl;
|
|
|
|
#ifndef GENERATE_FIXED_VALUES
|
|
/*
|
|
* Generate sine wave data using the actual sample rate, which may be
|
|
* different to that requested.
|
|
*/
|
|
if(!sineWaveTable.generate(realSampleRate, sineWaveFrequency)) {
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
// Pre-fill the buffers
|
|
fillBuffers();
|
|
|
|
// Schedule a periodic status report
|
|
statusTimer
|
|
.initializeMs<statusIntervalMs>([]() {
|
|
Serial.println(fillTime);
|
|
fillTime.clear();
|
|
Serial.println(callbackLatency);
|
|
callbackLatency.clear();
|
|
})
|
|
.start();
|
|
|
|
// Test receive
|
|
// i2s_enable_loopback(true);
|
|
|
|
// and away we go
|
|
i2s_start();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void init()
|
|
{
|
|
/*
|
|
* Primary serial port pins are used by I2S.
|
|
* We're using the alternate pin mappings - see Serial.swap() below.
|
|
* Could use the second (output-only) UART instead
|
|
*/
|
|
// Serial.setPort(UART_ID_1);
|
|
|
|
Serial.begin(SERIAL_BAUD_RATE);
|
|
Serial.systemDebugOutput(true);
|
|
|
|
// See above - must call swap after port is initialised
|
|
Serial.swap();
|
|
|
|
// Network not required for this sample
|
|
#ifndef DISABLE_WIFI
|
|
WifiStation.enable(false, false);
|
|
WifiAccessPoint.enable(false, false);
|
|
#endif
|
|
|
|
// We could initialise I2S directly here, but not in any rush
|
|
System.onReady(initialiseI2S);
|
|
}
|