Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.16)

project(OpenMix
VERSION 1.0.5
VERSION 1.0.6
DESCRIPTION "Stage mixing software for live theatre that lets engineers program, automate, and run cues seamlessly across 30+ digital mixing consoles"
LANGUAGES CXX
)
Expand Down Expand Up @@ -178,6 +178,7 @@ set(SOURCES

src/protocol/allenheath/AllenHeathMidiProtocol.cpp
src/protocol/allenheath/SQProtocol.cpp
src/protocol/allenheath/QuProtocol.cpp
src/protocol/allenheath/GLDProtocol.cpp
src/protocol/allenheath/AllenHeathTcpProtocol.cpp
src/protocol/allenheath/AvantisProtocol.cpp
Expand All @@ -195,7 +196,8 @@ set(SOURCES
src/protocol/discovery/ConsoleDiscoveryService.cpp
src/protocol/discovery/probes/BehringerX32ProbeStrategy.cpp
src/protocol/discovery/probes/BehringerWingProbeStrategy.cpp
src/protocol/discovery/probes/YamahaOscProbeStrategy.cpp
src/protocol/discovery/probes/YamahaYsdpProbeStrategy.cpp
src/protocol/discovery/probes/AllenHeathProbeStrategy.cpp

src/ui/ConsoleDiscoveryWidget.cpp

Expand Down Expand Up @@ -299,6 +301,7 @@ set(HEADERS

src/protocol/allenheath/AllenHeathMidiProtocol.h
src/protocol/allenheath/SQProtocol.h
src/protocol/allenheath/QuProtocol.h
src/protocol/allenheath/GLDProtocol.h
src/protocol/allenheath/AllenHeathTcpProtocol.h
src/protocol/allenheath/AvantisProtocol.h
Expand All @@ -317,7 +320,8 @@ set(HEADERS
src/protocol/discovery/ConsoleDiscoveryService.h
src/protocol/discovery/probes/BehringerX32ProbeStrategy.h
src/protocol/discovery/probes/BehringerWingProbeStrategy.h
src/protocol/discovery/probes/YamahaOscProbeStrategy.h
src/protocol/discovery/probes/YamahaYsdpProbeStrategy.h
src/protocol/discovery/probes/AllenHeathProbeStrategy.h

src/ui/ConsoleDiscoveryWidget.h

Expand Down
76 changes: 46 additions & 30 deletions src/app/Application.cpp
Original file line number Diff line number Diff line change
@@ -1,38 +1,40 @@
#include "Application.h"
#include "OscRemoteServer.h"
#include "CuePlayerClient.h"
#include "ScsClient.h"
#include "OscRemoteServer.h"
#include "QLabClient.h"
#include "ReaperClient.h"
#include "ui/MainWindow.h"
#include "ScsClient.h"
#include "core/AppLogger.h"
#include "core/ChannelMonitor.h"
#include "core/ConnectionLogBridge.h"
#include "core/Cue.h"
#include "core/CueList.h"
#include "core/CueZero.h"
#include "core/Position.h"
#include "core/TimecodeTrigger.h"
#include "core/CueValidator.h"
#include "core/CueZero.h"
#include "core/DCAMapping.h"
#include "core/DryRunEngine.h"
#include "core/PlaybackEngine.h"
#include "core/PlaybackGuard.h"
#include "core/PlaybackLogger.h"
#include "core/Position.h"
#include "core/ScribbleController.h"
#include "core/ShortcutManager.h"
#include "core/Show.h"
#include "core/SpareBackup.h"
#include "core/TimecodeTrigger.h"
#include "io/AutosaveManager.h"
#include "io/CrashRecovery.h"
#include "midi/MidiInputManager.h"
#include "protocol/MixerProtocol.h"
#include "protocol/ProtocolFactory.h"
#include "protocol/allenheath/AllenHeathTcpProtocol.h"
#include "protocol/discovery/ConsoleDiscoveryService.h"
#include "protocol/discovery/DiscoveredConsole.h"
#include "protocol/discovery/probes/AllenHeathProbeStrategy.h"
#include "protocol/discovery/probes/BehringerWingProbeStrategy.h"
#include "protocol/discovery/probes/BehringerX32ProbeStrategy.h"
#include "protocol/discovery/probes/YamahaOscProbeStrategy.h"
#include "protocol/discovery/probes/YamahaYsdpProbeStrategy.h"
#include "ui/MainWindow.h"
#include <QDir>
#include <QSettings>
#include <QStandardPaths>
Expand Down Expand Up @@ -65,7 +67,8 @@ Application::Application(QObject* parent) : QObject(parent) {
m_discoveryService = new ConsoleDiscoveryService(this);
m_discoveryService->registerStrategy(std::make_shared<BehringerX32ProbeStrategy>());
m_discoveryService->registerStrategy(std::make_shared<BehringerWingProbeStrategy>());
m_discoveryService->registerStrategy(std::make_shared<YamahaOscProbeStrategy>());
m_discoveryService->registerStrategy(std::make_shared<YamahaYsdpProbeStrategy>());
m_discoveryService->registerStrategy(std::make_shared<AllenHeathProbeStrategy>());

// inbound OSC remote control (QLab / stage-manager)
m_oscRemoteServer = new OscRemoteServer(this);
Expand Down Expand Up @@ -209,13 +212,13 @@ void Application::initialize() {
});
m_oscRemoteServer->loadFromSettings();
if (m_oscRemoteServer->start(m_oscRemoteServer->port())) {
m_appLogger->log(LogLevel::Info, LogSource::System,
QString("OSC remote control listening on port %1")
.arg(m_oscRemoteServer->port()));
m_appLogger->log(
LogLevel::Info, LogSource::System,
QString("OSC remote control listening on port %1").arg(m_oscRemoteServer->port()));
} else {
m_appLogger->log(LogLevel::Warning, LogSource::System,
QString("OSC remote control could not bind port %1")
.arg(m_oscRemoteServer->port()));
m_appLogger->log(
LogLevel::Warning, LogSource::System,
QString("OSC remote control could not bind port %1").arg(m_oscRemoteServer->port()));
}

// outbound QLab/DAW remote: fire a linked QLab cue when a cue executes
Expand Down Expand Up @@ -302,14 +305,13 @@ void Application::initialize() {
// the spare channel.
connect(m_playbackEngine, &PlaybackEngine::cueExecuted, m_show->spareBackup(),
&SpareBackup::onCueFired);
connect(m_show->spareBackup(), &SpareBackup::stateChanged, this,
[this](SpareBackup::State state) {
if (state != SpareBackup::State::Active)
return;
SpareBackup* spare = m_show->spareBackup();
m_playbackEngine->applyBackupSwitch(spare->allocatedChannel(),
spare->spareChannel());
});
connect(
m_show->spareBackup(), &SpareBackup::stateChanged, this, [this](SpareBackup::State state) {
if (state != SpareBackup::State::Active)
return;
SpareBackup* spare = m_show->spareBackup();
m_playbackEngine->applyBackupSwitch(spare->allocatedChannel(), spare->spareChannel());
});
}

void Application::setupMixerConnection(const QString& type, const QString& host, int port) {
Expand Down Expand Up @@ -385,8 +387,18 @@ void Application::connectToDiscoveredConsole(const DiscoveredConsole& console) {
if (!m_mixer)
return;

setupMixerConnection(console.toCapabilities().protocolId,
console.address.toString(),
// pass the discovered firmware to A&H ACE protocols so their firmware-gated
// opcodes match the console exactly
if (auto* ace = dynamic_cast<AllenHeathTcpProtocol*>(m_mixer)) {
ace->setFirmwareVersion(console.firmwareVersion);
const int revIdx = console.firmwareVersion.indexOf("Rev. ");
if (revIdx >= 0) {
ace->setFirmwareRevision(
console.firmwareVersion.mid(revIdx + 5).remove(')').trimmed().toInt());
}
}

setupMixerConnection(console.toCapabilities().protocolId, console.address.toString(),
console.port);
}

Expand Down Expand Up @@ -429,21 +441,25 @@ void Application::startupScan() {

if (!savedHost.isEmpty()) {
auto conn = std::make_shared<QMetaObject::Connection>();
*conn = connect(m_discoveryService, &ConsoleDiscoveryService::consoleDiscovered,
this, [this, savedHost, conn](const DiscoveredConsole& console) {
*conn = connect(m_discoveryService, &ConsoleDiscoveryService::consoleDiscovered, this,
[this, savedHost, conn](const DiscoveredConsole& console) {
if (m_mixer == nullptr && console.address.toString() == savedHost) {
QObject::disconnect(*conn);
connectToDiscoveredConsole(console);
}
});

connect(m_discoveryService, &ConsoleDiscoveryService::scanFinished,
this, [conn]() {
QObject::disconnect(*conn);
}, Qt::SingleShotConnection);
connect(
m_discoveryService, &ConsoleDiscoveryService::scanFinished, this,
[conn]() { QObject::disconnect(*conn); }, Qt::SingleShotConnection);
}

m_discoveryService->startScan(3000);

// broadcast-hostile networks: also probe the last-used console directly
if (!savedHost.isEmpty()) {
m_discoveryService->probeHost(QHostAddress(savedHost));
}
}

void Application::setRecordFadersActive(bool active) {
Expand Down
123 changes: 115 additions & 8 deletions src/core/PlaybackEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "protocol/MixerProtocol.h"
#include <QDateTime>
#include <QRegularExpression>
#include <algorithm>
#include <cmath>
#include <memory>

Expand Down Expand Up @@ -62,7 +63,12 @@ void PlaybackEngine::onCueListReset() {
stop();
}

void PlaybackEngine::setMixer(MixerProtocol* mixer) { m_mixer = mixer; }
void PlaybackEngine::setMixer(MixerProtocol* mixer) {
m_mixer = mixer;
m_appliedChDcaMasks.clear();
m_appliedBusDcaMasks.clear();
m_engineOwnedDcas.clear();
}

void PlaybackEngine::setDCAMapping(DCAMapping* mapping) { m_dcaMapping = mapping; }

Expand Down Expand Up @@ -317,7 +323,7 @@ void PlaybackEngine::executeCueInternal(const Cue& cue) {
// recall console snippets (partial scene recalls)
recallSnippets(cue);

// apply DCA overrides (mute & label only)
applyDCAAssignments(cue, targetDCAs);
applyDCAOverrides(cue, targetDCAs);

setState(PlaybackState::Running);
Expand Down Expand Up @@ -363,14 +369,13 @@ void PlaybackEngine::applyDCAOverrides(const Cue& cue, const QSet<int>& targetDC

if (override.mute.has_value()) {
const bool muting = override.mute.value();
QString path = QString("/dca/%1/mute").arg(dca);
m_mixer->sendParameter(path, muting ? 1 : 0);
m_mixer->setDcaMute(dca, muting);

// optional console behaviors, only on the muting edge
if (muting && m_dimDcaFaders)
m_mixer->sendParameter(QString("/dca/%1/fader").arg(dca), 0.0);
m_mixer->setDcaFader(dca, 0.0);
if (muting && m_muteDcaUnassign)
m_mixer->sendParameter(QString("/dca/%1/assign").arg(dca), 0);
clearDcaFromMembers(dca);
}

if (override.label.has_value()) {
Expand All @@ -379,10 +384,112 @@ void PlaybackEngine::applyDCAOverrides(const Cue& cue, const QSet<int>& targetDC
if (label.length() > caps.maxDCANameLength) {
label = label.left(caps.maxDCANameLength);
}
QString path = QString("/dca/%1/config/name").arg(dca);
m_mixer->sendParameter(path, label);
m_mixer->setDcaName(dca, label);
}
}
}

void PlaybackEngine::applyDCAAssignments(const Cue& cue, const QSet<int>& targetDCAs) {
if (!m_mixer || !m_mixer->isConnected())
return;

const bool hasMapping = cue.hasCustomDCAMapping() || m_dcaMapping;
if (!hasMapping)
return;

const MixerCapabilities& caps = m_mixer->capabilities();

QSet<int> mappedDcas;
for (int d = 1; d <= caps.dcaCount; ++d) {
if (!getChannelsForDCA(cue, d).isEmpty() || !getBusesForDCA(cue, d).isEmpty())
mappedDcas.insert(d);
}

QSet<int> controlled;
quint32 controlledBits = 0;
for (int d : targetDCAs) {
if (d < 1 || d > caps.dcaCount || m_inactiveDcas.contains(d))
continue;
if (mappedDcas.contains(d) || m_engineOwnedDcas.contains(d)) {
controlled.insert(d);
controlledBits |= (1u << (d - 1));
}
}
if (controlled.isEmpty())
return;

const int channelCount = std::min(caps.inputChannels, 32);
const int busCount = std::min(caps.mixBuses, 16);

QHash<int, quint32> chAssign;
QHash<int, quint32> busAssign;
for (int d : controlled) {
const quint32 bit = (1u << (d - 1));
for (int ch : getChannelsForDCA(cue, d)) {
if (ch >= 1 && ch <= channelCount)
chAssign[ch] |= bit;
}
for (int bus : getBusesForDCA(cue, d)) {
if (bus >= 1 && bus <= busCount)
busAssign[bus] |= bit;
}
}

for (int ch = 1; ch <= channelCount; ++ch)
writeDcaMask(ch, chAssign.value(ch, 0u), controlledBits, /*isBus=*/false);
for (int bus = 1; bus <= busCount; ++bus)
writeDcaMask(bus, busAssign.value(bus, 0u), controlledBits, /*isBus=*/true);

m_engineOwnedDcas.unite(controlled);
}

void PlaybackEngine::writeDcaMask(int entity, quint32 assignedBits, quint32 controlledBits,
bool isBus) {
if (!m_mixer)
return;

QHash<int, quint32>& shadow = isBus ? m_appliedBusDcaMasks : m_appliedChDcaMasks;

quint32 base = 0;
bool baseKnown = false;
const std::optional<quint32> live =
isBus ? m_mixer->readBusDcaMask(entity) : m_mixer->readChannelDcaMask(entity);
if (live.has_value()) {
base = live.value();
baseKnown = true;
} else if (shadow.contains(entity)) {
base = shadow.value(entity);
baseKnown = true;
}
if (!baseKnown)
return;

const quint32 newMask = (base & ~controlledBits) | assignedBits;
shadow[entity] = newMask;
if (newMask == base)
return;

if (isBus)
m_mixer->setBusDcaMask(entity, newMask);
else
m_mixer->setChannelDcaMask(entity, newMask);
}

void PlaybackEngine::clearDcaFromMembers(int dca) {
if (!m_mixer || !m_mixer->isConnected() || dca < 1)
return;
const MixerCapabilities& caps = m_mixer->capabilities();
if (dca > caps.dcaCount || m_inactiveDcas.contains(dca))
return;

const quint32 bit = (1u << (dca - 1));
const int channelCount = std::min(caps.inputChannels, 32);
const int busCount = std::min(caps.mixBuses, 16);
for (int ch = 1; ch <= channelCount; ++ch)
writeDcaMask(ch, 0u, bit, /*isBus=*/false);
for (int bus = 1; bus <= busCount; ++bus)
writeDcaMask(bus, 0u, bit, /*isBus=*/true);
m_engineOwnedDcas.insert(dca);
}

void PlaybackEngine::expandChannelProfiles(const Cue& cue) {
Expand Down
6 changes: 6 additions & 0 deletions src/core/PlaybackEngine.h
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ class PlaybackEngine : public QObject {
// first index after 'from' whose cue is not skipped, or -1 at end of list
[[nodiscard]] int nextEnabledIndex(int from) const;
void executeCueInternal(const Cue& cue);
void applyDCAAssignments(const Cue& cue, const QSet<int>& targetDCAs);
void writeDcaMask(int entity, quint32 assignedBits, quint32 controlledBits, bool isBus);
void clearDcaFromMembers(int dca);
void applyDCAOverrides(const Cue& cue, const QSet<int>& targetDCAs);
void executeMacroCue(const Cue& cue);
void executeGoToCue(const Cue& cue);
Expand Down Expand Up @@ -179,6 +182,9 @@ class PlaybackEngine : public QObject {

FadeEngine m_fadeEngine;
QMap<int, double> m_appliedChannelLevels; // last-applied fader per channel (fade source)
QHash<int, quint32> m_appliedChDcaMasks;
QHash<int, quint32> m_appliedBusDcaMasks;
QSet<int> m_engineOwnedDcas;
QList<QPair<int, int>> m_channelGangs; // ganged input-channel pairs
QHash<int, int> m_gangPartners; // channel -> ganged partner (both directions)
bool m_checkMode = false; // soundcheck: GO holds on current cue
Expand Down
Loading
Loading