Initial CHERIoT Soundstream demo

Converts the bare-metal soundstream demo app to a hypothetical sencha
platform (derived from matcha by replacing the SMC Ibex core by a RISC-V
CHERIoT core). This depends on the SEC to boot the SMC from flash and
uses DIF support where possible for peripherals.

Specific changes:
- stick drivers in compartments
- use cheriot-rtos interrupts & threads
- use capabilities for mmio + shared memory refs
- allocate app data from the heap instead of using global vars (temp
  until toolchain is updated)
- hide hard-to-use-directly bits in a compat.h
- use cheriot api's where appropriate (e.g.  __builtin_unreachable -> panic)
- band-aid dif include files to workaround c++ usage

NB: interrupt thread stacks are super-sized to support logging

Bug: 330741645

Bypass-Presubmit-Reason: no sencha presubmit tests

Change-Id: Iea2122422231571eb0a477b31b47eed0cb0c18f7
diff --git a/sw/device/cheriot/soundstream/.gitignore b/sw/device/cheriot/soundstream/.gitignore
new file mode 100644
index 0000000..b39c548
--- /dev/null
+++ b/sw/device/cheriot/soundstream/.gitignore
@@ -0,0 +1 @@
+*.proto
diff --git a/sw/device/cheriot/soundstream/README.md b/sw/device/cheriot/soundstream/README.md
new file mode 100644
index 0000000..12f2f32
--- /dev/null
+++ b/sw/device/cheriot/soundstream/README.md
@@ -0,0 +1,144 @@
+Soundstream bare-metal demo example
+===================================
+
+Cheriot port of the shodan soundstream bare-metal demo.
+
+If you do not have a shodan repo setup, follow the instructions at
+https://spacebeaker.googlesource.com/shodan/docs/+/refs/heads/master/GettingStarted.md.
+
+Be sure ROOTDIR is set in the environment pointing to a current
+shodan repo checkout and the target platform is "sencha"; e.g.
+```shell
+cd ~/shodan
+source build/setup.sh
+set-platform sencha
+printenv ROOTDIR
+/usr/local/google/home/sleffler/shodan
+```
+
+The first time you setup a "sencha" platform you need to install the
+necessary tools:
+```shell
+set-platform sencha
+m tools
+```
+(note the tools are platform-dependent and only installed when
+the current platform is set to "sencha").
+
+You also need a current `xmake` to build cheriot firmware. Note the most
+recent prebuilt package is too old so you need to do something like:
+```shell
+$ sudo apt-get install xmake
+Reading package lists... Done
+Building dependency tree... Done
+Reading state information... Done
+The following NEW packages will be installed:
+  xmake
+0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded.
+...
+Setting up xmake (2.8.6+ds-3) ...
+...
+$ which xmake
+/usr/bin/xmake
+$ xmake update
+update version v2.9.1 from official source ..
+  => download https://gitlab.com/tboox/xmake.git .. ok
+  => install to ~/.local/bin .. ok
+```
+(if you use an old xmake you will see this failure:
+```shell
+        xmake build
+checking for platform ... cheriot
+checking for architecture ... cheriot
+error: decode json failed, @programdir/core/base/json.lua:223: invalid json syntax starting at position 63: x2000000,
+```
+)
+
+Build a sencha platform image with the soundstream firmware for the SMC
+and run the simulator:
+
+```shell
+$ m simulate
+...
+export XMAKE_CONFIGDIR=/usr/local/google/home/sleffler/shodan/out/cheriot/sencha/release; \
+cd/usr/local/google/home/sleffler/shodan/hw/matcha/sw/device/cheriot/soundstream && \
+        xmake config \
+                -o /usr/local/google/home/sleffler/shodan/out/cheriot/sencha/release \
+                --sdk=/usr/local/google/home/sleffler/shodan/cache/cheriot-tools \
+                --board=sencha \
+                --debug-scheduler=true --debug-allocator=true && \
+xmake build
+checking for platform ... cheriot
+checking for architecture ... cheriot
+generating /usr/local/google/home/sleffler/shodan/sw/cheriot-rtos/sdk/firmware.ldscript.in ... ok
+[ 31%]: cache compiling.release i2s.cc
+[ 31%]: cache compiling.release ../../lib/dif/dif_i2s.c
+[ 32%]: cache compiling.release soundstream.cc
+[ 32%]: cache compiling.release ../../lib/dif/autogen/dif_i2s_autogen.c
+[ 32%]: cache compiling.release ../../../../../../sw/cheriot-rtos/sdk/lib/crt/cz.c
+[ 32%]: cache compiling.release ../../../../../../sw/cheriot-rtos/sdk/lib/crt/arith64.c
+[ 32%]: cache compiling.release ../../../../../../sw/cheriot-rtos/sdk/core/scheduler/main.cc
+[ 32%]: cache compiling.release ../../../../../../sw/cheriot-rtos/sdk/lib/atomic/atomic1.cc
+[ 33%]: cache compiling.release encode.cc
+[ 34%]: cache compiling.release ../../../../hw/top_matcha/sw/autogen/top_matcha.c
+[ 37%]: cache compiling.release ../../../../../../sw/cheriot-rtos/sdk/lib/freestanding/memcmp.c
+[ 38%]: cache compiling.release ../../../../../../sw/cheriot-rtos/sdk/lib/freestanding/memcpy.c
+[ 38%]: compiling.release ../../../../../../sw/cheriot-rtos/sdk/core/token_library/token_unseal.S
+[ 39%]: cache compiling.release ../../../../../../sw/cheriot-rtos/sdk/lib/freestanding/memset.c
+[ 40%]: cache compiling.release mailbox.cc
+[ 42%]: cache compiling.release ../../lib/dif/dif_tlul_mailbox.c
+[ 43%]: cache compiling.release ../../lib/dif/autogen/dif_tlul_mailbox_autogen.c
+[ 44%]: compiling.release ../../../../../../sw/cheriot-rtos/sdk/core/switcher/entry.S
+[ 45%]: cache compiling.release ../../../../../../sw/cheriot-rtos/sdk/core/loader/boot.cc
+[ 46%]: compiling.release ../../../../../../sw/cheriot-rtos/sdk/core/loader/boot.S
+[ 46%]: cache compiling.release ../../../../../../sw/cheriot-rtos/sdk/core/software_revoker/revoker.cc
+[ 48%]: cache compiling.release ../../../../../../sw/cheriot-rtos/sdk/lib/debug/debug.cc
+[ 49%]: cache compiling.release ../../../../../../sw/cheriot-rtos/sdk/core/allocator/main.cc
+[ 50%]: cache compiling.release ../../../../../../sw/cheriot-rtos/sdk/lib/compartment_helpers/claim_fast.cc
+[ 51%]: cache compiling.release ../../../../../../sw/cheriot-rtos/sdk/lib/compartment_helpers/check_pointer.cc
+[ 53%]: cache compiling.release ../../../../../../sw/cheriot-rtos/sdk/lib/locks/locks.cc
+[ 54%]: cache compiling.release ../../../../../../sw/cheriot-rtos/sdk/lib/locks/semaphore.cc
+[ 55%]: cache compiling.release ../../../../../../sw/cheriot-rtos/sdk/lib/atomic/atomic4.cc
+[ 56%]: cache compiling.release ml_top.cc
+[ 57%]: cache compiling.release ../../lib/dif/dif_ml_top.c
+[ 59%]: cache compiling.release ../../lib/dif/autogen/dif_ml_top_autogen.c
+[ 60%]: linking library crt.library
+[ 61%]: linking privileged library cheriot.token_library.library
+[ 62%]: linking library freestanding.library
+[ 65%]: linking compartment i2s.compartment
+[ 66%]: linking compartment soundstream.compartment
+[ 67%]: linking library atomic1.library
+[ 68%]: linking compartment mailbox.compartment
+[ 69%]: linking library debug.library
+[ 71%]: linking privileged compartment cheriot.software_revoker.compartment
+[ 83%]: linking library atomic4.library
+[ 85%]: linking library compartment_helpers.library
+[ 89%]: linking library locks.library
+[ 91%]: linking privileged compartment soundstream-firmware.scheduler.compartment
+[ 93%]: linking compartment ml_top.compartment
+[ 96%]: linking privileged compartment cheriot.allocator.compartment
+[ 98%]: linking firmware ../../../../../../out/cheriot/sencha/release/cheriot/cheriot/release/soundstream-firmware
+[ 98%]: Creating firmware report ../../../../../../out/cheriot/sencha/release/cheriot/cheriot/release/soundstream-firmware.json
+[ 98%]: Creating firmware dump ../../../../../../out/cheriot/sencha/release/cheriot/cheriot/release/soundstream-firmware.dump
+...
+mkdir /usr/local/google/home/sleffler/shodan/out/cheriot/sencha/release/tmp
+cp -f /usr/local/google/home/sleffler/shodan/out/matcha-bundle-release.elf /usr/local/google/home/sleffler/shodan/out/cheriot/sencha/release/tmp/matcha-tock-bundle
+riscv32-unknown-elf-strip /usr/local/google/home/sleffler/shodan/out/cheriot/sencha/release/tmp/matcha-tock-bundle
+riscv32-unknown-elf-objcopy -O binary -g /usr/local/google/home/sleffler/shodan/out/cheriot/sencha/release/tmp/matcha-tock-bundle /usr/local/google/home/sleffler/shodan/out/cheriot/sencha/release/tmp/matcha-tock-bundle.bin
+ln -sf /usr/local/google/home/sleffler/shodan/out/cheriot/sencha/release/cheriot/cheriot/release/soundstream-firmware /usr/local/google/home/sleffler/shodan/out/cheriot/sencha/release/tmp/kernel
+tar -C /usr/local/google/home/sleffler/shodan/out/cheriot/sencha/release/tmp -cvhf /usr/local/google/home/sleffler/shodan/out/cheriot/sencha/release/ext_flash.tar matcha-tock-bundle.bin kernel
+matcha-tock-bundle.bin
+kernel
+cd /usr/local/google/home/sleffler/shodan && /usr/local/google/home/sleffler/shodan/cache/renode/renode --disable-xwt --port 1234 -e "\
+    \$repl_file = @sim/config/platforms/sencha.repl; \
+    \$tar = @/usr/local/google/home/sleffler/shodan/out/cheriot/sencha/release/ext_flash.tar; \
+    \$sc_bin =@/usr/local/google/home/sleffler/shodan/out/cheriot/sencha/release/tmp/matcha-tock-bundle.bin; \
+    \$term_port = 3456; \$gdb_port = 3333; i @sim/config/sencha.resc; \
+        pause; cpu0 IsHalted false; start"
+16:20:42.1702 [INFO] Loaded monitor commands from: /usr/local/google/home/sleffler/shodan/cache/renode/scripts/monitor.py
+16:20:42.1895 [INFO] Monitor available in telnet mode on port 1234
+16:20:42.4740 [INFO] Including script: /usr/local/google/home/sleffler/shodan/sim/config/sencha.resc
+16:20:42.4910 [INFO] System bus created.
+16:20:45.5498 [INFO] Including script: /usr/local/google/home/sleffler/shodan/sim/config/sencha.resc
+...
+```
diff --git a/sw/device/cheriot/soundstream/compat.h b/sw/device/cheriot/soundstream/compat.h
new file mode 100644
index 0000000..70b1193
--- /dev/null
+++ b/sw/device/cheriot/soundstream/compat.h
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef EXAMPLES_SOUNDSTREAM_COMPAT_H_
+#define EXAMPLES_SOUNDSTREAM_COMPAT_H_
+
+// XXX maybe uart_write?
+// XXX fix var args
+#define LOG_ERROR(msg, ...) Debug::log("{}", msg)
+#define LOG_INFO(msg, ...) Debug::log("{}", msg)
+#define LOG_FATAL(msg, ...) Debug::Assert(false, "{}", msg)
+
+/**
+ * Checks that the given condition is true. If the condition is false, this
+ * function logs and then aborts.
+ *
+ * @param condition An expression to check.
+ * @param ... Arguments to a LOG_* macro, which are evaluated if the check
+ * fails.
+ */
+#define CHECK(condition, ...)                        \
+  do {                                               \
+    if (!(condition)) {                              \
+      /* NOTE: because the condition in this if      \
+         statement can be statically determined,     \
+         only one of the below string constants      \
+         will be included in the final binary.*/     \
+      if (OT_VA_ARGS_COUNT(_, ##__VA_ARGS__) == 0) { \
+        LOG_ERROR("CHECK-fail: " #condition);        \
+      } else {                                       \
+        LOG_ERROR("CHECK-fail: " __VA_ARGS__);       \
+      }                                              \
+      /* Currently, this macro will call into        \
+         the test failure code, which logs           \
+         "FAIL" and aborts. In the future,           \
+         we will try to condition on whether         \
+         or not this is a test.*/                    \
+      /* XXX fill me in */                           \
+    }                                                \
+  } while (false)
+
+/**
+ * Checks that the given DIF call returns kDifOk. If the DIF call returns a
+ * different dif_result_t value (defined in sw/device/lib/dif/dif_base.h), this
+ * function logs and then aborts.
+ *
+ * @param dif_call DIF call to invoke and check its return value.
+ * @param ... Arguments to a LOG_* macro, which are evaluated if the check
+ * fails.
+ */
+#define CHECK_DIF_OK(dif_call, ...)                                  \
+  do {                                                               \
+    dif_result_t dif_result = dif_call;                              \
+    if (dif_result != kDifOk) {                                      \
+      /* NOTE: because the condition in this if                      \
+         statement can be statically determined,                     \
+         only one of the below string constants                      \
+         will be included in the final binary.*/                     \
+      if (OT_VA_ARGS_COUNT(_, ##__VA_ARGS__) == 0) {                 \
+        LOG_ERROR("DIF-fail: " #dif_call " returns %d", dif_result); \
+      } else {                                                       \
+        LOG_ERROR("DIF-fail: " __VA_ARGS__);                         \
+      }                                                              \
+      /* Currently, this macro will call into                        \
+         the test failure code, which logs                           \
+         "FAIL" and aborts. In the future,                           \
+         we will try to condition on whether                         \
+         or not this is a test.*/                                    \
+      /* XXX fill me in */                                           \
+    }                                                                \
+  } while (false)
+
+// TODO(sleffler): need a scheduler yield kinda call
+inline void wait_for_interrupt(void) { asm volatile("wfi"); }
+
+#endif  // EXAMPLES_SOUNDSTREAM_COMPAT_H_
diff --git a/sw/device/cheriot/soundstream/encode.cc b/sw/device/cheriot/soundstream/encode.cc
new file mode 100644
index 0000000..da826d1
--- /dev/null
+++ b/sw/device/cheriot/soundstream/encode.cc
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "encode.h"
+
+const char base64_alphabet[64] = {
+    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
+    'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
+    'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
+    'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
+    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'};
+
+void encode(const uint8_t* const in, size_t in_len, char* out) {
+  size_t rem = in_len % 3;
+  size_t out_idx = 0;
+  for (int i = 0; i < (in_len - rem); i += 3) {
+    out[out_idx++] = base64_alphabet[(in[0 + i] >> 2)];
+    out[out_idx++] =
+        base64_alphabet[((in[0 + i] & 0x3) << 4) | (in[1 + i] >> 4)];
+    out[out_idx++] =
+        base64_alphabet[((in[1 + i] & 0xf) << 2) | (in[2 + i] >> 6)];
+    out[out_idx++] = base64_alphabet[(in[2 + i] & 0x3f)];
+  }
+
+  if (rem == 2) {
+    out[out_idx++] = base64_alphabet[in[in_len - rem] >> 2];
+    out[out_idx++] = base64_alphabet[((in[in_len - rem] & 0x3) << 4) |
+                                     (in[in_len - rem + 1] >> 4)];
+    out[out_idx++] = base64_alphabet[((in[in_len - rem + 1] & 0xf) << 2)];
+    out[out_idx++] = '=';
+  } else if (rem == 1) {
+    out[out_idx++] = base64_alphabet[in[in_len - rem] >> 2];
+    out[out_idx++] = base64_alphabet[((in[in_len - rem] & 0x3) << 4) |
+                                     (in[in_len - rem + 1] >> 4)];
+    out[out_idx++] = '=';
+    out[out_idx++] = '=';
+  }
+  // NULL-terminate.
+  out[out_idx] = 0;
+}
diff --git a/sw/device/cheriot/soundstream/encode.h b/sw/device/cheriot/soundstream/encode.h
new file mode 100644
index 0000000..01f9b3e
--- /dev/null
+++ b/sw/device/cheriot/soundstream/encode.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef EXAMPLES_SOUNDSTREAM_ENCODE_H_
+#define EXAMPLES_SOUNDSTREAM_ENCODE_H_
+
+#include <stddef.h>
+#include <stdint.h>
+
+#define ENCODE_OUT_SIZE(s) ((size_t)((((s) + 2) / 3) * 4 + 1))
+
+void encode(const uint8_t* in, size_t in_len, char* out);
+
+#endif  // EXAMPLES_SOUNDSTREAM_ENCODE_H_
diff --git a/sw/device/cheriot/soundstream/i2s.cc b/sw/device/cheriot/soundstream/i2s.cc
new file mode 100644
index 0000000..90a04bf
--- /dev/null
+++ b/sw/device/cheriot/soundstream/i2s.cc
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Barebones i2s for soundstream. Only supports recording and
+ * without any buffering. The caller must manage the rx fifo
+ * carefully to avoid data loss.
+ */
+#include "i2s.h"
+
+#include <fail-simulator-on-error.h>
+#include <futex.h>
+#include <interrupt.h>
+#include <locks.h>
+#include <thread.h>
+
+#include <debug.hh>
+#include <platform/sencha/platform-i2s.hh>
+
+#include "hw/top_matcha/sw/autogen/top_matcha.h"
+#include "i2s_regs.h"
+
+/// Expose debugging features unconditionally for this compartment.
+using Debug = ConditionalDebug<false, "I2S">;
+
+#include "compat.h"
+
+typedef uint32_t i2s_mmio_t[TOP_MATCHA_I2S0_SIZE_BYTES / sizeof(uint32_t)];
+
+static dif_i2s_t i2s;
+static void i2s_irq_acknowledge(i2s_irq_t irq_id);
+
+#define CHECK_INIT() CHECK(i2s.base_addr.base != 0)
+
+static CountingSemaphoreState startup = { 0, 1 };
+
+void i2s_init(void) {
+  CHECK_DIF_OK(dif_i2s_init(
+      mmio_region_from_addr((uintptr_t)MMIO_CAPABILITY(i2s_mmio_t, i2s)),
+      &i2s));
+  semaphore_put(&startup);
+}
+
+// NB: could be atomic but not needed for our ussage.
+volatile bool rx_watermark_seen = false;
+
+void i2s_isr(void) {
+  Timeout t = {0, UnlimitedTimeout};
+  semaphore_get(&t, &startup);
+  Debug::log("i2s_isr: i2s {} (Thread {})", i2s.base_addr.base, thread_id_get());
+
+  const uint32_t *rxWatermarkFutex = interrupt_futex_get(
+      STATIC_SEALED_VALUE(i2sRxWatermarkInterruptCapability));
+  uint32_t last = *rxWatermarkFutex;
+  for (;;) {
+    Debug::Assert(futex_wait(rxWatermarkFutex, last) == 0, "futex_wait");
+    last = *rxWatermarkFutex;
+    Debug::log("i2s_isr: i2s {} last {})", i2s.base_addr.base, last);
+    rx_watermark_seen = true;
+    // Acknowledge and disable the interrupt; it will be
+    // re-enabled after the RX FIFO is drained.
+    i2s_irq_acknowledge(kI2sIrqRxWatermark);
+    i2s_irq_set_enabled(kI2sIrqRxWatermark, /*enabled=*/false);
+    CHECK(interrupt_complete(
+        STATIC_SEALED_VALUE(i2sRxWatermarkInterruptCapability)) == 0);
+  }
+}
+bool i2s_rx_watermark_seen(void) {
+  // return atomic_swap(&rx_watermark_seen, false);
+  bool was_seen = rx_watermark_seen;
+  if (was_seen) {
+    rx_watermark_seen = false;
+  }
+  return was_seen;
+}
+
+static void i2s_irq_acknowledge(i2s_irq_t irq_id) {
+  CHECK_INIT();
+  CHECK_DIF_OK(dif_i2s_irq_acknowledge(&i2s, irq_id));
+}
+
+void i2s_irq_acknowledge_all() {
+  CHECK_INIT();
+  CHECK_DIF_OK(dif_i2s_irq_acknowledge_all(&i2s));
+}
+
+void i2s_irq_set_enabled(i2s_irq_t irq_id, bool enabled) {
+  CHECK_INIT();
+  CHECK_DIF_OK(dif_i2s_irq_set_enabled(
+      &i2s, irq_id, enabled ? kDifToggleEnabled : kDifToggleDisabled));
+}
+
+// Clear RX FIFO
+void i2s_rxfifo_clear(void) {
+  CHECK_INIT();
+  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);
+  mmio_region_write32(i2s.base_addr, I2S_FIFO_CTRL_REG_OFFSET, reg_val);
+}
+
+bool i2s_rxfifo_is_empty(void) {
+  CHECK_INIT();
+  bool empty;
+  CHECK_DIF_OK(dif_i2s_rxfifo_empty(&i2s, &empty));
+  return empty;
+}
+
+// Configure and enable recording
+void i2s_record_begin(void) {
+  CHECK_INIT();
+  uint32_t 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);
+}
+
+uint32_t i2s_get_rdata(void) {
+  CHECK_INIT();
+  return mmio_region_read32(i2s.base_addr, I2S_RDATA_REG_OFFSET);
+}
+
+// Disable recording
+void i2s_record_end(void) {
+  CHECK_INIT();
+  uint32_t 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);
+}
diff --git a/sw/device/cheriot/soundstream/i2s.h b/sw/device/cheriot/soundstream/i2s.h
new file mode 100644
index 0000000..08f6565
--- /dev/null
+++ b/sw/device/cheriot/soundstream/i2s.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef EXAMPLES_SOUNDSTREAM_I2S_H_
+#define EXAMPLES_SOUNDSTREAM_I2S_H_
+
+#include <compartment.h>
+
+#include "sw/device/lib/dif/dif_i2s.h"
+
+typedef dif_i2s_irq_t i2s_irq_t;
+const i2s_irq_t kI2sIrqRxWatermark = kDifI2sIrqRxWatermark;
+const i2s_irq_t kI2sIrqTxWatermark = kDifI2sIrqTxWatermark;
+const i2s_irq_t kI2sIrqTxEmpty = kDifI2sIrqTxEmpty;
+
+void __cheri_compartment("i2s") i2s_init(void);
+void __cheri_compartment("i2s") i2s_isr(void);
+void __cheri_compartment("i2s") i2s_irq_acknowledge_all(void);
+void __cheri_compartment("i2s")
+    i2s_irq_set_enabled(i2s_irq_t irq_id, bool enabled);
+bool __cheri_compartment("i2s") i2s_rx_watermark_seen(void);
+void __cheri_compartment("i2s") i2s_rxfifo_clear(void);
+bool __cheri_compartment("i2s") i2s_rxfifo_is_empty(void);
+void __cheri_compartment("i2s") i2s_record_begin(void);
+void __cheri_compartment("i2s") i2s_record_end(void);
+uint32_t __cheri_compartment("i2s") i2s_get_rdata(void);
+
+#endif  // EXAMPLES_SOUNDSTREAM_I2S_H_
diff --git a/sw/device/cheriot/soundstream/mailbox.cc b/sw/device/cheriot/soundstream/mailbox.cc
new file mode 100644
index 0000000..174c17f
--- /dev/null
+++ b/sw/device/cheriot/soundstream/mailbox.cc
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Special-purpose mailbox support for soundstream. Assumes a
+ * companion implementation is running on the security core.
+ */
+#include "mailbox.h"
+
+#include <fail-simulator-on-error.h>
+#include <futex.h>
+#include <interrupt.h>
+#include <locks.h>
+#include <thread.h>
+
+#include <debug.hh>
+#include <platform/sencha/platform-mailbox.hh>
+
+#include "hw/top_matcha/sw/autogen/top_matcha.h"
+
+/// Expose debugging features unconditionally for this compartment.
+using Debug = ConditionalDebug<false, "Mailbox">;
+
+#include "compat.h"
+
+const uint32_t kMessageGpioPressed = 0xdeadbeef;
+const uint32_t kMessageGpioReleased = 0x21524110;
+const uint32_t kMessageEnableLed = 0xcafeb0ba;
+const uint32_t kMessageDisableLed = 0x35014f45;
+
+typedef uint32_t
+    mailbox_mmio_t[TOP_MATCHA_TLUL_MAILBOX_SMC_SIZE_BYTES / sizeof(uint32_t)];
+
+static dif_tlul_mailbox_t mailbox;
+#define CHECK_INIT() CHECK(mailbox.base_addr.base != 0)
+
+static CountingSemaphoreState startup = { 0, 1 };
+
+void mailbox_init(void) {
+  CHECK_DIF_OK(
+      dif_tlul_mailbox_init(mmio_region_from_addr((uintptr_t)MMIO_CAPABILITY(
+                                mailbox_mmio_t, mailbox_smc)),
+                            &mailbox));
+  semaphore_put(&startup);
+}
+
+// NB: could be atomic but not needed for our ussage.
+volatile bool button_pressed = false;
+
+void mailbox_isr(void) {
+  Timeout t = {0, UnlimitedTimeout};
+  semaphore_get(&t, &startup);
+  Debug::log("mailbox_isr: mailbox {} (Thread {})", mailbox.base_addr.base, thread_id_get());
+
+  const uint32_t *mailboxRtFutex =
+      interrupt_futex_get(STATIC_SEALED_VALUE(mailboxRtInterruptCapability));
+  uint32_t last = *mailboxRtFutex;
+  for (;;) {
+    Debug::Assert(futex_wait(mailboxRtFutex, last) == 0, "futex_wait");
+    last = *mailboxRtFutex;
+    Debug::log("mailbox_isr: mailbox {} last {}", mailbox.base_addr.base, last);
+    interrupt_complete(STATIC_SEALED_VALUE(mailboxRtInterruptCapability));
+
+    // NB: this is NOT the TockOS mailbox protocol that exchanges
+    // variable length packets that are encoded by Rust's postcard
+    // crate.
+    uint32_t message;
+    CHECK_DIF_OK(dif_tlul_mailbox_read_message(&mailbox, &message));
+    if (message == kMessageGpioPressed) {
+      button_pressed = true;
+    } else if (message == kMessageGpioReleased) {
+      button_pressed = false;
+    } else {
+      CHECK(false, "Unknown message");
+    }
+  }
+}
+bool mailbox_button_pressed(void) { return button_pressed; }
+
+// XXX temp until gpio's on the SEC can be used (either in simulation or for real)
+void mailbox_set_button_pressed(bool pressed) { button_pressed = pressed; }
+
+void mailbox_set_led(bool enabled) {
+  CHECK_INIT();
+  // NB: this is not the TockOS mailbox protocol.
+  uint32_t message_enable_led =
+      enabled ? kMessageEnableLed : kMessageDisableLed;
+  CHECK_DIF_OK(dif_tlul_mailbox_send_message(&mailbox, &message_enable_led));
+}
diff --git a/sw/device/cheriot/soundstream/mailbox.h b/sw/device/cheriot/soundstream/mailbox.h
new file mode 100644
index 0000000..6663fca
--- /dev/null
+++ b/sw/device/cheriot/soundstream/mailbox.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef EXAMPLES_SOUNDSTREAM_MBOX_H_
+#define EXAMPLES_SOUNDSTREAM_MBOX_H_
+
+#include <compartment.h>
+
+#include "sw/device/lib/dif/dif_tlul_mailbox.h"
+
+typedef dif_tlul_mailbox_irq_t mailbox_irq_t;
+const mailbox_irq_t kMboxIrqRtIrq = kDifTlulMailboxIrqRtirq;
+
+void __cheri_compartment("mailbox") mailbox_init(void);
+void __cheri_compartment("mailbox") mailbox_isr(void);
+bool __cheri_compartment("mailbox") mailbox_button_pressed(void);
+// XXX temp
+void __cheri_compartment("mailbox") mailbox_set_button_pressed(bool);
+void __cheri_compartment("mailbox") mailbox_set_led(bool enabled);
+
+#endif  // EXAMPLES_SOUNDSTREAM_MBOX_H_
diff --git a/sw/device/cheriot/soundstream/ml_top.cc b/sw/device/cheriot/soundstream/ml_top.cc
new file mode 100644
index 0000000..dc931ce
--- /dev/null
+++ b/sw/device/cheriot/soundstream/ml_top.cc
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// NB: override default setup done for DIF code linked into this compartment
+#ifdef CHERIOT_NO_AMBIENT_MALLOC
+#undef CHERIOT_NO_AMBIENT_MALLOC
+#endif
+
+/*
+ * ML accelerator support for soundstream. There is no support for
+ * loading a model; it's assumed done by the security core which
+ * has access to the flash where model files are placed.
+ */
+#include "ml_top.h"
+
+#include <fail-simulator-on-error.h>
+#include <futex.h>
+#include <locks.h>
+#include <multiwaiter.h>
+#include <thread.h>
+
+#include <debug.hh>
+#include <platform/sencha/platform-ml_top.hh>
+
+#include "compat.h"
+#include "hw/top_matcha/sw/autogen/top_matcha.h"
+#include "ml_top_regs.h"
+
+typedef uint8_t ml_top_dmem_t[TOP_MATCHA_RAM_ML_DMEM_SIZE_BYTES];
+typedef uint32_t
+    ml_top_mmio_t[TOP_MATCHA_ML_TOP_CORE_SIZE_BYTES / sizeof(uint32_t)];
+
+/// Expose debugging features unconditionally for this compartment.
+using Debug = ConditionalDebug<false, "ML_TOP">;
+
+static dif_ml_top_t ml_top;
+#define CHECK_INIT() CHECK(ml_top.base_addr.base != 0)
+
+static CountingSemaphoreState startup = { 0, 1 };
+
+void ml_top_init(void) {
+  CHECK_DIF_OK(dif_ml_top_init(mmio_region_from_addr((uintptr_t)MMIO_CAPABILITY(
+                                   ml_top_mmio_t, ml_top_core)),
+                               &ml_top));
+  semaphore_put(&startup);
+}
+
+// NB: could be atomic but not needed for our ussage.
+volatile bool finish_done = false;
+
+void ml_top_isr(void) {
+  Timeout t = {0, UnlimitedTimeout};
+  semaphore_get(&t, &startup);
+  Debug::log("ml_top_isr: ml_top {} (Thread {})",
+             ml_top.base_addr.base, thread_id_get());
+
+  MultiWaiter* mw;
+  Timeout unlimited{0, UnlimitedTimeout};
+  int error = multiwaiter_create(&unlimited, MALLOC_CAPABILITY, &mw, 2);
+  Debug::Assert(error == 0 && mw != nullptr,
+                "multiwaiter_create failed: {}", error);
+
+  EventWaiterSource events[2];
+  const uint32_t* mlTopFinishFutex =
+      interrupt_futex_get(STATIC_SEALED_VALUE(mlTopFinishInterruptCapability));
+  events[0] = {(void*)mlTopFinishFutex, EventWaiterFutex, *mlTopFinishFutex};
+  const uint32_t* mlTopFaultFutex =
+      interrupt_futex_get(STATIC_SEALED_VALUE(mlTopFaultInterruptCapability));
+  events[1] = {(void*)mlTopFaultFutex, EventWaiterFutex, *mlTopFaultFutex};
+
+  for (;;) {
+    Debug::Assert(multiwaiter_wait(&unlimited, mw, events, 2) == 0, "multiwaiter_wait");
+    Debug::log("ml_top_isr: Finish:{} Fault:{}", events[0].value, events[1].value);
+    if (events[1].value == 1) {  // Fault signaled
+      LOG_FATAL("ML_TOP fault!");
+      abort();
+    }
+    if (events[0].value == 1) {  // Finish signaled
+      finish_done = true;
+      interrupt_complete(STATIC_SEALED_VALUE(mlTopFinishInterruptCapability));
+      CHECK_DIF_OK(dif_ml_top_reset_ctrl_en(&ml_top));
+    }
+  }
+}
+bool ml_top_finish_done(void) {
+  // return atomic_swap(&finish_done, false);
+  bool was_done = finish_done;
+  if (was_done) {
+    finish_done = false;
+  }
+  return was_done;
+}
+
+void ml_top_irq_acknowledge(ml_top_irq_t irq_id) {
+  CHECK_INIT();
+  CHECK_DIF_OK(dif_ml_top_irq_acknowledge(&ml_top, irq_id));
+}
+
+void ml_top_irq_acknowledge_all() {
+  CHECK_INIT();
+  CHECK_DIF_OK(dif_ml_top_irq_acknowledge_all(&ml_top));
+}
+
+void ml_top_irq_set_enabled(ml_top_irq_t irq_id, bool enabled) {
+  CHECK_INIT();
+  CHECK_DIF_OK(dif_ml_top_irq_set_enabled(
+      &ml_top, irq_id, enabled ? kDifToggleEnabled : kDifToggleDisabled));
+}
+
+void ml_top_resume_ctrl_en(uint32_t resume_pc) {
+  CHECK_INIT();
+  CHECK_DIF_OK(dif_ml_top_resume_ctrl_en(&ml_top, resume_pc));
+}
+
+void ml_top_set_input(void* const data, size_t data_len_bytes) {
+  void* input_ptr = (void*)(MMIO_CAPABILITY(ml_top_dmem_t, ml_top_dmem) +
+                            TOP_MATCHA_RAM_ML_DMEM_SIZE_BYTES - 4096);
+  memcpy(input_ptr, data, data_len_bytes);
+}
+
+void ml_top_get_output_header(struct output_header* header) {
+  uint8_t* ml_top_dmem_base = (uint8_t*) MMIO_CAPABILITY(ml_top_dmem_t, ml_top_dmem);
+  struct output_header* output_header_ptr = (struct output_header*)
+      (ml_top_dmem_base + (TOP_MATCHA_RAM_ML_DMEM_SIZE_BYTES - 0x40));
+  header->return_code = output_header_ptr->return_code;
+  header->output_ptr = output_header_ptr->output_ptr;
+  header->length = output_header_ptr->length;
+  header->resume_pc = output_header_ptr->resume_pc;
+#if 0
+  Debug::log("return_code: {}", header.return_code);
+  Debug::log("output_ptr: {}", header.output_ptr);
+  Debug::log("length: {}", header.length);
+  Debug::log("resume_pc: {}", header.resume_pc);
+#endif
+}
+
+void ml_top_get_output_data(struct output_header* const header, void* buffer) {
+  const void* src = (const void*)(MMIO_CAPABILITY(ml_top_dmem_t, ml_top_dmem) +
+                                  header->output_ptr);
+  memcpy(buffer, src, header->length);
+}
diff --git a/sw/device/cheriot/soundstream/ml_top.h b/sw/device/cheriot/soundstream/ml_top.h
new file mode 100644
index 0000000..5555576
--- /dev/null
+++ b/sw/device/cheriot/soundstream/ml_top.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef EXAMPLES_SOUNDSTREAM_ML_TOP_H_
+#define EXAMPLES_SOUNDSTREAM_ML_TOP_H_
+
+#include <compartment.h>
+
+#include "sw/device/lib/dif/dif_ml_top.h"
+
+typedef dif_ml_top_irq_t ml_top_irq_t;
+const uint32_t kMlTopIrqFinish = kDifMlTopIrqFinish;
+
+struct output_header {
+  uint32_t return_code;  // Populated in kelvin_start
+  uint32_t output_ptr;
+  uint32_t length;
+  uint32_t resume_pc;  // PC at which to resume for another invocation.
+};
+
+void __cheri_compartment("ml_top") ml_top_init(void);
+void __cheri_compartment("ml_top") ml_top_isr(void);
+void __cheri_compartment("ml_top") ml_top_irq_acknowledge_all(void);
+void __cheri_compartment("ml_top")
+    ml_top_irq_set_enabled(ml_top_irq_t irq_id, bool enabled);
+bool __cheri_compartment("ml_top") ml_top_finish_done(void);
+void __cheri_compartment("ml_top") ml_top_resume_ctrl_en(uint32_t resume_pc);
+void __cheri_compartment("ml_top")
+    ml_top_set_input(void* const data, size_t data_len_bytes);
+void __cheri_compartment("ml_top")
+    ml_top_get_output_header(struct output_header* header);
+void __cheri_compartment("ml_top")
+    ml_top_get_output_data(struct output_header* const header, void* buffer);
+
+#endif  // EXAMPLES_SOUNDSTREAM_ML_TOP_H_
diff --git a/sw/device/cheriot/soundstream/soundstream.cc b/sw/device/cheriot/soundstream/soundstream.cc
new file mode 100644
index 0000000..897a921
--- /dev/null
+++ b/sw/device/cheriot/soundstream/soundstream.cc
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Data structures that hold audio samples are allocated on the heap
+// because they are too big (for now) for .data/.bss. We use calloc
+// for this and need to override the default quota (4K) to satisfy our
+// large'ish requests.
+//#define kSamples (5 * 16000)
+#define kSamples (5000) // NB: reduced sample count for slow renode
+#define kFilterSamples (256)
+#define roundup(a, b) (((a) + (b) - 1) / (b)) * (b)
+#define MALLOC_QUOTA                                                         \
+  roundup(                                                                   \
+      (kSamples * sizeof(int32_t)) + 2 * (kFilterSamples * sizeof(int16_t)), \
+      4096)
+
+#include "soundstream.h"
+
+#include <fail-simulator-on-error.h>
+#include <thread.h>
+
+#include <debug.hh>
+
+#include "hw/top_matcha/sw/autogen/top_matcha.h"
+
+/// Expose debugging features unconditionally for this compartment.
+using Debug = ConditionalDebug<true, "SOUNDSTREAM">;
+
+#include "compat.h"
+
+#define ABS(x) (x > 0 ? x : -x)
+
+void __cheri_compartment("soundstream") entry(void) {
+  Debug::log("soundstream (Thread {})", thread_id_get());
+
+  i2s_init();
+  ml_top_init();
+  mailbox_init();
+
+  i2s_rxfifo_clear();
+  i2s_irq_acknowledge_all();
+  i2s_irq_set_enabled(kI2sIrqRxWatermark, /*enabled=*/true);
+
+  // NB: sample buffers are temporarily allocated from the heap because
+  // the toolchain does not support large'ish data structures. Support
+  // has been committed upstream but not yet brought in.
+
+  // XXX calloc returns 0xffffffff (v:0 0xfffffe00-0xfffffe00 l:0x0 o:0x0 p: -
+  // ------ -- ---) on failure so cannot check against NULL
+  //  int16_t samples_left[kFilterSamples] = {0};
+  //  int16_t samples_right[kFilterSamples] = {0};
+  int16_t* samples_left = (int16_t*)calloc(sizeof(int16_t), kFilterSamples);
+  Debug::Assert(__builtin_cheri_length_get(samples_left) > 0,
+                "samples_left allocation failed {}", samples_left);
+  int16_t* samples_right = (int16_t*)calloc(sizeof(int16_t), kFilterSamples);
+  Debug::Assert(__builtin_cheri_length_get(samples_right) > 0,
+                "samples_right allocation failed {}", samples_right);
+  size_t index_left = 0;
+  size_t index_right = 0;
+  int32_t total_left = 0;
+  int32_t total_right = 0;
+
+  //  static int32_t samples[kSamples];
+  int32_t* samples = (int32_t*)calloc(sizeof(int32_t), kSamples);
+  Debug::Assert(__builtin_cheri_length_get(samples) > 0,
+                "Sample allocation failed {}", samples);
+  memset(samples, 0xa5, sizeof(int32_t) * kSamples);
+
+  Debug::log("Setup complete");
+
+  while (true) {
+    // TODO(sleffler): need custom security core code running and
+    //   a way to toggle the gpio associated with the button; for now
+    //   just force it to appear as though the button has been pressed.
+    mailbox_set_button_pressed(true);
+
+    // Wait until the record switch is pushed
+    Debug::log("Wait for button press...");
+    while (!mailbox_button_pressed()) {
+      wait_for_interrupt();
+    }
+
+    mailbox_set_led(/*enabled=*/true);
+    Debug::log("Start recording (max {} samples)...", kSamples);
+    i2s_record_begin();
+
+    // Record until our buffer is full or the switch is released.
+    int sample = 0;
+    while (sample < kSamples && mailbox_button_pressed()) {
+      i2s_irq_set_enabled(kI2sIrqRxWatermark, /*enabled=*/true);
+      while (!i2s_rx_watermark_seen()) {
+        wait_for_interrupt();
+      }
+
+      while (!i2s_rxfifo_is_empty()) {
+        uint32_t reg_val = i2s_get_rdata();
+        // 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) % kFilterSamples;
+        index_right = (index_right + 1) % kFilterSamples;
+        int16_t mean_left = total_left / kFilterSamples;
+        int16_t mean_right = total_right / kFilterSamples;
+
+        // 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;
+        if (sample == kSamples || !mailbox_button_pressed()) {
+          break;
+        }
+      }
+    }
+
+    i2s_record_end();
+    mailbox_set_led(/*enabled=*/false);
+
+    Debug::log("Done recording {} 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;
+    }
+
+    Debug::log("Processing recorded audio...");
+
+    // 320 x int16
+    int iterations_to_process = samples_captured / 320;
+    int16_t process_buffer[320];
+    int16_t result_buffer[64];
+    char result_buffer_encoded[ENCODE_OUT_SIZE(sizeof(result_buffer))];
+
+    struct output_header header;
+    memset(&header, 0, sizeof(header));  // NB: resume_pc = 0
+
+    for (int i = 0; i < iterations_to_process; ++i) {
+      Debug::log("Iteration {}", i);
+      int16_t* samples_s16 = (int16_t*)samples;
+      // Extract left channel audio
+      for (int j = 0; j < 320; ++j) {
+        process_buffer[j] = samples_s16[(i * 320 * 2) + (j * 2) + 1];
+      }
+      ml_top_set_input(process_buffer, sizeof(process_buffer));
+
+      // Start/resume kelvin
+      (void) ml_top_finish_done();  // NB: reset state
+      ml_top_resume_ctrl_en(header.resume_pc);
+
+      // wfi
+      while (!ml_top_finish_done()) {
+        wait_for_interrupt();
+      }
+
+      ml_top_get_output_header(&header);
+      CHECK(header.length == sizeof(result_buffer), "Unexpected ML result size");
+      ml_top_get_output_data(&header, result_buffer);
+
+      encode((const unsigned char*)result_buffer, sizeof(result_buffer),
+             result_buffer_encoded);
+      Debug::log("[sound]::ENCODER:{}",
+        std::string_view(result_buffer_encoded, sizeof(result_buffer_encoded)));
+    }
+
+    Debug::log("[sound]::ENCODER: done");
+    Debug::log("Done with processing.");
+  }
+  panic();
+}
diff --git a/sw/device/cheriot/soundstream/soundstream.h b/sw/device/cheriot/soundstream/soundstream.h
new file mode 100644
index 0000000..640834c
--- /dev/null
+++ b/sw/device/cheriot/soundstream/soundstream.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef EXAMPLES_SOUNDSTREAM_SOUNDSTREAM_H_
+#define EXAMPLES_SOUNDSTREAM_SOUNDSTREAM_H_
+
+#include <stdint.h>
+
+#include "encode.h"
+#include "i2s.h"
+#include "mailbox.h"
+#include "ml_top.h"
+
+#endif  // EXAMPLES_SOUNDSTREAM_SOUNDSTREAM_H_
diff --git a/sw/device/cheriot/soundstream/xmake.lua b/sw/device/cheriot/soundstream/xmake.lua
new file mode 100644
index 0000000..3d2d1da
--- /dev/null
+++ b/sw/device/cheriot/soundstream/xmake.lua
@@ -0,0 +1,111 @@
+--
+-- Copyright 2023 Google LLC
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--      http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+
+set_project("CHERIoT soundstream demo")
+
+local shodan_dir = os.getenv("ROOTDIR")
+if shodan_dir == nil or shodan_dir == '' then
+  raise("ROOTDIR not set")
+end
+local cheriot_out_dir = os.getenv("CHERIOT_OUT_DIR")
+if cheriot_out_dir == nil or cheriot_out_dir == '' then
+  raise("CHERIOT_OUT_DIR not set")
+end
+
+local sdkdir = path.join(shodan_dir, "sw/cheriot-rtos/sdk")
+includes(sdkdir)
+set_toolchains("cheriot-clang")
+
+option("board")
+    set_default("sencha")
+
+local matcha_dir = path.join(shodan_dir, "hw/matcha")
+local dif_dir = path.join(matcha_dir, "sw/device/lib/dif")
+local dif_autogen_dir = path.join(dif_dir, "autogen")
+local opentitan_dir = path.join(shodan_dir, "hw/opentitan-upstream")
+local matcha_gen_dir = path.join(cheriot_out_dir, "opentitan-gen/include/opentitan")
+
+-- Support libraries
+includes(path.join(sdkdir, "lib"))
+includes(path.join(sdkdir, "lib/freestanding"))
+
+-- Each driver operates in a compartment.
+compartment("i2s")
+    add_files("i2s.cc")
+    add_files(path.join(dif_dir, "dif_i2s.c"),
+              path.join(dif_autogen_dir, "dif_i2s_autogen.c"))
+    add_includedirs(matcha_dir, matcha_gen_dir, opentitan_dir)
+    add_defines("CHERIOT_NO_AMBIENT_MALLOC")
+
+compartment("mailbox")
+    add_files("mailbox.cc")
+    add_files(path.join(dif_dir, "dif_tlul_mailbox.c"),
+              path.join(dif_autogen_dir, "dif_tlul_mailbox_autogen.c"))
+    add_includedirs(matcha_dir, matcha_gen_dir, opentitan_dir)
+    add_defines("CHERIOT_NO_AMBIENT_MALLOC")
+
+compartment("ml_top")
+    add_files("ml_top.cc")
+    add_files(path.join(dif_dir, "dif_ml_top.c"),
+              path.join(dif_autogen_dir, "dif_ml_top_autogen.c"))
+    add_includedirs(matcha_dir, matcha_gen_dir, opentitan_dir)
+    add_defines("CHERIOT_NO_AMBIENT_MALLOC")
+
+-- Soundstream application.
+compartment("soundstream")
+    add_files("soundstream.cc", "encode.cc")
+    add_files(path.join(matcha_dir, "hw/top_matcha/sw/autogen/top_matcha.c"));
+    add_includedirs(matcha_dir, matcha_gen_dir, opentitan_dir)
+
+-- Firmware image.
+firmware("soundstream-firmware")
+    add_deps("freestanding")
+    add_deps("debug", "i2s", "ml_top", "mailbox", "soundstream")
+    on_load(function(target)
+        target:values_set("board", "$(board)")
+        -- NB: trusted_stack_frames is a guess; +1'd for any
+        --     compartment error handler usage?
+        target:values_set("threads", {
+            {
+                compartment = "soundstream",
+                priority = 1,
+                entry_point = "entry",
+                stack_size = 0x1000, -- 4KB
+                trusted_stack_frames = 5
+            },
+            -- NB: stack sizes bumped for logging
+            {
+                compartment = "i2s",
+                priority = 1,
+                entry_point = "i2s_isr",
+                stack_size = 0x400, -- 512B
+                trusted_stack_frames = 3
+            },
+            {
+                compartment = "ml_top",
+                priority = 1,
+                entry_point = "ml_top_isr",
+                stack_size = 0x400, -- 512B
+                trusted_stack_frames = 3
+            },
+            {
+                compartment = "mailbox",
+                priority = 1,
+                entry_point = "mailbox_isr",
+                stack_size = 0x400, -- 512B
+                trusted_stack_frames = 3
+            }
+        }, {expand = false})
+    end)
diff --git a/sw/device/lib/dif/dif_i2s.h b/sw/device/lib/dif/dif_i2s.h
index 190c454..7d01197 100644
--- a/sw/device/lib/dif/dif_i2s.h
+++ b/sw/device/lib/dif/dif_i2s.h
@@ -16,8 +16,6 @@
  */
 
 
-
-
 #ifndef OPENTITAN_SW_DEVICE_LIB_DIF_DIF_I2S_H_
 #define OPENTITAN_SW_DEVICE_LIB_DIF_DIF_I2S_H_
 
@@ -36,6 +34,8 @@
 extern "C" {
 #endif  // __cplusplus
 
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wextern-c-compat"
 /**
  * Runtime configuration for I2S Audio.
  *
@@ -60,6 +60,7 @@
 typedef struct dif_i2s_output {
   // Your fields here.
 } dif_i2s_output_t;
+#pragma clang diagnostic pop
 
 /**
  * Configures I2S Audio with runtime information.
diff --git a/sw/device/lib/dif/dif_ml_top.h b/sw/device/lib/dif/dif_ml_top.h
index 57ed19e..684c461 100644
--- a/sw/device/lib/dif/dif_ml_top.h
+++ b/sw/device/lib/dif/dif_ml_top.h
@@ -48,6 +48,8 @@
  */
 
 
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wextern-c-compat"
 /**
  * Runtime configuration for ML_Top.
  *
@@ -72,6 +74,7 @@
 typedef struct dif_ml_top_output {
   // Your fields here.
 } dif_ml_top_output_t;
+#pragma clang diagnostic pop
 
 /**
  * Configures ML_Top with runtime information.
diff --git a/sw/device/lib/dif/dif_tlul_mailbox.h b/sw/device/lib/dif/dif_tlul_mailbox.h
index 69aaf20..6b09fdf 100644
--- a/sw/device/lib/dif/dif_tlul_mailbox.h
+++ b/sw/device/lib/dif/dif_tlul_mailbox.h
@@ -39,6 +39,8 @@
 dif_result_t dif_tlul_mailbox_read_message(const dif_tlul_mailbox_t *tlul_mailbox, uint32_t *buf);
 
 
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wextern-c-compat"
 /**
  * Runtime configuration for tlul_mailbox for SC<->SMC communication.
  *
@@ -62,6 +64,7 @@
 typedef struct dif_tlul_mailbox_output {
   // Your fields here.
 } dif_tlul_mailbox_output_t;
+#pragma clang diagnostic pop
 
 /**
  * Configures tlul_mailbox for SC<->SMC communication with runtime information.