For more information about the examples, such as how the Python and Mojo files interact with each other, see the Examples Overview
MPlotExample¶
Python Code¶
from PySide6.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QSizePolicy
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent))
from mmm_python import *
from umap import UMAP
from sklearn.neighbors import KDTree
import librosa
import numpy as np
import pickle
from sklearn.preprocessing import StandardScaler
def main():
# parameters for analysis
d = {
# "path": "resources/Shiverer.wav",
"path": "/Users/ted/Desktop/all_flucoma.wav",
# threshold for spectral flux onset detection, lower is more sensitive, higher is less sensitive
"thresh":2.0,
# minimum length of slices in seconds
"min_slice_len":0.1,
# window size and hop size used for all analyses
"window_size":1024,
"hop_size":512,
# num mfcc coefficients to compute (including 0th), we will discard the 0th coefficient later
"num_coeffs": 14
}
# use librosa to load audio file to get sample rate and samples for plotting waveform
y, sr = librosa.load(d["path"], sr=None)
# slice using spectral flux
slice_points = MBufAnalysis.spectral_flux_onsets(d)
print("num slice points:", len(slice_points))
print("avg slice duration:", np.diff(slice_points).mean() / sr)
slice_points = np.insert(slice_points, 0, 0) # add start of file as first slice point
slice_points = np.append(slice_points, len(y)) # add end of file as last slice point
data = np.ndarray((len(slice_points)-1, 13)) # create array to hold slice features
for i in range(len(slice_points)-1):
start = int(slice_points[i])
end = int(slice_points[i+1])
print(f"Slice {i} / {len(slice_points)-1}: start={start}, end={end}, duration={(end-start)/sr:.2f} seconds")
d["start_frame"] = start
d["num_frames"] = end - start
mfccs = MBufAnalysis.mfcc(d)
# remove 0th coefficient and take mean across time axis to get one feature vector per slice
data[i] = mfccs[:, 1:].mean(axis=0)
data = StandardScaler().fit_transform(data)
print("data shape:", data.shape)
data_umap = UMAP(n_components=2,learning_rate=0.1,min_dist=0.01,n_epochs=200).fit_transform(data)
kdtree = KDTree(data_umap)
ma = MMMAudio(128,graph_name="MPlotExample", package_name="examples")
ma.start_audio()
ma.send_string("load_sound", d["path"])
prev = None
def get_nearest(view, x, y, button, is_dragging, key, dblclick, step):
nonlocal prev
if step is None:
dist, idx = kdtree.query([[x, y]], k=1)
nearest = int(idx[0][0])
if nearest != prev:
prev = nearest
start = slice_points[nearest]
num = slice_points[nearest+1] - start
view.highlight_index(nearest)
waveform_win.highlight(start, num)
ma.send_ints("play_data", [start, num])
print(f"x: {x:.2f}, y: {y:.2f}")
print(f"Nearest idx: {idx[0][0]}, dist: {dist[0][0]:.4f}")
app = QApplication([])
main = QMainWindow()
root = QWidget()
layout = QVBoxLayout(root)
win = MPlot(data_umap, mouse_callback=get_nearest,xlabel="UMAP 1", ylabel="UMAP 2")
waveform_win = MWaveform(y, slice_points)
waveform_win.setFixedHeight(220)
waveform_win.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
layout.addWidget(win)
layout.addWidget(waveform_win)
layout.setStretch(0, 1)
layout.setStretch(1, 0)
main.setCentralWidget(root)
def shutdown_audio():
ma.stop_audio()
ma.stop_process()
app.aboutToQuit.connect(shutdown_audio)
main.closeEvent = lambda event: (shutdown_audio(), event.accept())
main.resize(900, 850)
main.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
Mojo Code¶
from mmm_audio import *
struct MPlotExample(Representable, Movable, Copyable):
var world: World
var buf: Buffer
var play: Play
var m: Messenger
var play_data: List[Int]
var path: String
fn __init__(out self, world: World):
self.world = world
self.play = Play(self.world)
self.m = Messenger(self.world)
self.play_data = List[Int](length=2, fill=0)
self.path = String("/Users/sam/Library/Application Support/SuperCollider/sounds/analogSynthSounds/drums/manyHits.wav")
self.buf = Buffer.load(self.path)
fn __repr__(self) -> String:
return String("MPlotExample")
fn next(mut self) -> MFloat[2]:
if self.m.notify_update(self.path, "load_sound"):
self.buf = Buffer.load(self.path)
trig = self.m.notify_update(self.play_data, "play_data")
if trig:
print("Playing slice: start=",self.play_data[0], ", num=", self.play_data[1])
out = self.play.next(self.buf, 1.0, False, trig, self.play_data[0], self.play_data[1])
return out