Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ check-c-api:
SOURCES += src/algorithms.c src/amy.c src/envelope.c src/examples.c src/parse.c \
src/filters.c src/oscillators.c src/pcm.c src/interp_partials.c src/custom.c \
src/delay.c src/log2_exp2.c src/patches.c src/transfer.c src/sequencer.c \
src/libminiaudio-audio.c src/instrument.c src/amy_midi.c src/api.c src/midi_mappings.c \
src/libminiaudio-audio.c src/instrument.c src/amy_midi.c src/api.c src/introspection.c src/midi_mappings.c \
src/cv_trigger.c

OBJECTS = $(patsubst %.c, %.o, $(SOURCES))
Expand Down Expand Up @@ -125,7 +125,7 @@ amy-message: $(OBJECTS) src/amy-message.o
# rollovers 50 days out, which you can only hit by fast-forwarding the counters.
CTESTS = tests/test_clock_wrap tests/test_sequencer_active tests/test_sequencer_bounds \
tests/test_bus_config tests/test_patch_slots \
tests/test_synth_readout tests/test_log2_lut tests/test_clone_on_grow \
tests/test_synth_readout tests/test_introspection tests/test_log2_lut tests/test_clone_on_grow \
tests/test_timebase_reset tests/test_osc_free_on_release \
tests/test_voice_osc_range

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import subprocess
import sys
# the c++ extension module
sources = ['algorithms.c', 'amy.c', 'delay.c', 'envelope.c', 'filters.c', 'parse.c', 'sequencer.c', 'transfer.c', 'midi_mappings.c', 'custom.c', 'patches.c', 'libminiaudio-audio.c', 'oscillators.c', 'interp_partials.c', 'pcm.c', 'pyamy.c', 'log2_exp2.c', 'instrument.c', 'amy_midi.c', 'api.c', 'cv_trigger.c']
sources = ['algorithms.c', 'amy.c', 'delay.c', 'envelope.c', 'filters.c', 'parse.c', 'sequencer.c', 'transfer.c', 'midi_mappings.c', 'custom.c', 'patches.c', 'libminiaudio-audio.c', 'oscillators.c', 'interp_partials.c', 'pcm.c', 'pyamy.c', 'log2_exp2.c', 'instrument.c', 'amy_midi.c', 'api.c', 'introspection.c', 'cv_trigger.c']

for i in range(len(sources)):
sources[i] = "src/"+sources[i]
Expand Down
10 changes: 10 additions & 0 deletions src/amy-message.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,19 @@
#ifndef ARDUINO

#include "amy.h"
#include "introspection.h"
#include "libminiaudio-audio.h"

void delay_ms(uint32_t ms) {
uint32_t start = amy_sysclock();
while(amy_sysclock() - start < ms) usleep(THREAD_USLEEP);
}

static void write_query_response(const char *data, size_t len, void *context) {
(void)context;
fwrite(data, 1, len, stdout);
}

int main(int argc, char ** argv) {
int8_t playback_device_id = -1;
int8_t capture_device_id = -1;
Expand Down Expand Up @@ -56,6 +63,9 @@ int main(int argc, char ** argv) {
char input[1024];
fprintf(stdout, "#;\n");
if (fgets(input, sizeof(input)-1, stdin) == NULL) break;
if (amy_introspection_query(input, write_query_response, NULL)) {
continue;
}
if (input[0] == '?') {
switch (input[1]) {
case 'c':
Expand Down
71 changes: 71 additions & 0 deletions src/introspection.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Compact AMY introspection built on the existing synth readback path.
#include "amy.h"
#include "introspection.h"

#include <ctype.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static void introspection_write(amy_introspection_write_fn write_fn,
void *context,
const char *text) {
if (write_fn == NULL || text == NULL) return;
write_fn(text, strlen(text), context);
}

static bool only_trailing_space(const char *s) {
while (*s != '\0') {
if (!isspace((unsigned char)*s)) return false;
++s;
}
return true;
}

static void write_synth_state(uint8_t synth_num,
amy_introspection_write_fn write_fn,
void *context) {
char command[MAX_MESSAGE_LEN];
char prefix[24];
void *state = NULL;

snprintf(prefix, sizeof(prefix), "!is%u:", (unsigned)synth_num);

do {
command[0] = '\0';
state = yield_synth_commands(synth_num, command, sizeof(command), true, state);
if (command[0] == '\0') continue;
introspection_write(write_fn, context, prefix);
introspection_write(write_fn, context, command);
introspection_write(write_fn, context, "\n");
} while (state != NULL);

introspection_write(write_fn, context, "!ie\n");
}

bool amy_introspection_query(const char *message,
amy_introspection_write_fn write_fn,
void *context) {
if (message == NULL || strncmp(message, "?i", 2) != 0) return false;

const char *query = message + 2;
if (query[0] == 'v' && only_trailing_space(query + 1)) {
char response[20];
snprintf(response, sizeof(response), "!iv%d\n", AMY_INTROSPECTION_PROTOCOL_VERSION);
introspection_write(write_fn, context, response);
return true;
}

if (query[0] == 's') {
char *end = NULL;
unsigned long synth_num = strtoul(query + 1, &end, 10);
if (end != query + 1 && synth_num <= UINT8_MAX && only_trailing_space(end)) {
write_synth_state((uint8_t)synth_num, write_fn, context);
return true;
}
}

introspection_write(write_fn, context, "!ix\n");
return true;
}
31 changes: 31 additions & 0 deletions src/introspection.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Compact, transport-neutral AMY introspection.
//
// This deliberately exposes AMY's existing wire representation instead of
// adding UI labels, ranges, units, or per-patch metadata to embedded builds.
#ifndef __AMY_INTROSPECTION_H
#define __AMY_INTROSPECTION_H

#include <stdbool.h>
#include <stddef.h>

#define AMY_INTROSPECTION_PROTOCOL_VERSION 1

typedef void (*amy_introspection_write_fn)(const char *data, size_t len, void *context);

// Handle an introspection query. Returns true when `message` belongs to the
// introspection namespace, false when the caller should pass it elsewhere.
//
// Queries:
// ?iv -> !iv1\n
// ?is<N> -> zero or more !is<N>:<AMY wire command>\n lines,
// followed by !ie\n
//
// The synth-state response is generated from AMY's live synth readback, so it
// works for built-in patches, memory patches, and directly configured synths.
// It intentionally returns the complete technical state. Presentation/filter
// policy belongs in the client, not in embedded AMY.
bool amy_introspection_query(const char *message,
amy_introspection_write_fn write_fn,
void *context);

#endif
102 changes: 102 additions & 0 deletions tests/test_introspection.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Tests the transport-neutral introspection query layer.
#include <stdio.h>
#include <stdint.h>
#include <string.h>

#include "amy.h"
#include "introspection.h"

static int failures = 0;
static char response[16384];
static size_t response_len = 0;

#define CHECK(cond, fmt, ...) do { \
if (cond) { printf(" ok " fmt "\n", ##__VA_ARGS__); } \
else { printf(" FAIL " fmt "\n", ##__VA_ARGS__); failures++; } \
} while (0)

static void collect(const char *data, size_t len, void *context) {
(void)context;
if (response_len + len >= sizeof(response)) {
failures++;
return;
}
memcpy(response + response_len, data, len);
response_len += len;
response[response_len] = '\0';
}

static void clear_response(void) {
response_len = 0;
response[0] = '\0';
}

static void render_a_bit(void) {
for (int i = 0; i < 16; ++i) amy_simple_fill_buffer();
}

static void send(const char *message) {
amy_add_message((char *)message);
render_a_bit();
}

static void query(const char *message) {
clear_response();
CHECK(amy_introspection_query(message, collect, NULL), "%s is handled", message);
}

static void test_namespace_and_version(void) {
printf("query namespace and protocol version\n");
clear_response();
CHECK(!amy_introspection_query("v0w0Z", collect, NULL), "ordinary wire message is not consumed");
query("?iv\n");
CHECK(strcmp(response, "!iv1\n") == 0, "protocol version response is compact");
}

static void test_direct_karplus_state(void) {
printf("directly configured Karplus-Strong state is introspectable\n");
send("i20iv1in1Z");
send("v0w6b0.985F1200R1.2i20Z");
query("?is20\n");
CHECK(strstr(response, "w6") != NULL, "wave is present");
CHECK(strstr(response, "b0.985") != NULL, "feedback/decay is present");
CHECK(strstr(response, "F1200") != NULL, "filter state is present");
CHECK(strstr(response, "R1.2") != NULL, "resonance state is present");
CHECK(strstr(response, "!ie\n") != NULL, "response has explicit end marker");
}

static void test_builtin_patch_state(void) {
printf("built-in patch expands through the same live readback path\n");
send("i21iv1K0Z");
query("?is21");
CHECK(strstr(response, "F179.93") != NULL, "patch 0 filter state is visible");
CHECK(strstr(response, "R0.93") != NULL, "patch 0 resonance state is visible");
}

static void test_undefined_and_bad_queries(void) {
printf("undefined synths and malformed queries remain bounded\n");
query("?is60");
CHECK(strcmp(response, "!ie\n") == 0, "undefined synth is an empty state stream");
query("?is999");
CHECK(strcmp(response, "!ix\n") == 0, "out-of-range synth is rejected");
query("?iwut");
CHECK(strcmp(response, "!ix\n") == 0, "unknown introspection query is rejected");
}

void delay_ms(uint32_t ms) { (void)ms; }

int main(void) {
amy_config_t config = amy_default_config();
config.features.startup_bleep = 0;
amy_start(config);
render_a_bit();

test_namespace_and_version();
test_direct_karplus_state();
test_builtin_patch_state();
test_undefined_and_bad_queries();

if (failures) { printf("%d FAILURES\n", failures); return 1; }
printf("all ok\n");
return 0;
}