Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,44 @@ cmake --build . --target CMBuild
```
This will compile the binaries, and place the .bin and .elf files in the build/ directory located in the root of the repository.

## AAC timing and seeking

`SourceAAC` measures time in source sample frames: one frame is one sample
instant across all encoded channels, at the ADTS sample rate. Channel count
does not multiply duration. The seconds-based API remains available and rounds
down to whole seconds.

ADTS indexing runs while a source is opened, before it is published to the
audio thread. Each seek entry is 8 bytes. The bounded index uses at most 128 KiB
per deck in PSRAM (16,384 frames), or 16 KiB without PSRAM (2,048 frames);
the temporary scan cache is 4 KiB. The larger strict-frame decode buffers add
35 KiB per source over the previous buffers. `getFrameIndexQuality()` reports
whether the whole track is seekable; duration remains frame-counted even when
the seek index reaches its cap. A `cm:esp32:jayd` build measured 1,026,334
bytes of flash and 44,464 bytes of static RAM: +312 bytes of flash and no
static-RAM increase versus `4ad5108`.

Seek targets use a 64-bit source-frame API, but the compact index stores 32-bit
frame positions, limiting frame-accurate seeking to the first 2^32 source
frames (about 24.9 hours at 48 kHz). Seeking starts at the preceding ADTS frame
and discards decoded PCM up to the target. Accuracy is therefore bounded by
the decoder's sample-rate conversion and AAC priming; one 256-sample output
block of accuracy has not been established on hardware.

To compile the binary, and upload it according to the port set in CMakeLists.txt, run

```cmake --build . --target CMBuild```

in the cmake directory.

## Deck rate control

`SpeedModifier` exposes a bounded Q16.16 rate (`0.5x` to `1.5x`, with `65536`
as neutral). The legacy 0-255 API maps `127` to neutral. Rate changes ramp over
at most 256 output samples and use linear interpolation without allocating in
`generate()`. This is resampling, so pitch changes with rate; key lock and time
stretching are not provided.

# Used libraries and copyright notices
[See NOTICE](https://github.com/CircuitMess/JayD-Library/blob/master/NOTICE.md)

Expand Down
109 changes: 109 additions & 0 deletions src/AudioLib/ADTSTiming.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#ifndef JAYD_ADTSTIMING_H
#define JAYD_ADTSTIMING_H

#include <stddef.h>
#include <stdint.h>

namespace ADTSTiming {

enum ParseResult : uint8_t {
INVALID,
NEED_MORE,
VALID
};

struct Header {
uint16_t frameLength;
uint16_t sourceFrames;
uint32_t sampleRate;
uint8_t channels;
uint8_t headerLength;
};

struct FrameIndexEntry {
uint32_t offset;
uint32_t sourceFrame;
};

class EofNotification {
public:
bool take(){
if(notified) return false;
notified = true;
return true;
}

void reset(){
notified = false;
}

private:
bool notified = false;
};

inline bool requiredDecodeBytes(size_t rawBlocks, size_t bytesPerBlock,
size_t capacity, size_t& required){
if(rawBlocks == 0 || bytesPerBlock == 0 ||
rawBlocks > SIZE_MAX / bytesPerBlock){
return false;
}
required = rawBlocks * bytesPerBlock;
return required <= capacity;
}

inline ParseResult parseHeader(const uint8_t* data, size_t size, Header& header){
static const uint32_t sampleRates[] = {
96000, 88200, 64000, 48000, 44100, 32000, 24000,
22050, 16000, 12000, 11025, 8000, 7350
};

if(size < 7) return NEED_MORE;
if(data[0] != 0xff || (data[1] & 0xf6) != 0xf0) return INVALID;

const uint8_t profile = (data[2] >> 6) & 0x03;
const uint8_t sampleRateIndex = (data[2] >> 2) & 0x0f;
if(profile == 3 || sampleRateIndex >= sizeof(sampleRates) / sizeof(sampleRates[0])) return INVALID;

header.headerLength = (data[1] & 0x01) ? 7 : 9;
if(size < header.headerLength) return NEED_MORE;

header.frameLength = uint16_t((uint16_t(data[3] & 0x03) << 11) |
(uint16_t(data[4]) << 3) |
(data[5] >> 5));
if(header.frameLength < header.headerLength) return INVALID;

header.sampleRate = sampleRates[sampleRateIndex];
header.channels = uint8_t(((data[2] & 0x01) << 2) | (data[3] >> 6));
header.sourceFrames = uint16_t(1024u * ((data[6] & 0x03) + 1u));
return VALID;
}

inline ParseResult parseFrame(const uint8_t* data, size_t size, Header& header){
const ParseResult result = parseHeader(data, size, header);
if(result != VALID) return result;
return header.frameLength <= size ? VALID : NEED_MORE;
}

inline bool secondsToFrames(uint64_t seconds, uint32_t sampleRate, uint64_t& frames){
if(sampleRate == 0 || seconds > UINT64_MAX / sampleRate) return false;
frames = seconds * sampleRate;
return true;
}

inline size_t findPreceding(const FrameIndexEntry* entries, size_t count, uint64_t target){
size_t low = 0;
size_t high = count;
while(low < high){
const size_t middle = low + (high - low) / 2;
if(entries[middle].sourceFrame <= target){
low = middle + 1;
}else{
high = middle;
}
}
return low == 0 ? 0 : low - 1;
}

}

#endif
109 changes: 109 additions & 0 deletions src/AudioLib/Effects/ThreeBandEQ.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#include "ThreeBandEQ.h"
#include "../../AudioSetup.hpp"
#include <cmath>
#include <cstdint>

namespace {

// Hardware calibration points: tune these against the Jay-D output path.
constexpr float LowCrossoverHz = 250.0f;
constexpr float HighCrossoverHz = 3000.0f;
constexpr float ButterworthQ = 0.70710678f;

}

static_assert(SAMPLE_RATE == 24000, "ThreeBandEQ is calibrated for 24 kHz audio");
static_assert(NUM_CHANNELS == 1, "ThreeBandEQ is calibrated for mono audio");
static_assert(BUFFER_SAMPLES == 256, "ThreeBandEQ is calibrated for 256-sample blocks");
static_assert(BYTES_PER_SAMPLE == 2, "ThreeBandEQ requires 16-bit samples");

ThreeBandEQ::ThreeBandEQ(){
for(uint8_t i = 0; i < 2; i++){
configure(lowLowPass[i], false, LowCrossoverHz);
configure(lowHighPass[i], true, LowCrossoverHz);
configure(highLowPass[i], false, HighCrossoverHz);
configure(highHighPass[i], true, HighCrossoverHz);
}
configureAllPass(highAllPass, HighCrossoverHz);
}

void ThreeBandEQ::applyEffect(int16_t* inBuffer, int16_t* outBuffer, size_t numSamples){
for(size_t i = 0; i < numSamples; i++){
const float input = inBuffer[i];
float low = lowLowPass[1].process(lowLowPass[0].process(input));
const float upper = lowHighPass[1].process(lowHighPass[0].process(input));
const float mid = highLowPass[1].process(highLowPass[0].process(upper));
const float high = highHighPass[1].process(highHighPass[0].process(upper));
low = highAllPass.process(low);

outBuffer[i] = saturate(low * gain[0] + mid * gain[1] + high * gain[2]);
}
}

void ThreeBandEQ::setIntensity(uint8_t intensity){
const uint8_t level = NeutralLevel - intensity;
setLevel(Band::Low, level);
setLevel(Band::Mid, level);
setLevel(Band::High, level);
}

bool ThreeBandEQ::setLevel(Band band, uint8_t level){
const uint8_t index = static_cast<uint8_t>(band);
if(index >= static_cast<uint8_t>(Band::Count)) return false;

gain[index] = static_cast<float>(level) / static_cast<float>(NeutralLevel);
return true;
}

void ThreeBandEQ::reset(){
for(uint8_t i = 0; i < 2; i++){
lowLowPass[i].clear();
lowHighPass[i].clear();
highLowPass[i].clear();
highHighPass[i].clear();
}
highAllPass.clear();
}

float ThreeBandEQ::Biquad::process(float input){
const float output = b0 * input + z1;
z1 = b1 * input - a1 * output + z2;
z2 = b2 * input - a2 * output;
return output;
}

void ThreeBandEQ::Biquad::clear(){
z1 = 0.0f;
z2 = 0.0f;
}

void ThreeBandEQ::configure(Biquad& filter, bool highPass, float frequency){
const float omega = 2.0f * std::acos(-1.0f) * frequency / static_cast<float>(SAMPLE_RATE);
const float cosine = std::cos(omega);
const float alpha = std::sin(omega) / (2.0f * ButterworthQ);
const float scale = 1.0f / (1.0f + alpha);
const float direction = highPass ? 1.0f : -1.0f;

filter.b0 = 0.5f * (1.0f + direction * cosine) * scale;
filter.b1 = -(1.0f + direction * cosine) * scale;
filter.b2 = filter.b0;
if(!highPass) filter.b1 = -filter.b1;
filter.a1 = -2.0f * cosine * scale;
filter.a2 = (1.0f - alpha) * scale;
}

void ThreeBandEQ::configureAllPass(Biquad& filter, float frequency){
Biquad prototype;
configure(prototype, false, frequency);
filter.b0 = prototype.a2;
filter.b1 = prototype.a1;
filter.b2 = 1.0f;
filter.a1 = prototype.a1;
filter.a2 = prototype.a2;
}

int16_t ThreeBandEQ::saturate(float sample){
if(sample >= static_cast<float>(INT16_MAX)) return INT16_MAX;
if(sample <= static_cast<float>(INT16_MIN)) return INT16_MIN;
return static_cast<int16_t>(sample);
}
53 changes: 53 additions & 0 deletions src/AudioLib/Effects/ThreeBandEQ.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#ifndef JAYD_LIBRARY_THREEBANDEQ_H
#define JAYD_LIBRARY_THREEBANDEQ_H

#include <Arduino.h>
#include "../Effect.h"

class ThreeBandEQ : public Effect {
public:
enum class Band : uint8_t {
Low,
Mid,
High,
Count
};

static constexpr uint8_t NeutralLevel = 255;
static constexpr uint8_t KillLevel = 0;

ThreeBandEQ();

void applyEffect(int16_t* inBuffer, int16_t* outBuffer, size_t numSamples) override;
void setIntensity(uint8_t intensity) override;

bool setLevel(Band band, uint8_t level);
void reset();

private:
struct Biquad {
float b0 = 0.0f;
float b1 = 0.0f;
float b2 = 0.0f;
float a1 = 0.0f;
float a2 = 0.0f;
float z1 = 0.0f;
float z2 = 0.0f;

float process(float input);
void clear();
};

Biquad lowLowPass[2];
Biquad lowHighPass[2];
Biquad highLowPass[2];
Biquad highHighPass[2];
Biquad highAllPass;
float gain[static_cast<uint8_t>(Band::Count)] = { 1.0f, 1.0f, 1.0f };

static void configure(Biquad& filter, bool highPass, float frequency);
static void configureAllPass(Biquad& filter, float frequency);
static int16_t saturate(float sample);
};

#endif //JAYD_LIBRARY_THREEBANDEQ_H
29 changes: 12 additions & 17 deletions src/AudioLib/Mixer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
#include "../AudioSetup.hpp"

//clipping wave to avoid overflows
int16_t clip(int32_t input){
int16_t clip(int32_t input){

if (input > 0x7FFF) return 0x7FFFF;
if (input < - 0x7FFF) return -0x7FFF;
if (input > INT16_MAX) return INT16_MAX;
if (input < INT16_MIN) return INT16_MIN;
return input;
}

Expand All @@ -23,25 +23,22 @@ Mixer::~Mixer()

size_t Mixer::generate(int16_t *outBuffer){
memset(outBuffer, 0, BUFFER_SIZE);
std::vector<size_t> receivedSamples(sourceList.size(), 0);
std::fill(receivedSamples.begin(), receivedSamples.end(), 0);

for(uint8_t i = 0; i < sourceList.size(); i++){
if(pauseList[i]) continue;
Generator* generator = sourceList[i];
int16_t* buffer = bufferList[i];
if(generator != nullptr && buffer != nullptr){
receivedSamples[i] = generator->generate(buffer);
if(receivedSamples[i] == 0){
pauseList[i] = true;
}
}
}

for(uint16_t i = 0; i < BUFFER_SAMPLES*NUM_CHANNELS; i++){
int32_t wave = 0;
for(uint8_t j = 0; j < sourceList.size(); j++){
if(pauseList[j]) continue;
if(bufferList[j] == nullptr || receivedSamples[j] < i/NUM_CHANNELS) break;
if(bufferList[j] == nullptr || receivedSamples[j] <= i/NUM_CHANNELS) continue;

if(sourceList.size() == 2){
wave += bufferList[j][i] * (float)((j == 1 ? (float)(mixRatio) : (float)(255.0 - mixRatio))/255.0); //use the mixer if only 2 tracks found
Expand All @@ -51,17 +48,14 @@ size_t Mixer::generate(int16_t *outBuffer){
}
outBuffer[i] = clip(wave);
}
size_t longestBuffer = *std::max_element(receivedSamples.begin(), receivedSamples.end());
size_t longestBuffer = receivedSamples.empty()
? 0
: *std::max_element(receivedSamples.begin(), receivedSamples.end());

if(longestBuffer == 0){
bool allPaused = true;
for(bool p : pauseList){
allPaused &= p;
}

if(allPaused){
return BUFFER_SAMPLES;
}
// SD-backed decoders can briefly underflow; silence this block without
// turning a recoverable read delay into a persistent deck pause.
return BUFFER_SAMPLES;
}

return longestBuffer;
Expand All @@ -86,6 +80,7 @@ void Mixer::addSource(Generator* generator){
}

bufferList.push_back(buffer);
receivedSamples.push_back(0);
pauseList.push_back(false);
}

Expand Down
Loading