Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
986cd16
TestToPython files
tedmoore Feb 22, 2026
e2c96e5
bottom of block
tedmoore Feb 22, 2026
20d8bbd
test to python works now
tedmoore Feb 23, 2026
fc783f0
Merge branch 'dev' into bottom-of-block
tedmoore Feb 23, 2026
745e876
return dict cpu = 5%
tedmoore Feb 23, 2026
b3f2034
using response_queue
tedmoore Feb 24, 2026
050bb42
looks like each "key" costs about 0.1% CPU.
tedmoore Feb 24, 2026
b815645
50 more only costs 1.4% cpu
tedmoore Feb 24, 2026
aed8beb
passing a list of 1025 floats (as one key) costs about 4% cpu
tedmoore Feb 24, 2026
dd7bda0
send only every 10 blocks
tedmoore Feb 24, 2026
8cb11a9
bump
tedmoore Feb 24, 2026
7dcbc08
to_python_dict
tedmoore Feb 24, 2026
dffb64a
pydict every 10 blocks
tedmoore Feb 24, 2026
afe2fef
only use pydict at very end
tedmoore Feb 24, 2026
68ee18c
ensure float 64
tedmoore Feb 24, 2026
9c36f0c
a comment
tedmoore Feb 24, 2026
aea3c96
Merge branch 'dev' into pydict-every-10-blocks
tedmoore Feb 24, 2026
1562d0b
more data types
tedmoore Feb 24, 2026
c7f5472
more data types
tedmoore Feb 24, 2026
b45d7a8
clean up
tedmoore Feb 24, 2026
e3cc01e
round trip test
tedmoore Feb 24, 2026
50741e2
python reply tests made and passing
tedmoore Feb 24, 2026
773ccbe
need flucoma results for tests
tedmoore Feb 24, 2026
d493cd2
documentation
tedmoore Feb 24, 2026
0fa82bb
made the "test" an example.
tedmoore Feb 24, 2026
d79b728
"accepting_stream_data" untested
tedmoore Feb 24, 2026
ca5f8fe
accepting_stream_data
tedmoore Feb 25, 2026
3988cef
spec outline
tedmoore Feb 25, 2026
4126c2a
"send_streams" and Spectrogram Example
tedmoore Feb 25, 2026
3a7e10a
send_messages documentation
tedmoore Feb 26, 2026
680d6c5
QGraphicsView
tedmoore Feb 26, 2026
c3352f8
erp
tedmoore Feb 26, 2026
771c0db
clean up
tedmoore Feb 26, 2026
4212f89
Merge branch 'dev' into mojo-to-python
tedmoore Feb 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,17 @@ jobs:

- name: Create Librosa Results for Testing Against
run: |
python testing/librosa_results_for_testing_against.py
python testing_mmm_audio/validation/librosa_results_for_testing_against.py

- name: Run UnitTests.mojo
id: run-tests
run: |
mojo testing/UnitTests.mojo
mojo testing_mmm_audio/UnitTests.mojo

- name: Test Building Mojo Files
run: |
python testing/test_build_mojo_files.py
python testing_mmm_audio/test_build_mojo_files.py

- name: Validate Against Snapshots
run: |
python testing/validate_against_snapshot.py
python testing_mmm_audio/validation/validate_against_snapshot.py
9 changes: 5 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -222,11 +222,12 @@ __marimo__/
doc_generation/docs_md
site
.vscode
testing/librosa_results
testing_mmm_audio/validation/librosa_results
.venv-257
venv0257
mine.worktrees
*.o
testing/outputs
testing/mojo_results
testing/validation_results
testing_mmm_audio/outputs
testing_mmm_audio/validation/mojo_results
testing_mmm_audio/validation/validation_results

6 changes: 3 additions & 3 deletions doc_generation/static_docs/contributing/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ There are three kinds of tests the MMMAudio is set to run (see below). All these

## 1. Unit Tests

These can be found in [UnitTests.mojo](https://github.com/spluta/MMMAudio/blob/dev/testing/UnitTests.mojo). Some of these tests rely on the Librosa "results" generated by [`/testing/librosa_results_for_testing_against.py`](https://github.com/spluta/MMMAudio/blob/dev/testing/librosa_results_for_testing_against.py), so be sure to run this first.
These can be found in [UnitTests.mojo](https://github.com/spluta/MMMAudio/blob/dev/testing_mmm_audio/UnitTests.mojo). Some of these tests rely on the Librosa "results" generated by [`/testing_mmm_audio/validation/librosa_results_for_testing_against.py`](https://github.com/spluta/MMMAudio/blob/dev/testing_mmm_audio/validation/librosa_results_for_testing_against.py), so be sure to run this first.

## 2. "Smoke" Tests

> Turn everything on and see if anything "catches on fire."

See [`testing/test_build_mojo_files.py`](https://github.com/spluta/MMMAudio/blob/dev/testing/test_build_mojo_files.py) to see how they are run. This script tries to "build" each `.mojo` file, generating a `.o` file which is then deleted. This way any syntax issues can be identified quickly and easily across all the (appropriate) `.mojo` files in the code base.
See [`testing_mmm_audio/test_build_mojo_files.py`](https://github.com/spluta/MMMAudio/blob/dev/testing_mmm_audio/test_build_mojo_files.py) to see how they are run. This script tries to "build" each `.mojo` file, generating a `.o` file which is then deleted. This way any syntax issues can be identified quickly and easily across all the (appropriate) `.mojo` files in the code base.

## 3. Snapshot Tests

Some code, such as the audio analyses, are difficult to test against a "ground truth" because different codebases have different opinions about how to implement them, have different levels of precision, or other concerns. These tests run select `.mojo` files, the outputs of which are [compared against](https://github.com/spluta/MMMAudio/blob/dev/testing/validate_against_snapshot.py) a "snapshot" of what they previously output (the previous snapshot is [taken](https://github.com/spluta/MMMAudio/blob/dev/testing/make_validation_snapshot.py) when the files are known to be working). These tests pass if the current outputs match the previous snapshot.
Some code, such as the audio analyses, are difficult to test against a "ground truth" because different codebases have different opinions about how to implement them, have different levels of precision, or other concerns. These tests run select `.mojo` files, the outputs of which are [compared against](https://github.com/spluta/MMMAudio/blob/dev/testing_mmm_audio/validation/validate_against_snapshot.py) a "snapshot" of what they previously output (the previous snapshot is [taken](https://github.com/spluta/MMMAudio/blob/dev/testing_mmm_audio/make_validation_snapshot.py) when the files are known to be working). These tests pass if the current outputs match the previous snapshot.
37 changes: 37 additions & 0 deletions examples/SpectrogramExample.mojo
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from mmm_audio import *

struct Spectrogram(FFTProcessable):
var world: World
var m: Messenger
var mags: List[Float64]

fn __init__(out self, world: World, fftsize: Int = 1024):
self.world = world
self.m = Messenger(world)
self.mags = List[Float64](length=(fftsize // 2) + 1, fill=0.0)

fn next_frame(mut self, mut mags: List[Float64], mut freqs: List[Float64]):
for i in range(len(mags)):
self.mags[i] = mags[i]

fn send_streams(mut self) -> None:
self.m.reply_stream("mags", self.mags)

struct SpectrogramExample(Movable, Copyable):
var world: World
var buf: Buffer
var play: Play
var m: Messenger
var fftproces: FFTProcess[Spectrogram]

fn __init__(out self, world: World):
self.world = world
self.buf = Buffer.load("resources/Shiverer.wav")
self.play = Play(world)
self.m = Messenger(world)
self.fftproces = FFTProcess(world, Spectrogram(world))

fn next(mut self) -> MFloat[2]:
sig = self.play.next(self.buf)
_ = self.fftproces.next(sig)
return sig
95 changes: 95 additions & 0 deletions examples/SpectrogramExample.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import sys
from pathlib import Path
import numpy as np

sys.path.insert(0, str(Path(__file__).parent.parent))

from mmm_python import *
from PySide6.QtWidgets import QApplication, QMainWindow, QGraphicsView, QGraphicsScene, QGraphicsRectItem
from PySide6.QtCore import Signal, QObject, Qt
from PySide6.QtGui import QBrush, QPen, QColor

class SpectrogramGraphicsView(QGraphicsView):

def __init__(self, num_bars=513, parent=None):
super().__init__(parent)
self.num_bars = num_bars
self.bar_height = 400

self.scene = QGraphicsScene(self)
self.setScene(self.scene)

self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setRenderHint(self.renderHints().Antialiasing, False)
self.setViewportUpdateMode(QGraphicsView.MinimalViewportUpdate)
self.setOptimizationFlags(QGraphicsView.DontSavePainterState)
self.setBackgroundBrush(QBrush(QColor(0, 0, 0)))

self.bars = []
self.bar_width = 800 / num_bars
brush = QBrush(QColor(255, 255, 255))
pen = QPen(Qt.NoPen)

for i in range(num_bars):
bar = QGraphicsRectItem(0, 0, self.bar_width, 0)
bar.setBrush(brush)
bar.setPen(pen)
bar.setPos(i * self.bar_width, self.bar_height)
self.scene.addItem(bar)
self.bars.append(bar)

self.scene.setSceneRect(0, 0, 800, self.bar_height)
self.setMinimumSize(800, 400)

def update_data(self, data):
for i, mag in enumerate(data):
normalized = mag / 15.0
bar_height = normalized * self.bar_height

self.bars[i].setRect(0, 0, self.bar_width, bar_height)
self.bars[i].setPos(i * self.bar_width, self.bar_height - bar_height)

def resizeEvent(self, event):
super().resizeEvent(event)
self.fitInView(self.scene.sceneRect(), Qt.IgnoreAspectRatio)


class SpectrogramWindow(QMainWindow):
data_ready = Signal(object)

def __init__(self):
super().__init__()
self.setWindowTitle("Real-time Spectrogram")

self.spectrogram_widget = SpectrogramGraphicsView(num_bars=513)
self.setCentralWidget(self.spectrogram_widget)

self.data_ready.connect(self.spectrogram_widget.update_data)

def callback(self, args):
self.data_ready.emit(args)

def closeEvent(self, event):
QApplication.quit()
event.accept()


if __name__ == "__main__":

app = QApplication()

window = SpectrogramWindow()
window.show()

m = MMMAudio(128, graph_name="SpectrogramExample", package_name="examples")
m.register_callback("mags", window.callback)
m.start_audio()

def shutdown():
m.stop_audio()
m.stop_process()

app.aboutToQuit.connect(shutdown)

sys.exit(app.exec())
31 changes: 31 additions & 0 deletions examples/ToPythonExample.mojo
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

from mmm_audio import *

struct ToPythonExample(Movable, Copyable):
var world: World
var m: Messenger
var yin: BufferedInput[YIN[1024],1024,512]
var buf: Buffer
var play: Play
var vals: List[Float64]

fn __init__(out self, world: World):
self.world = world
self.m = Messenger(self.world)
self.buf = Buffer.load("resources/Shiverer.wav")
self.play = Play(self.world)
yin = YIN[1024](self.world)
self.yin = BufferedInput[YIN[1024],1024,512](self.world,yin^)
self.vals = List[Float64]()
for i in range(1025):
self.vals.append(i / 1024.0)

fn next(mut self) -> SIMD[DType.float64, 2]:

sig = self.play.next(self.buf)
self.yin.next(sig)
self.m.reply_stream("pitch", self.yin.process.pitch)
self.m.reply_stream("vals", self.vals)
self.m.reply_stream("bool", random_float64() > 0.5)

return SIMD[DType.float64, 2](sig, sig)
9 changes: 9 additions & 0 deletions examples/ToPythonExample.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from mmm_python import *
m = MMMAudio(128, graph_name="ToPythonExample", package_name="examples")
m.register_callback("pitch", lambda args: print(f"pitch: {args}"))
m.register_callback("vals", lambda args: print(f"vals: {args}"))
m.register_callback("bool", lambda args: print(f"bool: {args}"))
m.register_callback("trig", lambda args: print(f"trig: {args}"))
m.start_audio()

m.stop_audio()
40 changes: 31 additions & 9 deletions mmm_audio/BufferedProcess_Module.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,42 @@ from math import floor
# parameters. I think `hop_size` would still be a parameter of the BufferedProcess struct.
trait BufferedProcessable(Movable, Copyable):
"""Trait that user structs must implement to be used with a BufferedProcess.

Requires two functions:

- `next_window(buffer: List[Float64]) -> None`: This function is called when enough samples have been buffered.
The user can process the input buffer in place meaning that the samples you want to return to the output need
to replace the samples that you receive in the input list.

- `get_messages() -> None`: This function is called at the top of each audio block to allow the user to retrieve any messages
they may have sent to this process. Put your [Messenger](Messenger.md) message retrieval code here. (e.g. `self.messenger.update(self.param, "param_name")`)
"""

fn next_window(mut self, mut buffer: List[Float64]) -> None:
"""This function is called when enough samples have been buffered.
The user can process the input buffer in place meaning that the samples you want to return to the output need
to replace the samples that you receive in the input list.

This function has a default implementation that does nothing so it is possible to *not*
implement it. This would probably be because a stereo process is implementing `next_stereo_window()` instead.
"""
return None

fn next_stereo_window(mut self, mut buffer: List[SIMD[DType.float64, 2]]) -> None:
"""The stereo version of `next_window()`. See that for details.

This function has a default implementation that does nothing so it is possible to *not*
implement it. This would probably be because a mono process is implementing `next_window()` instead.
"""
return None

fn get_messages(mut self) -> None:
"""This function is called at the top of each audio block to allow the user to retrieve any messages
they may have sent to this process. Put your [Messenger](Messenger.md) message retrieval code here.
(e.g. `self.messenger.update(self.param, "param_name")`).

This method has a default implementation that does nothing, so it is not necessary to
implement it if you don't need to retrieve any messages.
"""
return None

fn send_streams(mut self) -> None:
"""This function can be used to stream data back to Python. Put your [Messenger](Messenger.md) message sending code here.
(e.g. `self.messenger.reply_stream("stream_name", value)`).

This method has a default implementation that does nothing, so it is not necessary to implement it if you don't need to send any stream data.
"""
return None

struct BufferedInput[T: BufferedProcessable, window_size: Int = 1024, hop_size: Int = 512, input_window_shape: Int = WindowType.hann](Movable, Copyable):
Expand Down Expand Up @@ -161,6 +180,9 @@ struct BufferedProcess[T: BufferedProcessable, window_size: Int = 1024, hop_size
"""
if self.world[].top_of_block:
self.process.get_messages()

if self.world[].messengerManager.accepting_stream_data:
self.process.send_streams()

self.input_buffer[self.input_buffer_write_head] = input
self.input_buffer[self.input_buffer_write_head + Self.window_size] = input
Expand Down
27 changes: 27 additions & 0 deletions mmm_audio/FFTProcess_Module.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ struct FFTProcessor[T: FFTProcessable, window_size: Int = 1024](BufferedProcessa
@doc_private
fn get_messages(mut self) -> None:
self.process.get_messages()

@doc_private
fn send_streams(mut self) -> None:
self.process.send_streams()

trait FFTProcessable(Movable,Copyable):
"""Implement this trait in a custom struct to pass to `FFTProcess`
Expand All @@ -50,10 +54,33 @@ trait FFTProcessable(Movable,Copyable):
using a struct that implements FFTProcessable.
"""
fn next_frame(mut self, mut magnitudes: List[Float64], mut phases: List[Float64]) -> None:
"""This function is called when the internal buffered process has enough samples to
perform an FFT. The user can modify the magnitudes and phases in place to achieve
their desired spectral processing. The modified magnitudes and phases will then be
used by the internal buffered process to perform an IFFT and return to the time domain.

This function has a default implementation that does nothing so it is possible to *not*
implement it. This would probably be because a stereo process is implementing
`next_stereo_frame()` instead.
"""
return None
fn next_stereo_frame(mut self, mut magnitudes: List[SIMD[DType.float64,2]], mut phases: List[SIMD[DType.float64,2]]) -> None:
"""The stereo version of `next_frame()`. See that for details.
"""
return None
fn get_messages(mut self) -> None:
"""This function is called at the top of each audio block to allow the user to retrieve any messages
they may have sent to this process. Put your [Messenger](Messenger.md) message retrieval code here. (e.g. `self.messenger.update(self.param, "param_name")`).

This method has a default implementation that does nothing, so it is not necessary to implement it if you don't need to retrieve any messages.
"""
return None
fn send_streams(mut self) -> None:
"""This function can be used to stream data back to Python. Put your [Messenger](Messenger.md) message sending code here.
(e.g. `self.messenger.reply_stream("stream_name", value)`).

This method has a default implementation that does nothing, so it is not necessary to implement it if you don't need to send any stream data.
"""
return None

struct FFTProcess[T: FFTProcessable, window_size: Int = 1024, hop_size: Int = 512, input_window_shape: Int = WindowType.hann, output_window_shape: Int = WindowType.hann](Movable,Copyable):
Expand Down
Loading