arduino-audio-tools
Loading...
Searching...
No Matches
PitchShift.h
Go to the documentation of this file.
1
37#pragma once
38#include <math.h>
39#include <stdio.h>
40#include <string.h>
41
44#include "AudioToolsConfig.h"
45
46namespace audio_tools {
47
59struct PitchShiftInfo : public AudioInfo {
61 channels = 2;
62 sample_rate = 44100;
63 bits_per_sample = 16;
64 }
65
67 float pitch_shift = 1.4f;
68
70 int buffer_size = 1000;
71};
72
87template <typename T = int16_t>
89 public:
95 VariableSpeedRingBufferSimple(int size = 0, float increment = 1.0) {
96 setIncrement(increment);
97 if (size > 0) resize(size);
98 }
99
104 void setIncrement(float increment) {
105#if PREFER_FIXEDPOINT
106 read_increment_fixed = (int32_t)roundf(increment * 65536.0f);
107#else
108 read_increment = increment;
109#endif
110 }
111
117 bool resize(size_t size) {
119 return buffer.resize(size);
120 }
121
127 bool read(T &result) {
128 peek(result);
129#if PREFER_FIXEDPOINT
130 read_pos_fixed += read_increment_fixed;
131 // on buffer overflow reset to beginning. Uses >= (unlike the float
132 // path's >): with an integer increment (e.g. 1.0) the position can land
133 // exactly on bufferSizeFixed(), and > would leave it there -- an index
134 // one past the end of the buffer on the next peek().
135 if (read_pos_fixed >= bufferSizeFixed()) {
136 read_pos_fixed -= bufferSizeFixed();
137 }
138#else
140 // on buffer overflow reset to beginning
143 }
144#endif
145 return true;
146 }
147
153 bool peek(T &result) {
154 if (buffer.size() == 0) {
155 LOGE("buffer has no memory");
156 result = 0;
157 } else {
158#if PREFER_FIXEDPOINT
159 result = buffer[(int)(read_pos_fixed >> 16)];
160#else
161 result = buffer[(int)read_pos_float];
162#endif
163 }
164 return true;
165 }
166
172 bool write(T sample) {
173 if (buffer.size() == 0) {
174 LOGE("buffer has no memory");
175 return false;
176 }
177 buffer[write_pos++] = sample;
178 // on buffer overflow reset to 0
179 if (write_pos >= buffer_size) {
180 write_pos = 0;
181 }
182 return true;
183 }
184
186 void reset() {
187#if PREFER_FIXEDPOINT
188 read_pos_fixed = 0;
189#else
190 read_pos_float = 0;
191#endif
192 write_pos = 0;
193 memset(buffer.data(), 0, sizeof(T) * buffer_size);
194 }
195
196 virtual bool isFull() { return false; }
197 virtual int available() { return buffer_size; }
198 virtual int availableForWrite() { return buffer_size; }
199 virtual T *address() { return nullptr; }
200 size_t size() { return buffer_size; }
201
202 protected:
204 int buffer_size = 0;
205 int write_pos = 0;
206#if PREFER_FIXEDPOINT
207 // Q16.16 fixed-point read position/increment: a plain int32_t add/compare
208 // per read() instead of a float add + compare, with the buffer index a
209 // shift instead of a float->int truncation -- no FPU needed on FPU-less
210 // MCUs, at the same 4-byte width as the float it replaces (int64_t would
211 // needlessly double the size of exactly the fields this optimization
212 // touches, on exactly the RAM-constrained MCUs it targets). int32_t caps
213 // buffer_size at < 32768 (buffer_size << 16 must fit in int32) -- not a
214 // real constraint here: 32767 samples is ~743ms of buffer at 44.1kHz,
215 // already far beyond a practical pitch-shift buffer (typically hundreds
216 // to low thousands of samples), and this path's target platforms don't
217 // have the RAM for a buffer that large anyway.
218 int32_t read_pos_fixed = 0;
219 int32_t read_increment_fixed = 1 << 16;
220 int32_t bufferSizeFixed() const { return buffer_size << 16; }
221#else
222 float read_pos_float = 0.0;
223 float read_increment = 1.0;
224#endif
225};
226
241template <typename T = int16_t>
243 public:
249 VariableSpeedRingBuffer180(int size = 0, float increment = 1.0) {
250 setIncrement(increment);
251 if (size > 0) resize(size);
252 }
253
258 void setIncrement(float increment) { pitch_shift = increment; }
259
265 bool resize(size_t size) {
267 overlap = buffer_size / 10;
268 return buffer.resize(size);
269 }
270
276 bool read(T &result) {
277 result = pitchRead();
278 return true;
279 }
280
286 bool peek(T &result) { return false; }
287
293 bool write(T sample) {
294 if (buffer.size() == 0) {
295 LOGE("buffer has no memory");
296 return false;
297 }
298 // write_pointer value is used in pitchRead()
300 buffer[write_pos++] = sample;
301 // on buffer overflow reset to 0
302 if (write_pos >= buffer_size) {
303 write_pos = 0;
304 }
305 return true;
306 }
307
309 void reset() {
310 read_pos_float = 0;
311 write_pos = 0;
312 cross_fade = 1.0f;
313 overlap = buffer_size / 10;
314 memset(buffer.data(), 0, sizeof(T) * buffer_size);
315 }
316
317 virtual bool isFull() { return false; }
318 virtual int available() { return buffer_size; }
319 virtual int availableForWrite() { return buffer_size; }
320 virtual T *address() { return nullptr; }
321 size_t size() { return buffer_size; }
322
323 protected:
325 float read_pos_float = 0.0;
326 float cross_fade = 1.0;
327 int write_pos = 0;
329 int buffer_size = 0;
330 int overlap = 0;
331 float pitch_shift = 0;
332
348 virtual T pitchRead() {
349 TRACED();
350 assert(pitch_shift > 0);
351 assert(buffer_size > 0);
352
353 // read fractional readpointer and generate 0° and 180° read-pointer in
354 // integer
355 int read_pointer_int = roundf(read_pos_float);
356 int read_pointer_int180 = 0;
357 if (read_pointer_int >= buffer_size / 2)
358 read_pointer_int180 = read_pointer_int - (buffer_size / 2);
359 else
360 read_pointer_int180 = read_pointer_int + (buffer_size / 2);
361
362 // read the two samples...
363 float read_sample = (float)buffer[read_pointer_int];
364 float read_sample_180 = (float)buffer[read_pointer_int180];
365
366 // Check if first readpointer starts overlap with write pointer?
367 // if yes -> do cross-fade to second read-pointer
368 if (overlap >= (write_pointer - read_pointer_int) &&
369 (write_pointer - read_pointer_int) >= 0 && pitch_shift != 1.0f) {
370 int rel = write_pointer - read_pointer_int;
371 cross_fade = ((float)rel) / (float)overlap;
372 } else if (write_pointer - read_pointer_int == 0)
373 cross_fade = 0.0f;
374
375 // Check if second readpointer starts overlap with write pointer?
376 // if yes -> do cross-fade to first read-pointer
377 if (overlap >= (write_pointer - read_pointer_int180) &&
378 (write_pointer - read_pointer_int180) >= 0 && pitch_shift != 1.0f) {
379 int rel = write_pointer - read_pointer_int180;
380 cross_fade = 1.0f - ((float)rel) / (float)overlap;
381 } else if (write_pointer - read_pointer_int180 == 0)
382 cross_fade = 1.0f;
383
384 // do cross-fading and sum up
385 T sum = (read_sample * cross_fade + read_sample_180 * (1.0f - cross_fade));
386
387 // increment fractional read-pointer and write-pointer
389 if (roundf(read_pos_float) >= buffer_size) read_pos_float = 0.0f;
390
391 return sum;
392 }
393};
394
411template <typename T = int16_t>
413 public:
419 VariableSpeedRingBuffer(int size = 0, float increment = 1.0) {
420 setIncrement(increment);
421 if (size > 0) resize(size);
422 }
423
428 void setIncrement(float increment) { read_increment = increment; }
429
435 bool resize(size_t size) {
437 // prevent an overrun at the start
438 read_pos_float = size / 2;
439 return buffer.resize(size);
440 }
441
442 bool read(T &result) {
443 assert(read_increment != 0.0f);
444 peek(result);
449 }
450 return true;
451 }
452
453 bool peek(T &result) {
454 if (buffer.size() == 0) {
455 result = 0;
456 } else {
457 result = interpolate(read_pos_float);
458 }
459 return true;
460 }
461
462 bool write(T sample) {
463 if (buffer.size() == 0) return false;
465 buffer[write_pos++] = sample;
466 // on buffer overflow reset to 0
467 if (write_pos >= buffer_size) {
468 write_pos = 0;
469 }
470 return true;
471 }
472
474 void reset() {
475 read_pos_float = 0;
476 write_pos = 0;
477 memset(buffer.data(), 0, sizeof(T) * buffer_size);
478 }
479
480 virtual bool isFull() { return false; }
481 virtual int available() { return buffer_size; }
482 virtual int availableForWrite() { return buffer_size; }
483 virtual T *address() { return nullptr; }
484 size_t size() { return buffer_size; }
485
486 protected:
489 float read_pos_float = 0.0f;
490 float read_increment = 0.0f;
491 int write_pos = 0;
492 // used to handle overruns:
495
506 T interpolate(float read_pos) {
507 int read_pos_int = read_pos;
508 T value1 = getValue(read_pos_int);
509 T value2 = getValue(read_pos_int + 1);
510 incrementing = value2 - value1 >= 0;
511
512 // make sure that value1 is smaller then value 2
513 if (value2 < value1) {
514 T tmp = value2;
515 value2 = value1;
516 value1 = tmp;
517 }
518 // the result must be between value 1 and value 2: linear interpolation
519 float offset_in = read_pos - read_pos_int; // calculate fraction: e.g 0.5
520 LOGD("read_pos=%f read_pos_int=%d, offset_in=%f", read_pos, read_pos_int,
521 offset_in);
522 float diff_result =
523 abs(value2 - value1); // differrence between values: e.g. 10
524 float offset_result = offset_in * diff_result; // 0.5 * 10 = 5
525 float result = offset_result + value1;
526 LOGD("interpolate %d %d -> %f -> %f", value1, value2, offset_result,
527 result);
528
529 last_value = result;
530
531 return result;
532 }
533
539 T getValue(int pos) { return buffer[pos % buffer_size]; }
540
554 bool isMatching(T value1, bool incrementing, T v1, T v2) {
555 bool v_incrementing = v2 - v1 >= 0;
556 // eff sample was ascending so we need to select a ascending value
557 if (incrementing && v_incrementing && value1 >= v1 && value1 <= v2) {
558 return true;
559 }
560 // eff sample was descending so we need to select a descending value
561 if (!incrementing && !v_incrementing && value1 <= v1 && value1 >= v2) {
562 return true;
563 }
564 return false;
565 }
566
584 // handle overflow - we need to allign the phase
585 int read_pos_int = read_pos_float; // round down
586 if (write_pos == read_pos_int ||
587 write_pos == (buffer_size % (read_pos_int + 1))) {
588 LOGD("handleReadWriteOverrun write_pos=%d read_pos_int=%d", write_pos,
589 read_pos_int);
590 bool found = false;
591
592 // find the closest match for the last value
593 for (int j = read_increment * 2; j < buffer_size; j++) {
594 int pos = read_pos_int + j;
595 float v1 = getValue(pos);
596 float v2 = getValue(pos + 1);
597 // find corresponging matching sample in buffer for last sample
598 if (isMatching(last_value, incrementing, v1, v2)) {
599 // interpolate new position
600 float diff_value = abs(v1 - v2);
601 float diff_last_value = abs(v1 - last_value);
602 float fraction = 0;
603 if (diff_value > 0) {
604 fraction = diff_last_value / diff_value;
605 }
606
607 read_pos_float = fraction + pos;
608 // move to next value
610 // if we are at the end of the buffer we restart from 0
613 }
614 LOGD("handleReadWriteOverrun -> read_pos pos=%d pos_float=%f", pos,
616 found = true;
617 break;
618 }
619 }
620 if (!found) {
621 LOGW("phase allign failed: maybe the buffer is too small")
622 }
623 }
624 }
625};
626
651template <typename T, class BufferT>
653 public:
658 PitchShiftOutput(Print &out) { p_out = &out; }
659
665 PitchShiftInfo result;
666 result.bits_per_sample = sizeof(T) * 8;
667 return result;
668 }
669
676 TRACED();
677 cfg = info;
678 return begin();
679 }
680
685 bool begin() {
687 buffer.resize(cfg.buffer_size);
688 buffer.reset();
689 buffer.setIncrement(cfg.pitch_shift);
690 active = true;
691 return active;
692 }
693
706 size_t write(const uint8_t *data, size_t len) override {
707 LOGD("PitchShiftOutput::write %d bytes", (int)len);
708 if (!active) return 0;
709
710 size_t result = 0;
711 int channels = cfg.channels;
712 T *p_in = (T *)data;
713 int sample_count = len / sizeof(T);
714
715 for (int j = 0; j < sample_count; j += channels) {
716 float value = 0;
717 for (int ch = 0; ch < channels; ch++) {
718 value += p_in[j + ch];
719 }
720 // calculate avg sample value
721 value /= cfg.channels;
722
723 // output values
724 T out_value = pitchShift(value);
725 LOGD("PitchShiftOutput %f -> %d", value, (int)out_value);
726 T out_array[channels];
727 for (int ch = 0; ch < channels; ch++) {
728 out_array[ch] = out_value;
729 }
730 result += p_out->write((uint8_t *)out_array, sizeof(T) * channels);
731 }
732 return result;
733 }
734
738 void end() { active = false; }
739
740 protected:
741 BufferT buffer;
743 Print *p_out = nullptr;
744 bool active = false;
745
757 T pitchShift(T value) {
758 TRACED();
759 if (!active) return 0;
760 buffer.write(value);
761 T result = 0;
762 buffer.read(result);
763 return result;
764 }
765};
766
767} // namespace audio_tools
#define LOGW(...)
Definition AudioLoggerIDF.h:29
#define TRACED()
Definition AudioLoggerIDF.h:31
#define LOGD(...)
Definition AudioLoggerIDF.h:27
#define LOGE(...)
Definition AudioLoggerIDF.h:30
#define assert(T)
Definition avr.h:10
Definition Arduino.h:56
virtual size_t write(const uint8_t *data, size_t len)
Definition Arduino.h:120
Abstract Audio Ouptut class.
Definition AudioOutput.h:25
virtual void setAudioInfo(AudioInfo newInfo) override
Defines the input AudioInfo.
Definition AudioOutput.h:49
Shared functionality of all buffers.
Definition Buffers.h:23
Real-time pitch shifting audio effect.
Definition PitchShift.h:652
bool active
Whether pitch shifting is currently active.
Definition PitchShift.h:744
bool begin(PitchShiftInfo info)
Initialize pitch shifting with configuration.
Definition PitchShift.h:675
bool begin()
Initialize pitch shifting with current configuration.
Definition PitchShift.h:685
size_t write(const uint8_t *data, size_t len) override
Process and write audio data with pitch shifting applied.
Definition PitchShift.h:706
Print * p_out
Output stream for processed audio.
Definition PitchShift.h:743
void end()
Stop pitch shifting and deactivate the effect.
Definition PitchShift.h:738
BufferT buffer
Variable speed buffer for pitch shifting.
Definition PitchShift.h:741
T pitchShift(T value)
Execute pitch shift on a single sample.
Definition PitchShift.h:757
PitchShiftInfo cfg
Current configuration.
Definition PitchShift.h:742
PitchShiftInfo defaultConfig()
Get default configuration for pitch shifting.
Definition PitchShift.h:664
PitchShiftOutput(Print &out)
Constructor.
Definition PitchShift.h:658
Variable speed ring buffer with 180-degree phase shifting.
Definition PitchShift.h:242
virtual T * address()
returns the address of the start of the physical read buffer
Definition PitchShift.h:320
size_t size()
Definition PitchShift.h:321
int write_pointer
Definition PitchShift.h:328
void setIncrement(float increment)
Set the pitch shift factor.
Definition PitchShift.h:258
virtual int availableForWrite()
provides the number of entries that are available to write
Definition PitchShift.h:319
virtual int available()
provides the number of entries that are available to read
Definition PitchShift.h:318
bool peek(T &result)
Peek operation not supported in this buffer implementation.
Definition PitchShift.h:286
int write_pos
Definition PitchShift.h:327
float pitch_shift
Definition PitchShift.h:331
bool write(T sample)
Write a sample to the buffer.
Definition PitchShift.h:293
virtual bool isFull()
checks if the buffer is full
Definition PitchShift.h:317
Vector< T > buffer
Definition PitchShift.h:324
bool read(T &result)
Read the next pitch-shifted sample.
Definition PitchShift.h:276
void reset()
Reset pointer positions and clear buffer.
Definition PitchShift.h:309
virtual T pitchRead()
Core pitch shifting algorithm with 180° phase offset blending.
Definition PitchShift.h:348
float cross_fade
Definition PitchShift.h:326
float read_pos_float
Definition PitchShift.h:325
VariableSpeedRingBuffer180(int size=0, float increment=1.0)
Constructor.
Definition PitchShift.h:249
bool resize(size_t size)
Resize the internal buffer and recalculate overlap region.
Definition PitchShift.h:265
int overlap
Definition PitchShift.h:330
int buffer_size
Definition PitchShift.h:329
Optimized buffer implementation for pitch shifting with interpolation.
Definition PitchShift.h:412
virtual T * address()
returns the address of the start of the physical read buffer
Definition PitchShift.h:483
size_t size()
Definition PitchShift.h:484
float read_increment
Definition PitchShift.h:490
VariableSpeedRingBuffer(int size=0, float increment=1.0)
Constructor.
Definition PitchShift.h:419
void setIncrement(float increment)
Set the reading speed increment for pitch shifting.
Definition PitchShift.h:428
T interpolate(float read_pos)
Calculate exact sample value for fractional buffer position using linear interpolation.
Definition PitchShift.h:506
virtual int availableForWrite()
provides the number of entries that are available to write
Definition PitchShift.h:482
virtual int available()
provides the number of entries that are available to read
Definition PitchShift.h:481
bool peek(T &result)
peeks the actual entry from the buffer
Definition PitchShift.h:453
int write_pos
Definition PitchShift.h:491
void handleReadWriteOverrun(T last_value)
Handle read/write pointer collisions with phase alignment.
Definition PitchShift.h:583
bool write(T sample)
write add an entry to the buffer
Definition PitchShift.h:462
virtual bool isFull()
checks if the buffer is full
Definition PitchShift.h:480
bool incrementing
Track if last read trend was increasing or decreasing.
Definition PitchShift.h:494
Vector< T > buffer
Definition PitchShift.h:487
bool read(T &result)
reads a single value
Definition PitchShift.h:442
void reset()
Reset pointer positions and clear buffer.
Definition PitchShift.h:474
T getValue(int pos)
Get buffer value with wraparound support.
Definition PitchShift.h:539
float read_pos_float
Definition PitchShift.h:489
bool resize(size_t size)
Resize buffer and set initial read position to prevent immediate overrun.
Definition PitchShift.h:435
T last_value
Record last read value for phase alignment.
Definition PitchShift.h:493
int buffer_size
Definition PitchShift.h:488
bool isMatching(T value1, bool incrementing, T v1, T v2)
Check if a value fits between two samples considering trend direction.
Definition PitchShift.h:554
Very Simple Buffer implementation for Pitch Shift.
Definition PitchShift.h:88
virtual T * address()
returns the address of the start of the physical read buffer
Definition PitchShift.h:199
size_t size()
Definition PitchShift.h:200
float read_increment
Definition PitchShift.h:223
void setIncrement(float increment)
Set the reading speed increment.
Definition PitchShift.h:104
virtual int availableForWrite()
provides the number of entries that are available to write
Definition PitchShift.h:198
virtual int available()
provides the number of entries that are available to read
Definition PitchShift.h:197
bool peek(T &result)
Peek at the current sample without advancing the read pointer.
Definition PitchShift.h:153
VariableSpeedRingBufferSimple(int size=0, float increment=1.0)
Constructor.
Definition PitchShift.h:95
int write_pos
Definition PitchShift.h:205
bool write(T sample)
Write a sample to the buffer.
Definition PitchShift.h:172
virtual bool isFull()
checks if the buffer is full
Definition PitchShift.h:196
Vector< T > buffer
Definition PitchShift.h:203
bool read(T &result)
Read the next sample and advance the read pointer.
Definition PitchShift.h:127
void reset()
Reset pointer positions and clear buffer.
Definition PitchShift.h:186
float read_pos_float
Definition PitchShift.h:222
bool resize(size_t size)
Resize the internal buffer.
Definition PitchShift.h:117
int buffer_size
Definition PitchShift.h:204
Vector implementation which provides the most important methods as defined by std::vector....
Definition Vector.h:21
Generic Implementation of sound input and output for desktop environments using portaudio.
Definition LMSEchoCancellationStream.h:6
Basic Audio information which drives e.g. I2S.
Definition AudioTypes.h:51
sample_rate_t sample_rate
Sample Rate: e.g 44100.
Definition AudioTypes.h:53
uint16_t channels
Number of channels: 2=stereo, 1=mono.
Definition AudioTypes.h:55
uint8_t bits_per_sample
Number of bits per sample (int16_t = 16 bits)
Definition AudioTypes.h:57
Configuration for PitchShiftOutput.
Definition PitchShift.h:59
PitchShiftInfo()
Definition PitchShift.h:60
float pitch_shift
Pitch shift factor: 1.0 = no change, >1.0 = higher pitch, <1.0 = lower pitch.
Definition PitchShift.h:67
int buffer_size
Size of the internal buffer used for pitch shifting (affects quality and latency)
Definition PitchShift.h:70