RetroMatic 2000 update 15: sound effects

It’s time to return to my Gotek USB floppy disk emulator, and see if I can add some authentic-sounding sound effects.

I’ve previously written about my plans to interface to the Gotek hardware.  In that post I linked to solutions other people have already used for sound effects.

They generally involve a pure hardware solution that generates a ‘click’ on a speaker every time the drive receives a ‘head step’ signal (to simulate the sound of the stepper motor).  But that means the sound effect happens if any drive attached to that cable is active, not just the Gotek drive.

I could build a circuit in hardware to make a NOR gate (activate the click if neither the drive select NOR the head step signal are high, indicating that both are active).  But I have an Arduino sitting on the board, so I might as well feed the signals into the processor, and then handle the logic in software.  That also allows me to do more advanced sounds if I want.

I started by connecting the drive-select, motor, and head-step pins from the Gotek to the Arduino, and adding a piezo-electric speaker to a pin on the Arduino.  For basic tones I can connect the speaker to any output pin, but for more advanced sounds I need to use a PWM pin.

For head-step sounds, I found the most realistic sound was a short, low-pitched, buzzing square-wave tone:

#define PIN_SPEAKER           6
#define HEAD_STEP_FREQUENCY   50
#define HEAD_STEP_LENGTH      100

void headStepSound()
{
    tone(PIN_SPEAKER, HEAD_STEP_FREQUENCY, HEAD_STEP_LENGTH);
}

For motor sound effects, I experimented with random white noise.  It requires handleSounds() to be called from the main loop().  This was the best result I got:

#define PIN_SPEAKER                    6        // needs to be PWM
#define MOTOR_SOUND_FREQUENCY_MICROS   1000
#define MOTOR_SOUND_AMPLITUDE          2

bool motorSoundPlaying = false;

void startMotor()
{
    motorSoundPlaying = true;
}

void stopMotor()
{
    motorSoundPlaying = false;
}

void handleSounds()
{
    static unsigned long lastClick = 0;
    
    if (motorSoundPlaying)
    {
        unsigned long now = micros();
        
        if (lastClick > now)
        {
            // clock wrapped, reset it
            lastClick = now;
        }
        
        if (now > lastClick + MOTOR_SOUND_FREQUENCY_MICROS)
        {
            lastClick = now;
            analogWrite(SPEAKER_PIN, random(MOTOR_SOUND_AMPLITUDE));
        }
    }
}

Although acceptable, it’s a rather rough ‘whiny’ noise, and I think it could quickly become irritating!  For now I’ve turned it off ;-)

To activate the head-step sounds, I need to poll the pins connected in my main loop(), and activate the sounds appropriately.  But I found I was missing head-step events, and also I was sometimes picking up head-step events intending for another drive on the cable (presumably the drive-select signal had changed by the time I’d polled the head-step signal).

So for maximum speed I needed to abandon the Arduino digitalRead() functions, and drop down to direct port access.  (I already do this in my interrupt handlers for the rotary encoder.)  This also has the advantage that I can read both signals in one go (provided the pins are on the same port) so I can eliminate events destined for a different drive.

Here’s my code:

#define DRIVE_GET_PINS  (PIND & 0b11)  // pins 0 and 1 are bits 0 and 1 of PIND

void handleSounds()
{
    // direct memory access for speed and to guarantee both read simultaneously
    byte driveState = DRIVE_GET_PINS;
    static byte lastState = 0;

    // bit 1 is drive select, bit 0 is head-step, low (zero) indicates active
    // make a sound if this drive is starting (or continuing) a head-step (00)
    // also make a sound if this drive has just stopped a head-step (00->01)
    if (driveState == 0b00 || (driveState == 0b01 && lastState == 0b00))
    {
        headStepSound();
    }
    
    lastState = driveState;
}

Currently I poll for the signals by calling handleSounds() in my main loop().  That works provided I don’t do too much other processing in the loop (and since the Arduino is just doing UI, it’s unlikely that much UI will be happening during the disk access).

At some point I might move the head-step polling into an interrupt handler.  But that’s for another day, as it would require some re-factoring of the rotary encoder interrupt handler code.

Here’s a video of me testing it with my Video Genie II (you might have to turn up your playback volume to hear it – it’s loud enough in real life though!)