0

I am trying to write a python script which displays a macOS alert and starts an alarm at the same time.

The alarm sound should be stopped after the alert is closed, however it isn't.

def show_alert(message="Flashlight alarm"):
    """Display a macOS dialog."""
    message = json.dumps(str(message))
    exit_status = os.system("osascript dialog.scpt {0}".format(message))
    return exist_status

def play_alarm(file_name = "beep.wav", repeat=3):
    """Repeat the sound specified to mimic an alarm."""
    process = subprocess.Popen(['sh', '-c', 'while :; do afplay "$1"; done', '_', file_name], shell=False)
    return process

def alert_after_timeout(timeout, message, sound = True):
    """After timeout seconds, show an alert and play the alarm sound."""
    time.sleep(timeout)
    process = None
    if sound:
        process = play_alarm()
    exit_status = show_alert(message)
    if process is not None:
        os.killpg(os.getpgid(process.pid), signal.SIGINT)
        process.kill()
    # also, this below line doesn't seem to open an alert.
    show_alert(exit_status)

alert_after_timeout(1, "1s alarm")

The above code should display a macOS alert after starting to loop an alarm sound (in the file beep.wav). When the alert is closed, the alarm sound should instantly stop.

The AppleScript file dialog.scpt triggers the alert, it is only a few lines long:

on run argv
  tell application "System Events" to display dialog (item 1 of argv) with icon file (path of container of (path to me) & "Icon.png")
end run
theonlygusti
  • 11,032
  • 11
  • 64
  • 119
  • `if process is not None:` => `if process:`. Don't compare with `is`. – Jean-François Fabre Dec 28 '16 at 16:35
  • I'd rather create a thread to play the sound instead of using `subprocess` with a loop in the shell to run in the background... sounds fragile to me. – Jean-François Fabre Dec 28 '16 at 16:39
  • @Jean-FrançoisFabre I just implemented that suggestion (`if process:`), but the script still fails in the same way. – theonlygusti Dec 28 '16 at 16:39
  • @Jean-FrançoisFabre if you show me how to accomplish what I'm after using multi-threading I'd be so grateful! I'd love to be able to do it that way, but don't feel I currently have the expertise to do it myself. I spent about 6 hours yesterday trying to find out how to play a sound from pure python. – theonlygusti Dec 28 '16 at 16:40
  • I would just try `os.kill(process.pid,signal.SIGTERM)` and remove `os.killpg` call. I cannot test, I'm using windows – Jean-François Fabre Dec 28 '16 at 16:43
  • @Jean-FrançoisFabre that doesn't work either. – theonlygusti Dec 28 '16 at 16:46

1 Answers1

1

I admit I don't know why you cannot kill your process running in a shell, using subprocess to mimic running as background..., and the fact that no other command runs after that means that there's probably a deadlock somewhere. So let's drop that solution.

Let me propose a more pythonic solution. The audio play part was adapted from how to play wav file in python? but now plays in a loop and works with python 3 as well.

The idea is to start a thread that plays a sound in a loop using only python modules. The thread is aware of a global variable. If the stop_audio variable is set, then the thread knows it has to quit the infinite loop and stop playing.

you control the flag from the other procedure. Once the message has been clicked on, set the flag, audio stops playing immediately.

import pyaudio
import wave
import threading

# global variable used to gently tell the thread to stop playing
stop_audio = False

def show_alert(message="Flashlight alarm"):
    """Display a macOS dialog."""
    message = json.dumps(str(message))
    exit_status = os.system("osascript dialog.scpt {0}".format(message))
    return exit_status

# initialize audio

def play_alarm(file_name = "beep.wav"):
    #define stream chunk
    chunk = 1024

    #open a wav format music
    f = wave.open(file_name,"rb")

    #instantiate PyAudio
    p = pyaudio.PyAudio()
    #open stream
    stream = p.open(format = p.get_format_from_width(f.getsampwidth()),
                    channels = f.getnchannels(),
                    rate = f.getframerate(),
                    output = True)

    while not stop_audio:
        f.rewind()
        #read data
        data = f.readframes(chunk)

        #play stream
        while data and not stop_audio:
            stream.write(data)
            data = f.readframes(chunk)

    #stop stream
    stream.stop_stream()
    stream.close()

    #close PyAudio
    p.terminate()


def alert_after_timeout(timeout, message, sound = True):
    """After timeout seconds, show an alert and play the alarm sound."""
    time.sleep(timeout)
    process = None
    if sound:
       t = threading.Thread(target=play_alarm,args=("beep.wav",))
       t.start()
    exit_status = show_alert(message)

    global stop_sound
    if sound:
        stop_sound = True  # tell the thread to exit
        t.join()

    show_alert(exit_status)

alert_after_timeout(1, "1s alarm")

Note that I've dropped the repeat=3 parameter as it wasn't used and I made no sense of it.

An alternative without using pyaudio would be to call the external player in a loop, replace play_alarm above by this:

def play_alarm(file_name = "beep.wav"):
    global stop_sound
    while not stop_sound:
        subprocess.call(["afplay",file_name])

when stop_sound is True, the sound keeps on playing till the end, but doesn't resume. So the effect is not instantaneous, but it's simple.

And another alternative to cut the sound in a more reactive way:

def play_alarm(file_name = "beep.wav"):
    global stop_sound
    while not stop_sound:
        process = subprocess.Popen(["afplay",file_name])
        while not stop_sound:
           if process.poll() is not None:
               break  # process has ended
           time.sleep(0.1)  # wait 0.1s before testing process & stop_sound flag
        if stop_sound:
           process.kill()  # kill if exit by stop_sound
Community
  • 1
  • 1
Jean-François Fabre
  • 137,073
  • 23
  • 153
  • 219