'Record audio while holding a button, replay it on release and overwrite the previous file

I want to record audio while I'm pressing a button and replay it when I release the button.

It works fine for the first time, but when I record one more time, it appends the new record to the existing one. I can't figure out how to replace the file instead of appending to it. I've tried to delete it when I start to record, but it can't create a new one. Basically, I'd like to overwrite the file, instead of appending the sound to it.

from pynput import keyboard
import time
import pyaudio
import wave
import sched
from pygame import mixer

CHUNK = 8192
FORMAT = pyaudio.paInt16
CHANNELS = 2
RATE = 44100
RECORD_SECONDS = 5
WAVE_OUTPUT_FILENAME = "output.wav"

p = pyaudio.PyAudio()
frames = []


def callback(in_data, frame_count, time_info, status):
    frames.append(in_data)
    return in_data, pyaudio.paContinue


class MyListener(keyboard.Listener):
    def __init__(self):
        super(MyListener, self).__init__(self.on_press, self.on_release)
        self.key_pressed = None
        self.wf = wave.open(WAVE_OUTPUT_FILENAME, 'wb')
        self.wf.setnchannels(CHANNELS)
        self.wf.setsampwidth(p.get_sample_size(FORMAT))
        self.wf.setframerate(RATE)

    def on_press(self, key):
        if key.char == 'r':
            self.key_pressed = True
        return True

    def on_release(self, key):
        if key.char == 'r':
            self.key_pressed = False
        return True


listener = MyListener()
listener.start()
started = False
stream = None


def recorder():
    global started, p, stream, frames

    if listener.key_pressed and not started:
        # Start the recording
        try:
            stream = p.open(format=FORMAT,
                            channels=CHANNELS,
                            rate=RATE,
                            input=True,
                            frames_per_buffer=CHUNK,
                            stream_callback=callback)
            print("Stream active:", stream.is_active())
            started = True
            print("start Stream")
        except:
            raise

    elif not listener.key_pressed and started:
        try:
            started = False
            print("Stop recording")
            stream.stop_stream()
            stream.close()
            listener.wf.writeframes(b''.join(frames))

            mixer.init()
            mixer.music.load("output.wav")
            mixer.music.set_volume(0.8)
            mixer.music.play()
        except:
            raise

    # Reschedule the recorder function in 100 ms.
    task.enter(0.1, 1, recorder, ())


print("Press and hold the 'r' key to begin recording")
print("Release the 'r' key to end recording")
task = sched.scheduler(time.time, time.sleep)
task.enter(0.1, 1, recorder, ())
task.run()


Solution 1:[1]

audiomath provides some high-level classes that can help with tasks like this. Like pyaudio, it also wraps around PortAudio to allow recording and playback. Below is a shorter audiomath-based listing.

I'm guessing you probably don't sit with your finger on the 'r' button for longer than a few seconds. In that case, it's more efficient to pre-allocate several seconds of space in memory (provided it's more than enough) and record into/play back from that. For the sound you want to keep, you can always say player.sound.Write('output.wav') at the end. Or you can uncomment the line that saves every attempt.

If for any reason you definitely do want to stream open-endedly to a file while recording, you can supply a filename in the constructor and that will get passed to the Recorder instance (the memory-resident self.buffer still exists: it's a circular buffer retaining the last N seconds). Streaming to file requires an external ffmpeg installation, though. Note that audiomath's playback loads the entire thing back into memory, so nothing has really been made more memory-efficient overall...

Because of the use of ffmpeg, you can also stream directly to other audio formats, other than .wav if you want.

import time
import pynput
import audiomath as am  

class MyListener(pynput.keyboard.Listener):
    def __init__(self, filename=None, buffer_length_seconds=10):
        """
        Note:
        
        If you specify a long enough `buffer_length_seconds` for your
        purposes, then you do not need to record to file and so do not
        need to specify `filename`.
        
        If you specify `filename`, you need to have installed `ffmpeg`
        (see the help for `audiomath.ffmeg.Install`).
        """
        super(MyListener, self).__init__(self.on_press, self.on_release)
        self.key_pressed = None
        
        self.queue = []
        self.filename = filename
        self.buffer_length_seconds = buffer_length_seconds
        self.new_recorder()
        
    def new_recorder( self ):
        self.buffer = am.Sound(self.buffer_length_seconds, nChannels=2, fs=44100)
        self.recorder = am.Recorder(self.buffer, loop=True, recording=False, filename=self.filename)
        
    def on_press(self, key):
        try: key.char
        except: return
        if key.char == 'r' and not self.key_pressed:
            print('recording...')
            self.recorder.Record()
            self.key_pressed = True
        return True

    def on_release(self, key):
        try: key.char
        except: return
        if key.char == 'r' and self.key_pressed:
            print('finished')
            self.recorder.Stop()
            self.recorder = None # seems to be necessary when filename is used, to ensure the ffmpeg process and file are closed and cleaned up
            if self.filename:
                player = am.Player( self.filename )
            else:
                player = am.Player( self.buffer )
                # self.buffer.Write( 'output.wav')  # if you want to ensure every attempt gets saved. File will be overwritten.
            self.queue.append( player )
            self.new_recorder()
            self.key_pressed = False
        return True

#with MyListener('output.wav') as listener:  # this variant uses a file (and needs ffmpeg)
with MyListener() as listener:               # this variant just records to memory
    while True:
        if listener.queue:
            player = listener.queue.pop( 0 )
            player.Play()
        time.sleep(0.1)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1