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
2 changes: 1 addition & 1 deletion 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.6
VERSION 1.0.7
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
22 changes: 22 additions & 0 deletions src/app/Application.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@
#include "midi/MidiInputManager.h"
#include "protocol/MixerProtocol.h"
#include "protocol/ProtocolFactory.h"
#include "protocol/allenheath/AllenHeathMidiProtocol.h"
#include "protocol/allenheath/AllenHeathTcpProtocol.h"
#include "protocol/allenheath/GLDProtocol.h"
#include "protocol/digico/DiGiCoProtocol.h"
#include "protocol/discovery/ConsoleDiscoveryService.h"
#include "protocol/discovery/DiscoveredConsole.h"
#include "protocol/discovery/probes/AllenHeathProbeStrategy.h"
Expand Down Expand Up @@ -374,6 +377,25 @@ void Application::connectToMixer(const QString& type, const QString& host, int p
if (!m_mixer)
return;

// the console cannot report which fader law it is in, so hand the driver the
// one the show says the desk is set to
if (auto* midi = dynamic_cast<AllenHeathMidiProtocol*>(m_mixer)) {
midi->setFaderLaw(m_show->mixerConfig().faderLaw == "audio"
? AllenHeathMidiProtocol::FaderLaw::AudioTaper
: AllenHeathMidiProtocol::FaderLaw::LinearTaper);
}

if (auto* gld = dynamic_cast<GLDProtocol*>(m_mixer)) {
gld->setMidiChannel(m_show->mixerConfig().midiChannel);
}

// DiGiCo has no published address map, so the console's own patterns come
// from the show rather than from us
if (auto* digico = dynamic_cast<DiGiCoProtocol*>(m_mixer)) {
const MixerConfig cfg = m_show->mixerConfig();
digico->setTemplates({cfg.oscChannelFader, cfg.oscChannelMute, cfg.oscSceneRecall});
}

setupMixerConnection(type, host, port);
}

Expand Down
36 changes: 31 additions & 5 deletions src/core/Cue.cpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
#include "Cue.h"
#include "DCAMapping.h"
#include <QJsonValue>
#include <algorithm>

namespace OpenMix {

double Cue::dbFromLegacyPosition(double position) {
position = std::clamp(position, 0.0, 1.0);
if (position <= 0.0) {
return NEG_INF_DB;
}
if (position >= 1.0) {
return MAX_DB;
}
constexpr double unity = 0.75;
constexpr double floorDb = -60.0;
if (position < unity) {
return floorDb - (position / unity) * floorDb;
}
return (position - unity) / (1.0 - unity) * MAX_DB;
}

QString cueTypeToString(CueType type) {
switch (type) {
case CueType::Snapshot:
Expand Down Expand Up @@ -409,13 +426,15 @@ QJsonObject Cue::toJson() const {
json["channelProfiles"] = profilesObj;
}

// per-channel level overrides: { "<channel>": level }
// per-channel level overrides: { "<channel>": dB }. Written under a key of its
// own: "channelLevels" holds 0..1 positions in shows written before levels
// were stored in dB, and the two cannot be told apart by value.
if (!m_channelLevels.isEmpty()) {
QJsonObject levelsObj;
for (auto it = m_channelLevels.constBegin(); it != m_channelLevels.constEnd(); ++it) {
levelsObj[QString::number(it.key())] = it.value();
}
json["channelLevels"] = levelsObj;
json["channelLevelsDb"] = levelsObj;
}

// named-position assignments
Expand Down Expand Up @@ -563,12 +582,19 @@ Cue Cue::fromJson(const QJsonObject& json) {
}
}

// per-channel level overrides
if (json.contains("channelLevels")) {
const QJsonObject levelsObj = json["channelLevels"].toObject();
// per-channel level overrides. Shows that store 0..1 positions carry them
// under the old key; converting them is the best that can be done, since what
// a position meant in dB depended on the console it played back on.
if (json.contains("channelLevelsDb")) {
const QJsonObject levelsObj = json["channelLevelsDb"].toObject();
for (auto it = levelsObj.constBegin(); it != levelsObj.constEnd(); ++it) {
cue.m_channelLevels[it.key().toInt()] = it.value().toDouble();
}
} else if (json.contains("channelLevels")) {
const QJsonObject levelsObj = json["channelLevels"].toObject();
for (auto it = levelsObj.constBegin(); it != levelsObj.constEnd(); ++it) {
cue.m_channelLevels[it.key().toInt()] = dbFromLegacyPosition(it.value().toDouble());
}
}

// named-position assignments
Expand Down
33 changes: 24 additions & 9 deletions src/core/Cue.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#pragma once

#include "LevelDb.h"

#include "FadeCurve.h"
#include <QJsonArray>
#include <QJsonObject>
Expand All @@ -23,7 +25,9 @@ struct DCAOverride {
std::optional<bool> mute; // mute state override (nullopt = don't change)
std::optional<QString> label; // label override (nullopt = don't change)

[[nodiscard]] bool hasOverrides() const noexcept { return mute.has_value() || label.has_value(); }
[[nodiscard]] bool hasOverrides() const noexcept {
return mute.has_value() || label.has_value();
}

QJsonObject toJson() const;
[[nodiscard]] static DCAOverride fromJson(const QJsonObject& json);
Expand Down Expand Up @@ -163,7 +167,9 @@ class Cue {
// named-position assignments (channel# -> Position id; resolved at playback
// against the show's PositionLibrary to pan/delay sends)
[[nodiscard]] QMap<int, QString> channelPositions() const { return m_channelPositions; }
void setChannelPositions(const QMap<int, QString>& positions) { m_channelPositions = positions; }
void setChannelPositions(const QMap<int, QString>& positions) {
m_channelPositions = positions;
}
void setChannelPosition(int channel, const QString& positionId);
[[nodiscard]] QString channelPosition(int channel) const {
return m_channelPositions.value(channel);
Expand All @@ -184,12 +190,21 @@ class Cue {
void setChannelProfile(int channel, const QString& slot) { m_channelProfiles[channel] = slot; }
void removeChannelProfile(int channel) { m_channelProfiles.remove(channel); }

// per-channel fader level override (channel -> 0..1)
// per-channel fader level override in dB, so a cue means the same thing on
// every console (see LevelDb.h)
[[nodiscard]] QMap<int, double> channelLevels() const { return m_channelLevels; }
void setChannelLevels(const QMap<int, double>& levels) { m_channelLevels = levels; }
void setChannelLevel(int channel, double level) { m_channelLevels[channel] = level; }
void setChannelLevel(int channel, double dB) { m_channelLevels[channel] = dB; }
void removeChannelLevel(int channel) { m_channelLevels.remove(channel); }

static constexpr double NEG_INF_DB = OpenMix::NEG_INF_DB;
static constexpr double MAX_DB = OpenMix::MAX_DB;

// The law behind a 0..1 fader position: unity at 3/4 travel, +10 dB at the
// top, -60 dB at the bottom of the useful throw. Reads shows that store
// positions rather than dB.
static double dbFromLegacyPosition(double position);

// per-FX-unit mute state (fx unit index -> muted). Sent to the console on fire.
[[nodiscard]] QMap<int, bool> fxMutes() const { return m_fxMutes; }
void setFxMutes(const QMap<int, bool>& mutes) { m_fxMutes = mutes; }
Expand Down Expand Up @@ -285,12 +300,12 @@ class Cue {
QMap<int, QString> m_channelProfiles; // channel -> active profile slot id
QMap<int, double> m_channelLevels; // channel -> fader level override (0..1)

QMap<int, bool> m_fxMutes; // fx unit index -> muted
QList<int> m_snippets; // console snippet indices recalled on fire
QList<int> m_scenes; // console scene numbers recalled on fire
QMap<int, bool> m_fxMutes; // fx unit index -> muted
QList<int> m_snippets; // console snippet indices recalled on fire
QList<int> m_scenes; // console scene numbers recalled on fire
QMap<int, bool> m_channelFX; // channel -> fx active
QString m_color; // display color (hex)
bool m_skip = false; // skip during standby advance
QString m_color; // display color (hex)
bool m_skip = false; // skip during standby advance
};

} // namespace OpenMix
20 changes: 20 additions & 0 deletions src/core/LevelDb.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#pragma once

namespace OpenMix {

// Levels are dB everywhere: in cues, across the protocol API, and into each
// driver, which encodes dB the way its console wants. A 0..1 position is not a
// level until a console applies its fader law, so the same cue would play back
// differently per desk.

// dB at or below this is -inf. JSON cannot carry an infinity and each console
// encodes -inf its own way, so the sentinel is a plain number below every floor.
inline constexpr double NEG_INF_DB = -999.0;

// the top of the throw on every console OpenMix speaks to
inline constexpr double MAX_DB = 10.0;

// the floor levels are clamped to before encoding; under it is -inf
inline constexpr double MIN_DB = -95.0;

} // namespace OpenMix
21 changes: 15 additions & 6 deletions src/core/PlaybackEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "ActorProfileLibrary.h"
#include "CueList.h"
#include "DCAMapping.h"
#include "LevelDb.h"
#include "PlaybackGuard.h"
#include "Position.h"
#include "protocol/MixerCapabilities.h"
Expand Down Expand Up @@ -373,7 +374,7 @@ void PlaybackEngine::applyDCAOverrides(const Cue& cue, const QSet<int>& targetDC

// optional console behaviors, only on the muting edge
if (muting && m_dimDcaFaders)
m_mixer->setDcaFader(dca, 0.0);
m_mixer->setDcaFaderDb(dca, 0.0);
if (muting && m_muteDcaUnassign)
clearDcaFromMembers(dca);
}
Expand Down Expand Up @@ -536,14 +537,21 @@ void PlaybackEngine::driveChannelLevel(int channel, double target, double fadeMs

const double from = m_appliedChannelLevels.value(channel, target);

if (fadeMs > 0.0 && !qFuzzyCompare(from + 1.0, target + 1.0)) {
m_fadeEngine.start(QString("ch:%1").arg(channel), from, target, fadeMs, curve,
// Fades run in dB, but -inf is a sentinel far below the throw: ramping to it
// literally would dive through -900 dB and land as an abrupt cut. Ramp to the
// bottom of the throw instead, and let the last step become -inf.
const double rampFrom = std::max(from, MIN_DB);
const double rampTo = std::max(target, MIN_DB);

if (fadeMs > 0.0 && !qFuzzyCompare(rampFrom + 1.0, rampTo + 1.0)) {
m_fadeEngine.start(QString("ch:%1").arg(channel), rampFrom, rampTo, fadeMs, curve,
[this, channel](double v) {
if (m_mixer)
m_mixer->setChannelFader(channel, v);
m_mixer->setChannelFaderDb(channel,
v <= MIN_DB ? NEG_INF_DB : v);
});
} else {
m_mixer->setChannelFader(channel, target);
m_mixer->setChannelFaderDb(channel, target);
}
m_appliedChannelLevels[channel] = target;
}
Expand Down Expand Up @@ -666,7 +674,8 @@ void PlaybackEngine::verifyCue(int index, const Cue& cue) {
int index;
QStringList drifted;
};
auto acc = std::make_shared<Accumulator>(Accumulator{static_cast<int>(toVerify.size()), index, {}});
auto acc =
std::make_shared<Accumulator>(Accumulator{static_cast<int>(toVerify.size()), index, {}});

for (const QString& path : toVerify) {
const double expected = params.value(path).toDouble();
Expand Down
17 changes: 16 additions & 1 deletion src/core/Show.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ QJsonObject MixerConfig::toJson() const {
json["host"] = host;
json["port"] = port;
json["dcaCount"] = dcaCount;
json["faderLaw"] = faderLaw;
json["midiChannel"] = midiChannel;
json["oscChannelFader"] = oscChannelFader;
json["oscChannelMute"] = oscChannelMute;
json["oscSceneRecall"] = oscSceneRecall;
json["oscReceivePort"] = oscReceivePort;
return json;
}

Expand All @@ -20,12 +26,21 @@ MixerConfig MixerConfig::fromJson(const QJsonObject& json) {
config.host = json["host"].toString();
config.port = json["port"].toInt(10023);
config.dcaCount = json["dcaCount"].toInt(8);
config.faderLaw = json["faderLaw"].toString("linear");
config.midiChannel = json["midiChannel"].toInt(1);
config.oscChannelFader = json["oscChannelFader"].toString();
config.oscChannelMute = json["oscChannelMute"].toString();
config.oscSceneRecall = json["oscSceneRecall"].toString();
config.oscReceivePort = json["oscReceivePort"].toInt(8000);
return config;
}

bool MixerConfig::operator==(const MixerConfig& other) const {
return type == other.type && host == other.host && port == other.port &&
dcaCount == other.dcaCount;
dcaCount == other.dcaCount && faderLaw == other.faderLaw &&
midiChannel == other.midiChannel && oscChannelFader == other.oscChannelFader &&
oscChannelMute == other.oscChannelMute && oscSceneRecall == other.oscSceneRecall &&
oscReceivePort == other.oscReceivePort;
}

Show::Show(QObject* parent)
Expand Down
Loading
Loading