Pastery

syncognizer.py +

  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
New paste