#!/usr/bin/python3 """Gradual alarm clock, without daylight savings time support. This alarm wakes you up gradually, ramping up the volume from 0% to 103% over the course of several minutes, then up to 130% after that. It spawns off mpv to actually play the music as many times as is necessary to wake you up. Quitting mpv will not stop the alarm; you will have to kill it with ^C in the window where you launched it or something similar. """ import argparse import contextlib import os import subprocess import socket import sys import time # I was going to port this to Python 2, but the lack of things like # FileExistsError made me decide it wasn’t worth it. def parse_args(): formatter = argparse.RawDescriptionHelpFormatter p = argparse.ArgumentParser(description=__doc__, formatter_class=formatter) p.add_argument('-d', '--duration', type=float, default=10*60, help='duration of gradual wakeup in seconds (default %(default)s)') p.add_argument('-m', '--max-post-time', type=float, default=10*60, help=('maximum time to keep playing at full volume or above ' + 'before giving up (default %(default)s)')) default_time = '10:00' p.add_argument('-t', '--time', type=time_of_day, default=time_of_day(default_time), help='time of day for the alarm (default {})'.format(default_time)) p.add_argument('music', nargs='+', type=song_filename, help='filenames of music to play for the alarm') return p.parse_args() def song_filename(s): if not os.path.exists(s): raise ValueError(s) return s def time_of_day(s): hh, mm = map(int, s.split(':')) if not 0 <= hh <= 23 or not 0 <= mm <= 59: raise ValueError(s) return (hh * 60 + mm) * 60 def wait_until(desired_tod): now = time.localtime(time.time()) tod = (now.tm_hour * 60 + now.tm_min) * 60 + now.tm_sec delay = desired_tod - tod if -660 < delay <= 0: # XXX this will screw up on times just after midnight return # XXX note that this is the wrong thing in time zones with # daylight savings time delay %= 86400 print('Alarm will begin sounding in {}h{}m'.format(int(delay // 3600), int(delay // 60 % 60))) time.sleep(delay) def play_music(music, quit, volume_func, sockname): kid = subprocess.Popen(['mpv', '--volume=0', '--input-ipc-server={}'.format(sockname)] + music) cmd = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) while True: try: cmd.connect(sockname) break except (ConnectionRefusedError, FileNotFoundError): time.sleep(0.2) def do(line): cmd.send((line + '\n').encode('utf-8')) do('set volume 0') while True: if kid.poll() is None: if quit(): # Time to quit! do('quit') # Hope this works! sys.exit(0) # We used to do a whole dance with the `volume` audio # filter here, sending `af set volume=10` and the like to # raise the volume by 10dB, with a limit of 60. More # recent versions of mpv (I’m using 0.35.1) have changed # the interface in two ways: # # 1. The volume setting is now, like, a multiplier applied # to the standard volume, or something? Instead of # decibels. DANGEROUS! Fortunately I didn’t blow any # speakers this time. # # 2. The normal volume now goes up to 130, which is # probably plenty to wake me up. vol = min(130, volume_func()) # Sending on a closed socket raises BrokenPipeError. with contextlib.suppress(BrokenPipeError): do('set volume {}'.format(vol)) time.sleep(0.2) else: with contextlib.suppress(BrokenPipeError): cmd.close() break def main(): args = parse_args() subprocess.check_output(['mpv', '-help']) # Verify mpv is installed wait_until(args.time - args.duration) start = time.time() vol = lambda: round((time.time() - start)/args.duration * 100) quit = lambda: time.time() - start > args.max_post_time + args.duration try: while True: play_music(args.music, quit, vol, 'tmp.sock') finally: os.system('stty sane') os.unlink('tmp.sock') if __name__ == '__main__': main()