I need to have my program play wav files from a directory and skip to the next one if the user presses F9. I have found I need to set the winsound to Async mode to get it to skip to the next file while the previous file is still playing, but at the same time i need to run a background process like time.sleep(5) or else the program will exit immediately.
I have a for loop to run through the wav files, but I can't directly call continue from the on_press() function so I tried using a conditional global variable to sleep or not sleep. Thus, I can stop the current playing wav file with PlaySound(None, winsound.Purge) but I can't continue to the next iteration of the for loop or get my time.sleep(5) to conditionally run - it will always run and I will always have to wait those 5 seconds to start the next iteration. Is there a problem with the way I'm using the callback or should I use a different input lib or something?
from pynput import keyboard
import winsound
import os
mySkip = 0
currentIdx = 0
def clamp(n, minn, maxn):
return max(min(maxn, n), minn)
def on_press(key):
global mySkip
global currentIdx
if key == keyboard.Key.f9:
winsound.Beep(5500, 200)
mySkip = 1
currentIdx = clamp(currentIdx + 1, 0, 3)
winsound.PlaySound(None, winsound.SND_PURGE)
listener = keyboard.Listener(on_press=on_press)
listener.start()
WAVDir = "C:/Users/me/PycharmProjects/projectName/WAV"
wavList = os.listdir(WAVDir) #have 4 files in here, "test0.wav" to "test4.wav"
for idx, i in enumerate(wavList):
soundPath = WAVDir + "/test" + str(currentIdx) + ".wav"
print(soundPath)
winsound.PlaySound(soundPath, winsound.SND_ASYNC)
if mySkip == 0:
time.sleep(5)
currentIdx += 1
mySkip = 0
Here's a solution I came up with using polling - which is never my favorite thing to do, but winsound
is notoriously uncooperative when it comes to threading, and I believe that threading is the way to go here:
def main():
from pathlib import Path
from threading import Event, Thread
from pynput import keyboard
from collections import namedtuple
import wave
skip_sound = Event()
def play_sound_thread(path, max_duration_seconds):
from winsound import PlaySound, SND_ASYNC
PlaySound(path, SND_ASYNC)
elapsed_seconds = 0
poll_interval = 1 / 8
while not skip_sound.is_set() and elapsed_seconds < max_duration_seconds:
skip_sound.wait(poll_interval)
elapsed_seconds += poll_interval
def on_press(key):
if key is keyboard.Key.f9:
nonlocal skip_sound
skip_sound.set()
Wave = namedtuple("Wave", ["path", "file_name", "duration_seconds"])
waves = []
wav_dir = Path("./wavs")
for wav_path in wav_dir.glob("*.wav"):
wav_path_as_str = str(wav_path)
file_name = wav_path.name
with wave.open(wav_path_as_str, "rb") as wave_read:
sample_rate = wave_read.getframerate()
number_of_samples = wave_read.getnframes()
duration_seconds = number_of_samples / sample_rate
wav = Wave(
path=wav_path_as_str,
file_name=file_name,
duration_seconds=duration_seconds
)
waves.append(wav)
listener = keyboard.Listener(on_press=on_press)
listener.start()
for wav in waves:
print(f"Playing {wav.file_name}")
thread = Thread(
target=play_sound_thread,
args=(wav.path, wav.duration_seconds),
daemon=True
)
thread.start()
thread.join()
if skip_sound.is_set():
print(f"Skipping {wav.file_name}")
skip_sound.clear()
return 0
if __name__ == "__main__":
import sys
sys.exit(main())
How it works:
Every time a sound needs to be played, a new thread is spawned and the winsound.PlaySound
stuff is done in the thread. PlaySound
receives the SND_ASYNC
argument, so that the sound is played asynchronously, and this operation by itself does not block execution in the thread. The thread's execution is then blocked, however, by a loop, which terminates when either a certain amount of time has elapsed (the sound has had enough time to finish playing) or when the skip_sound
event is set.
The on_press
callback sets the skip_sound
event when the f9 key is pressed.
I've created a namedtuple
called Wave
, which is just meant to act like a simple data bucket. All the relevant information we need for any single wave file is contained in one of these.
The main for-loop (the second one, the first one just compiles a list of Wave
objects), we iterate through our Wave
objects. We then spawn a new thread for the current Wave
object. We then start the thread to start playing the sound, and thread.join
it so that the main thread waits for this sub-thread to finish - basically blocking until the sub-thread is finished. When the on_press
callback is triggered, it terminates the sub-thread prematurely, and as a result the join
operation will no longer be blocking, allowing us to reset our skip_sound
event if necessary and move on to the next iteration.
Things I like about this solution:
Things I don't like about this solution:
play_sound_thread
function plays the sound
asynchronously, and then polls until either the skip_sound
event is
triggered, or a certain amount of time has elapsed. Polling is gross. I just sort of arbitrarily decided to poll in 1/8th of a second intervals.wave.open
, and again in winsound.PlaySound
.