from typing import Optional, Self, Type, cast from PyQt6.QtCore import ( QBuffer, QByteArray, QCryptographicHash, QDir, QObject, QStandardPaths, QUrl, pyqtSlot, ) from PyQt6.QtMultimedia import ( QAudioOutput, QMediaDevices, QMediaPlayer, QSoundEffect, ) from PyQt6.QtNetwork import ( QNetworkAccessManager, QNetworkDiskCache, QNetworkReply, QNetworkRequest, ) # from PyQt6.QtWidgets import QWidget class SoundOff(QObject): _instance = None def __new__(cls: Type[Self]) -> Self: if cls._instance: return cls._instance cls._instance = super(SoundOff, cls).__new__(cls) # # Setup devices # cls.virtualDevice = None for output in QMediaDevices.audioOutputs(): if output.id().data().decode("utf-8") == "virt-input": cls.virtualDevice = output if output.isDefault(): cls.localDevice = output cls.alertEffect = QSoundEffect() cls.alertEffect.setSource(QUrl("qrc:/beep.wav")) cls.alertEffect.setAudioDevice(cls.localDevice) cls.alertEffect.setVolume(0.25) cls.alertEffect.setLoopCount(1) cls.localPlayer = QMediaPlayer() cls.localPlayer.setObjectName("localPlayer") cls.localOutput = QAudioOutput() cls.localOutput.setDevice(cls.localDevice) cls.localPlayer.setAudioOutput(cls.localOutput) if cls.virtualDevice: cls.virtualPlayer = QMediaPlayer() cls.virtualPlayer.setObjectName("virtualPlayer") cls.virtualOutput = QAudioOutput() cls.virtualOutput.setVolume(1.0) cls.virtualOutput.setDevice(cls.virtualDevice) cls.virtualPlayer.setAudioOutput(cls.virtualOutput) cacheDir = QDir( QStandardPaths.writableLocation( QStandardPaths.StandardLocation.GenericCacheLocation ) ) cacheDir.mkdir("Troglodite") cacheDir = QDir(cacheDir.path() + QDir.separator() + "Troglodite") netCache = QNetworkDiskCache() netCache.setCacheDirectory(cacheDir.path()) cls.nam = QNetworkAccessManager() cls.nam.setCache(netCache) return cls._instance def __init__(self) -> None: super().__init__() if self.localPlayer.receivers(self.localPlayer.errorOccurred) > 0: print("SoundOff, __init__() after __init__()") # # Connections # self.localPlayer.errorOccurred.connect(self.mediaError) self.localPlayer.mediaStatusChanged.connect(self.mediaStatus) self.localPlayer.playbackStateChanged.connect(self.playbackState) if self.virtualDevice: self.virtualPlayer.errorOccurred.connect(self.mediaError) self.virtualPlayer.mediaStatusChanged.connect(self.mediaStatus) self.virtualPlayer.playbackStateChanged.connect(self.playbackState) self.nam.finished.connect(self.finished) return @pyqtSlot(QMediaPlayer.Error, str) def mediaError(self, error: QMediaPlayer.Error, msg: str) -> None: print(error) print(msg) return @pyqtSlot(QMediaPlayer.MediaStatus) def mediaStatus(self, status: QMediaPlayer.MediaStatus) -> None: # print(f"mediaStatus: {status}") if status == QMediaPlayer.MediaStatus.LoadedMedia: player: Optional[QMediaPlayer] = cast(QMediaPlayer, self.sender()) assert player is not None player.play() return @pyqtSlot(QMediaPlayer.PlaybackState) def playbackState(self, state: QMediaPlayer.PlaybackState) -> None: # print(f"playbackState: {state}") return # # Communications slots # @pyqtSlot() def soundAlert(self) -> None: self.alertEffect.play() return @pyqtSlot(str) def playSound(self, url: str | QUrl) -> None: if isinstance(url, str): url = QUrl(url) if not self.localPlayer.audioOutput(): self.localPlayer.setAudioOutput(self.localOutput) if self.virtualPlayer and not self.virtualPlayer.audioOutput(): self.virtualPlayer.setAudioOutput(self.virtualOutput) if url != self._lastUrl: request = QNetworkRequest(url) self.nam.get(request) self._lastUrl = url return for player in [self.localPlayer, self.virtualPlayer]: if not player: continue player.setPosition(0) if player.mediaStatus() == QMediaPlayer.MediaStatus.LoadedMedia: player.play() return @pyqtSlot() def alert(self) -> None: self.alertEffect.play() return # # Network slots # _storage: dict[QMediaPlayer, QByteArray] = {} _buffer: dict[QMediaPlayer, QBuffer] = {} _lastUrl = QUrl() @pyqtSlot(QNetworkReply) def finished(self, reply: QNetworkReply) -> None: storage = reply.readAll() reply.close() crypto = QCryptographicHash(QCryptographicHash.Algorithm.Sha256) for player in [self.localPlayer, self.virtualPlayer]: if not player: continue self._storage[player] = QByteArray(storage) crypto.addData(self._storage[player]) # print(player, crypto.result().toHex()) crypto.reset() self._buffer[player] = QBuffer(self._storage[player]) url = reply.request().url() player.setSourceDevice(self._buffer[player], url) player.setPosition(0) if player.mediaStatus() == QMediaPlayer.MediaStatus.LoadedMedia: player.play() # print("play") return