1#!/usr/bin/env python3 2#-*- coding:utf-8 -*- 3 4import datetime 5import json 6import subprocess 7 8import alsaaudio 9from acrcloud.recognizer import ACRCloudRecognizer, ACRCloudRecognizeType 10 11 12class Song: 13 def __init__(self, data: dict): 14 self._data = data 15 16 def __eq__(self, other: "Song") -> bool: 17 return isinstance(other, Song) and self.yt_id == other.yt_id and abs(self.offset - other.offset) < 30 18 19 def __ne__(self, other: "Song") -> bool: 20 return not self == other 21 22 @property 23 def offset(self) -> int: 24 return int(self._data["play_offset_ms"] / 1000) 25 26 @property 27 def artist(self) -> str: 28 return self._data["artists"][0]["name"] 29 30 @property 31 def title(self) -> str: 32 return self._data["title"] 33 34 @property 35 def album(self) -> str: 36 return self._data["album"]["name"] 37 38 @property 39 def yt_id(self) -> str: 40 if "youtube" not in self._data["external_metadata"]: 41 return None 42 return self._data["external_metadata"]["youtube"]["vid"] 43 44 45def record(secs: int): 46 inp = alsaaudio.PCM(alsaaudio.PCM_CAPTURE, alsaaudio.PCM_NORMAL, device="default") 47 48 # Set attributes: Mono, 44100 Hz, 16 bit little endian samples 49 inp.setchannels(1) 50 inp.setrate(44100) 51 inp.setformat(alsaaudio.PCM_FORMAT_S16_LE) 52 53 # The period size controls the internal number of frames per period. 54 # The significance of this parameter is documented in the ALSA api. 55 # For our purposes, it is suficcient to know that reads from the device 56 # will return this many frames. Each frame being 2 bytes long. 57 # This means that the reads below will return either 320 bytes of data 58 # or 0 bytes of data. The latter is possible because we are in nonblocking 59 # mode. 60 inp.setperiodsize(160) 61 62 # The wav header. 63 buf = bytearray( 64 b'RIFF$\xc7\x06\x00WAVEfmt \x10\x00\x00\x00\x01\x00\x01\x00D\xac\x00\x00\x88X\x01\x00\x02\x00\x10\x00data\x00\xc7\x06\x00' 65 ) 66 for x in range(int(secs * (1000 / 3.6))): 67 l, data = inp.read() 68 buf.extend(data) 69 return bytes(buf) 70 71 72def recognize(buf: bytes): 73 config = { 74 'host': 'identify-eu-west-1.acrcloud.com', 75 'access_key': 'key', 76 'access_secret': 'secret', 77 'recognize_type': ACRCloudRecognizeType.ACR_OPT_REC_AUDIO, 78 'debug': False, 79 'timeout': 10, 80 } 81 re = ACRCloudRecognizer(config) 82 return (json.loads(re.recognize_by_filebuffer(buf, 0, 10))) 83 84 85def stop(): 86 subprocess.call("killall mpv", shell=True) 87 88 89def play(yt_id: str, time: int): 90 subprocess.Popen(["mpv", "--no-audio", "https://www.youtube.com/watch?v=%s" % yt_id, "--start", str(time)]) 91 92 93if __name__ == "__main__": 94 last_song = None 95 duration = 8 96 while True: 97 audio = record(duration) 98 data = recognize(audio) 99100 if "metadata" not in data or Song(data["metadata"]["music"][0]).yt_id is None:101 print("Song not recognized.")102 stop()103 last_song = None104 else:105 song = Song(data["metadata"]["music"][0])106107 print(108 "Song is %s - %s from %s at %s." %109 (song.artist, song.title, song.album, datetime.timedelta(seconds=song.offset))110 )111112 if song != last_song:113 print("Song changed, playing...")114 stop()115 play(song.yt_id, song.offset + duration)116117 last_song = song