mirror of https://gitlab.com/openlp/openlp.git
Compare commits
6 Commits
c46ee0122c
...
3ac25d276b
Author | SHA1 | Date |
---|---|---|
JessyJP | 3ac25d276b | |
Raoul Snyman | 6bbfe00ec0 | |
Tim Bentley | cee0a9d573 | |
JessyJP | f05d64be07 | |
JessyJP | f6f6c0291f | |
JessyJP | 6fca42a753 |
|
@ -0,0 +1,36 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
##########################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2024 OpenLP Developers #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
|
||||
"""
|
||||
The :mod:`midi` module provides the MIDI plugin.
|
||||
The MIDI plugin offers the capability for duplex MIDI control and interaction within OpenLP.
|
||||
"""
|
||||
|
||||
# NOTE: Technically it would be good to have a separate class to act as a profile template.
|
||||
# An instance of this template could be unpacked in the ORM, or alternatively, the template could be created
|
||||
# (unpacked from the ORM). This will provide validation methods and consistency.
|
||||
# However, at this point it is not necessary. The midi-action-event mappings are always utilized dynamically.
|
||||
# The full profile is only fully utilized by the configuration menu tab. When it exports it's state
|
||||
# it packs it into a dictionary(not as important) and the midi-action-event into a SimpleNamespace to maintain
|
||||
# consistency when the set property is called. Therefore, this is sufficient.
|
||||
# All other methods only read the full profile and get it in the form of a dictionary,
|
||||
# thus consistency is maintained. If there are more ways to import a profile then a template calls should be used.
|
||||
# In all cases the database manager will ensure the type checking of the input profile state or individual fields.
|
|
@ -0,0 +1,44 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
##########################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2024 OpenLP Developers #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
"""
|
||||
The MIDI plugin in OpenLP provides integration with MIDI devices and allows users to configure MIDI events to interact
|
||||
with OpenLP functions.
|
||||
|
||||
Forms in OpenLP's MIDI plugin follow a two-class approach similar to other plugins:
|
||||
|
||||
1. The **Dialog** class, named `Ui_<name>Dialog`, which holds the graphical elements. This is a version adapted from the
|
||||
output of the `pyuic5` tool, typically using single quotes and OpenLP's `translate()` function for translatable strings.
|
||||
|
||||
2. The **Form** class, named `<name>Form`, which contains the functionality. It is instantiated and used in the main
|
||||
application. This class inherits from both a Qt widget (often `QtWidgets.QDialog`) and the corresponding Ui class,
|
||||
enabling access to GUI elements via `self.object`.
|
||||
|
||||
For example::
|
||||
|
||||
class MidiSettingsForm(QtWidgets.QDialog, Ui_MidiSettingsDialog):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(MidiSettingsForm, self).__init__(parent)
|
||||
self.setup_ui(self)
|
||||
|
||||
This dual inheritance pattern allows for a separation of GUI and logic, and also facilitates future changes to the GUI
|
||||
from the .ui files, if required.
|
||||
"""
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
##########################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2024 OpenLP Developers #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
##########################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2024 OpenLP Developers #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
|
@ -0,0 +1,226 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
##########################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2024 OpenLP Developers #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
|
||||
import mido
|
||||
import rtmidi
|
||||
import pygame
|
||||
import pygame.midi
|
||||
import re
|
||||
import logging
|
||||
from openlp.plugins.midi.lib.types_definitions.constants import midi_ch_any
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Mido_DeviceHandler:
|
||||
@staticmethod
|
||||
def _get_input_midi_devices() -> list:
|
||||
"""
|
||||
Get a list of input MIDI device names.
|
||||
"""
|
||||
try:
|
||||
devices = mido.get_input_names()
|
||||
return devices
|
||||
except Exception as e:
|
||||
log.error(f"Error getting input MIDI devices: {e}")
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def _get_output_midi_devices() -> list:
|
||||
"""
|
||||
Get a list of output MIDI device names.
|
||||
|
||||
Returns:
|
||||
list: A list of detected output MIDI device names.
|
||||
Returns an empty list if there's an error or no devices are detected.
|
||||
"""
|
||||
try:
|
||||
devices = mido.get_output_names()
|
||||
return devices
|
||||
except Exception as e:
|
||||
log.error(f"Error getting output MIDI devices: {e}")
|
||||
return []
|
||||
|
||||
|
||||
class RtMidi_DeviceHandler:
|
||||
midi_in = rtmidi.MidiIn()
|
||||
midi_out = rtmidi.MidiOut()
|
||||
|
||||
@staticmethod
|
||||
def _get_input_midi_devices() -> list:
|
||||
"""
|
||||
Get a list of input MIDI device names using rtmidi.
|
||||
"""
|
||||
try:
|
||||
devices = RtMidi_DeviceHandler.midi_in.get_ports()
|
||||
return devices
|
||||
except Exception as e:
|
||||
log.error(f"Error getting RTMidi input devices: {e}")
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def _get_output_midi_devices() -> list:
|
||||
"""
|
||||
Get a list of output MIDI device names using rtmidi.
|
||||
"""
|
||||
try:
|
||||
devices = RtMidi_DeviceHandler.midi_out.get_ports()
|
||||
return devices
|
||||
except Exception as e:
|
||||
log.error(f"Error getting RTMidi output devices: {e}")
|
||||
return []
|
||||
|
||||
|
||||
class PygameMidi_DeviceHandler:
|
||||
@staticmethod
|
||||
def init_pygame_midi():
|
||||
pygame.init()
|
||||
pygame.midi.init()
|
||||
|
||||
@staticmethod
|
||||
def _get_input_midi_devices() -> list:
|
||||
"""
|
||||
Get a list of input MIDI device names using pygame.
|
||||
"""
|
||||
try:
|
||||
PygameMidi_DeviceHandler.init_pygame_midi()
|
||||
# devices = [pygame.midi.get_device_info(i) for i in range(pygame.midi.get_count())
|
||||
devices = [pygame.midi.get_device_info(i)[1].decode() for i in range(pygame.midi.get_count())
|
||||
if pygame.midi.get_device_info(i)[1] and pygame.midi.get_device_info(i)[2] == 1]
|
||||
return devices
|
||||
except Exception as e:
|
||||
log.error(f"Error getting pygame input MIDI devices: {e}")
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def _get_output_midi_devices() -> list:
|
||||
"""
|
||||
Get a list of output MIDI device names using pygame.
|
||||
"""
|
||||
try:
|
||||
PygameMidi_DeviceHandler.init_pygame_midi()
|
||||
devices = [pygame.midi.get_device_info(i)[1].decode() for i in range(pygame.midi.get_count())
|
||||
if pygame.midi.get_device_info(i)[1] and pygame.midi.get_device_info(i)[2] == 0]
|
||||
return devices
|
||||
except Exception as e:
|
||||
log.error(f"Error getting pygame output MIDI devices: {e}")
|
||||
return []
|
||||
|
||||
|
||||
class MidiDeviceHandler(Mido_DeviceHandler):
|
||||
_should_sanitize_names = True # Private static switch to control name sanitization
|
||||
|
||||
@staticmethod
|
||||
def should_sanitize_names():
|
||||
"""
|
||||
Get the current status of the sanitize_names property.
|
||||
"""
|
||||
return MidiDeviceHandler._should_sanitize_names
|
||||
|
||||
@staticmethod
|
||||
def _get_superclass_name():
|
||||
return MidiDeviceHandler.__bases__[0].__name__
|
||||
|
||||
@staticmethod
|
||||
def _sanitize_device_name(name):
|
||||
"""
|
||||
Sanitize the MIDI device name by removing trailing index numbers.
|
||||
"""
|
||||
# This regular expression matches a space followed by any number of digits at the end of the string
|
||||
return re.sub(r'\s+\d+$', '', name)
|
||||
|
||||
@staticmethod
|
||||
def get_input_midi_devices() -> list:
|
||||
"""
|
||||
Get a list of input MIDI device names and optionally sanitize them.
|
||||
"""
|
||||
# Choose which library to use (mido, rtmidi, or pygame) for getting device names
|
||||
# This is a placeholder logic; adjust as needed
|
||||
devices = MidiDeviceHandler._get_input_midi_devices()
|
||||
log.debug(f"Detected input MIDI devices via [{MidiDeviceHandler._get_superclass_name()}]: {devices}")
|
||||
print(f"Detected input MIDI devices via [{MidiDeviceHandler._get_superclass_name()}]: {devices}")
|
||||
|
||||
if MidiDeviceHandler._should_sanitize_names:
|
||||
return [MidiDeviceHandler._sanitize_device_name(name) for name in devices]
|
||||
else:
|
||||
return devices
|
||||
|
||||
@staticmethod
|
||||
def get_output_midi_devices() -> list:
|
||||
"""
|
||||
Get a list of output MIDI device names and optionally sanitize them.
|
||||
"""
|
||||
# Choose which library to use (mido, rtmidi, or pygame) for getting device names
|
||||
# This is a placeholder logic; adjust as needed
|
||||
devices = MidiDeviceHandler._get_output_midi_devices()
|
||||
log.debug(f"Detected output MIDI devices via [{MidiDeviceHandler._get_superclass_name()}]: {devices}")
|
||||
print(f"Detected output MIDI devices via [{MidiDeviceHandler._get_superclass_name()}]: {devices}")
|
||||
|
||||
if MidiDeviceHandler._should_sanitize_names:
|
||||
return [MidiDeviceHandler._sanitize_device_name(name) for name in devices]
|
||||
else:
|
||||
return devices
|
||||
|
||||
@staticmethod
|
||||
def match_exact_input_device_name(sanitized_name):
|
||||
"""
|
||||
Get the exact name of an input MIDI device from its sanitized name.
|
||||
"""
|
||||
# Placeholder logic for matching sanitized name to exact name
|
||||
devices = MidiDeviceHandler._get_input_midi_devices()
|
||||
for device in devices:
|
||||
if MidiDeviceHandler._sanitize_device_name(device) == sanitized_name:
|
||||
return device
|
||||
return sanitized_name
|
||||
|
||||
@staticmethod
|
||||
def match_exact_output_device_name(sanitized_name):
|
||||
"""
|
||||
Get the exact name of an output MIDI device from its sanitized name.
|
||||
"""
|
||||
# Placeholder logic for matching sanitized name to exact name
|
||||
devices = MidiDeviceHandler._get_output_midi_devices()
|
||||
for device in devices:
|
||||
if MidiDeviceHandler._sanitize_device_name(device) == sanitized_name:
|
||||
return device
|
||||
return sanitized_name
|
||||
|
||||
@staticmethod
|
||||
def get_midi_channels_list():
|
||||
"""
|
||||
For this example, we'll return a static list of 16 MIDI channels.
|
||||
In a more complex setup, you might want to detect available channels or have other logic.
|
||||
"""
|
||||
return [midi_ch_any] + [f"{i}" for i in range(1, 17)]
|
||||
|
||||
@staticmethod
|
||||
def get_channel_index(channel):
|
||||
"""
|
||||
Get the index of a given channel in the MIDI channels list.
|
||||
The channel can be an int or a string.
|
||||
"""
|
||||
midi_channel_option_list = MidiDeviceHandler.get_midi_channels_list()
|
||||
|
||||
# Convert channel to string if it's not, to match the format in the list
|
||||
channel_str = str(channel)
|
||||
|
||||
# Return the index if the channel is in the list, otherwise return None
|
||||
return midi_channel_option_list.index(channel_str) if channel_str in midi_channel_option_list else None
|
|
@ -0,0 +1,186 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
##########################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2024 OpenLP Developers #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
|
||||
"""
|
||||
The :mod:`midi_receiver_service` module contains the MIDI receiver service. This service listens for MIDI events in real
|
||||
time and performs necessary actions.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Union
|
||||
|
||||
from openlp.core.common.mixins import LogMixin, RegistryProperties
|
||||
from openlp.core.common.registry import Registry, RegistryBase
|
||||
from openlp.core.threading import run_thread, make_remove_thread, is_thread_finished
|
||||
from openlp.plugins.midi.lib.handlers_managers.profile_db_manager import get_midi_configuration
|
||||
from openlp.plugins.midi.lib.midi.transmitter import MidiEventTransmitter
|
||||
from openlp.plugins.midi.lib.midi.listener import MidiEventListener
|
||||
from openlp.plugins.midi.lib.handlers_managers.state_event_manager import EventStateManager, dict_diff
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
# TODO: This could be moved to the midi plugin file
|
||||
state_mgr = EventStateManager()
|
||||
|
||||
|
||||
@dataclass
|
||||
class MidiServiceMessage:
|
||||
plugin: Optional[str]
|
||||
key: str
|
||||
value: Union[int, str, dict, list]
|
||||
|
||||
|
||||
class MidiControlService(RegistryBase, RegistryProperties, QtCore.QObject, LogMixin):
|
||||
"""
|
||||
This is the MIDI control service that handles, the states of the MIDI control components and workers.
|
||||
Wrapper around the MIDI listener instance.
|
||||
"""
|
||||
_send_message_signal = QtCore.pyqtSignal(MidiServiceMessage)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""
|
||||
Initialise the MIDI receiver service.
|
||||
"""
|
||||
super(MidiControlService, self).__init__()
|
||||
# TODO: super(MidiControlService, self).__init__(parent)
|
||||
# TODO: do i need to specify the parent here
|
||||
self.listener_worker = None
|
||||
self.transmitter_worker = None
|
||||
self._send_message_signal.connect(self.__handle_message_signal)
|
||||
Registry().register('midi_service_handle', self)
|
||||
self.post_bootstrap = False
|
||||
|
||||
def bootstrap_post_set_up(self):
|
||||
if Registry().get_flag('midi_service_active'):
|
||||
self.start()
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Start the MIDI listener.
|
||||
"""
|
||||
if self.listener_worker is None:
|
||||
self.start_listener()
|
||||
|
||||
if self.transmitter_worker is None:
|
||||
self.transmitter_worker = MidiEventTransmitter()
|
||||
if not self.transmitter_worker.is_disabled():
|
||||
state_mgr.poller_changed.connect(self.handle_poller_signal)
|
||||
# Only hooking poller signals after all UI is available
|
||||
if not self.post_bootstrap:
|
||||
# NOTE: it's simply a matter of not know if the first time, it will be initialized post UI ready
|
||||
Registry().register_function('bootstrap_completion', self.try_poller_hook_signals)
|
||||
self.post_bootstrap = True
|
||||
else:
|
||||
self.try_poller_hook_signals()
|
||||
self.transmitter_worker.reset_device_midi_state()
|
||||
self.initialize_device_state_to_openlp()
|
||||
|
||||
state_mgr.set_variable_control_offset(get_midi_configuration().control_offset) # TODO: a bit of lazy setup
|
||||
state_mgr.set_play_action_type(get_midi_configuration().play_button_is_toggle) # TODO: a bit of lazy setup
|
||||
|
||||
def start_listener(self):
|
||||
if self.listener_worker is None:
|
||||
self.listener_worker = MidiEventListener(on_receive_callback_ref=state_mgr.doEventCallbackSignal.emit)
|
||||
self.listener_worker.transmit_callback = self.initialize_device_state_to_openlp
|
||||
# After initialization figure out the type
|
||||
if (self.listener_worker.__str__() == 'MidiEventListener_worker_v1' or
|
||||
self.listener_worker.__str__() == 'MidiEventListener_worker_NB' or
|
||||
self.listener_worker.__str__() == 'MidiEventListener_INSEPTION'): # TODO TRIPPY
|
||||
make_remove_thread(self.listener_worker.__str__()) # TODO just in case
|
||||
run_thread(self.listener_worker, self.listener_worker.__str__())
|
||||
else:
|
||||
self.listener_worker.start()
|
||||
|
||||
def stop_listener(self):
|
||||
if self.listener_worker:
|
||||
# TODO: this is for testing A/B ... /C/D/E comparison
|
||||
self.listener_worker.stop()
|
||||
if self.listener_worker.__str__() == "MidiEventListener_worker_v1": # NOTE: this will be
|
||||
# MidiEventListener_worker <= the blocking listener
|
||||
make_remove_thread(self.listener_worker.__str__()) # TODO: it never coms here because it hasn't
|
||||
# TODO ugly -- managed to close the thread
|
||||
import time
|
||||
time.sleep(1) # TODO: we block to test if this will allow the thread to close in the mean time
|
||||
if is_thread_finished(self.listener_worker.__str__()):
|
||||
make_remove_thread(self.listener_worker.__str__()) # TODO: it never coms here because it hasn't
|
||||
# TODO ugly -- managed to close the thread
|
||||
# else:
|
||||
# print(f"The [{self.listener_worker.__str__()}] thread is not finished!")
|
||||
# thread_info = Registry().get('application').worker_threads.get
|
||||
# (self.listener_worker.__str__())['thread']
|
||||
# del thread_info
|
||||
# make_remove_thread(self.listener_worker.__str__())
|
||||
# # raise("The thread is not stopping. It's likely blocked!")
|
||||
|
||||
@QtCore.pyqtSlot()
|
||||
def handle_poller_signal(self):
|
||||
if self.transmitter_worker is not None:
|
||||
self.transmitter_worker.handle_state_change(state_mgr.get_state_diff())
|
||||
|
||||
def initialize_device_state_to_openlp(self):
|
||||
state = state_mgr.get_openlp_state()
|
||||
dummy = {event: None for event, _ in state.items()}
|
||||
diff = dict_diff(dummy, state)
|
||||
|
||||
self.transmitter_worker.handle_state_change(diff)
|
||||
|
||||
def send_message(self, message: MidiServiceMessage):
|
||||
# Using a signal to run emission on this thread
|
||||
self._send_message_signal.emit(message)
|
||||
|
||||
def __handle_message_signal(self, message: MidiServiceMessage):
|
||||
if self.listener_worker is not None:
|
||||
self.listener_worker.add_message_to_queues(message)
|
||||
|
||||
def try_poller_hook_signals(self):
|
||||
try:
|
||||
state_mgr.post_initialization_get_state()
|
||||
state_mgr.hook_signals()
|
||||
except Exception:
|
||||
log.error('ERROR: Failed to hook poller signals!')
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Closes the Midi service and detach associated signals
|
||||
"""
|
||||
if not Registry().get_flag('midi_service_active'):
|
||||
return
|
||||
|
||||
try:
|
||||
if not self.transmitter_worker.is_disabled():
|
||||
state_mgr.poller_changed.disconnect(self.handle_poller_signal)
|
||||
state_mgr.unhook_signals()
|
||||
# Close the emitter
|
||||
self.transmitter_worker.close()
|
||||
finally:
|
||||
self.transmitter_worker = None
|
||||
|
||||
try:
|
||||
# Stop the listener
|
||||
self.stop_listener()
|
||||
finally:
|
||||
self.listener_worker = None
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
|
@ -0,0 +1,230 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
##########################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2024 OpenLP Developers #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
"""
|
||||
The :mod:`db` module provides the database and schema that is the backend for the Midi plugin.
|
||||
"""
|
||||
import copy
|
||||
from typing import List, Any
|
||||
from types import SimpleNamespace
|
||||
from openlp.core.db.manager import DBManager
|
||||
from openlp.plugins.midi.lib.types_definitions.define_midi_event_action_mapping import get_default_action_midi_mappings
|
||||
from openlp.plugins.midi.lib.types_definitions.config_profile_orm import (MidiConfigurationProfileDbORM,
|
||||
ProfileNotFoundException,
|
||||
init_schema_midi_plugin_dtb)
|
||||
|
||||
|
||||
def get_default_action_midi_mappings_as_dict() -> dict:
|
||||
# Convert the list to a dictionary
|
||||
mappings_dict = {mapping.mapping_key: mapping for mapping in get_default_action_midi_mappings()}
|
||||
return mappings_dict
|
||||
|
||||
|
||||
class MidiProfileManager:
|
||||
"""
|
||||
MIDI Database Manager to encapsulate database interactions.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialize the MidiDatabaseManager.
|
||||
"""
|
||||
# TODO: is this needed => upgrade_mod=upgrade
|
||||
self.sql_lite_db_manager = DBManager('midi', init_schema=init_schema_midi_plugin_dtb)
|
||||
self.session = self.sql_lite_db_manager.session
|
||||
self.event_action_mappings = get_default_action_midi_mappings_as_dict()
|
||||
# Perform a startup check that the database and the default profile exist
|
||||
self._check_default_profile_at_startup()
|
||||
|
||||
def create_profile(self, profile_name: str) -> None:
|
||||
"""
|
||||
Create a new MIDI profile.
|
||||
|
||||
:param profile_name: The name of the profile to be created.
|
||||
"""
|
||||
MidiConfigurationProfileDbORM.create_profile(self.session, profile_name)
|
||||
|
||||
def delete_profile(self, profile_name: str) -> None:
|
||||
"""
|
||||
Delete a MIDI profile.
|
||||
|
||||
:param profile_name: The name of the profile to be deleted.
|
||||
"""
|
||||
MidiConfigurationProfileDbORM.delete_profile(self.session, profile_name)
|
||||
|
||||
def rename_profile(self, old_name: str, new_name: str) -> None:
|
||||
"""
|
||||
Rename a MIDI profile.
|
||||
|
||||
:param old_name: Current name of the profile.
|
||||
:param new_name: New name for the profile.
|
||||
"""
|
||||
MidiConfigurationProfileDbORM.rename_profile(self.session, old_name, new_name)
|
||||
|
||||
def get_profile_state(self, profile_name: str) -> dict:
|
||||
"""
|
||||
Retrieve all properties for a given MIDI profile.
|
||||
|
||||
:param profile_name: The name of the profile.
|
||||
:return: Dictionary containing all properties of the profile.
|
||||
"""
|
||||
profile = self.session.query(MidiConfigurationProfileDbORM).filter_by(profile=profile_name).first()
|
||||
if not profile:
|
||||
raise ProfileNotFoundException(f"Profile '{profile_name}' not found.")
|
||||
|
||||
# Construct the profile state dictionary. It is more efficient than calling get property individually in a loop
|
||||
profile_state = {}
|
||||
for column in MidiConfigurationProfileDbORM.__table__.columns:
|
||||
profile_state[column.name] = getattr(profile, column.name)
|
||||
# Parse the events to a structure
|
||||
if "event" in column.name:
|
||||
# TODO: consider if we want to have a copy or pass the reference as is
|
||||
orm_str = profile_state[column.name]
|
||||
profile_state[column.name] = self.event_action_mappings[column.name].copy()
|
||||
profile_state[column.name].update_from_orm_string(orm_str)
|
||||
|
||||
return profile_state
|
||||
|
||||
def get_all_profiles(self) -> List[str]:
|
||||
"""
|
||||
Retrieve all the available MIDI profiles.
|
||||
|
||||
:return: List of profile names.
|
||||
"""
|
||||
return MidiConfigurationProfileDbORM.get_all_profiles(self.session)
|
||||
|
||||
def get_selected_profile_name(self) -> str:
|
||||
"""
|
||||
Get the name of the currently selected profile.
|
||||
|
||||
:return: Name of the selected profile.
|
||||
"""
|
||||
for profile in self.get_all_profiles():
|
||||
if self.get_property(profile, 'is_selected_profile'):
|
||||
return profile
|
||||
|
||||
return None
|
||||
|
||||
def set_profile_as_currently_selected(self, currently_selected_profile: str) -> None:
|
||||
"""
|
||||
Set a profile as the currently selected one.
|
||||
|
||||
:param currently_selected_profile: The name of the profile to be set as selected.
|
||||
"""
|
||||
for profile in self.get_all_profiles():
|
||||
self.set_property(profile, 'is_selected_profile', profile == currently_selected_profile)
|
||||
|
||||
def set_property(self, profile_name: str, property_name: str, value: Any) -> None:
|
||||
"""
|
||||
Set a property for a given MIDI profile.
|
||||
|
||||
:param profile_name: The name of the profile.
|
||||
:param property_name: The property name.
|
||||
:param value: The value to set.
|
||||
"""
|
||||
profile = self.session.query(MidiConfigurationProfileDbORM).filter_by(profile=profile_name).first()
|
||||
if not profile:
|
||||
raise ProfileNotFoundException(f"Profile '{profile_name}' not found.")
|
||||
|
||||
if "event" in property_name and not isinstance(value, str):
|
||||
mapping_cpy = self.event_action_mappings[property_name]
|
||||
mapping_cpy.update_from_ui_fields(str(value.midi_type), int(value.midi_data))
|
||||
value = mapping_cpy.export_to_orm_string()
|
||||
|
||||
profile.set_property(property_name, value)
|
||||
self.session.commit()
|
||||
|
||||
def get_property(self, profile_name: str, property_name: str) -> Any:
|
||||
"""
|
||||
Retrieve a property for a given MIDI profile.
|
||||
|
||||
:param profile_name: The name of the profile.
|
||||
:param property_name: The property name.
|
||||
:return: The value of the property.
|
||||
"""
|
||||
profile = self.session.query(MidiConfigurationProfileDbORM).filter_by(profile=profile_name).first()
|
||||
if not profile:
|
||||
raise ProfileNotFoundException(f"Profile '{profile_name}' not found.")
|
||||
|
||||
prop_value = profile.get_property(property_name)
|
||||
if "event" in property_name:
|
||||
# TODO: consider if we want to have a copy or pass the reference as is
|
||||
orm_str = prop_value
|
||||
prop_value = self.event_action_mappings[property_name].copy()
|
||||
prop_value.update_from_orm_string(orm_str)
|
||||
|
||||
return prop_value
|
||||
|
||||
def _check_default_profile_at_startup(self) -> None:
|
||||
"""
|
||||
Check and create the default profile at startup if necessary.
|
||||
"""
|
||||
profile_name = "default"
|
||||
profile = self.session.query(MidiConfigurationProfileDbORM).filter_by(profile=profile_name).first()
|
||||
if not profile:
|
||||
# If no profile is available create a default profile.
|
||||
self.create_profile(profile_name)
|
||||
# Recheck if the reaction of the default profile was successful
|
||||
profile = self.session.query(MidiConfigurationProfileDbORM).filter_by(profile=profile_name).first()
|
||||
if not profile:
|
||||
raise ProfileNotFoundException(f"Profile '{profile_name}' not found.")
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Wrap some of the ORM static get methods
|
||||
@staticmethod
|
||||
def get_midi_config_properties_key_list() -> List[str]:
|
||||
"""
|
||||
Retrieve a list of MIDI configuration property keys.
|
||||
|
||||
:return: List of property keys.
|
||||
"""
|
||||
return MidiConfigurationProfileDbORM.get_midi_config_properties_key_list()
|
||||
|
||||
@staticmethod
|
||||
def get_midi_event_action_key_list() -> List[str]:
|
||||
"""
|
||||
Retrieve a list of MIDI event action keys.
|
||||
|
||||
:return: List of event action keys.
|
||||
"""
|
||||
return MidiConfigurationProfileDbORM.get_midi_event_action_key_list()
|
||||
|
||||
def copy_event_action_mapping_dict(self) -> dict:
|
||||
"""
|
||||
Creates a deep copy of the event action mappings dictionary.
|
||||
|
||||
:return: A deep copy of the event action mappings dictionary.
|
||||
"""
|
||||
return copy.deepcopy(self.event_action_mappings)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# TODO: (commit to design decision) this could also be integrated in the class,
|
||||
# but we will have to pass handles around to access the method0
|
||||
def get_midi_configuration():
|
||||
# Get the midi configuration state
|
||||
profile_manager = MidiProfileManager()
|
||||
selected_profile_name = profile_manager.get_selected_profile_name()
|
||||
# Get the midi configuration state as a dictionary.
|
||||
midi_config = profile_manager.get_profile_state(selected_profile_name)
|
||||
# Convert to a simple obj that can be accessed via dot notation
|
||||
midi_config = SimpleNamespace(**midi_config)
|
||||
return midi_config
|
|
@ -0,0 +1,392 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
##########################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2024 OpenLP Developers #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
from functools import wraps
|
||||
from PyQt5 import QtCore
|
||||
from openlp.core.common.mixins import RegistryProperties
|
||||
from openlp.core.ui.media import is_looping_playback
|
||||
from openlp.plugins.midi.lib.types_definitions.midi_event_action_map import ActionType
|
||||
|
||||
|
||||
def error_handling_decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
print(f"EventStateManager | ERROR: An error occurred in {func.__name__}: {e}")
|
||||
# Handle the error or re-raise
|
||||
# raise
|
||||
return None
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def dict_diff(dict1, dict2):
|
||||
diffs = {}
|
||||
|
||||
def compare(d1, d2, path=''):
|
||||
for key in d1.keys() | d2.keys():
|
||||
if isinstance(d1.get(key), dict) and isinstance(d2.get(key), dict):
|
||||
compare(d1[key], d2[key], path + key + '.')
|
||||
elif d1.get(key) != d2.get(key):
|
||||
full_key = path + key
|
||||
diffs[full_key] = {'from': d1.get(key), 'to': d2.get(key)}
|
||||
|
||||
compare(dict1, dict2)
|
||||
return diffs
|
||||
|
||||
|
||||
class EventStateManager(QtCore.QObject, RegistryProperties):
|
||||
"""
|
||||
Accessed by midi controllers to get status type information from the application
|
||||
"""
|
||||
# Listener callback
|
||||
doEventCallbackSignal = QtCore.pyqtSignal([str, int])
|
||||
# Triggers the Transmission callback
|
||||
poller_changed = QtCore.pyqtSignal()
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Constructor for the midi controller poll builder class.
|
||||
"""
|
||||
super(EventStateManager, self).__init__()
|
||||
self.event_callback_mapping = None
|
||||
self._prev_state = None
|
||||
self._state = None
|
||||
self._variable_control_velocity_offset = 0
|
||||
self._play_action_type = ActionType.TRIGGER
|
||||
self.add_receive_event_callback_mappings()
|
||||
|
||||
self._triggered_ = {event: False for event in
|
||||
['event_item_next', 'event_item_previous', 'event_slide_next', 'event_slide_previous']}
|
||||
|
||||
# Connect signals to respective slots
|
||||
self.doEventCallbackSignal.connect(self._do_event_callback)
|
||||
|
||||
# TODO: i might have to use " live = Registry().get('live_controller') "
|
||||
|
||||
def set_variable_control_offset(self, offset: int = 0):
|
||||
self._variable_control_velocity_offset = offset
|
||||
|
||||
def set_play_action_type(self, action_type: bool):
|
||||
if action_type:
|
||||
self._play_action_type = ActionType.TOGGLE
|
||||
else:
|
||||
self._play_action_type = ActionType.TRIGGER
|
||||
|
||||
# ---------------------------------- Used for event reception ----------------------------------
|
||||
def add_receive_event_callback_mappings(self):
|
||||
# Mapping of event names to their callbacks
|
||||
self.event_callback_mapping = {
|
||||
# Callback Group 1: Screen-related actions
|
||||
'event_screen_show': self.event_screen_show_cb,
|
||||
'event_screen_theme': self.event_screen_theme_cb,
|
||||
'event_screen_blank': self.event_screen_blank_cb,
|
||||
'event_screen_desktop': self.event_screen_desktop_cb,
|
||||
'event_clear_live': self.event_clear_live_cb,
|
||||
|
||||
# Callback Group 2: Video item actions
|
||||
'event_video_play': self.event_video_play_cb,
|
||||
'event_video_pause': self.event_video_pause_cb,
|
||||
'event_video_stop': self.event_video_stop_cb,
|
||||
'event_video_loop': self.event_video_loop_cb,
|
||||
'event_video_seek': self.event_video_seek_cb,
|
||||
'event_video_volume': self.event_video_volume_cb,
|
||||
|
||||
# Callback Group 3: General item actions
|
||||
'event_item_goto': self.event_item_goto_cb,
|
||||
'event_item_next': self.event_item_next_cb,
|
||||
'event_item_previous': self.event_item_previous_cb,
|
||||
|
||||
# Callback Group 4: Slide/Song-specific actions
|
||||
'event_slide_goto': self.event_slide_goto_cb,
|
||||
'event_slide_next': self.event_slide_next_cb,
|
||||
'event_slide_previous': self.event_slide_previous_cb,
|
||||
|
||||
# Callback Group 5: Song-specific transpose actions
|
||||
'event_transpose_up': self.event_transpose_up_cb,
|
||||
'event_transpose_down': self.event_transpose_down_cb
|
||||
}
|
||||
|
||||
def _do_event_callback(self, event_key, value=None):
|
||||
|
||||
# Locate the correct event and call the event
|
||||
callback = self.event_callback_mapping.get(event_key)
|
||||
if callback:
|
||||
callback(value)
|
||||
else:
|
||||
print(f"EventStateManager | No callback found for event key: {event_key}")
|
||||
# Handle the case where no callback is found for the gi
|
||||
|
||||
# ---------------------------------- Used for event transmission ----------------------------------
|
||||
def get_openlp_state(self):
|
||||
LC = self.live_controller
|
||||
MC = self.media_controller
|
||||
|
||||
state = {
|
||||
# Other state changes
|
||||
'counter': LC.slide_count if LC.slide_count else 0,
|
||||
'service_id': self.service_manager.service_id,
|
||||
'chordNotation': self.settings.value('songs/chord notation'),
|
||||
|
||||
# TODO: those sections need to be finished!
|
||||
# TODO: It would be a good to have a test to check if all states are matching
|
||||
# 'twelve': self.settings.value('api/twelve hour'),
|
||||
# 'isSecure': self.settings.value('api/authentication enabled'),
|
||||
|
||||
# MIDI event action Group 1: Screen-related actions
|
||||
'event_screen_show': LC.show_screen.isChecked(),
|
||||
'event_screen_theme': LC.theme_screen.isChecked(),
|
||||
'event_screen_blank': LC.blank_screen.isChecked(),
|
||||
'event_screen_desktop': LC.desktop_screen.isChecked(),
|
||||
'event_clear_live': True if LC.service_item else False,
|
||||
|
||||
# MIDI event action Group 2: Video item actions
|
||||
'event_video_play': LC.media_info.is_playing,
|
||||
'event_video_pause': not LC.media_info.is_playing and LC.controller_type in MC.current_media_players,
|
||||
'event_video_stop': LC.controller_type not in MC.current_media_players,
|
||||
'event_video_loop': self.is_video_loop(),
|
||||
'event_video_seek': self.get_current_video_position(),
|
||||
'event_video_volume': self.get_audio_level(),
|
||||
|
||||
# MIDI event action Group 3: General item actions
|
||||
'event_item_goto': self.get_event_item_goto(),
|
||||
'event_item_next': self._triggered_['event_item_next'],
|
||||
'event_item_previous': self._triggered_['event_item_previous'],
|
||||
|
||||
# MIDI event action Group 4: Slide/Song-specific actions
|
||||
'event_slide_goto': self.live_controller.selected_row + self._variable_control_velocity_offset or 0,
|
||||
'event_slide_next': self._triggered_['event_slide_next'],
|
||||
'event_slide_previous': self._triggered_['event_slide_previous'],
|
||||
|
||||
# Callback Group 5: Song-specific transpose actions
|
||||
# TODO: maybe we need a goto for the transpose
|
||||
'event_transpose_up': None, # TODO: Needs an implementation
|
||||
'event_transpose_reset': None, # TODO: Needs an implementation
|
||||
'event_transpose_down': None, # TODO: Needs an implementation
|
||||
}
|
||||
|
||||
# Reset Triggers
|
||||
self._triggered_ = {event: False for event in
|
||||
['event_item_next', 'event_item_previous', 'event_slide_next', 'event_slide_previous']}
|
||||
|
||||
return state
|
||||
|
||||
def get_state_diff(self): # The internal state variable will not be updated
|
||||
A = self._prev_state.copy()
|
||||
B = self._state.copy()
|
||||
diff = dict_diff(A, B)
|
||||
return diff
|
||||
|
||||
def hook_signals(self):
|
||||
self.live_controller.slidecontroller_changed.connect(self.on_signal_received_for_state_change)
|
||||
self.service_manager.servicemanager_changed.connect(self.on_signal_received_for_state_change)
|
||||
self.media_controller.vlc_live_media_tick.connect(self.on_video_position_change)
|
||||
|
||||
def unhook_signals(self):
|
||||
try:
|
||||
self.live_controller.slidecontroller_changed.disconnect(self.on_signal_received_for_state_change)
|
||||
self.service_manager.servicemanager_changed.disconnect(self.on_signal_received_for_state_change)
|
||||
self.media_controller.vlc_live_media_tick.disconnect(self.on_video_position_change)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def post_initialization_get_state(self):
|
||||
if self._prev_state is None:
|
||||
self._state = self.get_openlp_state()
|
||||
|
||||
@QtCore.pyqtSlot(list)
|
||||
@QtCore.pyqtSlot(str)
|
||||
@QtCore.pyqtSlot()
|
||||
def on_signal_received_for_state_change(self):
|
||||
self._prev_state = self._state.copy()
|
||||
self._state = self.get_openlp_state()
|
||||
self.poller_changed.emit()
|
||||
|
||||
def on_video_position_change(self):
|
||||
if (self.get_current_video_position() != self._state['event_video_seek'] or
|
||||
self.get_audio_level() != self._state['event_video_volume']):
|
||||
# Call a signal only when there is actual change
|
||||
self.on_signal_received_for_state_change()
|
||||
|
||||
# ================================ List of state check callbacks ================================
|
||||
|
||||
def get_current_live_item_type(self): # TODO: will that actually be used or should it be removed
|
||||
"""
|
||||
Get the type of the currently live item.
|
||||
"""
|
||||
live_item = self.live_controller.service_item
|
||||
return type(live_item).__name__ if live_item else None
|
||||
|
||||
@error_handling_decorator
|
||||
def is_video_loop(self):
|
||||
if self.live_controller.service_item and self.live_controller.service_item.name == 'media':
|
||||
return is_looping_playback(self.live_controller)
|
||||
|
||||
@error_handling_decorator
|
||||
def get_current_video_position(self):
|
||||
"""
|
||||
Get the current playback position in milliseconds.
|
||||
|
||||
:return: Current playback position in milliseconds, or -1 if no media is loaded.
|
||||
"""
|
||||
if self.live_controller.service_item and self.live_controller.service_item.name == 'media':
|
||||
# if self.live_controller.vlc_media_player.is_playing():
|
||||
L = self.live_controller.vlc_media_player.get_length()
|
||||
if L < 0:
|
||||
return 0
|
||||
t = self.live_controller.vlc_media_player.get_time()
|
||||
val = 127 * (t / L)
|
||||
val = round(val)
|
||||
return val
|
||||
else:
|
||||
return 0
|
||||
|
||||
def get_audio_level(self):
|
||||
if self.live_controller.service_item and self.live_controller.service_item.name == 'media':
|
||||
volume = self.live_controller.vlc_media_player.audio_get_volume()
|
||||
return volume if volume >= 0 else 0
|
||||
else:
|
||||
return 0
|
||||
|
||||
@error_handling_decorator
|
||||
def get_event_item_goto(self):
|
||||
live_item = self.live_controller.service_item
|
||||
if live_item:
|
||||
for index, item in enumerate(self.service_manager.service_items):
|
||||
if item['service_item'] == live_item:
|
||||
return index + self._variable_control_velocity_offset # Position of live item in the service
|
||||
return 0
|
||||
|
||||
# ================================ List of event action callbacks ================================
|
||||
|
||||
# ---------------------- # Callback Group 1: Screen-related actions ----------------------
|
||||
@error_handling_decorator
|
||||
def event_screen_show_cb(self, vel):
|
||||
self.live_controller.slidecontroller_toggle_display.emit('show')
|
||||
|
||||
@error_handling_decorator
|
||||
def event_screen_theme_cb(self, vel):
|
||||
if vel > 0:
|
||||
self.live_controller.slidecontroller_toggle_display.emit('theme')
|
||||
else:
|
||||
self.live_controller.slidecontroller_toggle_display.emit('show')
|
||||
|
||||
@error_handling_decorator
|
||||
def event_screen_desktop_cb(self, vel):
|
||||
if vel > 0:
|
||||
self.live_controller.slidecontroller_toggle_display.emit('desktop')
|
||||
else:
|
||||
self.live_controller.slidecontroller_toggle_display.emit('show')
|
||||
|
||||
@error_handling_decorator
|
||||
def event_screen_blank_cb(self, vel):
|
||||
# if self.live_controller.service_item and self.live_controller.service_item.name == 'media':
|
||||
if vel > 0:
|
||||
self.live_controller.slidecontroller_toggle_display.emit('blank')
|
||||
else:
|
||||
self.live_controller.slidecontroller_toggle_display.emit('show')
|
||||
|
||||
@error_handling_decorator
|
||||
def event_clear_live_cb(self, vel):
|
||||
self.event_video_stop_cb(vel)
|
||||
self.live_controller.slidecontroller_live_clear.emit()
|
||||
|
||||
# ---------------------- # Callback Group 2: Video item actions ----------------------
|
||||
@error_handling_decorator
|
||||
def event_video_play_cb(self, vel):
|
||||
# This is toggle mode for the play button
|
||||
if self.live_controller.service_item and self.live_controller.service_item.name == 'media':
|
||||
if self._play_action_type == ActionType.TOGGLE and vel == 0:
|
||||
self.live_controller.mediacontroller_live_pause.emit()
|
||||
else:
|
||||
self.live_controller.mediacontroller_live_play.emit()
|
||||
# TODO : consider resetting when not showing from MC.media_reset
|
||||
|
||||
@error_handling_decorator
|
||||
def event_video_pause_cb(self, vel):
|
||||
if self.live_controller.service_item and self.live_controller.service_item.name == 'media':
|
||||
self.live_controller.mediacontroller_live_pause.emit()
|
||||
|
||||
@error_handling_decorator
|
||||
def event_video_stop_cb(self, vel):
|
||||
if self.live_controller.service_item and self.live_controller.service_item.name == 'media':
|
||||
self.live_controller.mediacontroller_live_stop.emit()
|
||||
|
||||
@error_handling_decorator
|
||||
def event_video_loop_cb(self, vel):
|
||||
if self.live_controller.service_item and self.live_controller.service_item.name == 'media':
|
||||
self.media_controller.media_loop(self.live_controller)
|
||||
|
||||
@error_handling_decorator
|
||||
def event_video_seek_cb(self, vel):
|
||||
if self.live_controller.service_item and self.live_controller.service_item.name == 'media':
|
||||
p = (vel - self._variable_control_velocity_offset) / (127 - self._variable_control_velocity_offset)
|
||||
L = self.live_controller.vlc_media_player.get_length()
|
||||
time = round(p * L, None)
|
||||
print(f" => P={p * 100}% L={L} time={time}")
|
||||
self.media_controller.media_seek(self.live_controller, time)
|
||||
|
||||
@error_handling_decorator
|
||||
def event_video_volume_cb(self, vel):
|
||||
if self.live_controller.service_item and self.live_controller.service_item.name == 'media':
|
||||
self.media_controller.media_volume(self.live_controller, vel)
|
||||
|
||||
# ---------------------- # MIDI event action Group 3: General item actions ----------------------
|
||||
@error_handling_decorator
|
||||
def event_item_previous_cb(self, vel):
|
||||
self.service_manager.servicemanager_previous_item.emit()
|
||||
|
||||
@error_handling_decorator
|
||||
def event_item_next_cb(self, vel):
|
||||
self.service_manager.servicemanager_next_item.emit()
|
||||
|
||||
@error_handling_decorator
|
||||
def event_item_goto_cb(self, vel):
|
||||
vel = vel - self._variable_control_velocity_offset
|
||||
self.service_manager.servicemanager_set_item.emit(vel)
|
||||
|
||||
# ---------------------- # MIDI event action Group 4: Slide/Song-specific actions ----------------------
|
||||
@error_handling_decorator
|
||||
def event_slide_previous_cb(self, vel):
|
||||
self.live_controller.slidecontroller_live_previous.emit()
|
||||
|
||||
@error_handling_decorator
|
||||
def event_slide_next_cb(self, vel):
|
||||
self.live_controller.slidecontroller_live_next.emit()
|
||||
|
||||
@error_handling_decorator
|
||||
def event_slide_goto_cb(self, vel):
|
||||
vel = vel - self._variable_control_velocity_offset
|
||||
self.live_controller.slidecontroller_live_set.emit([vel])
|
||||
|
||||
# ---------------------- # Callback Group 5: Song-specific transpose actions ----------------------
|
||||
@error_handling_decorator
|
||||
def event_transpose_up_cb(self, vel):
|
||||
pass
|
||||
|
||||
@error_handling_decorator
|
||||
def event_transpose_reset_cb(self, vel):
|
||||
pass
|
||||
|
||||
@error_handling_decorator
|
||||
def event_transpose_down_cb(self, vel):
|
||||
pass
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
##########################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2024 OpenLP Developers #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
|
@ -0,0 +1,184 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
##########################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2024 OpenLP Developers #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
|
||||
"""
|
||||
The :mod:`midi_receiver_service` module contains the MIDI receiver service. This service listens for MIDI events in real
|
||||
time and performs necessary actions.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import mido
|
||||
from openlp.core.threading import ThreadWorker
|
||||
from openlp.plugins.midi.lib.midi.midi_listener_template import MidiListenerTemplate
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MidiEventListener(MidiListenerTemplate, ThreadWorker):
|
||||
"""
|
||||
A special Qt thread class to allow the MIDI listener to run at the same time as the UI.
|
||||
"""
|
||||
def __init__(self, on_receive_callback_ref):
|
||||
ThreadWorker.__init__(self)
|
||||
self._event_callback = on_receive_callback_ref
|
||||
|
||||
self.restart_listener_flag = False
|
||||
self.exit_listener_flag = False
|
||||
self.listening_is_active_flag = False
|
||||
self.device_not_available_timeout = 1 # wait for that many seconds before rechecking
|
||||
self._BLOCKING_LOOP_ = True # TODO: for testing!!!
|
||||
|
||||
def connect_to_device(self, input_midi_device=None):
|
||||
"""
|
||||
Exits if no MIDI input ports are available or if the selected MIDI input device is not available.
|
||||
Can be flagged to restart, in which case it will stop listening and exit the loop.
|
||||
"""
|
||||
# Check for restart flag early
|
||||
if self.restart_listener_flag:
|
||||
return False
|
||||
|
||||
self.unpacked_midi_configuration()
|
||||
|
||||
# List available MIDI input ports
|
||||
input_ports = MidiEventListener.get_input_midi_device_list()
|
||||
log.info("Available MIDI input ports:", input_ports)
|
||||
|
||||
# If no input ports are available, exit
|
||||
if not input_ports:
|
||||
log.error("No MIDI input ports found.")
|
||||
return False
|
||||
|
||||
# Use the device specified in the profile configuration
|
||||
if input_midi_device is not None:
|
||||
self.midi_config.input_midi_device = input_midi_device
|
||||
|
||||
# Check if the selected MIDI input device is available
|
||||
if self.midi_config.input_midi_device not in input_ports:
|
||||
log.error(f"The selected MIDI input device '{self.midi_config.input_midi_device}' is not available.")
|
||||
return False
|
||||
|
||||
self.listening_is_active_flag = True
|
||||
return True
|
||||
|
||||
def start(self):
|
||||
self.retry_to_connect_and_listen()
|
||||
|
||||
def listen_for_midi_events(self):
|
||||
"""
|
||||
Continuously listens for MIDI events on the selected MIDI input device and processes incoming MIDI messages.
|
||||
Can be flagged to restart, in which case it will stop listening and exit the loop.
|
||||
"""
|
||||
|
||||
if not self.listening_is_active_flag:
|
||||
return
|
||||
|
||||
try:
|
||||
with mido.open_input(self.midi_config.input_midi_device) as inport:
|
||||
self.inport = inport # TODO: we can attempt to close
|
||||
log.info(f"Listening to MIDI port: {inport.name}")
|
||||
print(f"Listening to MIDI port: {inport.name}")
|
||||
if self.restart_listener_flag:
|
||||
self.listening_is_active_flag = False
|
||||
log.info("Stopped listening to MIDI port.")
|
||||
return
|
||||
|
||||
if self._BLOCKING_LOOP_:
|
||||
for message in inport:
|
||||
# Handle the message here
|
||||
self.handle_midi_event(message)
|
||||
if self.restart_listener_flag:
|
||||
break
|
||||
else:
|
||||
while self.listening_is_active_flag and not self.restart_listener_flag:
|
||||
for message in inport.iter_pending():
|
||||
self.handle_midi_event(message)
|
||||
except Exception as e:
|
||||
log.error(f"Error occurred while listening to MIDI port: {e}")
|
||||
finally:
|
||||
self.listening_is_active_flag = False
|
||||
log.info("Stopped listening to MIDI port.")
|
||||
|
||||
def listen_for_a_single_midi_event(self):
|
||||
"""
|
||||
Listen for MIDI events on the selected MIDI input device and return the first incoming MIDI message.
|
||||
"""
|
||||
|
||||
if not self.listening_is_active_flag:
|
||||
return None
|
||||
|
||||
try:
|
||||
with mido.open_input(self.midi_config.input_midi_device) as inport:
|
||||
log.info(f"Listening to MIDI port: {inport.name}")
|
||||
print(f"Listening to MIDI port: {inport.name}")
|
||||
if self._BLOCKING_LOOP_:
|
||||
for message in inport:
|
||||
if self.exit_listener_flag:
|
||||
break # Stop listening if the flag is set
|
||||
return message
|
||||
else:
|
||||
while self.listening_is_active_flag and not self.exit_listener_flag:
|
||||
for message in inport.iter_pending():
|
||||
return message
|
||||
except Exception as e:
|
||||
print(f"Error occurred while listening to MIDI port: {e}") # TODO: cleanup
|
||||
log.error(f"Error occurred while listening to MIDI port: {e}")
|
||||
finally:
|
||||
self.listening_is_active_flag = False
|
||||
log.info("Stopped listening to MIDI port.")
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Request to exit the MIDI listener.
|
||||
"""
|
||||
log.info("Request received to stop the MIDI listener.") # TODO:
|
||||
if self.inport: # TODO: we can attempt to close
|
||||
self.inport.close()
|
||||
self.request_exit()
|
||||
self.quit.emit()
|
||||
self.listening_is_active_flag = False
|
||||
self.send_unblocking_dummy_event()
|
||||
log.info("MIDI listener stop end of method.") # TODO:
|
||||
|
||||
def send_unblocking_dummy_event(self):
|
||||
try:
|
||||
midi_output = mido.open_output(self.midi_config.output_midi_device)
|
||||
try:
|
||||
unblocking_midi_message = mido.Message('active_sensing')
|
||||
midi_output.send(unblocking_midi_message)
|
||||
except Exception as e:
|
||||
log.error(f"Error sending unblocking MIDI message: {e}")
|
||||
finally:
|
||||
midi_output.close() # Ensure the output is closed after sending
|
||||
except Exception as e:
|
||||
if self.midi_config is not None:
|
||||
log.error(f"Error opening MIDI output device '{self.midi_config.output_midi_device}': {e}")
|
||||
else:
|
||||
log.info(f"The midi configuration is empty which caused: {e}")
|
||||
|
||||
def request_restart(self):
|
||||
"""
|
||||
Request to restart the MIDI listener.
|
||||
"""
|
||||
self.restart_listener_flag = True
|
||||
self.send_unblocking_dummy_event()
|
||||
|
||||
def __str__(self):
|
||||
return 'MidiEventListener_worker_v1'
|
|
@ -0,0 +1,33 @@
|
|||
import time
|
||||
from typing import Callable
|
||||
|
||||
from openlp.core.threading import ThreadWorker # , run_thread, make_remove_thread # TODO: might be used, if not cleanup
|
||||
from openlp.plugins.midi.lib.midi.mido import MidiEventListener as Selected_MidiEventListener
|
||||
|
||||
MidiEventListener = Selected_MidiEventListener
|
||||
|
||||
|
||||
# Service <= ThreadWorker Class <= Listener Async class
|
||||
class MidiEventListener_DISABLE(ThreadWorker):
|
||||
def __init__(self, event_callback_reference: Callable):
|
||||
ThreadWorker.__init__(self)
|
||||
self.worker_thread = Selected_MidiEventListener(on_receive_callback_ref=event_callback_reference)
|
||||
self.running = False
|
||||
|
||||
def start(self):
|
||||
self.running = True
|
||||
self.worker_thread.start()
|
||||
while self.running:
|
||||
time.sleep(0.1) # Adjust the sleep time as needed
|
||||
print("Exit WORKER THREAD START METHOD")
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
time.sleep(0.2)
|
||||
self.worker_thread.stop()
|
||||
print("IN WORKER THREAD METHOD STOP ")
|
||||
time.sleep(2)
|
||||
print("IN WORKER THREAD METHOD STOP after pause")
|
||||
|
||||
def __str__(self):
|
||||
return 'MidiEventListener_INSEPTION'
|
|
@ -0,0 +1,193 @@
|
|||
from abc import abstractmethod # ABC, # TODO: abstraction could be used
|
||||
from typing import Callable
|
||||
import time
|
||||
from threading import Thread
|
||||
|
||||
from openlp.plugins.midi.lib.handlers_managers.device_handler import MidiDeviceHandler
|
||||
from openlp.plugins.midi.lib.handlers_managers.profile_db_manager import get_midi_configuration
|
||||
from openlp.plugins.midi.lib.types_definitions.constants import midi_ch_any
|
||||
|
||||
|
||||
class MidiListenerTemplate():
|
||||
def __init__(self, on_receive_callback_ref: Callable):
|
||||
"""
|
||||
Initialize the MIDI listener with a callback for MIDI events.
|
||||
|
||||
:param on_receive_callback_ref: A callback function that will be called with each MIDI event.
|
||||
"""
|
||||
self.thread = None
|
||||
self._event_callback = on_receive_callback_ref
|
||||
self.transmit_callback = None
|
||||
|
||||
# Device connection retry time out
|
||||
self.device_not_available_timeout = 1 # wait for that many seconds before rechecking
|
||||
|
||||
self.listening_is_active_flag = False
|
||||
self.exit_listener_flag = False
|
||||
self.receiver_disabled_flag = False
|
||||
|
||||
# MIDI configuration
|
||||
self.midi_config = None
|
||||
# Shorthand mappings
|
||||
self.mappings = None
|
||||
|
||||
@abstractmethod
|
||||
def connect_to_device(self, device: str) -> bool:
|
||||
"""
|
||||
Establish a connection to a specified MIDI device and port.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def listen_for_a_single_midi_event(self):
|
||||
"""
|
||||
Listen for MIDI events on the selected MIDI input device and return the first incoming MIDI message.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def listen_for_midi_events(self):
|
||||
"""
|
||||
Continuously listen for MIDI messages.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def stop(self):
|
||||
"""
|
||||
Stop listening for MIDI messages.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def stop_from_inside(self):
|
||||
"""
|
||||
Stop listening for MIDI messages but doesn't stop the thread. Used for MIDI mapping assignments.
|
||||
"""
|
||||
pass
|
||||
|
||||
# ======= Methods to inherit =======
|
||||
def is_disabled(self):
|
||||
return self.receiver_disabled_flag
|
||||
|
||||
def _async_pause(self, pause_duration=0.01):
|
||||
"""
|
||||
Pause the thread execution asynchronously for a specified duration.
|
||||
|
||||
:param pause_duration: Duration in seconds for each pause cycle. Default is 0.1 seconds.
|
||||
"""
|
||||
# NOTE: TO AVOID 100% THREAD UTILIZATION DO AN UGLY PAUSE
|
||||
time.sleep(pause_duration)
|
||||
|
||||
@staticmethod
|
||||
def get_input_midi_device_list() -> list:
|
||||
"""
|
||||
Return a list of available MIDI input devices.
|
||||
"""
|
||||
return MidiDeviceHandler.get_input_midi_devices()
|
||||
|
||||
def start(self):
|
||||
self.thread = Thread(target=self.retry_to_connect_and_listen)
|
||||
self.thread.start()
|
||||
|
||||
def get_single_midi_event_thread_start(self):
|
||||
self.thread = Thread(target=self.listen_for_a_single_midi_event)
|
||||
self.thread.start()
|
||||
|
||||
def retry_to_connect_and_listen(self):
|
||||
while not self.connect_to_device() and not self.exit_listener_flag and not self.is_disabled():
|
||||
print(f"MidiListenerTemplate | Retrying to connect to in {self.device_not_available_timeout} seconds...")
|
||||
time.sleep(self.device_not_available_timeout)
|
||||
|
||||
if self.listening_is_active_flag:
|
||||
self.listen_for_midi_events()
|
||||
|
||||
def request_exit(self):
|
||||
self.exit_listener_flag = True
|
||||
|
||||
def request_restart(self):
|
||||
if self.listening_is_active_flag:
|
||||
self.stop() # Stop the current listening process
|
||||
self.start() # Start a new listening process
|
||||
|
||||
def handle_midi_event(self, midi_message):
|
||||
|
||||
# Filter messages based on the selected channel
|
||||
if ((self.midi_config.input_device_channel == midi_ch_any or
|
||||
(midi_message.channel + 1) == self.midi_config.input_device_channel)):
|
||||
|
||||
# Get the MIDI data value, i.e. Note or CC or Program etc.
|
||||
midi_data_1 = self.extract_midi_data1(midi_message)
|
||||
velocity_or_value = self.extract_midi_data2(midi_message)
|
||||
|
||||
# Check for event matches
|
||||
matching_events = []
|
||||
for event_key, event in self.mappings.items():
|
||||
if event.midi_type == midi_message.type and event.midi_data == midi_data_1:
|
||||
matching_events.append(event)
|
||||
|
||||
# Check and validate the matching
|
||||
if len(matching_events) == 1:
|
||||
# Found exactly one matching event
|
||||
print(f"MidiListenerTemplate | MIDI message detected {midi_message}")
|
||||
self._event_callback(matching_events[0].mapping_key, velocity_or_value)
|
||||
# We can just update only if the midi event was recognized
|
||||
if self.midi_config.device_sync:
|
||||
self.transmit_callback()
|
||||
|
||||
if len(matching_events) > 1:
|
||||
# We should never really come here if the mapping is unique as it should be.
|
||||
raise ValueError("MidiListenerTemplate | Multiple events found for MIDI message.")
|
||||
|
||||
def unpacked_midi_configuration(self):
|
||||
self.midi_config = get_midi_configuration()
|
||||
# Extract only the mappings
|
||||
self.mappings = {key: value for key, value in self.midi_config.__dict__.items() if key.startswith("event")}
|
||||
|
||||
# UI labels need to be formatted # TODO: again the direct conversion (ugly)
|
||||
for key, value in self.mappings.items():
|
||||
self.mappings[key].midi_type = value.midi_type.lower().replace(" ", "_")
|
||||
|
||||
if MidiDeviceHandler.should_sanitize_names():
|
||||
self.midi_config.input_midi_device = (
|
||||
MidiDeviceHandler.match_exact_input_device_name(self.midi_config.input_midi_device))
|
||||
|
||||
def extract_midi_data1(self, midi_message) -> int:
|
||||
"""
|
||||
Extracts the relevant MIDI data 1 from the message (e.g. note value, control CC parameter, program number, etc.)
|
||||
"""
|
||||
if hasattr(midi_message, 'note'):
|
||||
return midi_message.note
|
||||
elif hasattr(midi_message, 'control'):
|
||||
return midi_message.control
|
||||
elif hasattr(midi_message, 'program'):
|
||||
return 0 # NOTE: here we return a static value to identify the control type.
|
||||
elif hasattr(midi_message, 'pitch'):
|
||||
return 0 # NOTE: here we return a static value to identify the control type.
|
||||
else:
|
||||
raise NotImplementedError(f'MidiEventListener | MIDI conversion not implemented for {midi_message}.')
|
||||
|
||||
def extract_midi_data2(self, midi_message) -> int:
|
||||
"""
|
||||
Extracts the relevant MIDI data 2 (like note velocity or control CC value ) from a mido message.
|
||||
"""
|
||||
if hasattr(midi_message, 'velocity'):
|
||||
return midi_message.velocity
|
||||
elif hasattr(midi_message, 'value'):
|
||||
return midi_message.value
|
||||
# NOTE: The program and pitch are technically not data2.
|
||||
# However, in terms of control, here it's more useful as such!
|
||||
# Because data2 is the control variable similar to velocity.
|
||||
# TODO: we should probably reflect that in the UI assignment
|
||||
elif hasattr(midi_message, 'program'):
|
||||
return midi_message.program
|
||||
elif hasattr(midi_message, 'pitch'):
|
||||
return midi_message.pitch
|
||||
# Add more cases as needed for different message types
|
||||
else:
|
||||
print(f'MidiEventListener | MIDI conversion not implemented or supported for {midi_message}.')
|
||||
return 0 # Default value for messages without a 'data2' equivalent
|
||||
|
||||
@abstractmethod
|
||||
def __str__(self):
|
||||
return 'MidiEventListenerTemplate'
|
|
@ -0,0 +1,79 @@
|
|||
import mido
|
||||
from openlp.plugins.midi.lib.midi.midi_listener_template import MidiListenerTemplate
|
||||
from openlp.plugins.midi.lib.types_definitions.constants import openlp_midi_device, disabled_midi_device
|
||||
|
||||
|
||||
class MidiEventListener(MidiListenerTemplate):
|
||||
def __init__(self, on_receive_callback_ref):
|
||||
super().__init__(on_receive_callback_ref)
|
||||
self.inport = None
|
||||
self.thread = None
|
||||
self.listening_is_active_flag = False
|
||||
|
||||
def connect_to_device(self, device: str = None) -> bool:
|
||||
self.unpacked_midi_configuration()
|
||||
if device is None:
|
||||
device = self.midi_config.input_midi_device
|
||||
|
||||
if device == disabled_midi_device['input']:
|
||||
self.receiver_disabled_flag = True
|
||||
return False
|
||||
|
||||
# Create and connect to own virtual device
|
||||
if device == openlp_midi_device['gui_label']:
|
||||
try:
|
||||
self.inport = mido.open_input(name=openlp_midi_device['name'], virtual=True) # TODO: hardcode some more
|
||||
print("MidiEventListener | Virtual MIDI device created.")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"MidiEventListener | Failed to create virtual MIDI device. Error: {e}")
|
||||
return False
|
||||
|
||||
try:
|
||||
self.inport = mido.open_input(device) # Assuming 'device' is the name of the MIDI input port
|
||||
self.listening_is_active_flag = True
|
||||
print(f"MidiEventListener | Listening to MIDI port: {self.inport.name}")
|
||||
return True
|
||||
except IOError as e:
|
||||
print(f"MidiEventListener | Failed to connect to {device}. Error {e}")
|
||||
return False
|
||||
|
||||
def listen_for_midi_events(self):
|
||||
# TODO: NOTE itis not tested if this helps mitigate any errors. The try bock might have to be removed.
|
||||
try: # TODO: consider having extra redundancy in the other listener implementations
|
||||
while self.listening_is_active_flag:
|
||||
for msg in self.inport.iter_pending():
|
||||
self.handle_midi_event(msg)
|
||||
self._async_pause()
|
||||
except Exception as e:
|
||||
print(f"MidiEventListener | Error: Midi Receiver error detected {e}")
|
||||
self.request_restart()
|
||||
|
||||
def listen_for_a_single_midi_event(self):
|
||||
while self.listening_is_active_flag:
|
||||
for msg in self.inport.iter_pending():
|
||||
self._event_callback(msg)
|
||||
return # Return after the message is processed
|
||||
self._async_pause(0.001)
|
||||
|
||||
def stop(self):
|
||||
print("MidiEventListener| Request received to stop the MIDI listener.")
|
||||
self.request_exit()
|
||||
self.listening_is_active_flag = False
|
||||
if self.thread and self.thread.is_alive():
|
||||
self.thread.join()
|
||||
if self.inport:
|
||||
self.inport.close()
|
||||
print("MidiEventListener | Stop the MIDI listener. End of method")
|
||||
|
||||
def stop_from_inside(self):
|
||||
print("MidiEventListener | Request received to stop the MIDI listener from INSIDE.")
|
||||
self.request_exit()
|
||||
self.listening_is_active_flag = False
|
||||
self._async_pause(0.005)
|
||||
if self.inport:
|
||||
self.inport.close()
|
||||
print("MidiEventListener | Stop the MIDI listener. End of method from INSIDE")
|
||||
|
||||
def __str__(self):
|
||||
return 'MidiEventListener-MIDO'
|
|
@ -0,0 +1,120 @@
|
|||
import pygame.midi
|
||||
from openlp.plugins.midi.lib.midi.midi_listener_template import MidiListenerTemplate
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
from openlp.plugins.midi.lib.types_definitions.constants import openlp_midi_device, disabled_midi_device
|
||||
|
||||
# Define a simple MIDI message structure similar to Mido
|
||||
MidiMessage = namedtuple('MidiMessage', ['type', 'note', 'velocity', 'channel'])
|
||||
|
||||
|
||||
def convert_pygame_midi_to_mido(pygame_midi_data):
|
||||
"""
|
||||
Convert Pygame MIDI data to a format similar to Mido's Message.
|
||||
This is a basic example and might need adjustments based on the specific MIDI data format.
|
||||
"""
|
||||
status_byte = pygame_midi_data[0][0][0]
|
||||
data_bytes = pygame_midi_data[0][0][1:]
|
||||
|
||||
# Extract MIDI message components
|
||||
channel = status_byte & 0x0F
|
||||
status = status_byte & 0xF0
|
||||
|
||||
# Define MIDI message type based on the status byte
|
||||
if status == 0x90: # Note On
|
||||
message_type = 'note_on'
|
||||
elif status == 0x80: # Note Off
|
||||
message_type = 'note_off'
|
||||
else:
|
||||
message_type = 'unknown'
|
||||
|
||||
# TODO : a full list should be implemented here!!!!!
|
||||
|
||||
# Assuming standard Note On/Off messages with two data bytes: note and velocity
|
||||
note = data_bytes[0] if len(data_bytes) > 0 else None
|
||||
velocity = data_bytes[1] if len(data_bytes) > 1 else None
|
||||
|
||||
return MidiMessage(type=message_type, note=note, velocity=velocity, channel=channel)
|
||||
|
||||
|
||||
class MidiEventListener(MidiListenerTemplate):
|
||||
def __init__(self, on_receive_callback_ref):
|
||||
super().__init__(on_receive_callback_ref)
|
||||
pygame.midi.init()
|
||||
self.midi_in = None
|
||||
self.thread = None
|
||||
self.listening_is_active_flag = False
|
||||
|
||||
def connect_to_device(self, device: str = None):
|
||||
self.unpacked_midi_configuration()
|
||||
if device is None:
|
||||
device = self.midi_config.input_midi_device
|
||||
|
||||
if device == disabled_midi_device['input']:
|
||||
self.receiver_disabled_flag = True
|
||||
return False
|
||||
|
||||
if device == openlp_midi_device['gui_label']: # Replace with your virtual device identifier
|
||||
# Handle virtual device creation
|
||||
# Note: pygame.midi might not support virtual MIDI devices directly
|
||||
print("Virtual MIDI device creation is not supported by pygame.midi.")
|
||||
return False
|
||||
|
||||
device_id = self._get_device_id_by_name(device)
|
||||
if device_id is not None:
|
||||
try:
|
||||
self.midi_in = pygame.midi.Input(device_id)
|
||||
self.listening_is_active_flag = True
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Failed to connect to {device}. Error: {e}")
|
||||
return False
|
||||
else:
|
||||
print(f"Device '{device}' not found.")
|
||||
return False
|
||||
|
||||
def listen_for_midi_events(self):
|
||||
while self.listening_is_active_flag:
|
||||
if self.midi_in.poll():
|
||||
pygame_midi_message = self.midi_in.read(1)
|
||||
self.handle_midi_event(convert_pygame_midi_to_mido(pygame_midi_message))
|
||||
self._async_pause()
|
||||
|
||||
def listen_for_a_single_midi_event(self):
|
||||
while self.listening_is_active_flag:
|
||||
if self.midi_in.poll():
|
||||
pygame_midi_message = self.midi_in.read(1)
|
||||
self._event_callback(convert_pygame_midi_to_mido(pygame_midi_message))
|
||||
return # Return after the message is processed
|
||||
self._async_pause(0.001)
|
||||
|
||||
def stop(self):
|
||||
print("Request received to stop the MIDI listener.")
|
||||
self.request_exit()
|
||||
self.listening_is_active_flag = False
|
||||
if self.thread and self.thread.is_alive():
|
||||
self.thread.join()
|
||||
if self.midi_in:
|
||||
self.midi_in.close()
|
||||
print("Stop the MIDI listener. End of method")
|
||||
|
||||
def stop_from_inside(self):
|
||||
print("Request received to stop the MIDI listener from INSIDE.")
|
||||
self.request_exit()
|
||||
self.listening_is_active_flag = False
|
||||
self._async_pause(0.02)
|
||||
if self.midi_in:
|
||||
self.midi_in.close()
|
||||
print("Stop the MIDI listener. End of method from INSIDE")
|
||||
|
||||
def _get_device_id_by_name(self, name):
|
||||
device_count = pygame.midi.get_count()
|
||||
for i in range(device_count):
|
||||
info = pygame.midi.get_device_info(i)
|
||||
if info[1].decode() == name and info[2] == 1:
|
||||
return i
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
return 'PygameMidiEventListener'
|
|
@ -0,0 +1,124 @@
|
|||
import rtmidi
|
||||
from collections import namedtuple
|
||||
import sys
|
||||
from openlp.plugins.midi.lib.midi.midi_listener_template import MidiListenerTemplate
|
||||
from openlp.plugins.midi.lib.types_definitions.constants import openlp_midi_device, disabled_midi_device
|
||||
|
||||
# Define a simple MIDI message structure similar to Mido
|
||||
MidiMessage = namedtuple('MidiMessage', ['type', 'note', 'velocity', 'channel'])
|
||||
|
||||
|
||||
def convert_rtmidi_to_mido(rtmidi_data):
|
||||
"""
|
||||
Convert rtmidi MIDI data to a format similar to Mido's Message.
|
||||
"""
|
||||
midi_data, _ = rtmidi_data
|
||||
status_byte = midi_data[0]
|
||||
data_bytes = midi_data[1:]
|
||||
|
||||
# Extract MIDI message components
|
||||
channel = status_byte & 0x0F
|
||||
status = status_byte & 0xF0
|
||||
|
||||
# Define MIDI message type based on the status byte
|
||||
if status == 0x90: # Note On
|
||||
message_type = 'note_on'
|
||||
elif status == 0x80: # Note Off
|
||||
message_type = 'note_off'
|
||||
else:
|
||||
message_type = 'unknown'
|
||||
# TODO: here and in py game we have a similar thing.
|
||||
# TODO...: We should unify booth methods and put the in the MIDI definitions class
|
||||
|
||||
# Assuming standard Note On/Off messages with two data bytes: note and velocity
|
||||
note = data_bytes[0] if len(data_bytes) > 0 else None
|
||||
velocity = data_bytes[1] if len(data_bytes) > 1 else None
|
||||
|
||||
return MidiMessage(type=message_type, note=note, velocity=velocity, channel=channel)
|
||||
|
||||
|
||||
class MidiEventListener(MidiListenerTemplate):
|
||||
def __init__(self, on_receive_callback_ref):
|
||||
super().__init__(on_receive_callback_ref)
|
||||
self.midi_in = None
|
||||
self.thread = None
|
||||
self.listening_is_active_flag = False
|
||||
|
||||
def connect_to_device(self, device: str = None):
|
||||
self.unpacked_midi_configuration()
|
||||
if device is None:
|
||||
device = self.midi_config.input_midi_device
|
||||
|
||||
if device == disabled_midi_device['input']:
|
||||
self.receiver_disabled_flag = True
|
||||
return False
|
||||
|
||||
self.midi_in = rtmidi.MidiIn()
|
||||
available_ports = self.midi_in.get_ports()
|
||||
|
||||
# Virtual port creation (if supported)
|
||||
if device == openlp_midi_device['gui_label']:
|
||||
if sys.platform != "win32": # Virtual ports not supported on Windows
|
||||
try:
|
||||
self.midi_in.open_virtual_port(openlp_midi_device['name'])
|
||||
print("Virtual MIDI device created.")
|
||||
self.listening_is_active_flag = True
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Failed to create virtual MIDI device. Error: {e}")
|
||||
return False
|
||||
else:
|
||||
print("Virtual MIDI ports are not supported on Windows.")
|
||||
return False
|
||||
|
||||
# Connect to a physical MIDI device
|
||||
if device in available_ports:
|
||||
try:
|
||||
port_index = available_ports.index(device)
|
||||
self.midi_in.open_port(port_index)
|
||||
self.listening_is_active_flag = True
|
||||
print(f"Listening to MIDI port: {device}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Failed to connect to {device}. Error: {e}")
|
||||
return False
|
||||
else:
|
||||
print(f"Device '{device}' not found.")
|
||||
return False
|
||||
|
||||
def listen_for_midi_events(self):
|
||||
while self.listening_is_active_flag:
|
||||
rtmidi_message = self.midi_in.get_message()
|
||||
if rtmidi_message:
|
||||
self.handle_midi_event(convert_rtmidi_to_mido(rtmidi_message))
|
||||
self._async_pause(1 / 1000) # 1ms
|
||||
|
||||
def listen_for_a_single_midi_event(self):
|
||||
while self.listening_is_active_flag:
|
||||
rtmidi_message = self.midi_in.get_message()
|
||||
if rtmidi_message:
|
||||
self._event_callback(convert_rtmidi_to_mido(rtmidi_message))
|
||||
return # Return after the message is processed
|
||||
self._async_pause(1 / 1000) # 1ms
|
||||
|
||||
def stop(self):
|
||||
print("Request received to stop the MIDI listener.")
|
||||
self.request_exit()
|
||||
self.listening_is_active_flag = False
|
||||
if self.thread and self.thread.is_alive():
|
||||
self.thread.join()
|
||||
if self.midi_in:
|
||||
self.midi_in.close_port()
|
||||
print("Stop the MIDI listener. End of method")
|
||||
|
||||
def stop_from_inside(self):
|
||||
print("Request received to stop the MIDI listener from INSIDE.")
|
||||
self.request_exit()
|
||||
self.listening_is_active_flag = False
|
||||
self._async_pause(0.02)
|
||||
if self.midi_in:
|
||||
self.midi_in.close_port()
|
||||
print("Stop the MIDI listener. End of method from INSIDE")
|
||||
|
||||
def __str__(self):
|
||||
return 'PythonRtMidiEventListener'
|
|
@ -0,0 +1,270 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
##########################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2024 OpenLP Developers #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
|
||||
import logging
|
||||
import time
|
||||
import mido
|
||||
|
||||
from openlp.plugins.midi.lib.handlers_managers.device_handler import MidiDeviceHandler
|
||||
from openlp.plugins.midi.lib.handlers_managers.profile_db_manager import get_midi_configuration
|
||||
from openlp.plugins.midi.lib.types_definitions.constants import disabled_midi_device
|
||||
from openlp.plugins.midi.lib.types_definitions.midi_event_action_map import ActionType
|
||||
|
||||
|
||||
class MidiEventTransmitter:
|
||||
def __init__(self):
|
||||
self.midi_config = None
|
||||
self.midi_output = None
|
||||
self.transmitter_disabled_flag = False
|
||||
self.reset_transmitter_requested_flag = False
|
||||
self.initialize_transmitter()
|
||||
|
||||
def initialize_transmitter(self):
|
||||
"""
|
||||
Initialize or reinitialize the MIDI configuration and output.
|
||||
"""
|
||||
self.midi_config = get_midi_configuration()
|
||||
if self.midi_config.output_midi_device == disabled_midi_device['output']:
|
||||
self.transmitter_disabled_flag = True
|
||||
return
|
||||
self.close_midi_output() # Ensure previous connections are closed
|
||||
self.connect_to_device()
|
||||
|
||||
def is_disabled(self) -> bool:
|
||||
return self.transmitter_disabled_flag
|
||||
|
||||
def connect_to_device(self):
|
||||
"""
|
||||
Connect to the MIDI output device.
|
||||
"""
|
||||
if MidiDeviceHandler.should_sanitize_names():
|
||||
self.midi_config.output_midi_device = (
|
||||
MidiDeviceHandler.match_exact_output_device_name(self.midi_config.output_midi_device))
|
||||
|
||||
try:
|
||||
self.midi_output = mido.open_output(self.midi_config.output_midi_device)
|
||||
logging.info(f"Connected to MIDI output device: {self.midi_config.output_midi_device}")
|
||||
print(f"MidiEventTransmitter | Transmitting on MIDI port: {self.midi_output.name}")
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to connect to MIDI output device: {e}")
|
||||
|
||||
def handle_state_change(self, state_diff):
|
||||
"""
|
||||
Handles the state change by sending appropriate MIDI messages.
|
||||
Checks for reset request before proceeding.
|
||||
"""
|
||||
if self.is_disabled():
|
||||
return
|
||||
|
||||
if self.reset_transmitter_requested_flag:
|
||||
self.initialize_transmitter()
|
||||
self.reset_transmitter_requested_flag = False
|
||||
|
||||
if not self.midi_output:
|
||||
logging.warning("MIDI output device is not connected. Cannot transmit MIDI events.")
|
||||
self.reset_transmitter_requested_flag = True # Set to retry to reconnect when there is new state change
|
||||
return
|
||||
|
||||
print(f"MidiEventTransmitter | State difference {state_diff}")
|
||||
# TODO: Implement the logic to convert state_diff to MIDI messages and send them
|
||||
for event_type in state_diff:
|
||||
midi_message = self.state_change_to_midi_message(event_type, state_diff[event_type])
|
||||
if midi_message:
|
||||
self.send_midi_message(midi_message)
|
||||
|
||||
def state_change_to_midi_message(self, event_type, change):
|
||||
"""
|
||||
Converts a state change to a MIDI message.
|
||||
This method is a placeholder and should be tailored to your application's specific needs.
|
||||
"""
|
||||
# Define variables
|
||||
mapping = None
|
||||
vMax, vMin = 127, 0
|
||||
to_value = change['to']
|
||||
midi_message = None
|
||||
|
||||
# TODO: handle the "ANY" case. Maybe change any to "OMNI" or just send to channel 1
|
||||
# Specify the MIDI channel (0-15 for channels 1-16)
|
||||
_channel = int(self.midi_config.output_device_channel - 1)
|
||||
|
||||
if not ("event_" in event_type):
|
||||
print(f"MidiEventTransmitter | Ignore state [{event_type}]. It is not a mapping.")
|
||||
return midi_message
|
||||
|
||||
try:
|
||||
mapping = getattr(self.midi_config, event_type)
|
||||
except Exception:
|
||||
logging.error(f"Event mapping not found for [{event_type}] !")
|
||||
print(f"MidiEventTransmitter | ERROR: Event mapping not found for [{event_type}]!")
|
||||
|
||||
if mapping:
|
||||
# Get the Message type
|
||||
_midi_type = mapping.midi_type.lower().replace(" ", "_")
|
||||
# The velocity will be handled depending on the type
|
||||
velocity_or_value = vMin
|
||||
if ActionType.TRIGGER == mapping.tx_action_type:
|
||||
velocity_or_value = vMax if to_value else vMin
|
||||
elif ActionType.TOGGLE == mapping.tx_action_type:
|
||||
velocity_or_value = vMax if to_value else vMin
|
||||
elif ActionType.VARIABLE == mapping.tx_action_type:
|
||||
if vMin <= to_value <= vMax:
|
||||
velocity_or_value = to_value
|
||||
else:
|
||||
logging.error(f"Event velocity error for [{event_type}]! Cannot map velocity!")
|
||||
return midi_message
|
||||
|
||||
midi_message = self.create_midi_message(msg_type=_midi_type, channel=_channel,
|
||||
data1=mapping.midi_data, data2=velocity_or_value)
|
||||
|
||||
event_type_disp = event_type.replace('_', ' ')
|
||||
print(f"MidiEventTransmitter | Show {event_type_disp} changed to [{to_value}] with [{midi_message}]!")
|
||||
|
||||
return midi_message
|
||||
|
||||
def create_midi_message(self, msg_type, channel, data1, data2=None):
|
||||
if msg_type in ['note_on', 'note_off']:
|
||||
# Note messages require note and velocity
|
||||
velocity = data2 if data2 is not None else 64 # Default velocity
|
||||
return mido.Message(msg_type, note=data1, velocity=velocity, channel=channel)
|
||||
|
||||
elif msg_type == 'control_change':
|
||||
# Control change messages require control number and value
|
||||
value = data2 if data2 is not None else 0 # Default value
|
||||
return mido.Message(msg_type, control=data1, value=value, channel=channel)
|
||||
|
||||
elif msg_type == 'program_change':
|
||||
# Program change message requires a program number
|
||||
return mido.Message(msg_type, program=data1, channel=channel)
|
||||
|
||||
elif msg_type == 'channel_pressure':
|
||||
# TODO: dormant entry. IGNORE
|
||||
# Channel pressure message requires a pressure value
|
||||
return mido.Message(msg_type, pressure=data1, channel=channel)
|
||||
|
||||
elif msg_type == 'aftertouch':
|
||||
# TODO: dormant entry. IGNORE
|
||||
# Aftertouch message requires note and value
|
||||
value = data2 if data2 is not None else 0 # Default value
|
||||
return mido.Message(msg_type, note=data1, value=value, channel=channel)
|
||||
|
||||
elif msg_type == 'pitchwheel':
|
||||
# Pitchwheel requires one data byte (expressed as a single 14-bit value)
|
||||
return mido.Message(msg_type, pitch=data1, channel=channel)
|
||||
|
||||
elif msg_type == 'polyphonic_aftertouch':
|
||||
# TODO: dormant entry. IGNORE
|
||||
# Polyphonic aftertouch requires note and value
|
||||
value = data2 if data2 is not None else 0 # Default value
|
||||
return mido.Message(msg_type, note=data1, value=value, channel=channel)
|
||||
|
||||
elif msg_type == "..._disabled_...": # TODO: this is the reason we need a mapping method
|
||||
return None
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unsupported MIDI message type: {msg_type}")
|
||||
|
||||
def send_midi_message(self, midi_message):
|
||||
"""
|
||||
Sends the given MIDI message to the output device.
|
||||
"""
|
||||
if self.midi_output:
|
||||
try:
|
||||
self.midi_output.send(midi_message)
|
||||
except Exception as e:
|
||||
# This will catch unexpected device disconnection events
|
||||
print(f"MidiEventTransmitter | Error: Midi message was not successfully transmitted {e}")
|
||||
self.request_reset()
|
||||
|
||||
def request_reset(self):
|
||||
"""
|
||||
Request a reset of the MIDI configuration and output connection.
|
||||
"""
|
||||
self.reset_transmitter_requested_flag = True
|
||||
|
||||
def close_midi_output(self):
|
||||
"""
|
||||
Close the MIDI output connection if it exists.
|
||||
"""
|
||||
if self.midi_output:
|
||||
self.midi_output.close()
|
||||
self.midi_output = None
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Close the MIDI output connection when done.
|
||||
"""
|
||||
self.close_midi_output()
|
||||
|
||||
# -------------------------------------- Device reset methods --------------------------------------
|
||||
def generate_midi_space(self, channels, midi_types, notes_controls, velocities, send=False, store=False, pause=0):
|
||||
all_messages = []
|
||||
|
||||
start_time = time.time()
|
||||
print("MidiEventTransmitter | Starting generate_midi_space method...")
|
||||
|
||||
# This will give a cool effect if we put the velocity more towards the top of the loop nesting
|
||||
for channel in channels:
|
||||
for msg_type in midi_types:
|
||||
for velocity in velocities:
|
||||
for note_control in notes_controls:
|
||||
try:
|
||||
midi_message = self.create_midi_message(
|
||||
msg_type=msg_type,
|
||||
channel=channel,
|
||||
data1=note_control,
|
||||
data2=velocity
|
||||
)
|
||||
if store:
|
||||
all_messages.append(midi_message)
|
||||
if send:
|
||||
# print(midi_message)
|
||||
self.send_midi_message(midi_message)
|
||||
if pause:
|
||||
time.sleep(pause)
|
||||
except ValueError as e:
|
||||
print(f"MidiEventTransmitter | ERROR Skipping unsupported message type or combination: {e}")
|
||||
|
||||
end_time = time.time()
|
||||
elapsed_time = end_time - start_time
|
||||
print(f"MidiEventTransmitter | Completed generate_midi_space method in {elapsed_time:.2f} seconds.")
|
||||
|
||||
return all_messages
|
||||
|
||||
def reset_device_midi_state(self):
|
||||
if self.is_disabled() or not self.midi_output or not self.midi_config.reset_midi_state:
|
||||
return
|
||||
|
||||
# Notes off reset
|
||||
midi_types = ['note_on', 'note_off', 'control_change', 'program_change']
|
||||
channels = [int(self.midi_config.output_device_channel - 1)] # range(16) # MIDI channels 0-15
|
||||
notes_controls = range(128) # MIDI notes/controls 0-127
|
||||
velocities = [0] # Simple reset to zero velocity/data2
|
||||
# Do reset
|
||||
self.generate_midi_space(channels, midi_types, notes_controls, velocities, send=True)
|
||||
|
||||
# Flashy effect
|
||||
midi_types = ['note_on', 'control_change']
|
||||
channels = [int(self.midi_config.output_device_channel - 1)] # range(16) # MIDI channels 0-15
|
||||
notes_controls = range(128) # MIDI notes/controls 0-127
|
||||
velocities = [i for i in range(128) if i % 32 == 0] + [127, 0]
|
||||
# TODO: more fancy options # [0, 127, 0] # range(128) # MIDI velocities 0-127
|
||||
# Do flashy effect
|
||||
self.generate_midi_space(channels, midi_types, notes_controls, velocities, send=True)
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
##########################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2024 OpenLP Developers #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
|
@ -0,0 +1,184 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
##########################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2024 OpenLP Developers #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
"""
|
||||
The :mod:`db` module provides the database and schema that is the backend for the Midi plugin.
|
||||
"""
|
||||
from typing import List, Any
|
||||
# Database imports
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy.orm import Session, declarative_base
|
||||
from sqlalchemy.types import Integer, UnicodeText, Boolean
|
||||
# Local imports
|
||||
from openlp.core.db.helpers import init_db
|
||||
from openlp.plugins.midi.lib.types_definitions.constants import default_midi_device, default_profile_name
|
||||
from openlp.plugins.midi.lib.types_definitions.define_midi_event_action_mapping import get_default_action_midi_mappings
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class ProfileExistsException(Exception):
|
||||
"""
|
||||
Custom exception raised when attempting to create a profile with a name which already exists in the database.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ProfileNotFoundException(Exception):
|
||||
"""
|
||||
Custom exception raised when a specific profile name is not found in the database.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class MidiConfigurationProfileDbORM(Base):
|
||||
"""
|
||||
Represents the database schema for MIDI settings in the Midi plugin.
|
||||
"""
|
||||
__tablename__ = 'midi_settings'
|
||||
|
||||
# Configuration profiles: ID column for referencing
|
||||
id: Column = Column(Integer, primary_key=True)
|
||||
profile: Column = Column(UnicodeText, default=default_profile_name)
|
||||
is_selected_profile: Column = Column(Boolean, default=True) # The profile is selected
|
||||
|
||||
# Device configuration for input & output
|
||||
reset_midi_state: Column = Column(Boolean, default=False)
|
||||
device_sync: Column = Column(Boolean, default=False)
|
||||
deferred_control_mode: Column = Column(Boolean, default=False)
|
||||
play_button_is_toggle: Column = Column(Boolean, default=False) # TODO: rename to => play_action_is_toggle
|
||||
control_offset: Column = Column(Integer, default=0)
|
||||
|
||||
input_midi_device: Column = Column(UnicodeText, default=default_midi_device['input'])
|
||||
input_device_channel: Column = Column(Integer, default=default_midi_device['input_channel'])
|
||||
|
||||
output_midi_device: Column = Column(UnicodeText, default=default_midi_device['output'])
|
||||
output_device_channel: Column = Column(Integer, default=default_midi_device['output_channel'])
|
||||
|
||||
# Dynamically create MIDI event action columns
|
||||
for mapping in get_default_action_midi_mappings():
|
||||
locals()[mapping.mapping_key] = Column(UnicodeText, default=mapping.export_to_orm_string())
|
||||
|
||||
# ------------------------------------------------------------------------------------------
|
||||
|
||||
@classmethod
|
||||
def get_midi_config_properties_key_list(cls) -> List[str]:
|
||||
"""
|
||||
Get a list of property names not related to MIDI events.
|
||||
|
||||
:return: List of other property names.
|
||||
"""
|
||||
return [column.name for column in cls.__table__.columns
|
||||
if not column.name.startswith('event_') and column.name != 'id']
|
||||
|
||||
@classmethod
|
||||
def get_midi_event_action_key_list(cls) -> List[str]:
|
||||
"""
|
||||
Get a list of property names related to MIDI events.
|
||||
|
||||
:return: List of property names related to MIDI events.
|
||||
"""
|
||||
return [column.name for column in cls.__table__.columns if column.name.startswith('event_')]
|
||||
|
||||
def set_property(self, column_name: str, value: Any) -> None:
|
||||
"""
|
||||
Set value for the given column.
|
||||
|
||||
:param column_name: Name of the column.
|
||||
:param value: Value to be set.
|
||||
"""
|
||||
setattr(self, column_name, value)
|
||||
|
||||
# ------------------------------------------------------------------------------------------
|
||||
|
||||
def get_property(self, column_name: str) -> Any:
|
||||
"""
|
||||
Get value for the given column.
|
||||
|
||||
:param column_name: Name of the column.
|
||||
:return: The value of the column.
|
||||
"""
|
||||
return getattr(self, column_name)
|
||||
|
||||
@classmethod
|
||||
def get_all_profiles(cls, session: Session) -> List[str]:
|
||||
"""
|
||||
Get a list of all existing profiles.
|
||||
|
||||
:param session: Database session.
|
||||
:return: List of profile names.
|
||||
"""
|
||||
return [profile.profile for profile in session.query(cls.profile).distinct()]
|
||||
|
||||
@classmethod
|
||||
def create_profile(cls, session: Session, profile_name: str) -> None:
|
||||
"""
|
||||
Create a new profile.
|
||||
|
||||
:param session: Database session.
|
||||
:param profile_name: Name of the profile to be created.
|
||||
"""
|
||||
if session.query(cls).filter_by(profile=profile_name).first():
|
||||
raise ProfileExistsException(f"Profile '{profile_name}' already exists.")
|
||||
new_profile = cls(profile=profile_name)
|
||||
session.add(new_profile)
|
||||
session.commit()
|
||||
|
||||
@classmethod
|
||||
def delete_profile(cls, session: Session, profile_name: str) -> None:
|
||||
"""
|
||||
Delete an existing profile.
|
||||
|
||||
:param session: Database session.
|
||||
:param profile_name: Name of the profile to be deleted.
|
||||
"""
|
||||
profile = session.query(cls).filter_by(profile=profile_name).first()
|
||||
if not profile:
|
||||
raise ProfileNotFoundException(f"Profile '{profile_name}' not found.")
|
||||
session.delete(profile)
|
||||
session.commit()
|
||||
|
||||
@classmethod
|
||||
def rename_profile(cls, session: Session, old_name: str, new_name: str) -> None:
|
||||
"""
|
||||
Rename an existing profile.
|
||||
|
||||
:param session: Database session.
|
||||
:param old_name: Current name of the profile.
|
||||
:param new_name: New name for the profile.
|
||||
"""
|
||||
if session.query(cls).filter_by(profile=new_name).first():
|
||||
raise ProfileExistsException(f"Profile '{new_name}' already exists.")
|
||||
profile = session.query(cls).filter_by(profile=old_name).first()
|
||||
if not profile:
|
||||
raise ProfileNotFoundException(f"Profile '{old_name}' not found.")
|
||||
profile.profile = new_name
|
||||
session.commit()
|
||||
|
||||
|
||||
def init_schema_midi_plugin_dtb(url: str) -> Session:
|
||||
"""
|
||||
Setup the midi database connection and initialise the database schema
|
||||
|
||||
:param url: The database location
|
||||
"""
|
||||
session, metadata = init_db(url, base=Base)
|
||||
metadata.create_all(bind=metadata.bind, checkfirst=True)
|
||||
return session
|
|
@ -0,0 +1,40 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
##########################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2024 OpenLP Developers #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
|
||||
default_profile_name = 'Default Profile'
|
||||
|
||||
disabled_midi_device = dict()
|
||||
disabled_midi_device['input'] = "... (Disabled) No input MIDI device selected ..."
|
||||
disabled_midi_device['output'] = "... (Disabled) No output MIDI device selected ..."
|
||||
|
||||
# TODO: replace the values in the ORM
|
||||
default_midi_device = dict()
|
||||
default_midi_device['input'] = disabled_midi_device['input']
|
||||
default_midi_device['input_channel'] = 15
|
||||
default_midi_device['output'] = disabled_midi_device['output']
|
||||
default_midi_device['output_channel'] = 16
|
||||
|
||||
openlp_midi_device = dict()
|
||||
openlp_midi_device['gui_label'] = "=== OpenLP MIDI Input ==="
|
||||
openlp_midi_device['name'] = 'OpenLP-MIDI-Input'
|
||||
|
||||
assignment_message = "== Waiting for MIDI input =="
|
||||
midi_ch_any = "Any"
|
|
@ -0,0 +1,157 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
##########################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2024 OpenLP Developers #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
|
||||
"""
|
||||
This module defines the default MIDI action mappings for the OpenLP Midi plugin.
|
||||
|
||||
Each mapping is defined as an instance of the MidiActionMapping class. These mappings
|
||||
are used as the default settings for MIDI actions within the application.
|
||||
"""
|
||||
from typing import List
|
||||
|
||||
from openlp.plugins.midi.lib.types_definitions.midi_event_action_map import (MidiActionMapping,
|
||||
ActionType as A, ModuleCategory as C)
|
||||
from openlp.plugins.midi.lib.types_definitions.midi_definitions import MIDI_Def
|
||||
|
||||
# Shorthand alias for message types
|
||||
M = MIDI_Def.map_midi_msg_types_var_to_str_id()
|
||||
|
||||
default_action_midi_mappings = [
|
||||
# Group 1: Screen-related actions
|
||||
MidiActionMapping('event_screen_show', 'Screen Show', C.SCREEN, A.TRIGGER,
|
||||
M.NOTE_ON, 12, 'Toggle the visibility of the screen'),
|
||||
MidiActionMapping('event_screen_theme', 'Screen Theme', C.SCREEN, A.TOGGLE,
|
||||
M.NOTE_ON, 14, 'Toggle the theme screen on or off'),
|
||||
MidiActionMapping('event_screen_blank', 'Screen Blank', C.SCREEN, A.TOGGLE,
|
||||
M.NOTE_ON, 16, 'Toggle blank screen on or off'),
|
||||
MidiActionMapping('event_screen_desktop', 'Screen Desktop', C.SCREEN, A.TOGGLE,
|
||||
M.NOTE_ON, 17, 'Toggle desktop screen on or off'),
|
||||
MidiActionMapping('event_clear_live', 'Clear Live', C.SCREEN, A.TRIGGER,
|
||||
M.NOTE_ON, 18, 'Clear the live to empty'),
|
||||
|
||||
# Group 2: Video item actions
|
||||
MidiActionMapping('event_video_play', 'Video Play', C.VIDEO, A.TRIGGER,
|
||||
M.NOTE_ON, 24, 'Control video playback'),
|
||||
MidiActionMapping('event_video_pause', 'Video Pause', C.VIDEO, A.TRIGGER,
|
||||
M.NOTE_ON, 26, 'Pause video playback'),
|
||||
MidiActionMapping('event_video_stop', 'Video Stop', C.VIDEO, A.TRIGGER,
|
||||
M.NOTE_ON, 28, 'Stop video playback'),
|
||||
MidiActionMapping('event_video_loop', 'Video Loop', C.VIDEO, A.TOGGLE,
|
||||
M.NOTE_ON, 29, 'Toggle video looping'),
|
||||
MidiActionMapping('event_video_seek', 'Video Seek', C.VIDEO, A.VARIABLE,
|
||||
M.NOTE_ON, 31, 'Seek through video with velocity'),
|
||||
MidiActionMapping('event_video_volume', 'Volume Level', C.VIDEO, A.VARIABLE,
|
||||
M.NOTE_ON, 33, 'Adjust volume with velocity'),
|
||||
|
||||
# Group 3: General item actions
|
||||
MidiActionMapping('event_item_previous', 'Item Previous ', C.ITEM, A.TRIGGER,
|
||||
M.NOTE_ON, 36, 'Go to the previous item'),
|
||||
MidiActionMapping('event_item_next', 'Item Next', C.ITEM, A.TRIGGER,
|
||||
M.NOTE_ON, 38, 'Go to the next item'),
|
||||
MidiActionMapping('event_item_goto', 'Item Go to Select', C.ITEM, A.VARIABLE,
|
||||
M.NOTE_ON, 40, 'Go to a specific item with velocity'),
|
||||
|
||||
# Group 4: Slide/Song-specific actions
|
||||
MidiActionMapping('event_slide_previous', 'Slide Previous', C.SLIDE, A.TRIGGER,
|
||||
M.NOTE_ON, 48, 'Go to the previous slide'),
|
||||
MidiActionMapping('event_slide_next', 'Slide Next', C.SLIDE, A.TRIGGER,
|
||||
M.NOTE_ON, 50, 'Go to the next slide'),
|
||||
MidiActionMapping('event_slide_goto', 'Slide Go to Select', C.SLIDE, A.VARIABLE,
|
||||
M.NOTE_ON, 52, 'Go to a specific section with velocity'),
|
||||
|
||||
# Group 5: Song-specific transpose actions
|
||||
MidiActionMapping('event_transpose_down', 'Transpose Down', C.TRANSPOSE, A.VARIABLE,
|
||||
M.NOTE_ON, 60, 'Transpose the song down'),
|
||||
MidiActionMapping('event_transpose_reset', 'Transpose Reset', C.TRANSPOSE, A.VARIABLE,
|
||||
M.NOTE_ON, 61, 'Reset the song transposition'),
|
||||
MidiActionMapping('event_transpose_up', 'Transpose Up', C.TRANSPOSE, A.VARIABLE,
|
||||
M.NOTE_ON, 62, 'Transpose the song up'),
|
||||
]
|
||||
|
||||
|
||||
def get_default_action_midi_mappings() -> List[MidiActionMapping]:
|
||||
"""
|
||||
Returns a list of default MIDI action mappings.
|
||||
|
||||
Each item in the list is an instance of MidiActionMapping representing the default configuration for that action.
|
||||
|
||||
:return: A list of default MIDI action mappings.
|
||||
"""
|
||||
return default_action_midi_mappings
|
||||
|
||||
|
||||
def get_default_action_midi_mappings_as_dict() -> dict[str, MidiActionMapping]:
|
||||
"""
|
||||
Returns a dictionary with the default MIDI action mappings.
|
||||
|
||||
Each key in the dictionary is a mapping key, and the corresponding value is an instance of MidiActionMapping
|
||||
representing the default configuration for that action.
|
||||
|
||||
:return: A dictionary of default MIDI action mappings.
|
||||
"""
|
||||
|
||||
mappings_dict = {mapping.mapping_key: mapping for mapping in default_action_midi_mappings}
|
||||
return mappings_dict
|
||||
|
||||
|
||||
# TODO: this class might not be needed
|
||||
class DefaultMidiActionMappings:
|
||||
"""
|
||||
Class to hold and share default MIDI action mappings across the application.
|
||||
"""
|
||||
|
||||
# Initialize the default action MIDI mappings as static variables
|
||||
_default_action_mappings = get_default_action_midi_mappings()
|
||||
_default_action_mappings_dict = get_default_action_midi_mappings_as_dict()
|
||||
|
||||
@staticmethod
|
||||
def get_mappings() -> List[MidiActionMapping]:
|
||||
"""
|
||||
Returns a list of default MIDI action mappings.
|
||||
|
||||
:return: A list of default MIDI action mappings.
|
||||
"""
|
||||
return DefaultMidiActionMappings._default_action_mappings
|
||||
|
||||
@staticmethod
|
||||
def get_mappings_dict() -> dict[str, MidiActionMapping]:
|
||||
"""
|
||||
Returns a dictionary with the default MIDI action mappings.
|
||||
|
||||
:return: A dictionary of default MIDI action mappings.
|
||||
"""
|
||||
return DefaultMidiActionMappings._default_action_mappings_dict
|
||||
|
||||
@staticmethod
|
||||
def update_mapping(mapping_key: str, new_mapping: MidiActionMapping):
|
||||
"""
|
||||
Updates a specific MIDI action mapping.
|
||||
|
||||
:param mapping_key: The key of the mapping to update.
|
||||
:param new_mapping: The new MidiActionMapping instance.
|
||||
"""
|
||||
if mapping_key in DefaultMidiActionMappings._default_action_mappings_dict:
|
||||
DefaultMidiActionMappings._default_action_mappings_dict[mapping_key] = new_mapping
|
||||
# Update the list as well
|
||||
for i, mapping in enumerate(DefaultMidiActionMappings._default_action_mappings):
|
||||
if mapping.mapping_key == mapping_key:
|
||||
DefaultMidiActionMappings._default_action_mappings[i] = new_mapping
|
||||
break
|
|
@ -0,0 +1,151 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
##########################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2024 OpenLP Developers #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
"""
|
||||
Simple conversion and type listing/enumeration of basic midi properties.
|
||||
"""
|
||||
from types import SimpleNamespace
|
||||
# from enum import Enum # NOTE: would be used for the enum definitions
|
||||
|
||||
|
||||
class MIDI_Def:
|
||||
"""
|
||||
MIDI_states provides utility functions and type definitions for MIDI message handling.
|
||||
It includes methods for converting between MIDI notes and integers, and mappings
|
||||
between different MIDI message representations.
|
||||
"""
|
||||
|
||||
# MIDI C0 to int 0 mapping octave offset. Every octave shift adds +/-12 to the C0 int value
|
||||
midi_map_octave_offset = -2
|
||||
|
||||
# List of musical notes
|
||||
NOTES_LIST = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
|
||||
|
||||
# List of MIDI message types
|
||||
MIDI_MESSAGE_TYPES_LIST = [
|
||||
"... Disabled ...",
|
||||
"Note On",
|
||||
"Note Off",
|
||||
"Control Change",
|
||||
"Program Change",
|
||||
"Pitch Bend Change",
|
||||
# "Channel Pressure", # NOTE: this is disabled because it can't be used practically
|
||||
# "Polyphonic Key Pressure", # NOTE: this is disabled because it can't be used practically
|
||||
# "System Exclusive" # NOTE: this is disabled because it can't be used practically
|
||||
]
|
||||
|
||||
# Namespace for MIDI message types
|
||||
MESSAGE_NAMESPACE = SimpleNamespace(**{
|
||||
msg.replace(" ", "_").upper(): SimpleNamespace(
|
||||
ui_text_label=msg,
|
||||
midi_type=msg.replace(" ", "_").lower()
|
||||
) for msg in MIDI_MESSAGE_TYPES_LIST
|
||||
})
|
||||
|
||||
# Enum for MIDI message types
|
||||
# MESSAGE_ENUM = Enum('MIDIMessageType', {
|
||||
# name: getattr(MESSAGE_NAMESPACE, name)
|
||||
# for name in MESSAGE_NAMESPACE.__dict__
|
||||
# })
|
||||
|
||||
@classmethod
|
||||
def map_midi_msg_types_var_to_str_id(cls) -> SimpleNamespace:
|
||||
"""
|
||||
Maps variable names to MIDI type strings.
|
||||
|
||||
:return: SimpleNamespace mapping variable names to MIDI type strings.
|
||||
"""
|
||||
return SimpleNamespace(**{
|
||||
name: getattr(cls.MESSAGE_NAMESPACE, name).midi_type
|
||||
for name in cls.MESSAGE_NAMESPACE.__dict__
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def map_ui_label_to_midi_type(cls) -> dict:
|
||||
"""
|
||||
Maps UI text labels to MIDI types.
|
||||
|
||||
:return: Dictionary mapping UI text labels to MIDI types.
|
||||
"""
|
||||
return {
|
||||
getattr(cls.MESSAGE_NAMESPACE, name).ui_text_label:
|
||||
getattr(cls.MESSAGE_NAMESPACE, name).midi_type
|
||||
for name in cls.MESSAGE_NAMESPACE.__dict__
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def map_midi_type_to_ui_label(cls) -> dict:
|
||||
"""
|
||||
Maps MIDI types to UI text labels.
|
||||
|
||||
:return: Dictionary mapping MIDI types to UI text labels.
|
||||
"""
|
||||
return {
|
||||
getattr(cls.MESSAGE_NAMESPACE, name).midi_type:
|
||||
getattr(cls.MESSAGE_NAMESPACE, name).ui_text_label
|
||||
for name in cls.MESSAGE_NAMESPACE.__dict__
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def create_notes_namespace(cls) -> SimpleNamespace:
|
||||
"""
|
||||
Creates a SimpleNamespace for notes.
|
||||
|
||||
:return: SimpleNamespace with note names.
|
||||
"""
|
||||
# Implies: SHARP => # => s
|
||||
sharp = "s"
|
||||
return SimpleNamespace(**{
|
||||
note.replace("#", sharp).upper(): note for note in cls.NOTES_LIST
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
def note_to_int(note: str) -> int:
|
||||
"""
|
||||
Converts a MIDI note to an integer.
|
||||
|
||||
:param note: MIDI note as a string.
|
||||
:return: Integer value of the note.
|
||||
:raises ValueError: If the note is invalid.
|
||||
"""
|
||||
note_base = note[:-1].replace('-', '')
|
||||
octave = int(note[-1]) * (-1 if '-' in note else 1) - MIDI_Def.midi_map_octave_offset
|
||||
if note_base in MIDI_Def.NOTES_LIST:
|
||||
return MIDI_Def.NOTES_LIST.index(note_base) + (octave * 12)
|
||||
else:
|
||||
raise ValueError("Invalid note name")
|
||||
|
||||
@staticmethod
|
||||
def int_to_note(note_number: int) -> str:
|
||||
"""
|
||||
Converts an integer to a MIDI note.
|
||||
|
||||
:param note_number: Integer value of the note.
|
||||
:return: MIDI note as a string.
|
||||
:raises ValueError: If the note number is out of the valid range.
|
||||
"""
|
||||
if 0 <= note_number <= 127:
|
||||
note = MIDI_Def.NOTES_LIST[note_number % 12]
|
||||
octave = (note_number // 12) + MIDI_Def.midi_map_octave_offset
|
||||
if octave > 9:
|
||||
raise ValueError("Note number results in an octave higher than 9")
|
||||
return f"{note}{octave}"
|
||||
else:
|
||||
raise ValueError("Note number must be between 0 and 127")
|
|
@ -0,0 +1,177 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
##########################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2024 OpenLP Developers #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
"""
|
||||
MIDI mapping definition that will provide consistent format between the various classes & methods
|
||||
"""
|
||||
import copy
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ActionType(Enum):
|
||||
TRIGGER = 'Trigger'
|
||||
TOGGLE = 'Toggle'
|
||||
VARIABLE = 'Control' # TODO: decide on the best name for velocity/midi data value controlled
|
||||
|
||||
|
||||
class ModuleCategory(Enum):
|
||||
SCREEN = 'Screen'
|
||||
VIDEO = 'Video'
|
||||
ITEM = 'Item'
|
||||
SLIDE = 'Slide'
|
||||
TRANSPOSE = 'Transpose'
|
||||
|
||||
|
||||
# TODO: we need some kind of input validation when the inputs are given. Right now only the managers are
|
||||
# interracting with this class in a fixed manner, hance why extra validation is not compulsory
|
||||
|
||||
|
||||
class MidiActionMapping:
|
||||
"""
|
||||
Represents a mapping of a UI action to a MIDI event.
|
||||
|
||||
:param mapping_key (str): A unique key identifying the mapping.
|
||||
:param ui_action_label (str): Label for the UI action.
|
||||
:param tx_action_type (str): Type of action on transmission (e.g., 'Toggle', 'Control').
|
||||
:param category (str): Category of the action (e.g., 'Screen', 'Video').
|
||||
:param midi_type (str): Type of MIDI event (e.g., 'On/Off', 'Continuous').
|
||||
:param midi_data (int): MIDI data value associated with the event.
|
||||
:param description (str, optional): Description of the action.
|
||||
"""
|
||||
|
||||
def __init__(self, mapping_key: str, ui_action_label: str, category: ModuleCategory, tx_action_type: ActionType,
|
||||
midi_type: str, midi_data: int, description: str = None, is_mappable_in_ui: bool = True):
|
||||
self._mapping_key = mapping_key
|
||||
self._ui_action_label = ui_action_label
|
||||
self._tx_action_type = tx_action_type
|
||||
self._category = category
|
||||
self.midi_type = midi_type
|
||||
self.midi_data = midi_data
|
||||
self.description = description if description else ""
|
||||
self._is_mappable_in_ui = is_mappable_in_ui
|
||||
|
||||
@property
|
||||
def mapping_key(self) -> str:
|
||||
"""Unique key identifying the mapping."""
|
||||
return self._mapping_key
|
||||
|
||||
@property
|
||||
def ui_action_label(self) -> str:
|
||||
"""Label for the UI action."""
|
||||
return self._ui_action_label
|
||||
|
||||
@property
|
||||
def tx_action_type(self) -> ActionType:
|
||||
"""Type of action (e.g., 'Toggle', 'Control')."""
|
||||
return self._tx_action_type
|
||||
|
||||
@property
|
||||
def category(self) -> ModuleCategory:
|
||||
"""Category of the action (e.g., 'Screen', 'Video')."""
|
||||
return self._category
|
||||
|
||||
@property
|
||||
def is_mappable_in_ui(self) -> bool:
|
||||
"""Boolean indicating if the event mapping is mappable/configurable in the UI."""
|
||||
return self._is_mappable_in_ui
|
||||
|
||||
@tx_action_type.setter
|
||||
def tx_action_type(self, new_action_type: ActionType): # TODO: check if this method will be used or not!
|
||||
"""
|
||||
Set a new transmission action type.
|
||||
|
||||
:param new_action_type: The new ActionType to set.
|
||||
"""
|
||||
if not isinstance(new_action_type, ActionType):
|
||||
raise ValueError("Invalid action type. Must be an instance of ActionType.")
|
||||
self._tx_action_type = new_action_type
|
||||
|
||||
def format_ui_action_label(self):
|
||||
# NOTE: We will only add the transmission action type indicator
|
||||
max_space = max(len(action.name) for action in ActionType.__members__.values())
|
||||
space = (max_space - len(self._tx_action_type.value)) + 3 # Adds the extra space
|
||||
total = 0 # TODO may be redundant
|
||||
|
||||
ui_label = f"[{self._tx_action_type.value}]{' ' * space}{self._ui_action_label}"
|
||||
post_space = total - len(ui_label)
|
||||
post_space = post_space if post_space > 0 else 0
|
||||
return f"{ui_label}{' '*post_space}"
|
||||
|
||||
def update_midi_attributes(self, midi_type: str, midi_data: int) -> None:
|
||||
"""
|
||||
Updates the MIDI type and value attributes.
|
||||
|
||||
:param midi_type: The new MIDI type.
|
||||
:param midi_data: The new MIDI data.
|
||||
"""
|
||||
self.midi_type = midi_type
|
||||
self.midi_data = midi_data
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""
|
||||
Converts the MIDI action mapping to a dictionary.
|
||||
|
||||
:return: A dictionary representation of the MIDI action mapping.
|
||||
"""
|
||||
return {
|
||||
'mapping_key': self._mapping_key,
|
||||
'label': self._ui_action_label,
|
||||
'tx_action_type': self._tx_action_type,
|
||||
'category': self._category,
|
||||
'midi_type': self.midi_type,
|
||||
'midi_data': self.midi_data,
|
||||
'description': self.description
|
||||
}
|
||||
|
||||
def update_from_orm_string(self, orm_string: str) -> None:
|
||||
"""
|
||||
Creates a MidiActionMapping instance from an ORM string.
|
||||
|
||||
:param orm_string: The ORM string to parse.
|
||||
"""
|
||||
parts = orm_string.split(':')
|
||||
self.midi_type = str(parts[0])
|
||||
self.midi_data = int(parts[1])
|
||||
|
||||
def update_from_ui_fields(self, midi_type: str, midi_data: int) -> None:
|
||||
"""
|
||||
Updates the MIDI type and value attributes from UI fields.
|
||||
|
||||
:param midi_type: The new MIDI type as a string.
|
||||
:param midi_data: The new MIDI value as an integer.
|
||||
"""
|
||||
self.midi_type = str(midi_type)
|
||||
self.midi_data = int(midi_data)
|
||||
|
||||
def export_to_orm_string(self) -> str:
|
||||
"""
|
||||
Converts the MIDI action mapping to a string format suitable for ORM storage.
|
||||
|
||||
:return: A string representation of the MIDI action mapping for ORM.
|
||||
"""
|
||||
return f"{self.midi_type}:{self.midi_data}"
|
||||
|
||||
def copy(self) -> 'MidiActionMapping':
|
||||
"""
|
||||
Creates a deep copy of this MidiActionMapping instance.
|
||||
|
||||
:return: A deep copy of the MidiActionMapping instance.
|
||||
"""
|
||||
return copy.deepcopy(self)
|
|
@ -0,0 +1,154 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
##########################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2023 OpenLP Developers #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
|
||||
import logging
|
||||
|
||||
from openlp.core.common.i18n import translate
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.lib import build_icon
|
||||
from openlp.core.lib.plugin import Plugin, StringContent
|
||||
from openlp.core.state import State
|
||||
from openlp.core.ui.icons import UiIcons
|
||||
from openlp.core.common.enum import PluginStatus
|
||||
|
||||
from openlp.plugins.midi.forms.midisettingstab import MidiSettingsTab
|
||||
|
||||
from openlp.plugins.midi.lib.handlers_managers.midi_service import MidiControlService
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG) # TODO: remove me!
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(logging.DEBUG) # TODO: remove me!
|
||||
|
||||
midi_settings = {
|
||||
'midi/status': PluginStatus.Inactive,
|
||||
'midi/db type': 'sqlite',
|
||||
'midi/db username': '',
|
||||
'midi/db password': '',
|
||||
'midi/db hostname': '',
|
||||
'midi/db database': ''
|
||||
}
|
||||
midi_icons = {
|
||||
'midi': {'icon': 'mdi.midi'},
|
||||
'midi_port': {'icon': 'mdi.midi-port'}
|
||||
}
|
||||
|
||||
|
||||
class MidiPlugin(Plugin):
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Class __init__ method to Initialize the MIDI plugin.
|
||||
"""
|
||||
log.info('MIDI Plugin loaded')
|
||||
print("---------------------- midi ----------------------") # TODO: for testing/debugging. Remove later
|
||||
super(MidiPlugin, self).__init__('midi', settings_tab_class=MidiSettingsTab)
|
||||
|
||||
# Basic Initializations
|
||||
self.weight = -6
|
||||
UiIcons().load_icons(midi_icons)
|
||||
self.icon_path = UiIcons().midi_port
|
||||
self.icon = build_icon(self.icon_path)
|
||||
self.settings.extend_default_settings(midi_settings)
|
||||
|
||||
# State services
|
||||
State().add_service(self.name, self.weight, is_plugin=True, requires='media_live')
|
||||
# TODO: we need a provision for requires where requires states multiple modules
|
||||
|
||||
# Pre-conditions
|
||||
State().update_pre_conditions(self.name, self.check_pre_conditions())
|
||||
|
||||
Registry().set_flag('midi_service_active', False) # Set to False so that it will wait
|
||||
# Midi event service
|
||||
self.midi_event_handler = MidiControlService(self)
|
||||
|
||||
def check_pre_conditions(self):
|
||||
"""
|
||||
Check the plugin can run.
|
||||
For now, I'm returning True.
|
||||
You may want to add more conditions as needed.
|
||||
"""
|
||||
live_controller_up = Registry().get('live_controller') is not None
|
||||
service_manager_up = Registry().get('service_manager') is not None
|
||||
media_controller_up = Registry().get('media_controller') is not None
|
||||
media_controller_enabled = State().is_module_active('mediacontroller')
|
||||
return live_controller_up and service_manager_up and media_controller_up and media_controller_enabled
|
||||
|
||||
def initialise(self):
|
||||
"""
|
||||
Initialise plugin
|
||||
"""
|
||||
log.info('Midi plugin Initialising')
|
||||
Registry().set_flag('midi_service_active', True)
|
||||
super(MidiPlugin, self).initialise()
|
||||
self.midi_event_handler.start()
|
||||
|
||||
# Register midi control service update callbacks in the configuration panel
|
||||
# Every time the plugin is activated the callbacks will be overwritten.
|
||||
# This is ok, those that are the same will remain the same and new instances will overwrite the old callbacks.
|
||||
|
||||
# Register midi control service update callbacks in the configuration panel
|
||||
MidiSettingsTab.add_listener_callback(listener_id="MidiService_close", callback_type="on_cfg_open",
|
||||
callback=lambda event: self.midi_event_handler.close())
|
||||
MidiSettingsTab.add_listener_callback(listener_id="MidiService_start", callback_type="on_cfg_close",
|
||||
callback=lambda event: self.midi_event_handler.start())
|
||||
|
||||
log.info('MIDI Plugin successfully initialized.')
|
||||
|
||||
def finalise(self):
|
||||
"""
|
||||
Tidy up on exit
|
||||
"""
|
||||
log.info('Midi plugin Finalising')
|
||||
super(MidiPlugin, self).finalise()
|
||||
|
||||
self.midi_event_handler.close()
|
||||
|
||||
Registry().set_flag('midi_service_active', False)
|
||||
log.info('Midi plugin Finalized')
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def about():
|
||||
about_text = translate('MidiPlugin', '<strong>MIDI Plugin</strong>'
|
||||
'<br />The MIDI plugin provides the capability for duplex MIDI control '
|
||||
'with OpenLP.')
|
||||
return about_text
|
||||
|
||||
def set_plugin_text_strings(self):
|
||||
"""
|
||||
Called to define all translatable texts of the plugin.
|
||||
"""
|
||||
# Name PluginList
|
||||
self.text_strings[StringContent.Name] = {
|
||||
'singular': translate('MidiPlugin', 'MIDI Control', 'name singular'),
|
||||
'plural': translate('MidiPlugin', 'MIDI Controls', 'name plural')
|
||||
}
|
||||
# Name for MediaDockManager, SettingsManager
|
||||
self.text_strings[StringContent.VisibleName] = {'title': translate('MidiPlugin', 'MIDI', 'container title')}
|
||||
|
||||
# def exec_settings_dialog(self):
|
||||
# """
|
||||
# Display the MIDI settings dialog.
|
||||
# """
|
||||
# # if not self.midi_settings_dialog:
|
||||
# # self.midi_settings_dialog = MidiSettingsDialog(self.main_window, self)
|
||||
# # self.midi_settings_dialog.exec()
|
||||
# pass
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue