diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot
index 48d27d14b0be5..456f5e698dd11 100644
--- a/locale/circuitpython.pot
+++ b/locale/circuitpython.pot
@@ -739,6 +739,10 @@ msgstr ""
 msgid "Can only alarm on two low pins from deep sleep."
 msgstr ""
 
+#: ports/espressif/common-hal/audioio/AudioOut.c
+msgid "Can't construct AudioOut because continuous channel already open"
+msgstr ""
+
 #: ports/espressif/common-hal/_bleio/Characteristic.c
 #: ports/nordic/common-hal/_bleio/Characteristic.c
 msgid "Can't set CCCD on local Characteristic"
@@ -1001,15 +1005,43 @@ msgstr ""
 msgid "Failed to connect: timeout"
 msgstr ""
 
+#: ports/espressif/common-hal/audioio/AudioOut.c
+msgid "Failed to create continuous channels: invalid arg"
+msgstr ""
+
+#: ports/espressif/common-hal/audioio/AudioOut.c
+msgid "Failed to create continuous channels: invalid state"
+msgstr ""
+
+#: ports/espressif/common-hal/audioio/AudioOut.c
+msgid "Failed to create continuous channels: no mem"
+msgstr ""
+
+#: ports/espressif/common-hal/audioio/AudioOut.c
+msgid "Failed to create continuous channels: not found"
+msgstr ""
+
+#: ports/espressif/common-hal/audioio/AudioOut.c
+msgid "Failed to enable continuous"
+msgstr ""
+
 #: shared-module/audiomp3/MP3Decoder.c
 msgid "Failed to parse MP3 file"
 msgstr ""
 
+#: ports/espressif/common-hal/audioio/AudioOut.c
+msgid "Failed to register continuous events callback"
+msgstr ""
+
 #: ports/nordic/sd_mutex.c
 #, c-format
 msgid "Failed to release mutex, err 0x%04x"
 msgstr ""
 
+#: ports/espressif/common-hal/audioio/AudioOut.c
+msgid "Failed to start async audio"
+msgstr ""
+
 #: supervisor/shared/safe_mode.c
 msgid "Failed to write internal flash."
 msgstr ""
@@ -2400,6 +2432,10 @@ msgstr ""
 msgid "addresses is empty"
 msgstr ""
 
+#: ports/espressif/common-hal/audioio/AudioOut.c
+msgid "already playing"
+msgstr ""
+
 #: py/compile.c
 msgid "annotation must be an identifier"
 msgstr ""
@@ -2478,6 +2514,10 @@ msgstr ""
 msgid "attributes not supported"
 msgstr ""
 
+#: ports/espressif/common-hal/audioio/AudioOut.c
+msgid "audio format not supported"
+msgstr ""
+
 #: extmod/ulab/code/ulab_tools.c
 msgid "axis is out of bounds"
 msgstr ""
diff --git a/ports/espressif/common-hal/audioio/AudioOut.c b/ports/espressif/common-hal/audioio/AudioOut.c
new file mode 100644
index 0000000000000..7f32dbc58c9f6
--- /dev/null
+++ b/ports/espressif/common-hal/audioio/AudioOut.c
@@ -0,0 +1,648 @@
+
+#include <stdio.h>
+
+#include "py/runtime.h"
+
+#include "common-hal/audioio/AudioOut.h"
+#include "shared-bindings/audioio/AudioOut.h"
+#include "shared-bindings/microcontroller/__init__.h"
+#include "shared-bindings/microcontroller/Pin.h"
+#include "shared-module/audiocore/__init__.h"
+
+#include "driver/dac_continuous.h"
+
+
+#if defined(CONFIG_IDF_TARGET_ESP32)
+#define pin_CHANNEL_0 pin_GPIO25
+#define pin_CHANNEL_1 pin_GPIO26
+#elif defined(CONFIG_IDF_TARGET_ESP32S2)
+#define pin_CHANNEL_0 pin_GPIO17
+#define pin_CHANNEL_1 pin_GPIO18
+#endif
+
+static dac_continuous_handle_t _active_handle;
+
+#define INCREMENT_BUF_IDX(idx) ((idx + 1) % (NUM_DMA_BUFFERS + 1))
+
+static bool audioout_convert_noop(
+    void *in_buffer,
+    size_t in_buffer_size,
+    uint8_t **out_buffer,
+    uint32_t *out_buffer_size) {
+
+    *out_buffer = in_buffer;
+    *out_buffer_size = in_buffer_size;
+    return false;
+}
+
+static bool audioout_convert_u8s_u8m(
+    void *in_buffer,
+    size_t in_buffer_size,
+    uint8_t **out_buffer,
+    uint32_t *out_buffer_size) {
+
+    bool buffer_changed = false;
+    if (in_buffer_size / 2 > *out_buffer_size) {
+        *out_buffer = m_malloc(in_buffer_size / 2);
+        buffer_changed = true;
+    }
+    audiosample_convert_u8s_u8m(*out_buffer, (uint8_t *)in_buffer, in_buffer_size / 2);
+    *out_buffer_size = in_buffer_size / 2;
+    return buffer_changed;
+}
+
+static bool audioout_convert_u8m_u8s(
+    void *in_buffer,
+    size_t in_buffer_size,
+    uint8_t **out_buffer,
+    uint32_t *out_buffer_size) {
+
+    bool buffer_changed = false;
+    if (in_buffer_size * 2 > *out_buffer_size) {
+        *out_buffer = m_malloc(in_buffer_size * 2);
+        buffer_changed = true;
+    }
+    audiosample_convert_u8m_u8s(*out_buffer, (uint8_t *)in_buffer, in_buffer_size);
+    *out_buffer_size = in_buffer_size * 2;
+    return buffer_changed;
+}
+
+static bool audioout_convert_s8m_u8m(
+    void *in_buffer,
+    size_t in_buffer_size,
+    uint8_t **out_buffer,
+    uint32_t *out_buffer_size) {
+
+    bool buffer_changed = false;
+    if (in_buffer_size > *out_buffer_size) {
+        *out_buffer = m_malloc(in_buffer_size);
+        buffer_changed = true;
+    }
+    audiosample_convert_s8m_u8m(*out_buffer, (int8_t *)in_buffer, in_buffer_size);
+    *out_buffer_size = in_buffer_size;
+    return buffer_changed;
+}
+
+static bool audioout_convert_s8s_u8m(
+    void *in_buffer,
+    size_t in_buffer_size,
+    uint8_t **out_buffer,
+    uint32_t *out_buffer_size) {
+
+    bool buffer_changed = false;
+    if (in_buffer_size / 2 > *out_buffer_size) {
+        *out_buffer = m_malloc(in_buffer_size / 2);
+        buffer_changed = true;
+    }
+    audiosample_convert_s8s_u8m(*out_buffer, (int8_t *)in_buffer, in_buffer_size / 2);
+    *out_buffer_size = in_buffer_size / 2;
+    return buffer_changed;
+}
+
+static bool audioout_convert_s8m_u8s(
+    void *in_buffer,
+    size_t in_buffer_size,
+    uint8_t **out_buffer,
+    uint32_t *out_buffer_size) {
+
+    bool buffer_changed = false;
+    if (in_buffer_size * 2 > *out_buffer_size) {
+        *out_buffer = m_malloc(in_buffer_size * 2);
+        buffer_changed = true;
+    }
+    audiosample_convert_s8m_u8s(*out_buffer, (int8_t *)in_buffer, in_buffer_size);
+    *out_buffer_size = in_buffer_size * 2;
+    return buffer_changed;
+}
+
+static bool audioout_convert_s8s_u8s(
+    void *in_buffer,
+    size_t in_buffer_size,
+    uint8_t **out_buffer,
+    uint32_t *out_buffer_size) {
+
+    bool buffer_changed = false;
+    if (in_buffer_size > *out_buffer_size) {
+        *out_buffer = m_malloc(in_buffer_size);
+        buffer_changed = true;
+    }
+    audiosample_convert_s8s_u8s(*out_buffer, (int8_t *)in_buffer, in_buffer_size);
+    *out_buffer_size = in_buffer_size;
+    return buffer_changed;
+}
+
+static bool audioout_convert_u16m_u8m(
+    void *in_buffer,
+    size_t in_buffer_size,
+    uint8_t **out_buffer,
+    uint32_t *out_buffer_size) {
+
+    bool buffer_changed = false;
+    if (in_buffer_size / 2 > *out_buffer_size) {
+        *out_buffer = m_malloc(in_buffer_size / 2);
+        buffer_changed = true;
+    }
+    audiosample_convert_u16m_u8m(*out_buffer, (uint16_t *)in_buffer, in_buffer_size / 2);
+    *out_buffer_size = in_buffer_size / 2;
+    return buffer_changed;
+}
+
+static bool audioout_convert_u16m_u8s(
+    void *in_buffer,
+    size_t in_buffer_size,
+    uint8_t **out_buffer,
+    uint32_t *out_buffer_size) {
+
+    bool buffer_changed = false;
+    if (in_buffer_size > *out_buffer_size) {
+        *out_buffer = m_malloc(in_buffer_size);
+        buffer_changed = true;
+    }
+    audiosample_convert_u16m_u8s(*out_buffer, (uint16_t *)in_buffer, in_buffer_size / 2);
+    *out_buffer_size = in_buffer_size;
+    return buffer_changed;
+}
+
+static bool audioout_convert_u16s_u8m(
+    void *in_buffer,
+    size_t in_buffer_size,
+    uint8_t **out_buffer,
+    uint32_t *out_buffer_size) {
+
+    bool buffer_changed = false;
+    if (in_buffer_size / 4 > *out_buffer_size) {
+        *out_buffer = m_malloc(in_buffer_size / 4);
+        buffer_changed = true;
+    }
+    audiosample_convert_u16s_u8m(*out_buffer, (uint16_t *)in_buffer, in_buffer_size / 4);
+    *out_buffer_size = in_buffer_size / 4;
+    return buffer_changed;
+}
+
+static bool audioout_convert_u16s_u8s(
+    void *in_buffer,
+    size_t in_buffer_size,
+    uint8_t **out_buffer,
+    uint32_t *out_buffer_size) {
+
+    bool buffer_changed = false;
+    if (in_buffer_size / 2 > *out_buffer_size) {
+        *out_buffer = m_malloc(in_buffer_size / 2);
+        buffer_changed = true;
+    }
+    audiosample_convert_u16s_u8s(*out_buffer, (uint16_t *)in_buffer, in_buffer_size / 4);
+    *out_buffer_size = in_buffer_size / 2;
+    return buffer_changed;
+}
+
+static bool audioout_convert_s16m_u8m(
+    void *in_buffer,
+    size_t in_buffer_size,
+    uint8_t **out_buffer,
+    uint32_t *out_buffer_size) {
+
+    bool buffer_changed = false;
+    if (in_buffer_size / 2 > *out_buffer_size) {
+        *out_buffer = m_malloc(in_buffer_size / 2);
+        buffer_changed = true;
+    }
+    audiosample_convert_s16m_u8m(*out_buffer, (int16_t *)in_buffer, in_buffer_size / 2);
+    *out_buffer_size = in_buffer_size / 2;
+    return buffer_changed;
+}
+
+static bool audioout_convert_s16m_u8s(
+    void *in_buffer,
+    size_t in_buffer_size,
+    uint8_t **out_buffer,
+    uint32_t *out_buffer_size) {
+
+    bool buffer_changed = false;
+    if (in_buffer_size > *out_buffer_size) {
+        *out_buffer = m_malloc(in_buffer_size);
+        buffer_changed = true;
+    }
+    audiosample_convert_s16m_u8s(*out_buffer, (int16_t *)in_buffer, in_buffer_size / 2);
+    *out_buffer_size = in_buffer_size;
+    return buffer_changed;
+}
+
+static bool audioout_convert_s16s_u8m(
+    void *in_buffer,
+    size_t in_buffer_size,
+    uint8_t **out_buffer,
+    uint32_t *out_buffer_size) {
+
+    bool buffer_changed = false;
+    if (in_buffer_size / 4 > *out_buffer_size) {
+        *out_buffer = m_malloc(in_buffer_size / 4);
+        buffer_changed = true;
+    }
+    audiosample_convert_s16s_u8m(*out_buffer, (int16_t *)in_buffer, in_buffer_size / 4);
+    *out_buffer_size = in_buffer_size / 4;
+    return buffer_changed;
+}
+
+static bool audioout_convert_s16s_u8s(
+    void *in_buffer,
+    size_t in_buffer_size,
+    uint8_t **out_buffer,
+    uint32_t *out_buffer_size) {
+
+    bool buffer_changed = false;
+    if (in_buffer_size / 2 > *out_buffer_size) {
+        *out_buffer = m_malloc(in_buffer_size / 2);
+        buffer_changed = true;
+    }
+    audiosample_convert_s16s_u8s(*out_buffer, (int16_t *)in_buffer, in_buffer_size / 4);
+    *out_buffer_size = in_buffer_size / 2;
+    return buffer_changed;
+}
+
+#define CONV_MATCH(bps, sign, ichans, ochans) ((bps & 0xf) | ((sign & 0x1) << 4) | ((ichans & 0x3) << 5) | ((ochans & 0x3) << 7))
+
+static audioout_sample_convert_func_t audioout_get_samples_convert_func(
+    size_t in_bits_per_sample,
+    int in_channels,
+    bool in_signed,
+    int out_channels) {
+
+    switch CONV_MATCH(in_bits_per_sample, in_signed, in_channels, out_channels) {
+        case CONV_MATCH(8, false, 1, 1):
+        case CONV_MATCH(8, false, 2, 2):
+            return audioout_convert_noop;
+        case CONV_MATCH(8, false, 2, 1):
+            return audioout_convert_u8s_u8m;
+        case CONV_MATCH(8, false, 1, 2):
+            return audioout_convert_u8m_u8s;
+        case CONV_MATCH(8, true, 1, 1):
+            return audioout_convert_s8m_u8m;
+        case CONV_MATCH(8, true, 2, 1):
+            return audioout_convert_s8s_u8m;
+        case CONV_MATCH(8, true, 1, 2):
+            return audioout_convert_s8m_u8s;
+        case CONV_MATCH(8, true, 2, 2):
+            return audioout_convert_s8s_u8s;
+        case CONV_MATCH(16, false, 1, 1):
+            return audioout_convert_u16m_u8m;
+        case CONV_MATCH(16, false, 1, 2):
+            return audioout_convert_u16m_u8s;
+        case CONV_MATCH(16, false, 2, 1):
+            return audioout_convert_u16s_u8m;
+        case CONV_MATCH(16, false, 2, 2):
+            return audioout_convert_u16s_u8s;
+        case CONV_MATCH(16, true, 1, 1):
+            return audioout_convert_s16m_u8m;
+        case CONV_MATCH(16, true, 1, 2):
+            return audioout_convert_s16m_u8s;
+        case CONV_MATCH(16, true, 2, 1):
+            return audioout_convert_s16s_u8m;
+        case CONV_MATCH(16, true, 2, 2):
+            return audioout_convert_s16s_u8s;
+        default:
+            mp_raise_RuntimeError(MP_ERROR_TEXT("audio format not supported"));
+    }
+}
+
+static bool audioout_fill_buffer(audioio_audioout_obj_t *self) {
+    if (!self->playing) {
+        return false;
+    }
+
+    audioio_get_buffer_result_t get_buffer_result;
+
+    uint8_t dma_buf_idx = self->get_buffer_index;
+
+    if (dma_buf_idx == self->put_buffer_index) {
+        return false;
+    }
+
+    uint8_t *dma_buf = self->dma_buffers[dma_buf_idx].ptr;
+    size_t dma_buf_size = self->dma_buffers[dma_buf_idx].size;
+
+    self->get_buffer_index = INCREMENT_BUF_IDX(dma_buf_idx);
+
+    bool single_channel_output = true; // whether or not we have 1 or 2 output channels
+    uint8_t channel = 0; // which channel right now?
+    uint8_t *raw_sample_buf; // raw audio sample buffer
+    uint32_t raw_sample_buf_size; // raw audio sample buffer len
+    uint8_t *sample_buf = self->scratch_buffer; // converted audio sample buffer
+    uint32_t sample_buf_size = self->scratch_buffer_size; // converted audio sample buffer len
+    size_t bytes_loaded;
+    esp_err_t ret;
+
+    if (self->sample_buffer != NULL && self->sample_buffer_size > 0) {
+        sample_buf = self->sample_buffer;
+        sample_buf_size = self->sample_buffer_size;
+        get_buffer_result = self->sample_buffer_result;
+    } else {
+        get_buffer_result = audiosample_get_buffer(self->sample,
+            single_channel_output,
+            channel,
+            &raw_sample_buf, &raw_sample_buf_size);
+
+        if (get_buffer_result == GET_BUFFER_ERROR) {
+            common_hal_audioio_audioout_stop(self);
+            return false;
+        }
+
+        bool buffer_changed;
+        buffer_changed = self->samples_convert(
+            raw_sample_buf,
+            raw_sample_buf_size,
+            &sample_buf,
+            &sample_buf_size);
+
+        if (buffer_changed) {
+            if (self->scratch_buffer != NULL) {
+                m_free(self->scratch_buffer);
+            }
+            self->scratch_buffer = sample_buf;
+            self->scratch_buffer_size = sample_buf_size;
+        }
+    }
+
+    if (sample_buf_size > 0) {
+        ret = dac_continuous_write_asynchronously(self->handle,
+            dma_buf, dma_buf_size,
+            sample_buf, sample_buf_size,
+            &bytes_loaded);
+
+        if (ret != ESP_OK) {
+            return false;
+        }
+    }
+
+    sample_buf_size -= bytes_loaded;
+    if (sample_buf_size == 0) {
+        sample_buf = NULL;
+    } else {
+        sample_buf += bytes_loaded;
+    }
+
+    self->sample_buffer = sample_buf;
+    self->sample_buffer_size = sample_buf_size;
+    self->sample_buffer_result = get_buffer_result;
+
+    if (get_buffer_result == GET_BUFFER_DONE && sample_buf_size == 0) {
+        if (self->looping) {
+            audiosample_reset_buffer(self->sample, true, 0);
+        } else {
+            // TODO: figure out if it is ok to call this here or do we need
+            // to somehow wait for all of the samples to be flushed
+            common_hal_audioio_audioout_stop(self);
+            return false;
+        }
+    }
+
+    return true;
+}
+
+static void audioout_fill_buffers(audioio_audioout_obj_t *self) {
+    while (audioout_fill_buffer(self)) {
+        ;
+    }
+}
+
+static void audioout_buf_callback_fun(void *user_data) {
+    audioio_audioout_obj_t *self = (audioio_audioout_obj_t *)user_data;
+    audioout_fill_buffers(self);
+}
+
+static bool IRAM_ATTR handle_convert_done(dac_continuous_handle_t handle, const dac_event_data_t *event, void *user_data) {
+    audioio_audioout_obj_t *self = (audioio_audioout_obj_t *)user_data;
+
+    uint8_t *newly_freed_dma_buf = event->buf;
+    size_t newly_freed_dma_buf_size = event->buf_size;
+
+    uint8_t get_buf_idx = self->get_buffer_index;
+    uint8_t put_buf_idx = self->put_buffer_index;
+    uint8_t new_put_buf_idx = INCREMENT_BUF_IDX(put_buf_idx);
+
+    // if the ring buffer of dma buffers is full then drop this one
+    if (get_buf_idx == new_put_buf_idx) {
+        return false;
+    }
+
+    self->dma_buffers[put_buf_idx].ptr = newly_freed_dma_buf;
+    self->dma_buffers[put_buf_idx].size = newly_freed_dma_buf_size;
+
+    self->put_buffer_index = new_put_buf_idx;
+
+    background_callback_add(&self->callback, audioout_buf_callback_fun, user_data);
+
+    return false;
+}
+
+static void audioout_init(audioio_audioout_obj_t *self) {
+    dac_continuous_digi_clk_src_t clk_src = DAC_DIGI_CLK_SRC_DEFAULT;
+    if (self->freq_hz < 19600) {
+        clk_src = DAC_DIGI_CLK_SRC_APLL;
+    }
+
+    dac_continuous_config_t cfg = {
+        .chan_mask = self->channel_mask,
+        .desc_num = NUM_DMA_BUFFERS,
+        .buf_size = DMA_BUFFER_SIZE,
+        .freq_hz = self->freq_hz,
+        .offset = 0,
+        .clk_src = clk_src,
+        .chan_mode = self->channel_mode,
+    };
+
+    esp_err_t ret;
+
+    ret = dac_continuous_new_channels(&cfg, &self->handle);
+    if (ret == ESP_ERR_INVALID_ARG) {
+        mp_raise_RuntimeError(MP_ERROR_TEXT("Failed to create continuous channels: invalid arg"));
+    } else if (ret == ESP_ERR_INVALID_STATE) {
+        mp_raise_RuntimeError(MP_ERROR_TEXT("Failed to create continuous channels: invalid state"));
+    } else if (ret == ESP_ERR_NOT_FOUND) {
+        mp_raise_RuntimeError(MP_ERROR_TEXT("Failed to create continuous channels: not found"));
+    } else if (ret == ESP_ERR_NO_MEM) {
+        mp_raise_RuntimeError(MP_ERROR_TEXT("Failed to create continuous channels: no mem"));
+    }
+
+    _active_handle = self->handle;
+
+    dac_event_callbacks_t callbacks = {
+        .on_convert_done = handle_convert_done,
+        .on_stop = NULL,
+    };
+
+    ret = dac_continuous_register_event_callback(self->handle, &callbacks, (void *)self);
+    if (ret != ESP_OK) {
+        mp_raise_RuntimeError(MP_ERROR_TEXT("Failed to register continuous events callback"));
+    }
+
+    ret = dac_continuous_enable(self->handle);
+    if (ret != ESP_OK) {
+        mp_raise_RuntimeError(MP_ERROR_TEXT("Failed to enable continuous"));
+    }
+}
+
+void common_hal_audioio_audioout_construct(audioio_audioout_obj_t *self,
+    const mcu_pin_obj_t *left_channel_pin, const mcu_pin_obj_t *right_channel_pin, uint16_t quiescent_value) {
+
+    if (_active_handle != NULL) {
+        mp_raise_RuntimeError(MP_ERROR_TEXT("Can't construct AudioOut because continuous channel already open"));
+    }
+
+    self->playing = false;
+    self->paused = false;
+    self->freq_hz = DEFAULT_SAMPLE_RATE;
+
+    /* espressif has two dac channels and it can support true stereo or
+     * outputting the same signal to both channels (dual mono).
+     * if different pins are supplied for left and right then use true stereo.
+     * if the same pin is supplied for left and right then use dual mono.
+     */
+    if ((left_channel_pin == &pin_CHANNEL_0 &&
+         right_channel_pin == &pin_CHANNEL_1) ||
+        (left_channel_pin == &pin_CHANNEL_1 &&
+         right_channel_pin == &pin_CHANNEL_0)) {
+        self->channel_mask = DAC_CHANNEL_MASK_ALL;
+        self->num_channels = 2;
+        self->channel_mode = DAC_CHANNEL_MODE_ALTER;
+    } else if ((left_channel_pin == &pin_CHANNEL_0 ||
+                left_channel_pin == &pin_CHANNEL_1) &&
+               right_channel_pin == left_channel_pin) {
+        self->channel_mask = DAC_CHANNEL_MASK_ALL;
+        self->num_channels = 1;
+        self->channel_mode = DAC_CHANNEL_MODE_SIMUL;
+    } else if (left_channel_pin == &pin_CHANNEL_0 &&
+               right_channel_pin == NULL) {
+        self->channel_mask = DAC_CHANNEL_MASK_CH0;
+        self->num_channels = 1;
+        self->channel_mode = DAC_CHANNEL_MODE_SIMUL;
+    } else if (left_channel_pin == &pin_CHANNEL_1 &&
+               right_channel_pin == NULL) {
+        self->channel_mask = DAC_CHANNEL_MASK_CH1;
+        self->num_channels = 1;
+        self->channel_mode = DAC_CHANNEL_MODE_SIMUL;
+    } else {
+        raise_ValueError_invalid_pin();
+    }
+
+    audioout_init(self);
+}
+
+bool common_hal_audioio_audioout_deinited(audioio_audioout_obj_t *self) {
+    return self->handle == NULL;
+}
+
+void common_hal_audioio_audioout_deinit(audioio_audioout_obj_t *self) {
+    if (common_hal_audioio_audioout_deinited(self)) {
+        return;
+    }
+
+    if (self->playing) {
+        common_hal_audioio_audioout_stop(self);
+    }
+    dac_continuous_disable(self->handle);
+    dac_continuous_del_channels(self->handle);
+    if (self->scratch_buffer != NULL) {
+        m_free(self->scratch_buffer);
+        self->scratch_buffer = NULL;
+        self->scratch_buffer_size = 0;
+    }
+    self->handle = NULL;
+    _active_handle = NULL;
+}
+
+static void audioio_audioout_start(audioio_audioout_obj_t *self) {
+    esp_err_t ret;
+
+    self->playing = true;
+    self->paused = false;
+
+    ret = dac_continuous_start_async_writing(self->handle);
+    if (ret != ESP_OK) {
+        mp_raise_RuntimeError(MP_ERROR_TEXT("Failed to start async audio"));
+    }
+}
+
+static void audioio_audioout_stop(audioio_audioout_obj_t *self, bool full_stop) {
+    dac_continuous_stop_async_writing(self->handle);
+    if (full_stop) {
+        self->get_buffer_index = 0;
+        self->put_buffer_index = 0;
+        self->sample_buffer = NULL;
+        self->sample = NULL;
+        self->playing = false;
+        self->paused = false;
+    } else {
+        self->paused = true;
+    }
+}
+
+void common_hal_audioio_audioout_play(audioio_audioout_obj_t *self,
+    mp_obj_t sample, bool loop) {
+
+    if (self->playing) {
+        mp_raise_RuntimeError(MP_ERROR_TEXT("already playing"));
+    }
+
+    size_t samples_size;
+    uint8_t channel_count;
+    bool samples_signed;
+    bool _single_buffer;
+    uint32_t _max_buffer_length;
+    uint8_t _spacing;
+    uint32_t freq_hz;
+
+    audiosample_reset_buffer(sample, true, 0);
+
+    self->sample = sample;
+    self->looping = loop;
+    freq_hz = audiosample_sample_rate(self->sample);
+
+    if (freq_hz != self->freq_hz) {
+        common_hal_audioio_audioout_deinit(self);
+        self->freq_hz = freq_hz;
+        audioout_init(self);
+    }
+
+    samples_size = audiosample_bits_per_sample(self->sample);
+    channel_count = audiosample_channel_count(self->sample);
+    audiosample_get_buffer_structure(self->sample, false,
+        &_single_buffer, &samples_signed,
+        &_max_buffer_length, &_spacing);
+
+    self->samples_convert = audioout_get_samples_convert_func(
+        samples_size,
+        channel_count,
+        samples_signed,
+        self->num_channels);
+
+    audioio_audioout_start(self);
+}
+
+void common_hal_audioio_audioout_pause(audioio_audioout_obj_t *self) {
+    if (!self->playing || self->paused) {
+        return;
+    }
+    audioio_audioout_stop(self, false);
+}
+
+void common_hal_audioio_audioout_resume(audioio_audioout_obj_t *self) {
+    if (!self->playing || !self->paused) {
+        return;
+    }
+    audioio_audioout_start(self);
+}
+
+bool common_hal_audioio_audioout_get_paused(audioio_audioout_obj_t *self) {
+    return self->playing && self->paused;
+}
+
+void common_hal_audioio_audioout_stop(audioio_audioout_obj_t *self) {
+    if (!self->playing) {
+        return;
+    }
+    audioio_audioout_stop(self, true);
+}
+
+bool common_hal_audioio_audioout_get_playing(audioio_audioout_obj_t *self) {
+    return self->playing && !self->paused;
+}
diff --git a/ports/espressif/common-hal/audioio/AudioOut.h b/ports/espressif/common-hal/audioio/AudioOut.h
new file mode 100644
index 0000000000000..b35d9ffdcf7f9
--- /dev/null
+++ b/ports/espressif/common-hal/audioio/AudioOut.h
@@ -0,0 +1,47 @@
+
+#pragma once
+
+#include "common-hal/microcontroller/Pin.h"
+
+#include "py/obj.h"
+
+#include "supervisor/background_callback.h"
+#include "shared-module/audiocore/__init__.h"
+
+#include "driver/dac_continuous.h"
+
+
+#define NUM_DMA_BUFFERS 6
+#define DMA_BUFFER_SIZE 512
+
+#define DEFAULT_SAMPLE_RATE 32000
+
+typedef bool (*audioout_sample_convert_func_t)(void *in_buffer, size_t in_buffer_size, uint8_t **out_buffer, uint32_t *out_buffer_size);
+
+typedef struct {
+    uint8_t *ptr;
+    size_t size;
+} buf_info_t;
+
+typedef struct {
+    mp_obj_base_t base;
+    dac_continuous_handle_t handle;
+    dac_channel_mask_t channel_mask;
+    uint32_t freq_hz;
+    uint8_t num_channels;
+    dac_continuous_channel_mode_t channel_mode;
+    mp_obj_t sample;
+    bool playing;
+    bool paused;
+    bool looping;
+    uint8_t *sample_buffer;
+    size_t sample_buffer_size;
+    audioio_get_buffer_result_t sample_buffer_result;
+    uint8_t get_buffer_index;
+    uint8_t put_buffer_index;
+    buf_info_t dma_buffers[NUM_DMA_BUFFERS + 1];
+    background_callback_t callback;
+    uint8_t *scratch_buffer;
+    size_t scratch_buffer_size;
+    audioout_sample_convert_func_t samples_convert;
+} audioio_audioout_obj_t;
diff --git a/ports/espressif/common-hal/audioio/__init__.c b/ports/espressif/common-hal/audioio/__init__.c
new file mode 100644
index 0000000000000..f7af773c4b839
--- /dev/null
+++ b/ports/espressif/common-hal/audioio/__init__.c
@@ -0,0 +1,2 @@
+
+#include "common-hal/audioio/__init__.h"
diff --git a/ports/espressif/common-hal/audioio/__init__.h b/ports/espressif/common-hal/audioio/__init__.h
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/ports/espressif/mpconfigport.mk b/ports/espressif/mpconfigport.mk
index 06f8c87c579e0..310558b867a46 100644
--- a/ports/espressif/mpconfigport.mk
+++ b/ports/espressif/mpconfigport.mk
@@ -85,6 +85,7 @@ CIRCUITPY__EVE ?= 1
 ifeq ($(IDF_TARGET),esp32)
 # Modules
 CIRCUITPY_ALARM_TOUCH = 1
+CIRCUITPY_AUDIOIO = 1
 CIRCUITPY_RGBMATRIX = 0
 
 # SDMMC not supported yet
@@ -244,6 +245,7 @@ CIRCUITPY_ESPCAMERA = 0
 else ifeq ($(IDF_TARGET),esp32s2)
 # Modules
 CIRCUITPY_ALARM_TOUCH = 1
+CIRCUITPY_AUDIOIO = 1
 # No BLE in hw
 CIRCUITPY_BLEIO = 0
 
diff --git a/shared-module/audiocore/__init__.c b/shared-module/audiocore/__init__.c
index ff3c9652807c0..8be5be5425c3a 100644
--- a/shared-module/audiocore/__init__.c
+++ b/shared-module/audiocore/__init__.c
@@ -59,7 +59,6 @@ void audiosample_convert_u8m_s16s(int16_t *buffer_out, const uint8_t *buffer_in,
     }
 }
 
-
 void audiosample_convert_u8s_s16s(int16_t *buffer_out, const uint8_t *buffer_in, size_t nframes) {
     size_t nsamples = 2 * nframes;
     for (; nsamples--;) {
@@ -76,7 +75,6 @@ void audiosample_convert_s8m_s16s(int16_t *buffer_out, const int8_t *buffer_in,
     }
 }
 
-
 void audiosample_convert_s8s_s16s(int16_t *buffer_out, const int8_t *buffer_in, size_t nframes) {
     size_t nsamples = 2 * nframes;
     for (; nsamples--;) {
@@ -85,7 +83,6 @@ void audiosample_convert_s8s_s16s(int16_t *buffer_out, const int8_t *buffer_in,
     }
 }
 
-
 void audiosample_convert_u16m_s16s(int16_t *buffer_out, const uint16_t *buffer_in, size_t nframes) {
     for (; nframes--;) {
         int16_t sample = *buffer_in++ - 0x8000;
@@ -94,7 +91,6 @@ void audiosample_convert_u16m_s16s(int16_t *buffer_out, const uint16_t *buffer_i
     }
 }
 
-
 void audiosample_convert_u16s_s16s(int16_t *buffer_out, const uint16_t *buffer_in, size_t nframes) {
     size_t nsamples = 2 * nframes;
     for (; nsamples--;) {
@@ -110,3 +106,114 @@ void audiosample_convert_s16m_s16s(int16_t *buffer_out, const int16_t *buffer_in
         *buffer_out++ = sample;
     }
 }
+
+
+void audiosample_convert_u8s_u8m(uint8_t *buffer_out, const uint8_t *buffer_in, size_t nframes) {
+    for (; nframes--;) {
+        uint8_t sample = *buffer_in++ + 0x80;
+        *buffer_out++ = sample;
+        buffer_in++;
+    }
+}
+
+void audiosample_convert_s8m_u8m(uint8_t *buffer_out, const int8_t *buffer_in, size_t nframes) {
+    for (; nframes--;) {
+        uint8_t sample = *buffer_in++ + 0x80;
+        *buffer_out++ = sample;
+    }
+}
+
+void audiosample_convert_s8s_u8m(uint8_t *buffer_out, const int8_t *buffer_in, size_t nframes) {
+    for (; nframes--;) {
+        uint8_t sample = *buffer_in++ + 0x80;
+        *buffer_out++ = sample;
+        buffer_in++;
+    }
+}
+
+void audiosample_convert_u16m_u8m(uint8_t *buffer_out, const uint16_t *buffer_in, size_t nframes) {
+    for (; nframes--;) {
+        uint8_t sample = (*buffer_in++) >> 8;
+        *buffer_out++ = sample;
+    }
+}
+
+void audiosample_convert_u16s_u8m(uint8_t *buffer_out, const uint16_t *buffer_in, size_t nframes) {
+    for (; nframes--;) {
+        uint8_t sample = (*buffer_in++) >> 8;
+        *buffer_out++ = sample;
+        buffer_in++;
+    }
+}
+
+void audiosample_convert_s16m_u8m(uint8_t *buffer_out, const int16_t *buffer_in, size_t nframes) {
+    for (; nframes--;) {
+        uint8_t sample = (*buffer_in++ + 0x8000) >> 8;
+        *buffer_out++ = sample;
+    }
+}
+
+void audiosample_convert_s16s_u8m(uint8_t *buffer_out, const int16_t *buffer_in, size_t nframes) {
+    for (; nframes--;) {
+        uint8_t sample = (*buffer_in++ + 0x8000) >> 8;
+        *buffer_out++ = sample;
+        buffer_in++;
+    }
+}
+
+
+void audiosample_convert_u8m_u8s(uint8_t *buffer_out, const uint8_t *buffer_in, size_t nframes) {
+    for (; nframes--;) {
+        uint8_t sample = *buffer_in++;
+        *buffer_out++ = sample;
+        *buffer_out++ = sample;
+    }
+}
+
+void audiosample_convert_s8m_u8s(uint8_t *buffer_out, const int8_t *buffer_in, size_t nframes) {
+    for (; nframes--;) {
+        uint8_t sample = *buffer_in++ + 0x80;
+        *buffer_out++ = sample;
+        *buffer_out++ = sample;
+    }
+}
+
+void audiosample_convert_s8s_u8s(uint8_t *buffer_out, const int8_t *buffer_in, size_t nframes) {
+    size_t nsamples = 2 * nframes;
+    for (; nsamples--;) {
+        uint8_t sample = *buffer_in++ + 0x80;
+        *buffer_out++ = sample;
+    }
+}
+
+void audiosample_convert_u16m_u8s(uint8_t *buffer_out, const uint16_t *buffer_in, size_t nframes) {
+    for (; nframes--;) {
+        uint8_t sample = (*buffer_in++) >> 8;
+        *buffer_out++ = sample;
+        *buffer_out++ = sample;
+    }
+}
+
+void audiosample_convert_u16s_u8s(uint8_t *buffer_out, const uint16_t *buffer_in, size_t nframes) {
+    size_t nsamples = 2 * nframes;
+    for (; nsamples--;) {
+        uint8_t sample = (*buffer_in++) >> 8;
+        *buffer_out++ = sample;
+    }
+}
+
+void audiosample_convert_s16m_u8s(uint8_t *buffer_out, const int16_t *buffer_in, size_t nframes) {
+    for (; nframes--;) {
+        uint8_t sample = (*buffer_in++ + 0x8000) >> 8;
+        *buffer_out++ = sample;
+        *buffer_out++ = sample;
+    }
+}
+
+void audiosample_convert_s16s_u8s(uint8_t *buffer_out, const int16_t *buffer_in, size_t nframes) {
+    size_t nsamples = 2 * nframes;
+    for (; nsamples--;) {
+        uint8_t sample = (*buffer_in++ + 0x8000) >> 8;
+        *buffer_out++ = sample;
+    }
+}
diff --git a/shared-module/audiocore/__init__.h b/shared-module/audiocore/__init__.h
index 0e5b38d7b1445..2502045594d1e 100644
--- a/shared-module/audiocore/__init__.h
+++ b/shared-module/audiocore/__init__.h
@@ -60,3 +60,19 @@ void audiosample_convert_s8s_s16s(int16_t *buffer_out, const int8_t *buffer_in,
 void audiosample_convert_u16m_s16s(int16_t *buffer_out, const uint16_t *buffer_in, size_t nframes);
 void audiosample_convert_u16s_s16s(int16_t *buffer_out, const uint16_t *buffer_in, size_t nframes);
 void audiosample_convert_s16m_s16s(int16_t *buffer_out, const int16_t *buffer_in, size_t nframes);
+
+void audiosample_convert_u8s_u8m(uint8_t *buffer_out, const uint8_t *buffer_in, size_t nframes);
+void audiosample_convert_s8m_u8m(uint8_t *buffer_out, const int8_t *buffer_in, size_t nframes);
+void audiosample_convert_s8s_u8m(uint8_t *buffer_out, const int8_t *buffer_in, size_t nframes);
+void audiosample_convert_u16m_u8m(uint8_t *buffer_out, const uint16_t *buffer_in, size_t nframes);
+void audiosample_convert_u16s_u8m(uint8_t *buffer_out, const uint16_t *buffer_in, size_t nframes);
+void audiosample_convert_s16m_u8m(uint8_t *buffer_out, const int16_t *buffer_in, size_t nframes);
+void audiosample_convert_s16s_u8m(uint8_t *buffer_out, const int16_t *buffer_in, size_t nframes);
+
+void audiosample_convert_u8m_u8s(uint8_t *buffer_out, const uint8_t *buffer_in, size_t nframes);
+void audiosample_convert_s8m_u8s(uint8_t *buffer_out, const int8_t *buffer_in, size_t nframes);
+void audiosample_convert_s8s_u8s(uint8_t *buffer_out, const int8_t *buffer_in, size_t nframes);
+void audiosample_convert_u16m_u8s(uint8_t *buffer_out, const uint16_t *buffer_in, size_t nframes);
+void audiosample_convert_u16s_u8s(uint8_t *buffer_out, const uint16_t *buffer_in, size_t nframes);
+void audiosample_convert_s16m_u8s(uint8_t *buffer_out, const int16_t *buffer_in, size_t nframes);
+void audiosample_convert_s16s_u8s(uint8_t *buffer_out, const int16_t *buffer_in, size_t nframes);