Add Sound Module
This commit is contained in:
@@ -2,3 +2,4 @@ from .books import Book
|
||||
from .person import PersonDialog
|
||||
from .read import EditDialog
|
||||
from .session import SessionDialog
|
||||
from .sounds import SoundOff
|
||||
|
||||
62
lib/read.py
62
lib/read.py
@@ -1,5 +1,4 @@
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from typing import cast
|
||||
|
||||
@@ -31,7 +30,6 @@ from PyQt6.QtGui import (
|
||||
QTextDocument,
|
||||
QTextListFormat,
|
||||
)
|
||||
from PyQt6.QtMultimedia import QAudioOutput, QMediaDevices, QMediaPlayer
|
||||
from PyQt6.QtSql import QSqlDatabase, QSqlQuery, QSqlQueryModel
|
||||
from PyQt6.QtWidgets import QDialog, QPushButton
|
||||
|
||||
@@ -45,16 +43,12 @@ class EditDialog(QDialog, Ui_Dialog):
|
||||
sessionSignal = pyqtSignal()
|
||||
displayedWord = pyqtSignal(int)
|
||||
newParagraph = pyqtSignal(int, int)
|
||||
soundEffect = QMediaPlayer()
|
||||
|
||||
def __init__(self, session, person_id: int) -> None:
|
||||
def __init__(self, parent, session, person_id: int) -> None:
|
||||
self.session = session
|
||||
super(EditDialog, self).__init__()
|
||||
super(EditDialog, self).__init__(parent)
|
||||
print(self.parent())
|
||||
self.person_id = person_id
|
||||
if not QResource.registerResource(
|
||||
os.path.join(os.path.dirname(__file__), "../ui/resources.rcc"), "/"
|
||||
):
|
||||
raise Exception("Unable to register resources.rcc")
|
||||
styleSheet = QResource(":/display.css").data().decode("utf-8")
|
||||
self.setupUi(self)
|
||||
#
|
||||
@@ -63,16 +57,6 @@ class EditDialog(QDialog, Ui_Dialog):
|
||||
#
|
||||
# End overrides
|
||||
#
|
||||
audioOutput = QAudioOutput()
|
||||
dev = None
|
||||
for output in QMediaDevices.audioOutputs():
|
||||
if output.id().data().decode("UTF-8") == "virt-input":
|
||||
dev = output
|
||||
break
|
||||
if dev:
|
||||
audioOutput.setDevice(dev)
|
||||
self.audioOutput = audioOutput
|
||||
self.soundEffect.setAudioOutput(audioOutput)
|
||||
self.load_book(self.person_id)
|
||||
blockNumber = self.block
|
||||
self.paraEdit.setReadOnly(True)
|
||||
@@ -101,10 +85,6 @@ class EditDialog(QDialog, Ui_Dialog):
|
||||
#
|
||||
self.displayedWord.connect(self.session.addWord)
|
||||
self.newParagraph.connect(self.session.addBlock)
|
||||
|
||||
self.soundEffect.errorOccurred.connect(self.mediaError)
|
||||
self.soundEffect.playbackStateChanged.connect(self.changedState)
|
||||
self.soundEffect.mediaStatusChanged.connect(self.changedStatus)
|
||||
return
|
||||
|
||||
#
|
||||
@@ -140,33 +120,6 @@ class EditDialog(QDialog, Ui_Dialog):
|
||||
self.setDefEdit(selection, word_id, definition)
|
||||
return
|
||||
|
||||
@pyqtSlot(QMediaPlayer.MediaStatus)
|
||||
def changedStatus(self, status):
|
||||
if status == QMediaPlayer.MediaStatus.LoadedMedia:
|
||||
self.soundEffect.play()
|
||||
audioOutput = self.soundEffect.audioOutput()
|
||||
if not audioOutput:
|
||||
self.soundEffect.setAudioOutput(self.audioOutput)
|
||||
audioOutput = self.audioOutput
|
||||
audioDevice = audioOutput.device()
|
||||
print(status)
|
||||
return
|
||||
|
||||
@pyqtSlot(QMediaPlayer.PlaybackState)
|
||||
def changedState(self, status):
|
||||
audioOutput = self.soundEffect.audioOutput()
|
||||
if not audioOutput:
|
||||
return
|
||||
audioDevice = audioOutput.device()
|
||||
print(status)
|
||||
return
|
||||
|
||||
@pyqtSlot(QMediaPlayer.Error, str)
|
||||
def mediaError(self, error, string):
|
||||
print(error)
|
||||
print(string)
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def sessionAction(self) -> None:
|
||||
self.sessionSignal.emit()
|
||||
@@ -214,13 +167,8 @@ class EditDialog(QDialog, Ui_Dialog):
|
||||
print("Looking for audio")
|
||||
for entry in self.phonetics:
|
||||
if len(entry["audio"]) > 0:
|
||||
self.soundEffect.setSource(QUrl(entry["audio"]))
|
||||
if (
|
||||
self.soundEffect.mediaStatus()
|
||||
== QMediaPlayer.MediaStatus.LoadedMedia
|
||||
):
|
||||
self.soundEffect.play()
|
||||
return
|
||||
# self.parent().playAlert.emit()
|
||||
self.parent().playSound.emit(entry["audio"])
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
|
||||
@@ -29,6 +29,7 @@ class SessionDialog(QDialog, Ui_Dialog):
|
||||
sessionStart = None
|
||||
sessionEnd = None
|
||||
blocks = QStandardItemModel()
|
||||
session_id = -1
|
||||
|
||||
def __init__(self) -> None:
|
||||
super(SessionDialog, self).__init__()
|
||||
@@ -112,41 +113,14 @@ class SessionDialog(QDialog, Ui_Dialog):
|
||||
self.sessionEnd = datetime.now()
|
||||
query = QSqlQuery()
|
||||
query.prepare(
|
||||
"INSERT INTO sessions "
|
||||
"(person_id, start, stop, notes) "
|
||||
"VALUES (:person_id, :start, :stop, :notes)"
|
||||
"UPDATE sessions "
|
||||
"SET start=:start , SET stop=:stop, SET notes=:notes "
|
||||
"WHERE sesion_id = :session_id"
|
||||
)
|
||||
query.bindValue(":person_id", self.person_id)
|
||||
query.bindValue(":session_id", self.session_id)
|
||||
query.bindValue(":start", self.sessionStart.isoformat())
|
||||
query.bindValue(":stop", self.sessionEnd.isoformat())
|
||||
query.bindValue(":notes", self.textEdit.toPlainText())
|
||||
if not query.exec():
|
||||
query_error(query)
|
||||
session_id = query.lastInsertId()
|
||||
model = self.wordView.model()
|
||||
sql = "INSERT INTO session_word (session_id,word_id, important) VALUES "
|
||||
parameters = ["(?,?,?)" for x in range(model.rowCount())]
|
||||
sql += ", ".join(parameters)
|
||||
query.prepare(sql)
|
||||
for row in range(model.rowCount()):
|
||||
query.addBindValue(session_id)
|
||||
query.addBindValue(model.item(row).data(SessionDialog.WordIdRole))
|
||||
query.addBindValue(
|
||||
model.item(row).data(SessionDialog.WordImportantRole)
|
||||
)
|
||||
if not query.exec():
|
||||
query_error(query)
|
||||
sql = (
|
||||
"INSERT INTO session_block (session_id, section_id, block) VALUES "
|
||||
)
|
||||
parameters = ["(?,?,?)" for x in range(self.blocks.rowCount())]
|
||||
sql += ",".join(parameters)
|
||||
query.prepare(sql)
|
||||
for row in range(self.blocks.rowCount()):
|
||||
query.addBindValue(session_id)
|
||||
item = self.blocks.item(row)
|
||||
query.addBindValue(item.data(SessionDialog.SectionIdRole))
|
||||
query.addBindValue(item.data(SessionDialog.BlockRole))
|
||||
if not query.exec():
|
||||
query_error(query)
|
||||
super().accept()
|
||||
@@ -157,6 +131,19 @@ class SessionDialog(QDialog, Ui_Dialog):
|
||||
if state:
|
||||
if not self.sessionStart:
|
||||
self.sessionStart = datetime.now()
|
||||
if self.session_id <= 0:
|
||||
query = QSqlQuery()
|
||||
query.prepare(
|
||||
"INSERT INTO sessions "
|
||||
"(person_id, start) "
|
||||
"VALUES (:person_id, :start)"
|
||||
)
|
||||
query.bindValue(":person_id", self.person_id)
|
||||
query.bindValue(":start", self.sessionStart.isoformat())
|
||||
if not query.exec():
|
||||
query_error(query)
|
||||
self.session_id = query.lastInsertId()
|
||||
|
||||
self.startTime = datetime.now()
|
||||
self.timer.start()
|
||||
else:
|
||||
@@ -197,13 +184,13 @@ class SessionDialog(QDialog, Ui_Dialog):
|
||||
raise Exception(f"Word_id({word_id}) not found in DB")
|
||||
word = QStandardItem()
|
||||
word.setData(query.value("word"), Qt.ItemDataRole.DisplayRole)
|
||||
word.setData(query.value("word_id"), SessionDialog.WordIdRole)
|
||||
word.setData(word_id, SessionDialog.WordIdRole)
|
||||
word.setData(0, SessionDialog.WordImportantRole)
|
||||
model = self.wordView.model()
|
||||
matches = model.match(
|
||||
model.createIndex(0, 0),
|
||||
SessionDialog.WordIdRole,
|
||||
query.value("word_id"),
|
||||
word_id,
|
||||
1,
|
||||
Qt.MatchFlag.MatchExactly,
|
||||
)
|
||||
@@ -211,6 +198,15 @@ class SessionDialog(QDialog, Ui_Dialog):
|
||||
return
|
||||
self.wordView.model().appendRow(word)
|
||||
self.wordView.model().sort(0)
|
||||
query.prepare(
|
||||
"INSERT INTO session_word "
|
||||
"(session_id, word_id, important) "
|
||||
"VALUES (:session_id, :word_id, 0)"
|
||||
)
|
||||
query.bindValue(":session_id", self.session_id)
|
||||
query.bindValue(":word_id", word_id)
|
||||
if not query.exec():
|
||||
query_error(query)
|
||||
else:
|
||||
print(f"Not active: {word_id}")
|
||||
return
|
||||
@@ -255,4 +251,14 @@ class SessionDialog(QDialog, Ui_Dialog):
|
||||
)
|
||||
self.textBrowser.textCursor().insertBlock()
|
||||
self.textBrowser.ensureCursorVisible()
|
||||
query.prepare(
|
||||
"INSERT INTO session_block "
|
||||
"(session_id, section_id, block) "
|
||||
"VALUES (:session_id, :section_id, :block)"
|
||||
)
|
||||
query.bindValue(":session_id", self.session_id)
|
||||
query.bindValue(":section_id", section_id)
|
||||
query.bindValue(":block", block)
|
||||
if not query.exec():
|
||||
query_error(query)
|
||||
return
|
||||
|
||||
108
lib/sounds.py
Normal file
108
lib/sounds.py
Normal file
@@ -0,0 +1,108 @@
|
||||
from PyQt6.QtCore import QObject, Qt, QUrl, pyqtSlot
|
||||
from PyQt6.QtMultimedia import (
|
||||
QAudioDevice,
|
||||
QAudioOutput,
|
||||
QMediaDevices,
|
||||
QMediaPlayer,
|
||||
QSoundEffect,
|
||||
)
|
||||
|
||||
# from PyQt6.QtWidgets import QWidget
|
||||
|
||||
|
||||
class SoundOff(QObject):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
#
|
||||
# Setup devices
|
||||
#
|
||||
self.virtualDevice = None
|
||||
dev = None
|
||||
for output in QMediaDevices.audioOutputs():
|
||||
if output.id().data().decode("utf-8") == "virt-input":
|
||||
self.virtualDevice = output
|
||||
if output.isDefault():
|
||||
self.localDevice = output
|
||||
|
||||
self.alertEffect = QSoundEffect()
|
||||
self.alertEffect.setSource(QUrl("qrc:/beep.wav"))
|
||||
self.alertEffect.setAudioDevice(self.localDevice)
|
||||
self.alertEffect.setVolume(0.25)
|
||||
self.alertEffect.setLoopCount(1)
|
||||
|
||||
self.localPlayer = QMediaPlayer()
|
||||
self.localPlayer.setObjectName("localPlayer")
|
||||
self.localOutput = QAudioOutput()
|
||||
self.localOutput.setDevice(self.localDevice)
|
||||
self.localPlayer.setAudioOutput(self.localOutput)
|
||||
if self.virtualDevice:
|
||||
self.virtualPlayer = QMediaPlayer()
|
||||
self.virtualPlayer.setObjectName("virtualPlayer")
|
||||
self.virtualOutput = QAudioOutput()
|
||||
self.virtualOutput.setDevice(self.virtualDevice)
|
||||
self.virtualPlayer.setAudioOutput(self.virtualOutput)
|
||||
#
|
||||
# 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)
|
||||
|
||||
@pyqtSlot()
|
||||
def alert(self):
|
||||
self.alertEffect.play()
|
||||
return
|
||||
|
||||
@pyqtSlot(QMediaPlayer.Error, str)
|
||||
def mediaError(self, error, string):
|
||||
print(error)
|
||||
print(str)
|
||||
return
|
||||
|
||||
@pyqtSlot(QMediaPlayer.MediaStatus)
|
||||
def mediaStatus(self, status):
|
||||
if status == QMediaPlayer.MediaStatus.LoadedMedia:
|
||||
self.sender().play()
|
||||
return
|
||||
|
||||
@pyqtSlot(QMediaPlayer.PlaybackState)
|
||||
def playbackState(self, state):
|
||||
return
|
||||
|
||||
#
|
||||
# Communications slots
|
||||
#
|
||||
@pyqtSlot()
|
||||
def soundAlert(self):
|
||||
self.alertEffect.play()
|
||||
return
|
||||
|
||||
@pyqtSlot(str)
|
||||
def playSound(self, url):
|
||||
src = QUrl(url)
|
||||
if not self.localPlayer.audioOutput():
|
||||
self.localPlayer.setAudioOutput(self.localOutput)
|
||||
self.localPlayer.setSource(src)
|
||||
self.localPlayer.setPosition(0)
|
||||
if (
|
||||
self.localPlayer.mediaStatus()
|
||||
== QMediaPlayer.MediaStatus.LoadedMedia
|
||||
):
|
||||
self.localPlayer.play()
|
||||
if not self.virtualDevice:
|
||||
return
|
||||
return
|
||||
self.virtualPlayer.setSource(src)
|
||||
self.virtualPlayer.setPosition(0)
|
||||
if not self.virtualPlayer.audioOutput():
|
||||
self.virtualPlayer.setAudioOutput(self.virtualOutput)
|
||||
if (
|
||||
self.virtualPlayer.mediaStatus()
|
||||
== QMediaPlayer.MediaStatus.LoadedMedia
|
||||
):
|
||||
self.virtualPlayer.play()
|
||||
return
|
||||
32
main.py
32
main.py
@@ -1,15 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# TODO:
|
||||
# Record all words examined
|
||||
#
|
||||
# Add definition to definition
|
||||
# Follow definition links
|
||||
# Print subset of words, limit to words from this session's paragraphs
|
||||
# plus defined during session
|
||||
# Add Note per session
|
||||
# Add book import dialog
|
||||
# Add person create/edit dialog
|
||||
# Reading scroll with speed control
|
||||
# Move controls out of reading window.
|
||||
# Ability to edit text with updates to word-section links
|
||||
@@ -24,7 +18,14 @@ import sys
|
||||
from datetime import datetime, timedelta
|
||||
from typing import cast
|
||||
|
||||
from PyQt6.QtCore import QModelIndex, Qt, QTimer, pyqtSignal, pyqtSlot
|
||||
from PyQt6.QtCore import (
|
||||
QModelIndex,
|
||||
QResource,
|
||||
Qt,
|
||||
QTimer,
|
||||
pyqtSignal,
|
||||
pyqtSlot,
|
||||
)
|
||||
from PyQt6.QtGui import (
|
||||
QAction,
|
||||
QFont,
|
||||
@@ -59,10 +60,21 @@ def query_error(query: QSqlQuery) -> None:
|
||||
|
||||
|
||||
class MainWindow(QMainWindow, Ui_MainWindow):
|
||||
playAlert = pyqtSignal()
|
||||
playSound = pyqtSignal(str)
|
||||
|
||||
def __init__(self) -> None:
|
||||
super(MainWindow, self).__init__()
|
||||
self.setupUi(self)
|
||||
# model = ModelOverride()
|
||||
#
|
||||
# Setup resources
|
||||
#
|
||||
if not QResource.registerResource(
|
||||
os.path.join(os.path.dirname(__file__), "ui/resources.rcc"), "/"
|
||||
):
|
||||
raise Exception("Unable to register resources.rcc")
|
||||
self.soundOff = SoundOff()
|
||||
|
||||
model = QSqlQueryModel()
|
||||
query = QSqlQuery("SELECT * FROM people ORDER BY name")
|
||||
model.setQuery(query)
|
||||
@@ -86,6 +98,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
||||
) # Y
|
||||
self.peopleView.doubleClicked.connect(self.editPerson) # Y
|
||||
self.peopleView.clicked.connect(self.selectedPerson) # Y
|
||||
self.playAlert.connect(self.soundOff.alert)
|
||||
self.playSound.connect(self.soundOff.playSound)
|
||||
self.show()
|
||||
return
|
||||
|
||||
@@ -156,7 +170,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
||||
self.session.show()
|
||||
self.session.raise_()
|
||||
self.setPerson.emit(person_id)
|
||||
self.dlg = EditDialog(self.session, person_id)
|
||||
self.dlg = EditDialog(self, self.session, person_id)
|
||||
self.dlg.show()
|
||||
self.dlg.raise_()
|
||||
return
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<file>print.css</file>
|
||||
<file>display.css</file>
|
||||
<file>email.css</file>
|
||||
<file>beep.wav</file>
|
||||
<file>opendyslexic/OpenDyslexic-Regular.otf</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
BIN
ui/resources.rcc
BIN
ui/resources.rcc
Binary file not shown.
Reference in New Issue
Block a user