first sketch:

i made a low fidelity prototype (think one face).

the programming needs to be like so:

/*
to flash on each board.
*/
 
int mic = A0; 
int speaker = 8; 
 
void setup() {
  Serial.begin(9600); 
 
  pinMode(A0, INPUT); 
  pinMode(8, OUTPUT); 
}
 
void loop() {
  listen(); 
}
 
void speak(){
  
}
 
void listen(){
//
}
 

since i didn’t have time, i told claude to expose certain things from the listening, that i could then encode into the speaking:

/*
nano 33 iot. sparkfun electret breakout (60x preamp, output biased
at vcc/2): vcc -> 3.3v, gnd -> gnd, AUD -> A0 directly (no series cap).
piezo on d8.
 
listen() fills a Features struct once per frame (~32ms).
decide() reads it and calls speak(). edit decide() and the tuning
constants; leave the rest alone.
 
quiet reads ~2048 (12-bit). if f.peak nears 2048 the preamp is clipping.
*/
 
#include <math.h>
 
const int MIC_PIN   = A0;
const int PIEZO_PIN = 8;
 
// sampling
const int   N        = 256;     // frame size, power of 2
const float TARGET_FS = 8000.0; // actual rate is measured per frame
 
// band edges (hz)
const float LOW_MAX = 300.0;
const float MID_MAX = 1500.0;
 
// onset / noise tuning
const float ONSET_RATIO   = 2.5;  // rms must exceed noise floor * this
const float ONSET_RISE    = 1.5;  // and exceed previous rms * this
const float FLOOR_RISE    = 0.002;// how fast the noise floor creeps up
const float FLOOR_FALL    = 0.2;  // how fast it drops to a quieter level
const float ENV_SMOOTH    = 0.3;  // 0..1, higher = snappier envelope
const int   ONSET_COOLDOWN_MS = 120;
const float MIN_PITCH_MAG = 15.0; // fft peak below this = "no pitch"
const int   ZC_HYST       = 20;   // zero-cross hysteresis (adc counts)
 
struct Features {
  // amplitude (adc counts, 12-bit)
  float dc;          // mean level
  float peak;        // max |sample - dc|
  float p2p;         // peak to peak
  float rms;
  float envelope;    // smoothed rms
  float noiseFloor;  // slow estimate of ambient rms
  float level;       // rms / noiseFloor, ~1 = silence
 
  // pitch
  float freq;        // dominant frequency (hz), 0 if none
  float freqMag;     // strength of that peak
  float zcFreq;      // zero-crossing frequency (hz), cheap/rough
 
  // spectrum
  float low, mid, high;   // band energy share, sums to ~1
  float centroid;         // spectral centroid (hz), "brightness"
  float flatness;         // 0 = tonal, 1 = noisy
 
  // events
  bool  onset;            // true on the frame a new sound starts
  int   onsetCount;       // total onsets since boot
  unsigned long sinceOnset;   // ms since last onset
  unsigned long sinceQuiet;   // ms since it was last quiet
  unsigned long ioi;          // ms between the last two onsets
  float bpm;                  // 60000 / ioi, 0 if unknown
  bool  quiet;
  bool  self;             // true if the piezo was sounding this frame
 
  float fs;               // measured sample rate
};
 
Features f;
 
float re[N], im[N];
float mag[N / 2];
unsigned long speakingUntil = 0;
unsigned long lastOnset = 0, lastQuiet = 0;
 
const unsigned long INIT_MIN_MS = 3000;   // random silence before starting a conversation
const unsigned long INIT_MAX_MS = 10000;
const int INIT_LOW_HZ  = 300;             // pitch range for a random opener
const int INIT_HIGH_HZ = 1500;
 
unsigned long lastActive = 0;  // last time anything was heard or said
unsigned long initAfter  = 0;
 
void setup() {
  Serial.begin(115200);
  pinMode(MIC_PIN, INPUT);
  pinMode(PIEZO_PIN, OUTPUT);
 
  analogReadResolution(12);
  analogRead(MIC_PIN);
  // faster adc clock so we can actually hit ~8khz
  ADC->CTRLA.bit.ENABLE = 0;
  while (ADC->STATUS.bit.SYNCBUSY);
  ADC->CTRLB.bit.PRESCALER = ADC_CTRLB_PRESCALER_DIV64_Val;
  ADC->CTRLA.bit.ENABLE = 1;
  while (ADC->STATUS.bit.SYNCBUSY);
 
  f.noiseFloor = 0;
  randomSeed(analogRead(A1));  // A1 left floating, so each unit differs
  initAfter = random(INIT_MIN_MS, INIT_MAX_MS);
}
 
void loop() {
  listen();
  decide();
  report();
}
 
// ---------------------------------------------------------------- rules
 
// your rules go here. everything you can sense is in f.
const int   SPEAK_MS   = 600;  // how long each reply lasts
const int   GAP_MS     = 300;  // silence after speaking, so units take turns
const float HEAR_LEVEL = 3.0;  // f.level needed to count a sustained tone as heard
 
void decide() {
  unsigned long now = millis();
  if (f.self) { lastActive = now; return; }  // don't react to our own voice
  if (now < speakingUntil + GAP_MS) return;  // taking turns
 
  bool heard = f.onset || f.level > HEAR_LEVEL;
  if (heard) lastActive = now;
 
  // answer what we hear an octave up
  if (heard && f.freq > 0) {
    float out = f.freq * 2;
    if (out > 2000) out = f.freq * 0.5;  // fold down, stay under nyquist / piezo range
    speak(out, SPEAK_MS);
    initAfter = random(INIT_MIN_MS, INIT_MAX_MS);
    return;
  }
 
  // nothing for a while: start a conversation
  if (now - lastActive > initAfter) {
    speak(random(INIT_LOW_HZ, INIT_HIGH_HZ), SPEAK_MS);
    lastActive = now;
    initAfter = random(INIT_MIN_MS, INIT_MAX_MS);
  }
}
 
// freq in hz, dur in ms. non-blocking.
void speak(float freq, int dur) {
  if (freq < 31) return;
  tone(PIEZO_PIN, (unsigned int)freq, dur);
  speakingUntil = millis() + dur + 40;  // +40 lets the ring die down
}
 
// ------------------------------------------------------------- sensing
 
void listen() {
  // 1. sample a frame
  unsigned long period = (unsigned long)(1000000.0 / TARGET_FS);
  unsigned long t0 = micros();
  unsigned long next = t0;
  for (int i = 0; i < N; i++) {
    while ((long)(micros() - next) < 0);
    re[i] = analogRead(MIC_PIN);
    next += period;
  }
  float elapsed = (micros() - t0) / 1e6;
  f.fs = N / elapsed;
 
  unsigned long now = millis();
  f.self = now < speakingUntil;
 
  // 2. amplitude
  float sum = 0;
  for (int i = 0; i < N; i++) sum += re[i];
  f.dc = sum / N;
 
  float mn = 1e9, mx = -1e9, sq = 0, pk = 0;
  for (int i = 0; i < N; i++) {
    re[i] -= f.dc;
    if (re[i] < mn) mn = re[i];
    if (re[i] > mx) mx = re[i];
    if (fabsf(re[i]) > pk) pk = fabsf(re[i]);
    sq += re[i] * re[i];
  }
  float prevRms = f.rms;
  f.peak = pk;
  f.p2p  = mx - mn;
  f.rms  = sqrtf(sq / N);
  f.envelope += ENV_SMOOTH * (f.rms - f.envelope);
 
  // 3. zero-crossing frequency (hysteresis so noise doesn't count)
  int crossings = 0;
  bool high = re[0] > 0;
  for (int i = 1; i < N; i++) {
    if (high && re[i] < -ZC_HYST) { high = false; crossings++; }
    else if (!high && re[i] > ZC_HYST) { high = true; crossings++; }
  }
  f.zcFreq = (crossings / 2.0) / elapsed;
 
  // 4. fft (hann window)
  for (int i = 0; i < N; i++) {
    re[i] *= 0.5 - 0.5 * cosf(2 * PI * i / (N - 1));
    im[i] = 0;
  }
  fft();
  float binHz = f.fs / N;
 
  float total = 0, lo = 0, mi = 0, hi = 0, wsum = 0, logsum = 0;
  int best = 1;
  for (int k = 1; k < N / 2; k++) {
    mag[k] = sqrtf(re[k] * re[k] + im[k] * im[k]) / (N / 2);
    float hz = k * binHz;
    float p = mag[k] * mag[k];
    total += p;
    wsum += p * hz;
    logsum += logf(p + 1e-9);
    if (hz < LOW_MAX) lo += p; else if (hz < MID_MAX) mi += p; else hi += p;
    if (mag[k] > mag[best]) best = k;
  }
  if (total > 0) {
    f.low = lo / total; f.mid = mi / total; f.high = hi / total;
    f.centroid = wsum / total;
    f.flatness = expf(logsum / (N / 2 - 1)) / (total / (N / 2 - 1) + 1e-9);
  } else {
    f.low = f.mid = f.high = f.centroid = f.flatness = 0;
  }
 
  // parabolic interpolation around the peak for sub-bin accuracy
  f.freqMag = mag[best];
  if (best > 1 && best < N / 2 - 1 && f.freqMag >= MIN_PITCH_MAG) {
    float a = mag[best - 1], b = mag[best], c = mag[best + 1];
    float d = a - 2 * b + c;
    float shift = (d != 0) ? 0.5 * (a - c) / d : 0;
    f.freq = (best + shift) * binHz;
  } else {
    f.freq = 0;
  }
 
  // 5. noise floor, quiet, level
  if (!f.self) {
    if (f.noiseFloor == 0) f.noiseFloor = f.rms;
    else if (f.rms < f.noiseFloor) f.noiseFloor += FLOOR_FALL * (f.rms - f.noiseFloor);
    else f.noiseFloor += FLOOR_RISE * (f.rms - f.noiseFloor);
  }
  f.level = f.noiseFloor > 0 ? f.rms / f.noiseFloor : 0;
  f.quiet = f.level < 1.5;
  if (f.quiet) lastQuiet = now;
  f.sinceQuiet = now - lastQuiet;
 
  // 6. onsets, rhythm
  f.onset = false;
  if (!f.self && f.level > ONSET_RATIO && f.rms > prevRms * ONSET_RISE &&
      now - lastOnset > ONSET_COOLDOWN_MS) {
    f.onset = true;
    f.onsetCount++;
    if (lastOnset > 0) {
      f.ioi = now - lastOnset;
      f.bpm = f.ioi > 0 ? 60000.0 / f.ioi : 0;
    }
    lastOnset = now;
  }
  f.sinceOnset = lastOnset > 0 ? now - lastOnset : 0;
}
 
// in-place radix-2 fft on re[] / im[]
void fft() {
  for (int i = 1, j = 0; i < N; i++) {
    int bit = N >> 1;
    for (; j & bit; bit >>= 1) j ^= bit;
    j ^= bit;
    if (i < j) {
      float t = re[i]; re[i] = re[j]; re[j] = t;
      t = im[i]; im[i] = im[j]; im[j] = t;
    }
  }
  for (int len = 2; len <= N; len <<= 1) {
    float ang = -2 * PI / len;
    float wr = cosf(ang), wi = sinf(ang);
    for (int i = 0; i < N; i += len) {
      float cr = 1, ci = 0;
      for (int k = 0; k < len / 2; k++) {
        int a = i + k, b = i + k + len / 2;
        float xr = re[b] * cr - im[b] * ci;
        float xi = re[b] * ci + im[b] * cr;
        re[b] = re[a] - xr; im[b] = im[a] - xi;
        re[a] += xr;        im[a] += xi;
        float t = cr * wr - ci * wi;
        ci = cr * wi + ci * wr;
        cr = t;
      }
    }
  }
}
 
// csv for the serial plotter / monitor
void report() {
  Serial.print("rms:");      Serial.print(f.rms);
  Serial.print(",env:");     Serial.print(f.envelope);
  Serial.print(",floor:");   Serial.print(f.noiseFloor);
  Serial.print(",level:");   Serial.print(f.level);
  Serial.print(",freq:");    Serial.print(f.freq);
  Serial.print(",zc:");      Serial.print(f.zcFreq);
  Serial.print(",centroid:");Serial.print(f.centroid);
  Serial.print(",low:");     Serial.print(f.low);
  Serial.print(",mid:");     Serial.print(f.mid);
  Serial.print(",high:");    Serial.print(f.high);
  Serial.print(",onset:");   Serial.print(f.onset ? 1 : 0);
  Serial.print(",bpm:");     Serial.print(f.bpm);
  Serial.print(",fs:");      Serial.println(f.fs);
}
 
 

basic call & response:

to actually make this, i’d need to figure out the electronics first — which i assume to be a simpler problem, and then write the software. this’ll differ from the 2-d automata that i programmed in c earlier.


research here: