Prototype 2: Kick-starting the Ambient Machine

Prototype 2: Kick-starting the Ambient Machine

In the first prototype, I built a simple Wi-Fi beeper that reflects the signal strength (RSSI) of each signal by connecting it to a buzzer. It was a proof of concept that the system can create an interactive experienece that shows how the environment can have an impact on the individual in an invisible and untouchable way. 

Taking a step further, the goal of the second prototype is to:

  • Complicate sound sources to make more sophisticated ambient sound by switching to the Mozzi library for Arduino
    • Set up and explore how Mozzi works
    • Build a simple circuit example with Mozzi
    • Use sensors/inputs to generate sound
  •  Connect Wi-Fi scanning with the established sound generation program

This blog post will be divided in 3 parts: wiring, setting up Mozzi (2 examples), and working prototype 2.

Wiring

Photo: Prototype 2 Wiring 

For prototype 2, the wiring contains two potentiometers for analog input, and an additional potentiometer for volume control (together with a capacitor to smooth audio quality). These two potentiometers are connected to ESP32, which runs Mozzi. For output, a GPIO pin with Audio (DAC) capabilities is connected to the earphone. The earphone is connected using wire clamps, corresponding to each section of the 3.5mm audio jack.

In order from the inside to the outside: Microphone -> Ground -> Left -> Right

It’s a very temporary and experimental setup, which will change in the future to improve stability and cleaner connections.

 An ESP8266, responsible for Wi-Fi scanning, is connected to ESP32 via analog, more specifics will be in the following sections. 

Setting Up Mozzi: 2 Examples

Example 1: Sine Wave Synth (Full code included at the end)

In this example, two potentiometers are mapped to pitchValue (frequency) and cutoff frequency for a low-pass filter. The potentiometers can control the features of sinwave synth. This example demonstrates a basic synth which can be shaped and manipulated further. 

Example 2: Bluetooth paired with arppeggiator

This example utilizes bluetooth scan, trying to achieve scanning and sound production on the same board, but with little success.

The logic of the bluetooth scan is as follows: 

BLEScan *scan = BLEDevice::getScan();
scan->setActiveScan(true);
BLEScanResults results = scan->start(1);
int best = CUTOFF;
for (int i = 0; i < results.getCount(); i++) {
BLEAdvertisedDevice device = results.getDevice(i);
int rssi = device.getRSSI();
if (rssi > best) {
best = rssi;
}

This code snippet will grab the “best” bluetooth signal, and countinuously return the biggest signal source and its numerial signal strength value (RSSI). In turn, the signal value (RSSI) will define an arpeggiator pattern that “should” make the sinwave synth more musical.

However, a big problem is the incompatibility of running Bluetooth/Wi-Fi on a single board, with Mozzi. Mozzi code structure includes special sections such as UpdateControl(), updateAudio(), which operates on high frequencies (~16000Hz) to match audio control rate. Adding anything related to Serial Communication, WiFi, Bluetooth, or just timing functions in general would not work with Mozzi.

Therefore, the only option left is to use another board (ESP8266) and separate the bluetooth function with the sound board, and utilize analog output/input (PWM pin) to transmit data.

Example 3: Working Prototype 2

This example plays a fluctuating ambient wash in response to the Wi-Fi scan results and a potentiometer. The Wi-Fi scan controls the base frequency of the oscillator, and the potentiometer controls oscillator offset depending on the resistance.

There are two sets of oscillators. The first set uses 7 different Cosine wavetables to produce a harmonic synth sound. The second set duplicates but slightly off frequency for adding to originals. There is a pre-set offset scale to map WiFi scan result to base frequency drift.

The base midi notes are: C3 E3 G3 A3 C4 E4, and translates to

f1 = mtof(48.f);
f2 = mtof(52.f);
f3 = mtof(55.f);
f4 = mtof(60.f);
f5 = mtof(64.f);
f6 = mtof(67.f).
 
Since as noted before, none of the Serial or on-board solutions work for Mozzi, but apparently analogRead within Mozzi’s updateControl() function works fine. Specifically, using mozziAnalogRead() works the best.
 
However, in actual testing, the result is not really user “defined”, because there is random drifts in base frequency when supposedly signals should be stable and updates every second. The next iteration of prototype need to addrss this issue, but for the current iteration it proves that Mozzi can work well with analog inputs to produce reasonably good sound.
 

Conclusion and Next Steps

The current prototype took a step further and accompolished synth generation with Wi-Fi inputs. For the next prototype, the goal is to build a more defined user experience. Achiving that requires a re-thinking of the input variables that the system uses (currently is the number of Wi-Fi signals detected). Adding temperature, light, or other inputs might complicate things further and generate richer sound. However, that requires a deeper and higher level understanding of Mozzi, especially how to change synths parameters and control sounds. 

FULL CODE

EXAMPLE 1

//sinwave synth
#include <MozziGuts.h>
#include <Oscil.h>
#include <tables/sin2048_int8.h>
#include <LowPassFilter.h>
#include <mozzi_midi.h>
// Set up the oscillator using a sine wave table
Oscil<SIN2048_NUM_CELLS, AUDIO_RATE> oscil(SIN2048_DATA);
// Set up a low-pass filter
LowPassFilter lpf;
void setup() {
pinMode(13,INPUT);
pinMode(14,INPUT);
startMozzi(); // Initialize Mozzi
}
void updateControl() {
// Read the pitch control potentiometer
int pitchValue = analogRead(13);
int midiNote = map(pitchValue, 0, 1023, 36, 84); // Map the potentiometer value to a MIDI note range
oscil.setFreq(mtof(midiNote)); // Set the oscillator frequency based on the MIDI note
// Read the filter control potentiometer
int filterValue = analogRead(14);
int cutoffFreq = map(filterValue, 0, 1023, 50, 5000); // Map to a range of cutoff frequencies
lpf.setCutoffFreq(cutoffFreq); // Set the filter’s cutoff frequency
}
int updateAudio() {
// Generate the audio signal
int sound = oscil.next();
sound = lpf.next(sound); // Filter the sound
return sound;
}
void loop() {
audioHook();
}
 

EXAMPLE 2

#include <MozziGuts.h>
#include <Oscil.h>
#include <tables/sin2048_int8.h>
#include <LowPassFilter.h>
#include <BLEDevice.h>
#include <BLEScan.h>
#include <mozzi_midi.h>
// Set up the oscillator using a sine wave table
Oscil<SIN2048_NUM_CELLS, AUDIO_RATE> oscil(SIN2048_DATA);
// Set up a low-pass filter
LowPassFilter lpf;
// Arpeggiator pattern size
const int arpSize = 4;
int arpNotes[arpSize]; // Array to hold arpeggiator notes
// BLE scan interval
unsigned long lastScanTime = 0;
const unsigned long scanInterval = 100; // Interval between scans in milliseconds
int bestRSSI = -99; // Placeholder for the strongest RSSI value
void setup() {
pinMode(13, INPUT);
pinMode(14, INPUT);
startMozzi(); // Initialize Mozzi
BLEDevice::init(“”); // Initialize BLE with an empty name string
lpf.setCutoffFreq(500); // Set an initial cutoff frequency for the low-pass filter
}
void updateControl() {
// Check if it’s time for a new BLE scan
if (millis() – lastScanTime >= scanInterval) {
BLEScan* scan = BLEDevice::getScan();
scan->setActiveScan(true); // Active scan uses more power, but get results faster
BLEScanResults results = scan->start(1, false); // Scan for 1 second
bestRSSI = -99; // Reset best RSSI value
// Iterate over each device found during the scan
for (int i = 0; i < results.getCount(); i++) {
BLEAdvertisedDevice device = results.getDevice(i);
int rssi = device.getRSSI(); // Get the RSSI of the device
if (rssi > bestRSSI) {
bestRSSI = rssi; // Save the RSSI if it’s better than the last best
}
}
lastScanTime = millis(); // Update the last scan time
// Define the arpeggiator pattern based on RSSI
int arpBaseNote = map(bestRSSI, -100, 0, 48, 72); // Map RSSI to a base MIDI note
for (int i = 0; i < arpSize; i++) {
arpNotes[i] = arpBaseNote + i * 2; // Simple pattern: base note and next three notes in scale
}
}
}
int updateAudio() {
staticunsignedint arpIndex = 0; // Index of the current note in the arpeggio
staticunsignedlong lastArpTime = 0; // Last time the note was changed
constunsignedlong arpInterval = 200; // Time between arp notes in milliseconds
// Change the note in the arpeggiator pattern based on the interval
if (millis() – lastArpTime >= arpInterval) {
oscil.setFreq(mtof(arpNotes[arpIndex])); // Set the frequency for the current step
arpIndex = (arpIndex + 1) % arpSize; // Move to the next step in the arpeggiator
lastArpTime = millis(); // Reset the timer
}
// Generate the audio signal and apply the low-pass filter
int sound = oscil.next();
sound = lpf.next(sound); // Apply the low-pass filter to the sound
return sound; // Output the filtered sound
}
void loop() {
audioHook(); // Constantly update Mozzi sound generation
}

PROTOTYPE 2

#include <MozziGuts.h>
#include <Oscil.h>
#include <tables/cos8192_int8.h>
#include <mozzi_rand.h>
#include <mozzi_midi.h>
#define THERMISTOR_PIN 13
#define LDR_PIN 14
#define PinIn 15
int data = 0;
// harmonics
Oscil<COS8192_NUM_CELLS, AUDIO_RATE> aCos1(COS8192_DATA);
Oscil<COS8192_NUM_CELLS, AUDIO_RATE> aCos2(COS8192_DATA);
Oscil<COS8192_NUM_CELLS, AUDIO_RATE> aCos3(COS8192_DATA);
Oscil<COS8192_NUM_CELLS, AUDIO_RATE> aCos4(COS8192_DATA);
Oscil<COS8192_NUM_CELLS, AUDIO_RATE> aCos5(COS8192_DATA);
Oscil<COS8192_NUM_CELLS, AUDIO_RATE> aCos6(COS8192_DATA);
// duplicates but slightly off frequency for adding to originals
Oscil<COS8192_NUM_CELLS, AUDIO_RATE> aCos1b(COS8192_DATA);
Oscil<COS8192_NUM_CELLS, AUDIO_RATE> aCos2b(COS8192_DATA);
Oscil<COS8192_NUM_CELLS, AUDIO_RATE> aCos3b(COS8192_DATA);
Oscil<COS8192_NUM_CELLS, AUDIO_RATE> aCos4b(COS8192_DATA);
Oscil<COS8192_NUM_CELLS, AUDIO_RATE> aCos5b(COS8192_DATA);
Oscil<COS8192_NUM_CELLS, AUDIO_RATE> aCos6b(COS8192_DATA);
// base pitch frequencies
float f0, f1,f2,f3,f4,f5,f6;
// to map light input to frequency divergence of the b oscillators
const float DIVERGENCE_SCALE = 0.01; // 0.01*1023 = 10.23 Hz max divergence
// to map temperature to base freq drift
const float OFFSET_SCALE = 0.1; // 0.1*1023 = 102.3 Hz max drift
void setup(){
pinMode(PinIn, INPUT);
//analogReadResolution(10); // Set ADC resolution to 10 bits to match ESP8266
 
startMozzi();
// select base frequencies using mtof
// C E G A
f1 = mtof(48.f);
f2 = mtof(52.f);
f3 = mtof(55.f);
f4 = mtof(60.f);
f5 = mtof(64.f);
f6 = mtof(67.f);
// set Oscils with chosen frequencies
aCos1.setFreq(f1);
aCos2.setFreq(f2);
aCos3.setFreq(f3);
aCos4.setFreq(f4);
aCos5.setFreq(f5);
aCos6.setFreq(f6);
// set frequencies of duplicate oscillators
aCos1b.setFreq(f1);
aCos2b.setFreq(f2);
aCos3b.setFreq(f3);
aCos4b.setFreq(f4);
aCos5b.setFreq(f5);
aCos6b.setFreq(f6);
}
void loop(){
audioHook();
}
void updateControl(){
// read analog inputs
//int temperature = mozziAnalogRead(THERMISTOR_PIN); // not calibrated to degrees!
int temperature = mozziAnalogRead(PinIn); // read from 8266
//int temperature = data;
int light_input = mozziAnalogRead(LDR_PIN);
float base_freq_offset = OFFSET_SCALE*temperature;
float divergence = DIVERGENCE_SCALE*light_input;
float freq;
// change frequencies of the oscillators, randomly choosing one pair each time to change
switch (rand(6)+1){
case1:
freq = f1+base_freq_offset;
aCos1.setFreq(freq);
aCos1b.setFreq(freq+divergence);
break;
case2:
freq = f2+base_freq_offset;
aCos2.setFreq(freq);
aCos2b.setFreq(freq+divergence);
break;
case3:
freq = f3+base_freq_offset;
aCos3.setFreq(freq);
aCos3b.setFreq(freq+divergence);
break;
case4:
freq = f4+base_freq_offset;
aCos4.setFreq(freq);
aCos4b.setFreq(freq+divergence);
break;
case5:
freq = f5+base_freq_offset;
aCos5.setFreq(freq);
aCos5b.setFreq(freq+divergence);
break;
case6:
freq = f6+base_freq_offset;
aCos6.setFreq(freq);
aCos6b.setFreq(freq+divergence);
break;
}
}
AudioOutput_t updateAudio(){
int asig =
aCos1.next() + aCos1b.next() +
aCos2.next() + aCos2b.next() +
aCos3.next() + aCos3b.next() +
aCos4.next() + aCos4b.next() +
aCos5.next() + aCos5b.next() +
aCos6.next() + aCos6b.next();
return MonoOutput::fromAlmostNBit(12, asig);
}

ESP8266

/*
This sketch demonstrates how to scan WiFi networks.
The API is almost the same as with the WiFi Shield library,
the most obvious difference being the different file you need to include:
*/
#include <ESP8266WiFi.h>
int pwmPin = D1; // Replace with your PWM capable pin
void setup() {
pinMode(pwmPin, OUTPUT);
//Serial.println(F(“\nESP8266 WiFi scan example”));
// Set WiFi to station mode
WiFi.mode(WIFI_STA);
// Disconnect from an AP if it was previously connected
WiFi.disconnect();
delay(100);
}
void loop() {
String ssid;
int32_t rssi;
uint8_t encryptionType;
uint8_t *bssid;
int32_t channel;
bool hidden;
int scanResult;
int send;
//Serial.println(F(“Starting WiFi scan…”));
scanResult = WiFi.scanNetworks(/*async=*/false, /*hidden=*/true);
if (scanResult == 0) {
//Serial.println(F(“No networks found”));
} else if (scanResult > 0) {
//Serial.printf(PSTR(“%d networks found:\n”), scanResult);
send = map(scanResult, 0, 100, 0, 300);
analogWrite(pwmPin, send);
// Print unsorted scan results
for (int8_t i = 0; i < scanResult; i++) {
WiFi.getNetworkInfo(i, ssid, encryptionType, rssi, bssid, channel, hidden);
// get extra info
const bss_info *bssInfo = WiFi.getScanInfoByIndex(i);
String phyMode;
constchar *wps = “”;
if (bssInfo) {
phyMode.reserve(12);
phyMode = F(“802.11”);
String slash;
if (bssInfo->phy_11b) {
phyMode += ‘b’;
slash = ‘/’;
}
if (bssInfo->phy_11g) {
phyMode += slash + ‘g’;
slash = ‘/’;
}
if (bssInfo->phy_11n) {
phyMode += slash + ‘n’;
}
if (bssInfo->wps) {
wps = PSTR(“WPS”);
}
}
//Serial.printf(PSTR(” %02d: [CH %02d] [%02X:%02X:%02X:%02X:%02X:%02X] %ddBm %c %c %-11s %3S %s\n”), i, channel, bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], rssi, (encryptionType == ENC_TYPE_NONE) ? ‘ ‘ : ‘*’, hidden ? ‘H’ : ‘V’, phyMode.c_str(), wps, ssid.c_str());
//Serial.print(rssi);
yield();
}
} else {
//Serial.printf(PSTR(“WiFi scan error %d”), scanResult);
}
 
delay(1000);
 
}

Leave a Reply

Your email address will not be published. Required fields are marked *