Example to record and play back audio

- This example starts recording via I2S when the top-left switch on Nexus is
  pressed, and continues recording until either the buffer is full (5
  seconds), or the user releases the button -- whichever comes first.
  Afterwards, it will transmit the recorded audio out via I2S TX.
- When recording, a moving average of the two channels are calculated
  separately, and used to cancel out any DC offset in the input.
- Before transmitting, the amplitude of the audio is scaled to take
  about 75% of the dynamic range.
- For the live demo, https://www.adafruit.com/product/3421 was used as
  the microphone input, and https://www.adafruit.com/product/3006 and a
  generic speaker were used for output.

Change-Id: Ie4413d5578f3f07bbcc37e2640b5433528a22c61
diff --git a/sw/device/examples/i2s_record_playback/BUILD b/sw/device/examples/i2s_record_playback/BUILD
new file mode 100644
index 0000000..4457850
--- /dev/null
+++ b/sw/device/examples/i2s_record_playback/BUILD
@@ -0,0 +1,54 @@
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+load("//rules:matcha.bzl", "bin_to_c_file", "sec_flash_binary", "smc_flash_binary", "NEXUS_CORE_TARGETS")
+
+sec_flash_binary(
+    name = "i2s_record_playback",
+    srcs = [
+        "i2s_record_playback_sec.c",
+        "i2s_record_playback_smc_bin.h",
+        "i2s_record_playback.h",
+    ],
+    per_device_deps = {
+        "fpga_nexus": [NEXUS_CORE_TARGETS.get("secure_core")],
+    },
+    copts = [
+        "-nostdlib",
+        "-ffreestanding",
+    ],
+    deps = [
+        "//sw/device/tests:test_lib",
+        "//sw/device/lib/dif:smc_ctrl",
+        "//sw/device/lib/dif:tlul_mailbox",
+        "@lowrisc_opentitan//sw/device/lib/dif:rv_plic",
+    ],
+)
+
+smc_flash_binary(
+    name = "i2s_record_playback_smc",
+    srcs = [
+        "i2s_record_playback_smc.c",
+        "i2s_record_playback.h",
+    ],
+    per_device_deps = {
+        "fpga_nexus": [NEXUS_CORE_TARGETS.get("smc")],
+    },
+    copts = [
+        "-nostdlib",
+        "-ffreestanding",
+    ],
+    deps = [
+        "//sw/device/tests:test_lib_smc",
+        "//sw/device/lib/dif:i2s",
+        "//sw/device/lib/dif:tlul_mailbox",
+        "@lowrisc_opentitan//sw/device/lib/dif:rv_plic",
+    ],
+)
+
+bin_to_c_file(
+    name = "i2s_record_playback_smc_bin",
+    srcs = ["i2s_record_playback_smc_fpga_nexus_bin"],
+    var_name = "smc_bin",
+)
diff --git a/sw/device/examples/i2s_record_playback/i2s_record_playback.h b/sw/device/examples/i2s_record_playback/i2s_record_playback.h
new file mode 100644
index 0000000..d150ac9
--- /dev/null
+++ b/sw/device/examples/i2s_record_playback/i2s_record_playback.h
@@ -0,0 +1,11 @@
+#ifndef SW_DEVICE_EXAMPLES_I2S_RECORD_PLAYBACK_I2S_RECORD_PLAYBACK_H_
+#define SW_DEVICE_EXAMPLES_I2S_RECORD_PLAYBACK_I2S_RECORD_PLAYBACK_H_
+
+#include <stdint.h>
+
+const uint32_t kMessageGpioPressed = 0xdeadbeef;
+const uint32_t kMessageGpioReleased = 0x21524110;
+const uint32_t kMessageEnableLed = 0xcafeb0ba;
+const uint32_t kMessageDisableLed = 0x35014f45;
+
+#endif  // SW_DEVICE_EXAMPLES_I2S_RECORD_PLAYBACK_I2S_RECORD_PLAYBACK_H_
diff --git a/sw/device/examples/i2s_record_playback/i2s_record_playback_sec.c b/sw/device/examples/i2s_record_playback/i2s_record_playback_sec.c
new file mode 100644
index 0000000..c406e81
--- /dev/null
+++ b/sw/device/examples/i2s_record_playback/i2s_record_playback_sec.c
@@ -0,0 +1,119 @@
+// Copyright 2023 Google LLC.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#include "hw/top_matcha/sw/autogen/top_matcha.h"
+#include "sw/device/examples/i2s_record_playback/i2s_record_playback.h"
+#include "sw/device/examples/i2s_record_playback/i2s_record_playback_smc_bin.h"  // Generated.
+#include "sw/device/lib/arch/device.h"
+#include "sw/device/lib/dif/dif_gpio.h"
+#include "sw/device/lib/dif/dif_pinmux.h"
+#include "sw/device/lib/dif/dif_rv_plic.h"
+#include "sw/device/lib/dif/dif_smc_ctrl.h"
+#include "sw/device/lib/dif/dif_tlul_mailbox.h"
+#include "sw/device/lib/dif/dif_uart.h"
+#include "sw/device/lib/runtime/hart.h"
+#include "sw/device/lib/runtime/irq.h"
+#include "sw/device/lib/testing/test_framework/check.h"
+#include "sw/device/lib/testing/test_framework/ottf_test_config.h"
+#include "sw/device/lib/testing/test_framework/test_util.h"
+
+static dif_gpio_t gpio;
+static dif_pinmux_t pinmux;
+static dif_rv_plic_t plic;
+static dif_tlul_mailbox_t mailbox;
+static dif_smc_ctrl_t smc_ctrl;
+static dif_uart_t uart;
+
+OTTF_DEFINE_TEST_CONFIG();
+
+void ottf_external_isr(void) {
+  dif_rv_plic_irq_id_t interrupt_id;
+  CHECK_DIF_OK(
+      dif_rv_plic_irq_claim(&plic, kTopMatchaPlicTargetIbex0, &interrupt_id));
+
+  top_matcha_plic_peripheral_t peripheral_id =
+      top_matcha_plic_interrupt_for_peripheral[interrupt_id];
+  CHECK(peripheral_id == kTopMatchaPlicPeripheralGpio ||
+            peripheral_id == kTopMatchaPlicPeripheralTlulMailboxSec,
+        "ISR got an interrupt from a non-GPIO");
+
+  if (peripheral_id == kTopMatchaPlicPeripheralGpio) {
+    dif_gpio_irq_t gpio_num = interrupt_id - kTopMatchaPlicIrqIdGpioGpio0;
+    CHECK(gpio_num == 0);
+    CHECK_DIF_OK(dif_gpio_irq_acknowledge(&gpio, gpio_num));
+    bool gpio_state;
+    CHECK_DIF_OK(dif_gpio_read(&gpio, gpio_num, &gpio_state));
+    // The GPIO will be high when the button is not pressed.
+    uint32_t msg = gpio_state ? kMessageGpioReleased : kMessageGpioPressed;
+    CHECK_DIF_OK(dif_tlul_mailbox_send_message(&mailbox, &msg));
+  }
+  if (peripheral_id == kTopMatchaPlicPeripheralTlulMailboxSec) {
+    CHECK_DIF_OK(
+        dif_tlul_mailbox_irq_acknowledge(&mailbox, kDifTlulMailboxIrqRtirq));
+    uint32_t msg;
+    CHECK_DIF_OK(dif_tlul_mailbox_read_message(&mailbox, &msg));
+    if (msg == kMessageEnableLed) {
+      CHECK_DIF_OK(dif_gpio_write(&gpio, 1, true));
+    }
+    if (msg == kMessageDisableLed) {
+      CHECK_DIF_OK(dif_gpio_write(&gpio, 1, false));
+    }
+  }
+  CHECK_DIF_OK(
+      dif_rv_plic_irq_complete(&plic, kTopMatchaPlicTargetIbex0, interrupt_id));
+}
+
+void _ottf_main(void) {
+  // Initialize the UART to enable logging for non-DV simulation platforms.
+  if (kDeviceType != kDeviceSimDV) {
+    init_uart(TOP_MATCHA_UART0_BASE_ADDR, &uart);
+  }
+  CHECK_DIF_OK(
+      dif_gpio_init(mmio_region_from_addr(TOP_MATCHA_GPIO_BASE_ADDR), &gpio));
+  CHECK_DIF_OK(dif_pinmux_init(
+      mmio_region_from_addr(TOP_MATCHA_PINMUX_AON_BASE_ADDR), &pinmux));
+  CHECK_DIF_OK(dif_smc_ctrl_init(
+      mmio_region_from_addr(TOP_MATCHA_SMC_CTRL_BASE_ADDR), &smc_ctrl));
+  CHECK_DIF_OK(dif_tlul_mailbox_init(
+      mmio_region_from_addr(TOP_MATCHA_TLUL_MAILBOX_SEC_BASE_ADDR), &mailbox));
+
+  if (kDeviceType == kDeviceFpgaNexus) {
+    memcpy((void*)TOP_MATCHA_RAM_SMC_BASE_ADDR, smc_bin, smc_bin_len);
+  }
+  CHECK_DIF_OK(dif_smc_ctrl_set_en(&smc_ctrl));
+
+  // Bind switch on IOR5 to GPIO0 and configure interrupts.
+  CHECK_DIF_OK(dif_rv_plic_init(
+      mmio_region_from_addr(TOP_MATCHA_RV_PLIC_BASE_ADDR), &plic));
+  CHECK_DIF_OK(dif_pinmux_output_select(&pinmux, kTopMatchaPinmuxMioOutIoa8,
+                                        kTopMatchaPinmuxOutselGpioGpio1));
+  CHECK_DIF_OK(dif_gpio_output_set_enabled(&gpio, 1, kDifToggleEnabled));
+  CHECK_DIF_OK(dif_pinmux_input_select(&pinmux,
+                                       kTopMatchaPinmuxPeripheralInGpioGpio0,
+                                       kTopMatchaPinmuxInselIor5));
+  CHECK_DIF_OK(dif_gpio_output_set_enabled(&gpio, 0, kDifToggleDisabled));
+  CHECK_DIF_OK(dif_gpio_irq_set_trigger(&gpio, (1 << 0) /* GPIO 0*/,
+                                        kDifGpioIrqTriggerEdgeRisingFalling));
+  CHECK_DIF_OK(
+      dif_gpio_irq_set_enabled(&gpio, kDifGpioIrqGpio0, kDifToggleEnabled));
+  CHECK_DIF_OK(dif_rv_plic_irq_set_priority(&plic, kTopMatchaPlicIrqIdGpioGpio0,
+                                            kDifRvPlicMaxPriority));
+  CHECK_DIF_OK(dif_rv_plic_irq_set_enabled(&plic, kTopMatchaPlicIrqIdGpioGpio0,
+                                           kTopMatchaPlicTargetIbex0,
+                                           kDifToggleEnabled));
+  CHECK_DIF_OK(dif_tlul_mailbox_irq_set_enabled(
+      &mailbox, kDifTlulMailboxIrqRtirq, kDifToggleEnabled));
+  CHECK_DIF_OK(dif_rv_plic_irq_set_enabled(
+      &plic, kTopMatchaPlicIrqIdTlulMailboxSecRtirq, kTopMatchaPlicTargetIbex0,
+      kDifToggleEnabled));
+  CHECK_DIF_OK(dif_rv_plic_irq_set_priority(
+      &plic, kTopMatchaPlicIrqIdTlulMailboxSecRtirq, kDifRvPlicMaxPriority));
+  irq_global_ctrl(true);
+  irq_external_ctrl(true);
+
+  while (true) {
+    wait_for_interrupt();
+  }
+  __builtin_unreachable();
+}
diff --git a/sw/device/examples/i2s_record_playback/i2s_record_playback_smc.c b/sw/device/examples/i2s_record_playback/i2s_record_playback_smc.c
new file mode 100644
index 0000000..a236e84
--- /dev/null
+++ b/sw/device/examples/i2s_record_playback/i2s_record_playback_smc.c
@@ -0,0 +1,308 @@
+// Copyright 2022 Google LLC.
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#include <limits.h>
+
+#include "hw/top_matcha/ip/i2s/data/i2s_regs.h"
+#include "hw/top_matcha/sw/autogen/top_matcha.h"
+#include "sw/device/examples/i2s_record_playback/i2s_record_playback.h"
+#include "sw/device/lib/dif/dif_i2s.h"
+#include "sw/device/lib/dif/dif_rv_plic.h"
+#include "sw/device/lib/dif/dif_tlul_mailbox.h"
+#include "sw/device/lib/runtime/hart.h"
+#include "sw/device/lib/runtime/irq.h"
+#include "sw/device/lib/runtime/log.h"
+#include "sw/device/lib/runtime/print.h"
+#include "sw/device/lib/testing/test_framework/check.h"
+#include "sw/device/lib/testing/test_framework/ottf_test_config.h"
+#include "sw/device/lib/testing/test_framework/status.h"
+#include "sw/device/lib/testing/test_framework/test_util.h"
+
+OTTF_DEFINE_TEST_CONFIG();
+
+static dif_i2s_t i2s;
+static dif_rv_plic_t plic_smc;
+static dif_uart_t smc_uart;
+static dif_tlul_mailbox_t mailbox;
+
+volatile bool i2s_rx_watermark_seen = false;
+volatile bool i2s_tx_watermark_seen = false;
+volatile bool i2s_tx_empty_seen = false;
+volatile bool record = false;
+
+#define ABS(x) (x > 0 ? x : -x)
+
+#define kFilterSamples (256)
+int16_t samples_left[kFilterSamples] = {0};
+int16_t samples_right[kFilterSamples] = {0};
+size_t index_left = 0;
+size_t index_right = 0;
+int32_t total_left = 0;
+int32_t total_right = 0;
+
+void ottf_external_isr(void) {
+  dif_rv_plic_irq_id_t interrupt_id;
+  CHECK_DIF_OK(dif_rv_plic_irq_claim(&plic_smc, kTopMatchaPlicTargetIbex0Smc,
+                                     &interrupt_id));
+
+  top_matcha_plic_peripheral_smc_t peripheral_id =
+      top_matcha_plic_interrupt_for_peripheral_smc[interrupt_id];
+  switch (peripheral_id) {
+    case kTopMatchaPlicPeripheralTlulMailboxSmc: {
+      uint32_t message;
+      CHECK_DIF_OK(
+          dif_tlul_mailbox_irq_acknowledge(&mailbox, kDifTlulMailboxIrqRtirq));
+      CHECK_DIF_OK(dif_tlul_mailbox_read_message(&mailbox, &message));
+      if (message == kMessageGpioPressed) {
+        record = true;
+      } else if (message == kMessageGpioReleased) {
+        record = false;
+      } else {
+        CHECK(false, "Unknown message");
+      }
+      break;
+    }
+    case kTopMatchaPlicPeripheralI2s0: {
+      switch (interrupt_id) {
+        case kTopMatchaPlicIrqIdI2s0RxWatermark:
+          // Acknowledge and disable the interrupt.
+          // It will be re-enabled after the RX FIFO is drained.
+          CHECK_DIF_OK(dif_i2s_irq_acknowledge(&i2s, kDifI2sIrqRxWatermark));
+          CHECK_DIF_OK(dif_i2s_irq_set_enabled(&i2s, kDifI2sIrqRxWatermark,
+                                               kDifToggleDisabled));
+          i2s_rx_watermark_seen = true;
+          break;
+        case kTopMatchaPlicIrqIdI2s0TxWatermark:
+          // Acknowledge and disable the interrupt.
+          // It will be re-enabled after the TX FIFO is reloaded.
+          CHECK_DIF_OK(dif_i2s_irq_acknowledge(&i2s, kDifI2sIrqTxWatermark));
+          CHECK_DIF_OK(dif_i2s_irq_set_enabled(&i2s, kDifI2sIrqTxWatermark,
+                                               kDifToggleDisabled));
+          i2s_tx_watermark_seen = true;
+          break;
+        case kTopMatchaPlicIrqIdI2s0TxEmpty:
+          CHECK_DIF_OK(dif_i2s_irq_acknowledge(&i2s, kDifI2sIrqTxEmpty));
+          CHECK_DIF_OK(dif_i2s_irq_set_enabled(&i2s, kDifI2sIrqTxEmpty,
+                                               kDifToggleDisabled));
+          i2s_tx_empty_seen = true;
+          break;
+        default:
+          CHECK(false, "Unhandled I2S interrupt");
+      }
+      break;
+    }
+    default:
+      CHECK(false, "Unhandled peripheral!");
+  }
+
+  CHECK_DIF_OK(dif_rv_plic_irq_complete(&plic_smc, kTopMatchaPlicTargetIbex0Smc,
+                                        interrupt_id));
+}
+
+#define kSamples (5 * 16000)
+uint32_t samples[kSamples];
+
+void _ottf_main(void) {
+  // Initialize the SMC UART to enable logging for non-DV simulation platforms.
+  if (kDeviceType != kDeviceSimDV) {
+    init_uart(TOP_MATCHA_SMC_UART_BASE_ADDR, &smc_uart);
+  }
+  CHECK_DIF_OK(dif_rv_plic_init(
+      mmio_region_from_addr(TOP_MATCHA_RV_PLIC_SMC_BASE_ADDR), &plic_smc));
+  CHECK_DIF_OK(dif_tlul_mailbox_init(
+      mmio_region_from_addr(TOP_MATCHA_TLUL_MAILBOX_SMC_BASE_ADDR), &mailbox));
+
+  CHECK_DIF_OK(dif_tlul_mailbox_irq_set_enabled(
+      &mailbox, kDifTlulMailboxIrqRtirq, kDifToggleEnabled));
+  CHECK_DIF_OK(dif_rv_plic_irq_set_enabled(
+      &plic_smc, kTopMatchaPlicIrqIdTlulMailboxSmcRtirq,
+      kTopMatchaPlicTargetIbex0Smc, kDifToggleEnabled));
+  CHECK_DIF_OK(dif_rv_plic_irq_set_priority(
+      &plic_smc, kTopMatchaPlicIrqIdTlulMailboxSmcRtirq,
+      kDifRvPlicMaxPriority));
+
+  CHECK_DIF_OK(dif_rv_plic_irq_set_priority(
+      &plic_smc, kTopMatchaPlicIrqIdI2s0RxWatermark, kDifRvPlicMaxPriority));
+  CHECK_DIF_OK(dif_rv_plic_irq_set_enabled(
+      &plic_smc, kTopMatchaPlicIrqIdI2s0RxWatermark,
+      kTopMatchaPlicTargetIbex0Smc, kDifToggleEnabled));
+  CHECK_DIF_OK(dif_rv_plic_irq_set_priority(
+      &plic_smc, kTopMatchaPlicIrqIdI2s0TxWatermark, kDifRvPlicMaxPriority));
+  CHECK_DIF_OK(dif_rv_plic_irq_set_enabled(
+      &plic_smc, kTopMatchaPlicIrqIdI2s0TxWatermark,
+      kTopMatchaPlicTargetIbex0Smc, kDifToggleEnabled));
+  CHECK_DIF_OK(dif_rv_plic_irq_set_priority(
+      &plic_smc, kTopMatchaPlicIrqIdI2s0TxEmpty, kDifRvPlicMaxPriority));
+  CHECK_DIF_OK(dif_rv_plic_irq_set_enabled(
+      &plic_smc, kTopMatchaPlicIrqIdI2s0TxEmpty, kTopMatchaPlicTargetIbex0Smc,
+      kDifToggleEnabled));
+  irq_global_ctrl(true);
+  irq_external_ctrl(true);
+
+  memset(samples, 0xa5, sizeof(samples));
+  CHECK_DIF_OK(
+      dif_i2s_init(mmio_region_from_addr(TOP_MATCHA_I2S0_BASE_ADDR), &i2s));
+
+  // Clear RX FIFO
+  uint32_t reg_val;
+  reg_val = mmio_region_read32(i2s.base_addr, I2S_FIFO_CTRL_REG_OFFSET);
+  reg_val = bitfield_bit32_write(reg_val, I2S_FIFO_CTRL_RXRST_BIT, 1);
+  reg_val = bitfield_field32_write(reg_val, I2S_FIFO_CTRL_TXILVL_FIELD, 3);
+  mmio_region_write32(i2s.base_addr, I2S_FIFO_CTRL_REG_OFFSET, reg_val);
+
+  CHECK_DIF_OK(dif_i2s_irq_acknowledge_all(&i2s));
+  CHECK_DIF_OK(
+      dif_i2s_irq_set_enabled(&i2s, kDifI2sIrqRxWatermark, kDifToggleEnabled));
+
+  while (true) {
+    // Wait until the mailbox tells us to start
+    while (true) {
+      if (!record) {
+        wait_for_interrupt();
+      }
+      if (record) {
+        break;
+      }
+    }
+
+    uint32_t message_enable_led = kMessageEnableLed;
+    CHECK_DIF_OK(dif_tlul_mailbox_send_message(&mailbox, &message_enable_led));
+    LOG_INFO("Start recording...");
+    // Configure and enable recording
+    reg_val = mmio_region_read32(i2s.base_addr, I2S_CTRL_REG_OFFSET);
+    reg_val = bitfield_bit32_write(reg_val, I2S_CTRL_RX_BIT, 1);
+    reg_val = bitfield_field32_write(reg_val, I2S_CTRL_NCO_RX_FIELD,
+                                     24);  // divide by 24
+    mmio_region_write32(i2s.base_addr, I2S_CTRL_REG_OFFSET, reg_val);
+
+    // Record until our buffer is full, or the mailbox told us to stop.
+    int sample = 0;
+    while (sample < ARRAYSIZE(samples) && record) {
+      CHECK_DIF_OK(dif_i2s_irq_set_enabled(&i2s, kDifI2sIrqRxWatermark,
+                                           kDifToggleEnabled));
+      while (!i2s_rx_watermark_seen) {
+        wait_for_interrupt();
+      }
+      i2s_rx_watermark_seen = false;
+
+      bool empty;
+      CHECK_DIF_OK(dif_i2s_rxfifo_empty(&i2s, &empty));
+      while (!empty) {
+        reg_val = mmio_region_read32(i2s.base_addr, I2S_RDATA_REG_OFFSET);
+        // For each sample, split into left and right channel values,
+        // and update the moving average.
+        int16_t left = reg_val >> 16;
+        int16_t right = reg_val & 0xFFFF;
+        total_left -= samples_left[index_left];
+        total_right -= samples_right[index_right];
+        total_left += left;
+        samples_left[index_left] = left;
+        total_right += right;
+        samples_right[index_right] = right;
+        index_left = (index_left + 1) % ARRAYSIZE(samples_left);
+        index_right = (index_right + 1) % ARRAYSIZE(samples_right);
+        int16_t mean_left = total_left / ARRAYSIZE(samples_left);
+        int16_t mean_right = total_right / ARRAYSIZE(samples_right);
+
+        // Subtract the moving average from each channel, and repack into the
+        // sample buffer.
+        uint32_t offset_reg_val = (((left - mean_left) & 0xFFFF) << 16) |
+                                  ((right - mean_right) & 0xFFFF);
+        samples[sample++] = offset_reg_val;
+        CHECK_DIF_OK(dif_i2s_rxfifo_empty(&i2s, &empty));
+        if (sample == ARRAYSIZE(samples) || !record) {
+          break;
+        }
+      }
+    }
+
+    // Disable recording
+    reg_val = mmio_region_read32(i2s.base_addr, I2S_CTRL_REG_OFFSET);
+    reg_val = bitfield_bit32_write(reg_val, I2S_CTRL_RX_BIT, 0);
+    mmio_region_write32(i2s.base_addr, I2S_CTRL_REG_OFFSET, reg_val);
+    uint32_t message_disable_led = kMessageDisableLed;
+    CHECK_DIF_OK(dif_tlul_mailbox_send_message(&mailbox, &message_disable_led));
+
+    LOG_INFO("Done recording %d samples", sample);
+    int samples_captured = sample;
+
+    // Calculate the min/max of the audio, after correcting DC offsets
+    int32_t max = INT16_MIN;
+    int32_t min = INT16_MAX;
+    for (int i = 1; i < (samples_captured * 2); i += 2) {
+      int16_t* samples_s16 = (int16_t*)samples;
+      int32_t sample = samples_s16[i];
+      if (sample < min) {
+        min = sample;
+      }
+      if (sample > max) {
+        max = sample;
+      }
+    }
+
+    // Calculate a scaling factor, and use this to scale the waveform
+    // to a peak of 75% amplitude.
+    int32_t scale_max = ((int32_t)max * 100) / ((int32_t)INT16_MAX);
+    int32_t scale_min = ABS(((int32_t)min * 100) / ((int32_t)INT16_MIN));
+    int32_t scale = scale_max > scale_min ? scale_max : scale_min;
+    for (int i = 1; i < (samples_captured * 2); i += 2) {
+      int16_t* samples_s16 = (int16_t*)samples;
+      int16_t sample = samples_s16[i];
+      int16_t scaled_sample = (int16_t)(((int32_t)sample * 100) / scale);
+      scaled_sample = (((int32_t)scaled_sample * 75) / 100);
+      samples_s16[i] = scaled_sample;
+    }
+
+    LOG_INFO("Playing back recorded samples...");
+    // Enable TX interrupts, disable RX interrupts
+    CHECK_DIF_OK(dif_i2s_irq_set_enabled(&i2s, kDifI2sIrqRxWatermark,
+                                         kDifToggleDisabled));
+    CHECK_DIF_OK(dif_i2s_irq_set_enabled(&i2s, kDifI2sIrqTxWatermark,
+                                         kDifToggleEnabled));
+    CHECK_DIF_OK(
+        dif_i2s_irq_set_enabled(&i2s, kDifI2sIrqTxEmpty, kDifToggleEnabled));
+
+    reg_val = mmio_region_read32(i2s.base_addr, I2S_CTRL_REG_OFFSET);
+    reg_val = bitfield_bit32_write(reg_val, I2S_CTRL_TX_BIT, 1);
+    reg_val = bitfield_field32_write(reg_val, I2S_CTRL_NCO_TX_FIELD,
+                                     48);  // divide by 96
+    mmio_region_write32(i2s.base_addr, I2S_CTRL_REG_OFFSET, reg_val);
+    uint32_t samples_to_tx = sample;
+    uint32_t tx_audio_count = 0;
+    while (tx_audio_count < samples_to_tx) {
+      while (!i2s_tx_watermark_seen) {
+        wait_for_interrupt();
+      }
+      i2s_tx_watermark_seen = false;
+      i2s_tx_empty_seen = false;
+
+      // Push as many bytes into the FIFO as we can manage. If this
+      // goes until we write the all, so much the better.
+      bool full;
+      do {
+        CHECK_DIF_OK(dif_i2s_txfifo_full(&i2s, &full));
+        if (full) {
+          break;
+        }
+        uint32_t next_tx_val = samples[tx_audio_count++];
+        mmio_region_write32(i2s.base_addr, I2S_WDATA_REG_OFFSET, next_tx_val);
+      } while (tx_audio_count < samples_to_tx && !full);
+      CHECK_DIF_OK(dif_i2s_irq_set_enabled(&i2s, kDifI2sIrqTxWatermark,
+                                           kDifToggleEnabled));
+      CHECK_DIF_OK(
+          dif_i2s_irq_set_enabled(&i2s, kDifI2sIrqTxEmpty, kDifToggleEnabled));
+    }
+
+    // Wait for the FIFO to drain, we should receive the empty interrupt.
+    while (!i2s_tx_empty_seen) {
+      wait_for_interrupt();
+    }
+    reg_val = mmio_region_read32(i2s.base_addr, I2S_CTRL_REG_OFFSET);
+    reg_val = bitfield_bit32_write(reg_val, I2S_CTRL_TX_BIT, 0);
+    mmio_region_write32(i2s.base_addr, I2S_CTRL_REG_OFFSET, reg_val);
+    LOG_INFO("Done with playback.");
+  }
+  __builtin_unreachable();
+}