A CircuitPython project for indoor "windless" garden chimes that play along with the outdoor wind speed.
Introduction
Our patio garden chime collection playfully dances along with the wind to compose and perform new melodies and songs. Because it's quite breezy here, our chimes are also a windstorm warning system, giving us notice that we may be visited by neighborhood trash cans and trampolines. When the wind is strong, we can hear the outdoor chimes describing the wind speed in many of our indoor rooms. Except my office. I need a way to listen to the chimes.
The Weather Chimes project fills that need. It connects to the Adafruit NTP service for network time and to OpenWeatherMap.org for wind speed data. The wind speed data is retrieved every twenty minutes and is used to adjust wind chime playback in a pseudo random pattern. The chime voice synthesizer is provided by the CircuitPython_Chimes
class and for this project, is sent to an Adafruit MAX98357A I2S amplifier driving an Adafruit 40mm 4-ohm 3-watt speaker. Although an Unexpected Maker Feather S2 was used for this project, the code should work on just about any ESP32 device that's capable of running CircuitPython.
The Weather Chimes project consists of two primary code files, weather_chimes_code.py
and weather_chimes_wifi.py
. The weather_chimes_code.py
code is imported via the default code.py
file contained in the Feather S2's root directory. This code contains the primary non-wifi device definitions and the master while...
loop that plays the chimes. It also imports the WeatherChimesWiFi
class from weather_chimes_wifi.py
.
The WeatherChimesWiFi
class takes care of all the networking details for connecting and retrieving data from the internet. It also provide helpers for updating and retrieving time and weather as well as properties for including the local time and wind speed. The WiFi class uses the settings.toml
file for connecting to a home WiFi router as well as parameters needed for Adafruit NTP and the OpenWeatherMap.org API.
A fictional settings.toml
file:
CIRCUITPY_WIFI_SSID="MyLocalWiFiRouter" CIRCUITPY_WIFI_PASSWORD="secretpassword" CIRCUITPY_WEB_API_PASSWORD="passw0rd" CIRCUITPY_WEB_API_PORT=80 location="LosAngeles, CA, US" timezone="America/LosAngeles" openweather_token="ABCDEF0123456789ghijklmnoPQRSTUVWXYZ"
Besides the primary code files, the project depends on a typical collection of Adafruit libraries, a library from the community bundle, as well as a custom class to reproduce the voice of the chimes with synthio.
Simulating the Chimes
The imported CircuitPython_Chime
class does the hard work of building a synthio
object with all the overtones and ADSR envelope characteristics of a set of tubular chimes. The class also contains a collection of selectable musical songs ("scales"). For this project, an emulation of our family heirloom 1970's Harry and David Pear six-tube garden chime was selected from the collection. To further customize the chime voice, refer to the documentation in the CircuitPython_Chime GitHub repository.
The synthesized chime voice adds some unique overtones to the root frequency signal to achieve its realistic sound. For a metallic chime, the overtones are not "perfect scientific harmonics" with integer frequency multipliers of 1 (the root fundamental frequency), 2, 3, 4, and 5 with amplitude levels of 60%, 20%, 10%, 5%, and 5% respectively.
Instead, because the metal tube is open on both ends, the overtones have frequency multipliers of 1.00 (the root fundamental), 2.76, 5.40, and 8.93 with amplitude levels of 60%, 20%, 10%, and 10% respectively. The resultant output signal is more complex than the "perfect" combination and more characteristic of real tubular chimes.
If one end of the tubular chime is closed it sounds more like a bell. Empirical measurements of a well-designed metal bell found the overtone frequency multipliers to be 1.00 (root fundamental), 1.48, 1.35, and 1.72 with amplitude levels of 80%, 19%, 1%, and <1% respectively.
A simple test of the chime voice:
import time import board import audiobusio import audiomixer from cedargrove_chime import Chime, Scale # Instantiate chime synthesizer audio_output = audiobusio.I2SOut(bit_clock=board.D12, word_select=board.D9, data=board.D6) mixer = audiomixer.Mixer(sample_rate=11020, buffer_size=4096, voice_count=1, channel_count=1) audio_output.play(mixer) mixer.voice[0].level = 0.8 chime = Chime(mixer.voice[0], scale=Scale.HarryDavidPear) # Sequentially play all the notes in the scale for index, note in enumerate(chime.scale): chime.strike(note, 1) time.sleep(0.4) time.sleep(1)
Responding to the Wind
To simulate a typical garden wind chime, it's assumed that the note tubes are mounted in a circle and that no more than half the tubes will sound when the striker moves due to wind. The Chime.strike()
method randomly selects the first note and the number of notes to play from the chime scale. The initial note will be followed by a cluster of adjacent notes either to the right or left as determined by a random direction variable and the number of notes to play, each played in sequence after a random delay that ranges from 0.1 to 0.5 seconds. This algorithm mimics the observed behavior of the chime striker since it usually moves away from the first struck tube, hitting a few adjacent chime tubes in a left-to-right or right-to-left circular pattern.
Further achieving a realistic pseudo-random chime playback proportional to wind speed involves two additional factors. First, the amplitude (audio volume) of the chime notes is directly proportional to wind speed, varying from 40% to 100% amplitude. Next, the delay between clusters of notes is inversely proportional to wind speed; the delay time interval ranges proportionally from 2 seconds to 10 milliseconds. A random delay of up to 0.5 second is added to the calculated cluster interval delay time to reduce the possibility of discernible patterns.
An excerpt from weather_chimes_code.py
showing the wind speed simulation algorithm:
"""Populate the chime_index list with the initial note then add the additional adjacent notes.""" chime_index = [] chime_index.append(random.randrange(len(chime.scale))) direction = random.choice((-1, 1)) for count in range(1, len(chime.scale) // 2): chime_index.append((chime_index[count - 1] + direction) % len(chime.scale)) """Randomly select the number of notes to play in the sequence.""" notes_to_play = random.randrange(len(chime_index) + 1) """Play the note sequence with a random delay between each.""" note_amplitude = map_range(corr_wifi.wind_speed, 0, 50, 0.4, 1.0) for count in range(notes_to_play): chime.strike(chime.scale[chime_index[count]], note_amplitude) time.sleep( random.randrange(10, 60) * 0.01 ) # random delay of 0.10 to 0.50 seconds """Delay the next note sequence inversely based on wind speed plus a random interval.""" if corr_wifi.wind_speed < 1: time.sleep(30) else: time.sleep( map_range(corr_wifi.wind_speed, 0, 50, 2.0, 0.01) + (random.random() / 2) )
Dependencies
This project depends on:
Adafruit CircuitPython
CedarGrove CircuitPython_Chime
CedarGrove CircuitPython_MIDI_Tools
Acknowledgements and Thanks
- Lee Hite, Tubular Bell Chimes Design Handbook for the analysis of tubular chime physics and overtones.
- C. McKenzie, T. Schweisinger, and J. Wagner, A Mechanical Engineering Laboratory Experiment to Investigate the Frequency Analysis of Bells and Chimes with Assessment for the analysis of bell overtones.
- Liz Clark, Circle of Fifths Euclidean Synth with synthio and CircuitPython Adafruit Learning Guide for the waveform and noise methods.
- Todd Kurt for the fundamentally essential synthio hints, tricks, and examples.
- John Park for the foundational Adafruit Learning Guide, Audio Synthesis with CircuitPython synthio.
Also, special thanks to Jeff Epler and Adafruit for the comprehensive design and implementation of the amazing CircuitPython synthio module.