Improve Audacity connections
Replace pipeclient with much simpler audacity_controller Better error checking Deal with Audacity going away Fixes #264
This commit is contained in:
parent
ac2e811ed6
commit
28897500c8
158
app/audacity_controller.py
Normal file
158
app/audacity_controller.py
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
# Standard library imports
|
||||||
|
import os
|
||||||
|
import psutil
|
||||||
|
import socket
|
||||||
|
import select
|
||||||
|
|
||||||
|
# PyQt imports
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
|
||||||
|
# App imports
|
||||||
|
from classes import ApplicationError
|
||||||
|
from config import Config
|
||||||
|
from log import log
|
||||||
|
|
||||||
|
|
||||||
|
class AudacityController:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
method: str = "pipe",
|
||||||
|
socket_host: str = "localhost",
|
||||||
|
socket_port: int = 12345,
|
||||||
|
timeout: int = Config.AUDACITY_TIMEOUT_SECONDS,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Initialize the AudacityController.
|
||||||
|
:param method: Communication method ('pipe' or 'socket').
|
||||||
|
:param socket_host: Host for socket connection (if using sockets).
|
||||||
|
:param socket_port: Port for socket connection (if using sockets).
|
||||||
|
:param timeout: Timeout in seconds for pipe operations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.method = method
|
||||||
|
self.path: str = ""
|
||||||
|
self.timeout = timeout
|
||||||
|
if method == "pipe":
|
||||||
|
user_uid = os.getuid() # Get the user's UID
|
||||||
|
self.pipe_to = f"/tmp/audacity_script_pipe.to.{user_uid}"
|
||||||
|
self.pipe_from = f"/tmp/audacity_script_pipe.from.{user_uid}"
|
||||||
|
if not (os.path.exists(self.pipe_to) and os.path.exists(self.pipe_from)):
|
||||||
|
raise ApplicationError(
|
||||||
|
"AudacityController: Audacity pipes not found. Ensure scripting is enabled "
|
||||||
|
f"and pipes exist at {self.pipe_to} and {self.pipe_from}."
|
||||||
|
)
|
||||||
|
elif method == "socket":
|
||||||
|
self.socket_host = socket_host
|
||||||
|
self.socket_port = socket_port
|
||||||
|
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
try:
|
||||||
|
self.sock.connect((self.socket_host, self.socket_port))
|
||||||
|
self.sock.settimeout(self.timeout)
|
||||||
|
except socket.error as e:
|
||||||
|
raise ApplicationError(f"Failed to connect to Audacity socket: {e}")
|
||||||
|
else:
|
||||||
|
raise ApplicationError("Invalid method. Use 'pipe' or 'socket'.")
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""
|
||||||
|
Close the connection (for sockets).
|
||||||
|
"""
|
||||||
|
if self.method == "socket":
|
||||||
|
self.sock.close()
|
||||||
|
|
||||||
|
def export(self) -> None:
|
||||||
|
"""
|
||||||
|
Export file from Audacity
|
||||||
|
"""
|
||||||
|
|
||||||
|
log.info("export()")
|
||||||
|
|
||||||
|
self._sanity_check()
|
||||||
|
|
||||||
|
select_status = self._send_command("SelectAll")
|
||||||
|
log.info(f"{select_status=}")
|
||||||
|
|
||||||
|
export_cmd = f'Export2: Filename="{self.path}" NumChannels=2'
|
||||||
|
export_status = self._send_command(export_cmd)
|
||||||
|
log.info(f"{export_status=}")
|
||||||
|
self.path = ""
|
||||||
|
if not export_status.startswith("Exported"):
|
||||||
|
raise ApplicationError(f"Error writing from Audacity: {export_status=}")
|
||||||
|
|
||||||
|
def open(self, path: str) -> None:
|
||||||
|
"""
|
||||||
|
Open path in Audacity. Escape filename.
|
||||||
|
"""
|
||||||
|
|
||||||
|
log.info(f"open({path=})")
|
||||||
|
|
||||||
|
self._sanity_check()
|
||||||
|
|
||||||
|
escaped_path = path.replace('"', '\\"')
|
||||||
|
cmd = f'Import2: Filename="{escaped_path}"'
|
||||||
|
status = self._send_command(cmd)
|
||||||
|
self.path = path
|
||||||
|
|
||||||
|
log.info(f"_open_in_audacity {path=}, {status=}")
|
||||||
|
|
||||||
|
def _sanity_check(self) -> None:
|
||||||
|
"""
|
||||||
|
Check Audactity running and basic connectivity.
|
||||||
|
"""
|
||||||
|
|
||||||
|
log.info("_sanity_check")
|
||||||
|
|
||||||
|
# Check Audacity is running
|
||||||
|
if "audacity" not in [i.name() for i in psutil.process_iter()]:
|
||||||
|
log.warning("Audactity not running")
|
||||||
|
raise ApplicationError("Audacity is not running")
|
||||||
|
|
||||||
|
# Check pipes exist
|
||||||
|
if self.method == "pipe":
|
||||||
|
if not (os.path.exists(self.pipe_to) and os.path.exists(self.pipe_from)):
|
||||||
|
raise ApplicationError(
|
||||||
|
"AudacityController: Audacity pipes not found. Ensure scripting is enabled "
|
||||||
|
f"and pipes exist at {self.pipe_to} and {self.pipe_from}."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Send test command to Audacity
|
||||||
|
response = self._send_command(Config.AUDACITY_TEST_COMMAND)
|
||||||
|
if response != Config.AUDACITY_TEST_RESPONSE:
|
||||||
|
raise ApplicationError(
|
||||||
|
"Error testing Audacity connectivity\n"
|
||||||
|
f"Sent: {Config.AUDACITY_TEST_COMMAND}"
|
||||||
|
f"Received: {response}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _send_command(self, command: str) -> str:
|
||||||
|
"""
|
||||||
|
Send a command to Audacity.
|
||||||
|
:param command: Command to send (e.g., 'SelectAll').
|
||||||
|
:return: Response from Audacity.
|
||||||
|
"""
|
||||||
|
log.info(f"_send_command({command=})")
|
||||||
|
|
||||||
|
if self.method == "pipe":
|
||||||
|
try:
|
||||||
|
with open(self.pipe_to, "w") as to_pipe:
|
||||||
|
to_pipe.write(command + "\n")
|
||||||
|
with open(self.pipe_from, "r") as from_pipe:
|
||||||
|
ready, _, _ = select.select([from_pipe], [], [], self.timeout)
|
||||||
|
if ready:
|
||||||
|
response = from_pipe.readline()
|
||||||
|
else:
|
||||||
|
raise TimeoutError(
|
||||||
|
f"Timeout waiting for response from {self.pipe_from}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError(f"Error communicating with Audacity via pipes: {e}")
|
||||||
|
elif self.method == "socket":
|
||||||
|
try:
|
||||||
|
self.sock.sendall((command + "\n").encode("utf-8"))
|
||||||
|
response = self.sock.recv(1024).decode("utf-8")
|
||||||
|
except socket.timeout:
|
||||||
|
raise TimeoutError("Timeout waiting for response from Audacity socket.")
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError(f"Error communicating with Audacity via socket: {e}")
|
||||||
|
return response.strip()
|
||||||
@ -438,6 +438,14 @@ class _Music:
|
|||||||
self.player = None
|
self.player = None
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationError(Exception):
|
||||||
|
"""
|
||||||
|
Custom exception
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@singleton
|
@singleton
|
||||||
@dataclass
|
@dataclass
|
||||||
class MusicMusterSignals(QObject):
|
class MusicMusterSignals(QObject):
|
||||||
|
|||||||
@ -12,7 +12,9 @@ from typing import List, Optional
|
|||||||
|
|
||||||
|
|
||||||
class Config(object):
|
class Config(object):
|
||||||
AUDACITY_TIMEOUT_TENTHS = 100
|
AUDACITY_TEST_COMMAND = "Message"
|
||||||
|
AUDACITY_TEST_RESPONSE = "Some message"
|
||||||
|
AUDACITY_TIMEOUT_SECONDS = 20
|
||||||
AUDIO_SEGMENT_CHUNK_SIZE = 10
|
AUDIO_SEGMENT_CHUNK_SIZE = 10
|
||||||
BITRATE_LOW_THRESHOLD = 192
|
BITRATE_LOW_THRESHOLD = 192
|
||||||
BITRATE_OK_THRESHOLD = 300
|
BITRATE_OK_THRESHOLD = 300
|
||||||
|
|||||||
@ -44,7 +44,6 @@ from PyQt6.QtWidgets import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
import pipeclient
|
|
||||||
from pygame import mixer
|
from pygame import mixer
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
@ -290,9 +289,6 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
self.active_proxy_model = lambda: self.tabPlaylist.currentWidget().model()
|
self.active_proxy_model = lambda: self.tabPlaylist.currentWidget().model()
|
||||||
self.move_source_rows: Optional[List[int]] = None
|
self.move_source_rows: Optional[List[int]] = None
|
||||||
self.move_source_model: Optional[PlaylistProxyModel] = None
|
self.move_source_model: Optional[PlaylistProxyModel] = None
|
||||||
self.audacity_file_path: Optional[str] = None
|
|
||||||
self.audacity_client: Optional[pipeclient.PipeClient] = None
|
|
||||||
self.initialise_audacity()
|
|
||||||
|
|
||||||
self.disable_selection_timing = False
|
self.disable_selection_timing = False
|
||||||
self.clock_counter = 0
|
self.clock_counter = 0
|
||||||
@ -851,17 +847,6 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def initialise_audacity(self) -> None:
|
|
||||||
"""
|
|
||||||
Initialise access to audacity
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.audacity_client = pipeclient.PipeClient()
|
|
||||||
log.debug(f"{hex(id(self.audacity_client))=}")
|
|
||||||
except RuntimeError as e:
|
|
||||||
log.error(f"Unable to initialise Audacity: {str(e)}")
|
|
||||||
|
|
||||||
def insert_header(self) -> None:
|
def insert_header(self) -> None:
|
||||||
"""Show dialog box to enter header text and add to playlist"""
|
"""Show dialog box to enter header text and add to playlist"""
|
||||||
|
|
||||||
|
|||||||
@ -1,313 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""Automate Audacity via mod-script-pipe.
|
|
||||||
|
|
||||||
Pipe Client may be used as a command-line script to send commands to
|
|
||||||
Audacity via the mod-script-pipe interface, or loaded as a module.
|
|
||||||
Requires Python 3.
|
|
||||||
(Python 2.7 is now obsolete, so no longer supported)
|
|
||||||
|
|
||||||
======================
|
|
||||||
Command Line Interface
|
|
||||||
======================
|
|
||||||
|
|
||||||
usage: pipeclient.py [-h] [-t] [-s ] [-d]
|
|
||||||
|
|
||||||
Arguments
|
|
||||||
---------
|
|
||||||
-h,--help: optional
|
|
||||||
show short help and exit
|
|
||||||
-t, --timeout: float, optional
|
|
||||||
timeout for reply in seconds (default: 10)
|
|
||||||
-s, --show-time: bool, optional
|
|
||||||
show command execution time (default: True)
|
|
||||||
-d, --docs: optional
|
|
||||||
show this documentation and exit
|
|
||||||
|
|
||||||
Example
|
|
||||||
-------
|
|
||||||
$ python3 pipeclient.py -t 20 -s False
|
|
||||||
|
|
||||||
Launches command line interface with 20 second time-out for
|
|
||||||
returned message, and don't show the execution time.
|
|
||||||
|
|
||||||
When prompted, enter the command to send (not quoted), or 'Q' to quit.
|
|
||||||
|
|
||||||
$ Enter command or 'Q' to quit: GetInfo: Type=Tracks Format=LISP
|
|
||||||
|
|
||||||
============
|
|
||||||
Module Usage
|
|
||||||
============
|
|
||||||
|
|
||||||
Note that on a critical error (such as broken pipe), the module just exits.
|
|
||||||
If a more graceful shutdown is required, replace the sys.exit()'s with
|
|
||||||
exceptions.
|
|
||||||
|
|
||||||
Example
|
|
||||||
-------
|
|
||||||
|
|
||||||
# Import the module:
|
|
||||||
>>> import pipeclient
|
|
||||||
|
|
||||||
# Create a client instance:
|
|
||||||
>>> client = pipeclient.PipeClient()
|
|
||||||
|
|
||||||
# Send a command:
|
|
||||||
>>> client.write("Command", timer=True)
|
|
||||||
|
|
||||||
# Read the last reply:
|
|
||||||
>>> print(client.read())
|
|
||||||
|
|
||||||
See Also
|
|
||||||
--------
|
|
||||||
PipeClient.write : Write a command to _write_pipe.
|
|
||||||
PipeClient.read : Read Audacity's reply from pipe.
|
|
||||||
|
|
||||||
Copyright Steve Daulton 2018
|
|
||||||
Released under terms of the GNU General Public License version 2:
|
|
||||||
<http://www.gnu.org/licenses/old-licenses/gpl-2.0.html />
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from io import TextIOWrapper
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
import errno
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
if sys.version_info[0] < 3:
|
|
||||||
raise RuntimeError("PipeClient Error: Python 3.x required")
|
|
||||||
|
|
||||||
# Platform specific constants
|
|
||||||
if sys.platform == "win32":
|
|
||||||
WRITE_NAME: str = "\\\\.\\pipe\\ToSrvPipe"
|
|
||||||
READ_NAME: str = "\\\\.\\pipe\\FromSrvPipe"
|
|
||||||
EOL: str = "\r\n\0"
|
|
||||||
else:
|
|
||||||
# Linux or Mac
|
|
||||||
PIPE_BASE: str = "/tmp/audacity_script_pipe."
|
|
||||||
WRITE_NAME: str = PIPE_BASE + "to." + str(os.getuid())
|
|
||||||
READ_NAME: str = PIPE_BASE + "from." + str(os.getuid())
|
|
||||||
EOL: str = "\n"
|
|
||||||
|
|
||||||
|
|
||||||
class PipeClient:
|
|
||||||
"""Write / read client access to Audacity via named pipes.
|
|
||||||
|
|
||||||
Normally there should be just one instance of this class. If
|
|
||||||
more instances are created, they all share the same state.
|
|
||||||
|
|
||||||
__init__ calls _write_thread_start() and _read_thread_start() on
|
|
||||||
first instantiation.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
None
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
----------
|
|
||||||
reader_pipe_broken : event object
|
|
||||||
Set if pipe reader fails. Audacity may have crashed
|
|
||||||
reply_ready : event object
|
|
||||||
flag cleared when command sent and set when response received
|
|
||||||
timer : bool
|
|
||||||
When true, time the command execution (default False)
|
|
||||||
reply : string
|
|
||||||
message received when Audacity completes the command
|
|
||||||
|
|
||||||
See Also
|
|
||||||
--------
|
|
||||||
write : Write a command to _write_pipe.
|
|
||||||
read : Read Audacity's reply from pipe.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
reader_pipe_broken = threading.Event()
|
|
||||||
reply_ready = threading.Event()
|
|
||||||
|
|
||||||
_shared_state: dict = {}
|
|
||||||
|
|
||||||
def __new__(cls, *p, **k):
|
|
||||||
self = object.__new__(cls, *p, **k)
|
|
||||||
self.__dict__ = cls._shared_state
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.timer: bool = False
|
|
||||||
self._start_time: float = 0
|
|
||||||
self._write_pipe: Optional[TextIOWrapper] = None
|
|
||||||
self.reply: str = ""
|
|
||||||
if not self._write_pipe:
|
|
||||||
self._write_thread_start()
|
|
||||||
self._read_thread_start()
|
|
||||||
|
|
||||||
def _write_thread_start(self) -> None:
|
|
||||||
"""Start _write_pipe thread"""
|
|
||||||
# Pipe is opened in a new thread so that we don't
|
|
||||||
# freeze if Audacity is not running.
|
|
||||||
write_thread = threading.Thread(target=self._write_pipe_open)
|
|
||||||
write_thread.daemon = True
|
|
||||||
write_thread.start()
|
|
||||||
# Allow a little time for connection to be made.
|
|
||||||
time.sleep(0.1)
|
|
||||||
if not self._write_pipe:
|
|
||||||
raise RuntimeError("PipeClientError: Write pipe cannot be opened.")
|
|
||||||
|
|
||||||
def _write_pipe_open(self) -> None:
|
|
||||||
"""Open _write_pipe."""
|
|
||||||
self._write_pipe = open(WRITE_NAME, "w")
|
|
||||||
|
|
||||||
def _read_thread_start(self) -> None:
|
|
||||||
"""Start read_pipe thread."""
|
|
||||||
read_thread = threading.Thread(target=self._reader)
|
|
||||||
read_thread.daemon = True
|
|
||||||
read_thread.start()
|
|
||||||
|
|
||||||
def write(self, command: str, timer: Optional[bool] = False) -> None:
|
|
||||||
"""Write a command to _write_pipe.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
command : string
|
|
||||||
The command to send to Audacity
|
|
||||||
timer : bool, optional
|
|
||||||
If true, time the execution of the command
|
|
||||||
|
|
||||||
Example
|
|
||||||
-------
|
|
||||||
write("GetInfo: Type=Labels", timer=True):
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# If write pipe not defined, return
|
|
||||||
if self._write_pipe is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
if timer:
|
|
||||||
self.timer = timer
|
|
||||||
self._write_pipe.write(command + EOL)
|
|
||||||
# Check that read pipe is alive
|
|
||||||
if PipeClient.reader_pipe_broken.is_set():
|
|
||||||
raise RuntimeError("PipeClient: Read-pipe error.")
|
|
||||||
try:
|
|
||||||
self._write_pipe.flush()
|
|
||||||
if self.timer:
|
|
||||||
self._start_time = time.time()
|
|
||||||
self.reply = ""
|
|
||||||
PipeClient.reply_ready.clear()
|
|
||||||
except IOError as err:
|
|
||||||
if err.errno == errno.EPIPE:
|
|
||||||
raise RuntimeError("PipeClient: Write-pipe error.")
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
def _reader(self) -> None:
|
|
||||||
"""Read FIFO in worker thread."""
|
|
||||||
# Thread will wait at this read until it connects.
|
|
||||||
# Connection should occur as soon as _write_pipe has connected.
|
|
||||||
with open(READ_NAME, "r") as read_pipe:
|
|
||||||
message = ""
|
|
||||||
pipe_ok = True
|
|
||||||
while pipe_ok:
|
|
||||||
line = read_pipe.readline()
|
|
||||||
# Stop timer as soon as we get first line of response.
|
|
||||||
stop_time = time.time()
|
|
||||||
while pipe_ok and line != "\n":
|
|
||||||
message += line
|
|
||||||
line = read_pipe.readline()
|
|
||||||
if line == "":
|
|
||||||
# No data in read_pipe indicates that the pipe
|
|
||||||
# is broken (Audacity may have crashed).
|
|
||||||
PipeClient.reader_pipe_broken.set()
|
|
||||||
pipe_ok = False
|
|
||||||
if self.timer:
|
|
||||||
xtime = (stop_time - self._start_time) * 1000
|
|
||||||
message += f"Execution time: {xtime:.2f}ms"
|
|
||||||
self.reply = message
|
|
||||||
PipeClient.reply_ready.set()
|
|
||||||
message = ""
|
|
||||||
|
|
||||||
def read(self) -> str:
|
|
||||||
"""Read Audacity's reply from pipe.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
string
|
|
||||||
The reply from the last command sent to Audacity, or null string
|
|
||||||
if reply not received. Null string usually indicates that Audacity
|
|
||||||
is still processing the last command.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if not PipeClient.reply_ready.is_set():
|
|
||||||
return ""
|
|
||||||
return self.reply
|
|
||||||
|
|
||||||
|
|
||||||
def bool_from_string(strval: str) -> bool:
|
|
||||||
"""Return boolean value from string"""
|
|
||||||
if strval.lower() in ("true", "t", "1", "yes", "y"):
|
|
||||||
return True
|
|
||||||
if strval.lower() in ("false", "f", "0", "no", "n"):
|
|
||||||
return False
|
|
||||||
raise argparse.ArgumentTypeError("Boolean value expected.")
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
"""Interactive command-line for PipeClient"""
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument(
|
|
||||||
"-t",
|
|
||||||
"--timeout",
|
|
||||||
type=float,
|
|
||||||
metavar="",
|
|
||||||
default=10,
|
|
||||||
help="timeout for reply in seconds (default: 10",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"-s",
|
|
||||||
"--show-time",
|
|
||||||
metavar="True/False",
|
|
||||||
nargs="?",
|
|
||||||
type=bool_from_string,
|
|
||||||
const="t",
|
|
||||||
default="t",
|
|
||||||
dest="show",
|
|
||||||
help="show command execution time (default: True)",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"-d", "--docs", action="store_true", help="show documentation and exit"
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if args.docs:
|
|
||||||
print(__doc__)
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
client: PipeClient = PipeClient()
|
|
||||||
while True:
|
|
||||||
reply: str = ""
|
|
||||||
message: str = input("\nEnter command or 'Q' to quit: ")
|
|
||||||
start = time.time()
|
|
||||||
if message.upper() == "Q":
|
|
||||||
sys.exit(0)
|
|
||||||
elif message == "":
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
client.write(message, timer=args.show)
|
|
||||||
while reply == "":
|
|
||||||
time.sleep(0.1) # allow time for reply
|
|
||||||
if time.time() - start > args.timeout:
|
|
||||||
reply = "PipeClient: Reply timed-out."
|
|
||||||
else:
|
|
||||||
reply = client.read()
|
|
||||||
print(reply)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
104
app/playlists.py
104
app/playlists.py
@ -1,6 +1,5 @@
|
|||||||
# Standard library imports
|
# Standard library imports
|
||||||
from typing import Callable, cast, List, Optional, TYPE_CHECKING
|
from typing import Callable, cast, List, Optional, TYPE_CHECKING
|
||||||
import psutil
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
# PyQt imports
|
# PyQt imports
|
||||||
@ -35,7 +34,8 @@ from PyQt6.QtWidgets import (
|
|||||||
# Third party imports
|
# Third party imports
|
||||||
|
|
||||||
# App imports
|
# App imports
|
||||||
from classes import Col, MusicMusterSignals, TrackInfo, track_sequence
|
from audacity_controller import AudacityController
|
||||||
|
from classes import ApplicationError, Col, MusicMusterSignals, TrackInfo, track_sequence
|
||||||
from config import Config
|
from config import Config
|
||||||
from dialogs import TrackSelectDialog
|
from dialogs import TrackSelectDialog
|
||||||
from helpers import (
|
from helpers import (
|
||||||
@ -233,6 +233,13 @@ class PlaylistTab(QTableView):
|
|||||||
self.setModel(self.proxy_model)
|
self.setModel(self.proxy_model)
|
||||||
self._set_column_widths()
|
self._set_column_widths()
|
||||||
|
|
||||||
|
# Set up for Audacity
|
||||||
|
try:
|
||||||
|
self.ac = AudacityController()
|
||||||
|
except ApplicationError as e:
|
||||||
|
self.ac = None
|
||||||
|
show_warning(self.musicmuster, "Audacity error", str(e))
|
||||||
|
|
||||||
# Stretch last column *after* setting column widths which is
|
# Stretch last column *after* setting column widths which is
|
||||||
# *much* faster
|
# *much* faster
|
||||||
h_header = self.horizontalHeader()
|
h_header = self.horizontalHeader()
|
||||||
@ -404,52 +411,6 @@ class PlaylistTab(QTableView):
|
|||||||
dlg.exec()
|
dlg.exec()
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
def _audactity_command(self, cmd: str) -> bool:
|
|
||||||
"""
|
|
||||||
Send cmd to Audacity and monitor for response. Return True if successful
|
|
||||||
else False.
|
|
||||||
"""
|
|
||||||
|
|
||||||
log.debug(f"_audacity({cmd=})")
|
|
||||||
|
|
||||||
# Notify user if audacity not running
|
|
||||||
if "audacity" not in [i.name() for i in psutil.process_iter()]:
|
|
||||||
log.warning("Audactity not running")
|
|
||||||
show_warning(self.musicmuster, "Audacity", "Audacity is not running")
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not self.musicmuster.audacity_client:
|
|
||||||
self.musicmuster.initialise_audacity()
|
|
||||||
if not self.musicmuster.audacity_client:
|
|
||||||
log.error("Unable to access Audacity client")
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.musicmuster.audacity_client.write(cmd, timer=True)
|
|
||||||
|
|
||||||
reply = ""
|
|
||||||
count = 0
|
|
||||||
while reply == "" and count < Config.AUDACITY_TIMEOUT_TENTHS:
|
|
||||||
time.sleep(0.1)
|
|
||||||
reply = self.musicmuster.audacity_client.read()
|
|
||||||
count += 1
|
|
||||||
|
|
||||||
log.debug(f"_audactity_command: {count=}, {reply=}")
|
|
||||||
status = False
|
|
||||||
timing = ""
|
|
||||||
msgs = reply.split("\n")
|
|
||||||
for msg in msgs:
|
|
||||||
if msg == "BatchCommand finished: OK":
|
|
||||||
status = True
|
|
||||||
elif msg.startswith("Execution time:"):
|
|
||||||
timing = msg
|
|
||||||
if not status:
|
|
||||||
log.error(f"_audactity_command {msgs=}")
|
|
||||||
return False
|
|
||||||
if timing:
|
|
||||||
log.debug(f"_audactity_command {timing=}")
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _build_context_menu(self, item: QTableWidgetItem) -> None:
|
def _build_context_menu(self, item: QTableWidgetItem) -> None:
|
||||||
"""Used to process context (right-click) menu, which is defined here"""
|
"""Used to process context (right-click) menu, which is defined here"""
|
||||||
|
|
||||||
@ -473,7 +434,7 @@ class PlaylistTab(QTableView):
|
|||||||
|
|
||||||
# Open/import in/from Audacity
|
# Open/import in/from Audacity
|
||||||
if track_row and not this_is_current_row:
|
if track_row and not this_is_current_row:
|
||||||
if track_path == self.musicmuster.audacity_file_path:
|
if track_path == self.ac.path:
|
||||||
# This track was opened in Audacity
|
# This track was opened in Audacity
|
||||||
self._add_context_menu(
|
self._add_context_menu(
|
||||||
"Update from Audacity",
|
"Update from Audacity",
|
||||||
@ -575,7 +536,7 @@ class PlaylistTab(QTableView):
|
|||||||
that we have an edit open.
|
that we have an edit open.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.musicmuster.audacity_file_path = None
|
self.ac.path = None
|
||||||
|
|
||||||
def clear_selection(self) -> None:
|
def clear_selection(self) -> None:
|
||||||
"""Unselect all tracks and reset drag mode"""
|
"""Unselect all tracks and reset drag mode"""
|
||||||
@ -718,31 +679,12 @@ class PlaylistTab(QTableView):
|
|||||||
Import current Audacity track to passed row
|
Import current Audacity track to passed row
|
||||||
"""
|
"""
|
||||||
|
|
||||||
path = self.source_model.get_row_track_path(row_number)
|
try:
|
||||||
if not path:
|
self.ac.export()
|
||||||
log.error(f"_import_from_audacity: can't get path for {row_number=}")
|
|
||||||
return
|
|
||||||
|
|
||||||
select_cmd = "SelectAll:"
|
|
||||||
status = self._audactity_command(select_cmd)
|
|
||||||
if not status:
|
|
||||||
log.error(f"_import_from_audacity select {status=}")
|
|
||||||
show_warning(
|
|
||||||
self.musicmuster, "Audacity", "Error selecting track in Audacity"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
export_cmd = f'Export2: Filename="{path}" NumChannels=2'
|
|
||||||
status = self._audactity_command(export_cmd)
|
|
||||||
if not status:
|
|
||||||
log.error(f"_import_from_audacity export {status=}")
|
|
||||||
show_warning(
|
|
||||||
self.musicmuster, "Audacity", "Error exporting track from Audacity"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.musicmuster.audacity_file_path = None
|
|
||||||
self._rescan(row_number)
|
self._rescan(row_number)
|
||||||
|
except ApplicationError as e:
|
||||||
|
show_warning(self.musicmuster, "Audacity error", str(e))
|
||||||
|
self._cancel_audacity()
|
||||||
|
|
||||||
def _info_row(self, row_number: int) -> None:
|
def _info_row(self, row_number: int) -> None:
|
||||||
"""Display popup with info re row"""
|
"""Display popup with info re row"""
|
||||||
@ -774,21 +716,15 @@ class PlaylistTab(QTableView):
|
|||||||
Open track in passed row in Audacity
|
Open track in passed row in Audacity
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self.musicmuster.audacity_client:
|
|
||||||
self.musicmuster.initialise_audacity()
|
|
||||||
|
|
||||||
path = self.source_model.get_row_track_path(row_number)
|
path = self.source_model.get_row_track_path(row_number)
|
||||||
if not path:
|
if not path:
|
||||||
log.error(f"_open_in_audacity: can't get path for {row_number=}")
|
log.error(f"_open_in_audacity: can't get path for {row_number=}")
|
||||||
return
|
return
|
||||||
|
|
||||||
escaped_path = path.replace('"', '\\"')
|
try:
|
||||||
cmd = f'Import2: Filename="{escaped_path}"'
|
self.ac.open(path)
|
||||||
status = self._audactity_command(cmd)
|
except ApplicationError as e:
|
||||||
if status:
|
show_warning(self.musicmuster, "Audacity error", str(e))
|
||||||
self.musicmuster.audacity_file_path = path
|
|
||||||
|
|
||||||
log.debug(f"_open_in_audacity {path=}, {status=}")
|
|
||||||
|
|
||||||
def _rescan(self, row_number: int) -> None:
|
def _rescan(self, row_number: int) -> None:
|
||||||
"""Rescan track"""
|
"""Rescan track"""
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user