diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index bf4c5d110f673..0e7c1606378d7 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -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 "" diff --git a/shared-bindings/audiocore/WaveFile.c b/shared-bindings/audiocore/WaveFile.c index c93bedd4213ea..6ff1a82a01c3d 100644 --- a/shared-bindings/audiocore/WaveFile.c +++ b/shared-bindings/audiocore/WaveFile.c @@ -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 @@ -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); diff --git a/shared-bindings/audiocore/WaveFile.h b/shared-bindings/audiocore/WaveFile.h index 5249959a1cc50..87b2019ca481a 100644 --- a/shared-bindings/audiocore/WaveFile.h +++ b/shared-bindings/audiocore/WaveFile.h @@ -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); diff --git a/shared-module/audiocore/WaveFile.c b/shared-module/audiocore/WaveFile.c index 77ca2445edbf3..9bd7139281471 100644 --- a/shared-module/audiocore/WaveFile.c +++ b/shared-module/audiocore/WaveFile.c @@ -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) { @@ -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) { @@ -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, @@ -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; @@ -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; } diff --git a/shared-module/audiocore/WaveFile.h b/shared-module/audiocore/WaveFile.h index 74e25ffebfec0..68cd117f67b5c 100644 --- a/shared-module/audiocore/WaveFile.h +++ b/shared-module/audiocore/WaveFile.h @@ -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,