Buttons

Last time, we got a continous sine wave running. But that does hurt your ears on repeat quite a bit, and plus – don’t you use buttons on a drum machine? That’s what I’ll try to implement in this post: a button-triggered sound.

Detecting buttons

This is a fairly simple process on the software side – on the other hand, it was a bit complicated for to set the electronics up for me.

After about half an hour of testing and a lot of confusion, I’ve finally figured out how to wire a switch into a GPIO:

  1. Place the button across the “trench” of the breadboard (we’ll call each side across the breadboard sides A and B).
  2. Connect the 3.3V wire to side A.
  3. Connect your GPIO wire to side B – but: they have to be diagonally across from eachother. This is because sides A and B are directly connected across.
  4. On the same breadboard row as the GPIO, connect a 10kΩ resistor to GND. It turns out that connecting 3.3V directly to GND causes a short.

The software part is fairly easy though. We can define a pin as an input pin, set the pull-up/-down resistor and then poll it (or add an interrupt, but I’ll get to that in a moment).

let button = 
    esp_hal::gpio::Input::new(
    // not necessarily 17, just what I'm using here
    peripherals.GPIO17, 
    esp_hal::gpio::Input::Config::default().with_pull(esp_hal::gpio::Pull::Down)
);

Notice the .with_pull(...) line? That instructs the ESP32 to add a pull-down resistor to the GPIO. Omitting this line would cause it to “float” when the button isn’t pressed, which means that we would detect random triggers or discharges, such as fingers touching the GPIO.

Now, we have to detect button presses. Inside of the loop, we just poll this button and wait for it to be pressed.

loop {
    if button.is_high() {
        log::info!("Button pressed");
    }
}

This setup, however, presents itself with a problem. If you don’t press the button for an extremely short amount of time, the console will be flooded with this statement (which is not how a drum works, by the way. Hitting a drum and then holding the stick on it does not make the sound last that long). So we have to check whether the button has been pressed and whether it is being pressed.

The best way to do this is to introduce a Button struct, which keeps track of itself being pressed:

pub struct Button<'d> {
    button: esp_hal::gpio::Input<'d>,
    was_pressed: bool,
}

impl<'d> Button<'d> {
    pub const fn new(button: esp_hal::gpio::Input<'d>) -> Self {
        Self { button, was_pressed: false }
    }

    pub fn is_pressed(&mut self) -> bool {
        let is_high = self.button.is_high();
        self.was_pressed = is_high;
        is_high && !was_pressed
    }
}

So if we go back and update our code:

// --snip--
let button = Button::new(
    esp_hal::gpio::Input::new(
        peripherals.GPIO17,
        esp_hal::gpio::Input::Config::default()
            .with_pull(esp_hal::gpio::Pull::Down)
    )
);

loop {
    if button.is_pressed() {
        log::info!("Button pressed");
    }
}

This should now reliably only print “Button pressed” once when a button is pressed, even when held.

So let’s integrate this into our audio system. We can start by only playing a 440Hz sine wave for 1.25 seconds after the button is pressed.

// --snip--
let mut button = Button::new(peripherals.GPIO17);
let mut frame_ctr = (1.25 * 44100.0) as usize;

loop {
    if button.is_pressed() {
        frame_ctr = 0;
    }

    _ = transfer.push_with(|buf| {
        for frame in 0..buf.len() / 4 {
            let base = frame * 4;
            // 1.25 * 44100.0 is 1.25 seconds since 44100 samples per second
            let sample = if frame_ctr < (1.25 * 44100.0) as usize {
                ((i16::MAX / 2) as f32
                    // libm because no f32::sin without std
                    * libm::sinf(
                        2.0 * core::f32::consts::PI * 440.0 * (frame_ctr as f32 / 44100.0),
                    )) as i16
            } else {
                0
            };

            let bytes = sample.to_le_bytes();

            buf[base] = bytes[0];
            buf[base + 1] = bytes[1];
            buf[base + 2] = bytes[0];
            buf[base + 3] = bytes[1];

            frame_ctr = frame_ctr.wrapping_add(1);
        }
        buf.len()
    });
}