Add Sound Module

This commit is contained in:
Christopher T. Johnson
2023-12-19 10:01:09 -05:00
parent 11726900f7
commit bb5287743c
7 changed files with 177 additions and 99 deletions

View File

@@ -2,3 +2,4 @@ from .books import Book
from .person import PersonDialog from .person import PersonDialog
from .read import EditDialog from .read import EditDialog
from .session import SessionDialog from .session import SessionDialog
from .sounds import SoundOff

View File

@@ -1,5 +1,4 @@
import json import json
import os
import re import re
from typing import cast from typing import cast
@@ -31,7 +30,6 @@ from PyQt6.QtGui import (
QTextDocument, QTextDocument,
QTextListFormat, QTextListFormat,
) )
from PyQt6.QtMultimedia import QAudioOutput, QMediaDevices, QMediaPlayer
from PyQt6.QtSql import QSqlDatabase, QSqlQuery, QSqlQueryModel from PyQt6.QtSql import QSqlDatabase, QSqlQuery, QSqlQueryModel
from PyQt6.QtWidgets import QDialog, QPushButton from PyQt6.QtWidgets import QDialog, QPushButton
@@ -45,16 +43,12 @@ class EditDialog(QDialog, Ui_Dialog):
sessionSignal = pyqtSignal() sessionSignal = pyqtSignal()
displayedWord = pyqtSignal(int) displayedWord = pyqtSignal(int)
newParagraph = pyqtSignal(int, 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 self.session = session
super(EditDialog, self).__init__() super(EditDialog, self).__init__(parent)
print(self.parent())
self.person_id = person_id 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") styleSheet = QResource(":/display.css").data().decode("utf-8")
self.setupUi(self) self.setupUi(self)
# #
@@ -63,16 +57,6 @@ class EditDialog(QDialog, Ui_Dialog):
# #
# End overrides # 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) self.load_book(self.person_id)
blockNumber = self.block blockNumber = self.block
self.paraEdit.setReadOnly(True) self.paraEdit.setReadOnly(True)
@@ -101,10 +85,6 @@ class EditDialog(QDialog, Ui_Dialog):
# #
self.displayedWord.connect(self.session.addWord) self.displayedWord.connect(self.session.addWord)
self.newParagraph.connect(self.session.addBlock) 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 return
# #
@@ -140,33 +120,6 @@ class EditDialog(QDialog, Ui_Dialog):
self.setDefEdit(selection, word_id, definition) self.setDefEdit(selection, word_id, definition)
return 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() @pyqtSlot()
def sessionAction(self) -> None: def sessionAction(self) -> None:
self.sessionSignal.emit() self.sessionSignal.emit()
@@ -214,13 +167,8 @@ class EditDialog(QDialog, Ui_Dialog):
print("Looking for audio") print("Looking for audio")
for entry in self.phonetics: for entry in self.phonetics:
if len(entry["audio"]) > 0: if len(entry["audio"]) > 0:
self.soundEffect.setSource(QUrl(entry["audio"])) # self.parent().playAlert.emit()
if ( self.parent().playSound.emit(entry["audio"])
self.soundEffect.mediaStatus()
== QMediaPlayer.MediaStatus.LoadedMedia
):
self.soundEffect.play()
return
return return
@pyqtSlot() @pyqtSlot()

View File

@@ -29,6 +29,7 @@ class SessionDialog(QDialog, Ui_Dialog):
sessionStart = None sessionStart = None
sessionEnd = None sessionEnd = None
blocks = QStandardItemModel() blocks = QStandardItemModel()
session_id = -1
def __init__(self) -> None: def __init__(self) -> None:
super(SessionDialog, self).__init__() super(SessionDialog, self).__init__()
@@ -112,41 +113,14 @@ class SessionDialog(QDialog, Ui_Dialog):
self.sessionEnd = datetime.now() self.sessionEnd = datetime.now()
query = QSqlQuery() query = QSqlQuery()
query.prepare( query.prepare(
"INSERT INTO sessions " "UPDATE sessions "
"(person_id, start, stop, notes) " "SET start=:start , SET stop=:stop, SET notes=:notes "
"VALUES (:person_id, :start, :stop, :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(":start", self.sessionStart.isoformat())
query.bindValue(":stop", self.sessionEnd.isoformat()) query.bindValue(":stop", self.sessionEnd.isoformat())
query.bindValue(":notes", self.textEdit.toPlainText()) 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(): if not query.exec():
query_error(query) query_error(query)
super().accept() super().accept()
@@ -157,6 +131,19 @@ class SessionDialog(QDialog, Ui_Dialog):
if state: if state:
if not self.sessionStart: if not self.sessionStart:
self.sessionStart = datetime.now() 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.startTime = datetime.now()
self.timer.start() self.timer.start()
else: else:
@@ -197,13 +184,13 @@ class SessionDialog(QDialog, Ui_Dialog):
raise Exception(f"Word_id({word_id}) not found in DB") raise Exception(f"Word_id({word_id}) not found in DB")
word = QStandardItem() word = QStandardItem()
word.setData(query.value("word"), Qt.ItemDataRole.DisplayRole) 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) word.setData(0, SessionDialog.WordImportantRole)
model = self.wordView.model() model = self.wordView.model()
matches = model.match( matches = model.match(
model.createIndex(0, 0), model.createIndex(0, 0),
SessionDialog.WordIdRole, SessionDialog.WordIdRole,
query.value("word_id"), word_id,
1, 1,
Qt.MatchFlag.MatchExactly, Qt.MatchFlag.MatchExactly,
) )
@@ -211,6 +198,15 @@ class SessionDialog(QDialog, Ui_Dialog):
return return
self.wordView.model().appendRow(word) self.wordView.model().appendRow(word)
self.wordView.model().sort(0) 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: else:
print(f"Not active: {word_id}") print(f"Not active: {word_id}")
return return
@@ -255,4 +251,14 @@ class SessionDialog(QDialog, Ui_Dialog):
) )
self.textBrowser.textCursor().insertBlock() self.textBrowser.textCursor().insertBlock()
self.textBrowser.ensureCursorVisible() 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 return

108
lib/sounds.py Normal file
View 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
View File

@@ -1,15 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# #
# TODO: # TODO:
# Record all words examined
# #
# Add definition to definition # 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 book import dialog
# Add person create/edit dialog
# Reading scroll with speed control # Reading scroll with speed control
# Move controls out of reading window. # Move controls out of reading window.
# Ability to edit text with updates to word-section links # Ability to edit text with updates to word-section links
@@ -24,7 +18,14 @@ import sys
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import cast 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 ( from PyQt6.QtGui import (
QAction, QAction,
QFont, QFont,
@@ -59,10 +60,21 @@ def query_error(query: QSqlQuery) -> None:
class MainWindow(QMainWindow, Ui_MainWindow): class MainWindow(QMainWindow, Ui_MainWindow):
playAlert = pyqtSignal()
playSound = pyqtSignal(str)
def __init__(self) -> None: def __init__(self) -> None:
super(MainWindow, self).__init__() super(MainWindow, self).__init__()
self.setupUi(self) 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() model = QSqlQueryModel()
query = QSqlQuery("SELECT * FROM people ORDER BY name") query = QSqlQuery("SELECT * FROM people ORDER BY name")
model.setQuery(query) model.setQuery(query)
@@ -86,6 +98,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
) # Y ) # Y
self.peopleView.doubleClicked.connect(self.editPerson) # Y self.peopleView.doubleClicked.connect(self.editPerson) # Y
self.peopleView.clicked.connect(self.selectedPerson) # Y self.peopleView.clicked.connect(self.selectedPerson) # Y
self.playAlert.connect(self.soundOff.alert)
self.playSound.connect(self.soundOff.playSound)
self.show() self.show()
return return
@@ -156,7 +170,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.session.show() self.session.show()
self.session.raise_() self.session.raise_()
self.setPerson.emit(person_id) 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.show()
self.dlg.raise_() self.dlg.raise_()
return return

View File

@@ -3,6 +3,7 @@
<file>print.css</file> <file>print.css</file>
<file>display.css</file> <file>display.css</file>
<file>email.css</file> <file>email.css</file>
<file>beep.wav</file>
<file>opendyslexic/OpenDyslexic-Regular.otf</file> <file>opendyslexic/OpenDyslexic-Regular.otf</file>
</qresource> </qresource>
</RCC> </RCC>

Binary file not shown.