Close
0%
0%

Adventures in Bleep Bloops with CircuitPython

Experiments with polyphonic pythonic tone playing

Similar projects worth following
This project will log my adventures playing music and making sounds with CircuitPython. For now most of the content will be based on my experiences with a NeoTrellis M4 Express.

I've had a NeoTrellis M4 Express for a while now, and spent plenty of time playing with the dazzling RGB neopixels and their delightfully gummy button counterparts. But the thing I was excited for most when I got the NeoTrellis was using it to make sound toys with CircuitPython. This sequencer from the Adafruit learn guide was great fun. Now I am looking to dive in a bit more and explore some more of the possibilities for making music with circuitpython. Here is a quick preview of what I've done so far:

  • Progress Update Video

    foamyguy02/05/2020 at 03:44 0 comments

    I've recorded a quick video to demonstrate the new wave forms and code.py file from the previous two logs: 

  • Updating code.py with waveform selector

    foamyguy02/04/2020 at 02:24 0 comments

    Conveniently we have 8 columns of buttons on the Neotrellis and only 7 music notes, so we've got an extra column. I've updated the code.py file in the repo to to make use of this extra row for selecting among the 4 available wave form types. The currently selected type will light up white. Press the other keys in the last column to change the notes played by the other buttons between the different wave form types.  I'm hoping to make a video to show off the new functionality soon.

  • Level up - More waveforms

    foamyguy01/28/2020 at 04:23 0 comments

    After much trial and error, I've now got 4 different waveforms in the tone generator script. Sine (the original) is now joined by: sawtooth, triangle, and square waves. I've added a copy of the generated notes to the repo as well as a new testing script for the different waveforms. I'm adding images to the gallery of the waveforms that we've created.

  • Putting it all together

    foamyguy01/26/2020 at 23:08 0 comments

    Now we have 3 octaves of tone musical note wave files and some code that plays wave files when we press buttons. Lets smash those together and see where we end up.

    import board
    import audioio
    import digitalio
    import time
    import adafruit_trellism4
    trellis = adafruit_trellism4.TrellisM4Express()
    trellis.pixels.brightness = 0.15
    note_letters = ['c', 'd', 'e', 'f', 'g', 'a', 'b']
    colors = [(255, 0, 0), (255, 127, 0), (255, 255, 0), (0, 255, 0), (0, 0, 255), (46, 43, 95), (139, 0, 255)]
    notes = {}
    
    for octave in range(3,6):
        for note_letter in note_letters:
            cur_note = "%s%s" % (note_letter, octave)
            notes[cur_note] = audioio.WaveFile(open("notes/%s.wav" % (cur_note), "rb"))
    
    audio = audioio.AudioOut(left_channel=board.A0, right_channel=board.A1)
    mixer = audioio.Mixer(voice_count=7, sample_rate=8000, channel_count=2, bits_per_sample=16, samples_signed=True)
    
    audio.play(mixer)
    
    for i, color in enumerate(colors):
        trellis.pixels[i,0] = color
        trellis.pixels[i,1] = color
        trellis.pixels[i,2] = color
    
    prev_pressed = []
    while True:
        #print(trellis.pressed_keys)
        cur_keys = trellis.pressed_keys
        if cur_keys != prev_pressed:
            print("different than last iter")
            pressed_key_xs = []
            if cur_keys != []:
                #audio.play(mixer)
                for key in cur_keys:
                    
                    if key[0] < len(note_letters):
                        note_for_key = "%s%s" % (note_letters[key[0]], key[1]+3)
                        if note_for_key in notes:
                            pressed_key_xs.append(key[0])
                            mixer.play(notes[note_for_key], voice=key[0], loop=True)
                            print("afterplay")
    
            if mixer.playing:
                for i in range(0,7):
                    if i not in pressed_key_xs:
                        print("stopping %s" % i)
                        mixer.stop_voice(i)
    
        prev_pressed = cur_keys
    

    There we have it, polyphonic playback at a good volume. It seems lots of the clicking and other sounds have been minimized as well with this version of the program. There is a bit of a white noise hum in the background of everything but much better than some of the previous iterations. 

    The neotrellis library makes it so easy to set the on-board pixels so why not make a nice rainbow across the buttons?

    The wave file approach is kind of nice because we could probably switch out the tone files for other instruments. This will let us change the way the toy sounds without having to re-write the code, just swap out wave files. 

  • Making waves

    foamyguy01/26/2020 at 22:53 0 comments

    So it seems like using the Mixer object along with wave files should let us play the tones simultaneously. Now we need to get some musical note wave files, how can we do that?

    I heard you like python so I made some wave files with python to play with circuitpython.

    As I was on the search for wave files of tones I found myself wondering if we could use cpython to generate wave files. And of course we can! The code in this stack overflow answer gets us most of the way there. We can create some loops to generate each tone for us and save it into a wave file, perfect! I've got it set up to output 3 full octaves, but no sharps/flats currently.

    #!/usr/bin/python
    # based on : www.daniweb.com/code/snippet263775.html
    # Adapted from sample code in Stack Overflow answer: https://stackoverflow.com/a/33913403/507810
    import math
    import wave
    import struct
    
    # Audio will contain a long list of samples (i.e. floating point numbers describing the
    # waveform).  If you were working with a very long sound you'd want to stream this to
    # disk instead of buffering it all in memory list this.  But most sounds will fit in
    # memory.
    audio = []
    sample_rate = 8000.0 # 8k sample rate for "low quality".
                         # 44.1 kHz sample rate for "HD"
    
    TONE_FREQ = {
        'C3': 131, 'D3': 147, 'E3': 165, 'F3': 175, 'G3': 196, 'A3': 220, 'B3': 247,
        'C4': 262, 'D4': 294, 'E4': 330, 'F4': 349, 'G4': 392, 'A4': 440, 'B4': 494,
        'C5': 523, 'D5': 587, 'E5': 659, 'F5': 698, 'G5': 784, 'A5': 880, 'B5': 988
    }
    
    
    def append_silence(duration_milliseconds=500):
        """
        Adding silence is easy - we add zeros to the end of our array
        """
        num_samples = duration_milliseconds * (sample_rate / 1000.0)
    
        for x in range(int(num_samples)):
            audio.append(0.0)
    
        return
    
    
    def append_sinewave(
            freq=440.0,
            duration_milliseconds=500,
            volume=1.0):
        """
        The sine wave generated here is the standard beep.  If you want something
        more aggresive you could try a square or saw tooth waveform.   Though there
        are some rather complicated issues with making high quality square and
        sawtooth waves...
        """
    
        global audio  # using global variables isn't cool.
    
        num_samples = duration_milliseconds * (sample_rate / 1000.0)
    
        for x in range(int(num_samples)):
            audio.append(volume * math.sin(2 * math.pi * freq * (x / sample_rate)))
    
        return
    
    
    def save_wav(file_name):
        # Open up a wav file
        wav_file = wave.open(file_name, "w")
    
        # wav params
        nchannels = 2
    
        sampwidth = 2
    
        # 44100 is the industry standard sample rate - CD quality.  If you need to
        # save on file size you can adjust it downwards. The stanard for low quality
        # is 8000 or 8kHz.
        nframes = len(audio)
        comptype = "NONE"
        compname = "not compressed"
        wav_file.setparams((nchannels, sampwidth, sample_rate, nframes, comptype, compname))
    
        # WAV files here are using short, 16 bit, signed integers for the
        # sample size.  So we multiply the floating point data we have by 32767, the
        # maximum value for a short integer.  NOTE: It is theortically possible to
        # use the floating point -1.0 to 1.0 data directly in a WAV file but not
        # obvious how to do that using the wave module in python.
        for sample in audio:
            wav_file.writeframes(struct.pack('h', int(sample * 32767.0)))
    
        wav_file.close()
    
        return
    
    
    for note in TONE_FREQ.keys():
        append_sinewave(freq=TONE_FREQ[note], volume=0.01)
        # append_silence(90)
        save_wav("%s.wav" % note)
        audio = []
        print("after save %s" % note)
    

  • Level up - Mixer with wav files brings polyphonic playback

    foamyguy01/26/2020 at 20:06 0 comments

    I played with for a while with AudioOut trying to get multiple sounds playing at once. Eventually I found the Mixer object. This seemed like just the thing I needed to get it working. The sample code uses wav files, but I was able to modify it to work with the RawSample objects from before. It took a fair bit of tinkering but I did get it working and had the correct notes getting played when the correct buttons were pressed. However polyphonic playback still eluded me. When I attempted to play two samples at the same time on different voices the audio went quiet, only returning when I dropped back to playing a single sample only. There was also some extra clicking and other errant sounds that can be heard that ideally we'll want to minimize. 

    Not dissuaded by the first failed attempt I decided to take a short field trip through wav file land. After all the mixer example code is using wav files, not RawSamples, perhaps that is part of my issue. This led me to the ABC sound board learn guide which loads up tons of wav files and plays them back when buttons are pressed. I was able to modify this example to use the Mixer object and to my delight I was eventually able to get it to play multiple wav files on top of each other. Armed with this knowledge and modified sample code I headed back to switch the basic synth program to use pre-created wav files instead of RawSamples generated by CircuitPython code. 

  • Level up - AudioOut brings Volume and Stereo

    foamyguy01/26/2020 at 19:07 0 comments

    The next thing I found was this learn guide for AudioOut. This is also playing tones, but it's a bit of a different approach. Instead wobbling the pin directly to make auditory pixies, it looks like this is creating a RawSample object ahead of time and then playing it back later using the AudioOut objects play function. This one also is creating sine waves instead of square waves like before. We'll file that away for later when we try to figure out how to make other patters like triangle and sawtooth. Another neat thing going on with this example is that it is controlling the volume, there is a variable tone_volume that we can change to make the beeps and boops louder and softer. I don't quite grok how it works, but it seems this variable is being used when it generates the raw wave data that is getting turned into a RawSample for us to play back later. 

    At this point I re-worked my previous super basic synth program to use AudioOut. I created a list to store my RawSample objects and created enough of them to play one full octave. Once the modifications were everything from the users perspective worked pretty much the same as before, but now at a volume comfortable enough to play with the earbuds in.

    The next thing I tried to tackle was stereo playback. So far we're only playing audio out of one channel of the available two in the neotrellis' 3.5mm jack backed by dual DAC's in the SAMD51. Poking around a bit online I found myself at the docs page for AudioOut. I probably should have started looking there sooner, because it has the exact thing we need to enable stereo! In the constructor we can pass left_channel and right_channel pins. A quick tweak to the code and sure enough now we've got sound in both ears

  • My adventure began here

    foamyguy01/26/2020 at 18:50 0 comments

    Once I sat down to try making my own music toy the first place thing I started trying was using pulsio to play tones as shown in this piezo buzzer learn guide. This is pretty fun, we can edit the frequencies to play different notes, and change sleep times to play faster or slower. The next logical step was to hook up the neotrellis buttons to play some different notes when they are pressed. This was pretty easy due to the neotrellism4 library doing most the heavy lifting and giving us back a nice list of buttons currently pressed.

    At this point we've got a super basic little synth with one octave of square wave notes. Pretty darn cool, but is room for improvement. We buzzing a single tone at a time we can't play multiples yet. The volume is pretty loud, up to this point I've been keeping the earbuds out of my ears. We're also only playing out of one of the two audio channels coming out of the 3.5mm audio port on the neotrellis. 

View all 8 project logs

Enjoy this project?

Share

Discussions

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates