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
4 changes: 4 additions & 0 deletions locale/circuitpython.pot
Original file line number Diff line number Diff line change
Expand Up @@ -4104,6 +4104,10 @@ msgstr ""
msgid "pull masks conflict with direction masks"
msgstr ""

#: shared-bindings/audiocore/WaveFile.c
msgid "rate must be positive"
msgstr ""

#: extmod/ulab/code/numpy/fft/fft_tools.c
msgid "real and imaginary parts must be of equal length"
msgstr ""
Expand Down
26 changes: 26 additions & 0 deletions shared-bindings/audiocore/WaveFile.c
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,32 @@ static MP_DEFINE_CONST_FUN_OBJ_1(audioio_wavefile_deinit_obj, audioio_wavefile_d
//| channel_count: int
//| """Number of audio channels. (read only)"""
//|

//| rate: float
//| """Playback speed as a floating-point multiplier. 1.0 is normal speed,
//| 2.0 is double speed, 0.5 is half speed. Uses phase accumulation with
//| nearest-neighbor resampling. Default is 1.0."""
//|
static mp_obj_t audioio_wavefile_obj_get_rate(mp_obj_t self_in) {
audioio_wavefile_obj_t *self = MP_OBJ_TO_PTR(self_in);
return mp_obj_new_float(common_hal_audioio_wavefile_get_rate(self));
}
MP_DEFINE_CONST_FUN_OBJ_1(audioio_wavefile_get_rate_obj, audioio_wavefile_obj_get_rate);

static mp_obj_t audioio_wavefile_obj_set_rate(mp_obj_t self_in, mp_obj_t rate_in) {
audioio_wavefile_obj_t *self = MP_OBJ_TO_PTR(self_in);
mp_float_t rate = mp_obj_get_float(rate_in);
if (rate <= (mp_float_t)0.0) {
mp_raise_ValueError(MP_ERROR_TEXT("rate must be positive"));
}
common_hal_audioio_wavefile_set_rate(self, rate);
return mp_const_none;
}
MP_DEFINE_CONST_FUN_OBJ_2(audioio_wavefile_set_rate_obj, audioio_wavefile_obj_set_rate);

MP_PROPERTY_GETSET(audioio_wavefile_rate_obj,
(mp_obj_t)&audioio_wavefile_get_rate_obj,
(mp_obj_t)&audioio_wavefile_set_rate_obj);

static const mp_rom_map_elem_t audioio_wavefile_locals_dict_table[] = {
// Methods
Expand All @@ -124,6 +149,7 @@ static const mp_rom_map_elem_t audioio_wavefile_locals_dict_table[] = {

// Properties
AUDIOSAMPLE_FIELDS,
{ MP_ROM_QSTR(MP_QSTR_rate), MP_ROM_PTR(&audioio_wavefile_rate_obj) },
};
static MP_DEFINE_CONST_DICT(audioio_wavefile_locals_dict, audioio_wavefile_locals_dict_table);

Expand Down
3 changes: 3 additions & 0 deletions shared-bindings/audiocore/WaveFile.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ void common_hal_audioio_wavefile_construct(audioio_wavefile_obj_t *self,
pyb_file_obj_t *file, uint8_t *buffer, size_t buffer_size);

void common_hal_audioio_wavefile_deinit(audioio_wavefile_obj_t *self);

void common_hal_audioio_wavefile_set_rate(audioio_wavefile_obj_t *self, mp_float_t rate);
mp_float_t common_hal_audioio_wavefile_get_rate(audioio_wavefile_obj_t *self);
204 changes: 157 additions & 47 deletions shared-module/audiocore/WaveFile.c
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ void common_hal_audioio_wavefile_construct(audioio_wavefile_obj_t *self,
self->file_length = chunk_length;
self->data_start = self->file->fp.fptr;

// Default rate = 1.0 (unity)
self->phase_inc = WAVEFILE_PHASE_UNITY;
self->phase_accum = 0;

// Try to allocate two buffers, one will be loaded from file and the other
// DMAed to DAC.
if (buffer_size) {
Expand Down Expand Up @@ -139,6 +143,14 @@ void common_hal_audioio_wavefile_deinit(audioio_wavefile_obj_t *self) {
audiosample_mark_deinit(&self->base);
}

void common_hal_audioio_wavefile_set_rate(audioio_wavefile_obj_t *self, mp_float_t rate) {
self->phase_inc = (uint32_t)(rate * WAVEFILE_PHASE_UNITY);
}

mp_float_t common_hal_audioio_wavefile_get_rate(audioio_wavefile_obj_t *self) {
return (mp_float_t)self->phase_inc / WAVEFILE_PHASE_UNITY;
}

void audioio_wavefile_reset_buffer(audioio_wavefile_obj_t *self,
bool single_channel_output,
uint8_t channel) {
Expand All @@ -152,6 +164,8 @@ void audioio_wavefile_reset_buffer(audioio_wavefile_obj_t *self,
self->read_count = 0;
self->left_read_count = 0;
self->right_read_count = 0;
self->phase_accum = 0;
self->buffer_length = 0; // Force reload on first get_buffer when resampling
}

audioio_get_buffer_result_t audioio_wavefile_get_buffer(audioio_wavefile_obj_t *self,
Expand All @@ -163,68 +177,161 @@ audioio_get_buffer_result_t audioio_wavefile_get_buffer(audioio_wavefile_obj_t *
channel = 0;
}

uint32_t channel_read_count = self->left_read_count;
if (channel == 1) {
channel_read_count = self->right_read_count;
}
// Early out: rate == 1.0, use original fixed-speed implementation
if (self->phase_inc == WAVEFILE_PHASE_UNITY) {
uint32_t channel_read_count = self->left_read_count;
if (channel == 1) {
channel_read_count = self->right_read_count;
}

bool need_more_data = self->read_count == channel_read_count;
bool need_more_data = self->read_count == channel_read_count;

if (self->bytes_remaining == 0 && need_more_data) {
*buffer = NULL;
*buffer_length = 0;
return GET_BUFFER_DONE;
}
if (self->bytes_remaining == 0 && need_more_data) {
*buffer = NULL;
*buffer_length = 0;
return GET_BUFFER_DONE;
}

if (need_more_data) {
uint32_t num_bytes_to_load = self->len;
if (num_bytes_to_load > self->bytes_remaining) {
num_bytes_to_load = self->bytes_remaining;
if (need_more_data) {
uint32_t num_bytes_to_load = self->len;
if (num_bytes_to_load > self->bytes_remaining) {
num_bytes_to_load = self->bytes_remaining;
}
UINT length_read;
if (self->buffer_index % 2 == 1) {
*buffer = self->second_buffer;
} else {
*buffer = self->buffer;
}
if (f_read(&self->file->fp, *buffer, num_bytes_to_load, &length_read) != FR_OK || length_read != num_bytes_to_load) {
return GET_BUFFER_ERROR;
}
self->bytes_remaining -= length_read;
// Pad the last buffer to word align it.
if (self->bytes_remaining == 0 && length_read % sizeof(uint32_t) != 0) {
uint32_t pad = length_read % sizeof(uint32_t);
length_read += pad;
if (self->base.bits_per_sample == 8) {
for (uint32_t i = 0; i < pad; i++) {
((uint8_t *)(*buffer))[length_read / sizeof(uint8_t) - i - 1] = 0x80;
}
} else if (self->base.bits_per_sample == 16) {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-align"
((int16_t *)(*buffer))[length_read / sizeof(int16_t) - 1] = 0;
#pragma GCC diagnostic pop
}
}
*buffer_length = length_read;
if (self->buffer_index % 2 == 1) {
self->second_buffer_length = length_read;
} else {
self->buffer_length = length_read;
}
self->buffer_index += 1;
self->read_count += 1;
}
UINT length_read;
if (self->buffer_index % 2 == 1) {

uint32_t buffers_back = self->read_count - 1 - channel_read_count;
if ((self->buffer_index - buffers_back) % 2 == 0) {
*buffer = self->second_buffer;
*buffer_length = self->second_buffer_length;
} else {
*buffer = self->buffer;
*buffer_length = self->buffer_length;
}
if (f_read(&self->file->fp, *buffer, num_bytes_to_load, &length_read) != FR_OK || length_read != num_bytes_to_load) {
return GET_BUFFER_ERROR;

if (channel == 0) {
self->left_read_count += 1;
} else if (channel == 1) {
self->right_read_count += 1;
*buffer = *buffer + self->base.bits_per_sample / 8;
}
self->bytes_remaining -= length_read;
// Pad the last buffer to word align it.
if (self->bytes_remaining == 0 && length_read % sizeof(uint32_t) != 0) {
uint32_t pad = length_read % sizeof(uint32_t);
length_read += pad;
if (self->base.bits_per_sample == 8) {
for (uint32_t i = 0; i < pad; i++) {
((uint8_t *)(*buffer))[length_read / sizeof(uint8_t) - i - 1] = 0x80;

return self->bytes_remaining == 0 ? GET_BUFFER_DONE : GET_BUFFER_MORE_DATA;
}

// Resampled path: rate != 1.0
// Uses self->buffer as persistent source data from file,
// and self->second_buffer as resampled output.

uint32_t channel_read_count = (channel == 1) ? self->right_read_count : self->left_read_count;
bool need_more_data = (self->read_count == channel_read_count);

uint32_t bytes_per_frame = (self->base.bits_per_sample / 8) * self->base.channel_count;

if (need_more_data) {
uint32_t src_frames_avail = self->buffer_length / bytes_per_frame;

// Check if completely done (no file data left AND source buffer exhausted)
if (self->bytes_remaining == 0 && (self->phase_accum >> 16) >= src_frames_avail) {
*buffer = NULL;
*buffer_length = 0;
return GET_BUFFER_DONE;
}

uint8_t *src_buf = self->buffer;
uint8_t *out_buf = self->second_buffer;
uint32_t out_buf_frames = self->len / bytes_per_frame;
uint32_t out_pos = 0;

while (out_pos < out_buf_frames) {
uint32_t src_frame = self->phase_accum >> 16;

// Need to load more source data?
if (src_frame >= src_frames_avail) {
if (self->bytes_remaining == 0) {
break;
}
// Shift phase accumulator back by consumed frames
self->phase_accum -= src_frames_avail << 16;
src_frame = self->phase_accum >> 16;

// Load new source data
uint32_t to_load = self->len;
if (to_load > self->bytes_remaining) {
to_load = self->bytes_remaining;
}
UINT length_read;
if (f_read(&self->file->fp, src_buf, to_load, &length_read) != FR_OK || length_read == 0) {
return GET_BUFFER_ERROR;
}
self->bytes_remaining -= length_read;
self->buffer_length = length_read;
src_frames_avail = length_read / bytes_per_frame;

if (src_frame >= src_frames_avail) {
break;
}
} else if (self->base.bits_per_sample == 16) {
// We know the buffer is aligned because we allocated it onto the heap ourselves.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-align"
((int16_t *)(*buffer))[length_read / sizeof(int16_t) - 1] = 0;
#pragma GCC diagnostic pop
}

// Nearest-neighbor: copy one frame from source to output
memcpy(out_buf + out_pos * bytes_per_frame,
src_buf + src_frame * bytes_per_frame,
bytes_per_frame);
out_pos++;
self->phase_accum += self->phase_inc;
}
*buffer_length = length_read;
if (self->buffer_index % 2 == 1) {
self->second_buffer_length = length_read;
} else {
self->buffer_length = length_read;

uint32_t out_bytes = out_pos * bytes_per_frame;

// Pad the last buffer to word-align it
if (out_pos < out_buf_frames && out_bytes % sizeof(uint32_t) != 0) {
uint32_t pad = sizeof(uint32_t) - (out_bytes % sizeof(uint32_t));
if (self->base.bits_per_sample == 8) {
memset(out_buf + out_bytes, 0x80, pad);
} else {
memset(out_buf + out_bytes, 0, pad);
}
out_bytes += pad;
}
self->buffer_index += 1;

self->second_buffer_length = out_bytes;
self->read_count += 1;
}

uint32_t buffers_back = self->read_count - 1 - channel_read_count;
if ((self->buffer_index - buffers_back) % 2 == 0) {
*buffer = self->second_buffer;
*buffer_length = self->second_buffer_length;
} else {
*buffer = self->buffer;
*buffer_length = self->buffer_length;
}
*buffer = self->second_buffer;
*buffer_length = self->second_buffer_length;

if (channel == 0) {
self->left_read_count += 1;
Expand All @@ -233,5 +340,8 @@ audioio_get_buffer_result_t audioio_wavefile_get_buffer(audioio_wavefile_obj_t *
*buffer = *buffer + self->base.bits_per_sample / 8;
}

return self->bytes_remaining == 0 ? GET_BUFFER_DONE : GET_BUFFER_MORE_DATA;
// Done when file is exhausted AND source buffer is exhausted
uint32_t src_frames_avail = self->buffer_length / bytes_per_frame;
bool all_done = (self->bytes_remaining == 0) && ((self->phase_accum >> 16) >= src_frames_avail);
return all_done ? GET_BUFFER_DONE : GET_BUFFER_MORE_DATA;
}
5 changes: 5 additions & 0 deletions shared-module/audiocore/WaveFile.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,13 @@ typedef struct {
uint32_t read_count;
uint32_t left_read_count;
uint32_t right_read_count;

uint32_t phase_accum; // 16.16 fixed-point position in source samples
uint32_t phase_inc; // 16.16 fixed-point rate (0x10000 = 1.0)
} audioio_wavefile_obj_t;

#define WAVEFILE_PHASE_UNITY 0x10000

// These are not available from Python because it may be called in an interrupt.
void audioio_wavefile_reset_buffer(audioio_wavefile_obj_t *self,
bool single_channel_output,
Expand Down
Loading