*.d
*.o
+*.pb.h
+*.pb.cc
*.moc.cpp
ui_aboutdialog.h
ui_audio_expanded_view.h
CXX=g++
+PROTOC=protoc
INSTALL=install
EMBEDDED_BMUSB=no
-PKG_MODULES := Qt5Core Qt5Gui Qt5Widgets Qt5OpenGLExtensions Qt5OpenGL libusb-1.0 movit lua52 libmicrohttpd epoxy x264
+PKG_MODULES := Qt5Core Qt5Gui Qt5Widgets Qt5OpenGLExtensions Qt5OpenGL libusb-1.0 movit lua52 libmicrohttpd epoxy x264 protobuf
CXXFLAGS ?= -O2 -g -Wall # Will be overridden by environment.
CXXFLAGS += -std=gnu++11 -fPIC $(shell pkg-config --cflags $(PKG_MODULES)) -pthread -DMOVIT_SHADER_DIR=\"$(shell pkg-config --variable=shaderdir movit)\" -Idecklink/
OBJS += glwidget.moc.o mainwindow.moc.o vumeter.moc.o lrameter.moc.o correlation_meter.moc.o aboutdialog.moc.o ellipsis_label.moc.o input_mapping_dialog.moc.o nonlinear_fader.moc.o clickable_label.moc.o
# Mixer objects
-AUDIO_MIXER_OBJS = audio_mixer.o alsa_input.o ebu_r128_proc.o stereocompressor.o resampling_queue.o flags.o correlation_measurer.o filter.o disk_space_estimator.o
+AUDIO_MIXER_OBJS = audio_mixer.o alsa_input.o ebu_r128_proc.o stereocompressor.o resampling_queue.o flags.o correlation_measurer.o filter.o input_mapping.o state.pb.o
OBJS += mixer.o pbo_frame_allocator.o context.o ref_counted_frame.o theme.o httpd.o flags.o image_input.o alsa_output.o disk_space_estimator.o $(AUDIO_MIXER_OBJS)
# Streaming and encoding objects
$(CXX) -MMD -MP $(CPPFLAGS) $(CXXFLAGS) -o $@ -c $<
%.o: %.cc
$(CXX) -MMD -MP $(CPPFLAGS) $(CXXFLAGS) -o $@ -c $<
+%.pb.cc %.pb.h : %.proto
+ $(PROTOC) --cpp_out=. $<
%.h: %.ui
uic $< -o $@
apt install qtbase5-dev libqt5opengl5-dev qt5-default pkg-config libmicrohttpd-dev \
libusb-1.0-0-dev liblua5.2-dev libzita-resampler-dev libva-dev \
libavcodec-dev libavformat-dev libswscale-dev libavresample-dev \
- libmovit-dev libegl1-mesa-dev libasound2-dev libx264-dev libbmusb-dev
+ libmovit-dev libegl1-mesa-dev libasound2-dev libx264-dev libbmusb-dev \
+ protobuf-compiler libprotobuf-dev
Exceptions as of October 2016:
#include "alsa_input.h"
#include "audio_mixer.h"
#include "defs.h"
+#include "state.pb.h"
#include <sys/inotify.h>
+#include <algorithm>
#include <functional>
#include <unordered_map>
return devices.size() - 1;
}
+unsigned ALSAPool::create_dead_card(const string &name, const string &info, unsigned num_channels)
+{
+ lock_guard<mutex> lock(mu);
+
+ // See if there are any empty slots. If not, insert one at the end.
+ vector<Device>::iterator free_device =
+ find_if(devices.begin(), devices.end(),
+ [](const Device &device) { return device.state == Device::State::EMPTY; });
+ if (free_device == devices.end()) {
+ devices.push_back(Device());
+ inputs.emplace_back(nullptr);
+ free_device = devices.end() - 1;
+ }
+
+ free_device->state = Device::State::DEAD;
+ free_device->name = name;
+ free_device->info = info;
+ free_device->num_channels = num_channels;
+ free_device->held = true;
+
+ return distance(devices.begin(), free_device);
+}
+
+void ALSAPool::serialize_device(unsigned index, DeviceSpecProto *serialized)
+{
+ lock_guard<mutex> lock(mu);
+ assert(index < devices.size());
+ assert(devices[index].held);
+ serialized->set_type(DeviceSpecProto::ALSA_INPUT);
+ serialized->set_index(index);
+ serialized->set_display_name(devices[index].display_name());
+ serialized->set_alsa_name(devices[index].name);
+ serialized->set_alsa_info(devices[index].info);
+ serialized->set_num_channels(devices[index].num_channels);
+ serialized->set_address(devices[index].address);
+}
+
void ALSAPool::free_card(unsigned index)
{
DeviceSpec spec{InputSourceType::ALSA_INPUT, index};
#include "timebase.h"
class ALSAPool;
+class DeviceSpecProto;
class ALSAInput {
public:
// EMPTY or DEAD state. Only for ALSAInput and for internal use.
void free_card(unsigned index);
+ // Create a new card, mark it immediately as DEAD and hold it.
+ // Returns the new index.
+ unsigned create_dead_card(const std::string &name, const std::string &info, unsigned num_channels);
+
+ // Make a protobuf representation of the given card, so that it can be
+ // matched against at a later stage. For AudioMixer only.
+ // The given card must be held.
+ void serialize_device(unsigned index, DeviceSpecProto *serialized);
+
private:
mutable std::mutex mu;
std::vector<Device> devices; // Under mu.
#include "db.h"
#include "flags.h"
#include "mixer.h"
+#include "state.pb.h"
#include "timebase.h"
using namespace bmusb;
DeviceInfo info;
info.display_name = device.display_name();
info.num_channels = device.num_channels;
+ info.alsa_name = device.name;
+ info.alsa_info = device.info;
+ info.alsa_address = device.address;
devices.insert(make_pair(spec, info));
}
return devices;
device->display_name = name;
}
+void AudioMixer::serialize_device(DeviceSpec device_spec, DeviceSpecProto *device_spec_proto)
+{
+ lock_guard<timed_mutex> lock(audio_mutex);
+ switch (device_spec.type) {
+ case InputSourceType::SILENCE:
+ device_spec_proto->set_type(DeviceSpecProto::SILENCE);
+ break;
+ case InputSourceType::CAPTURE_CARD:
+ device_spec_proto->set_type(DeviceSpecProto::CAPTURE_CARD);
+ device_spec_proto->set_index(device_spec.index);
+ device_spec_proto->set_display_name(video_cards[device_spec.index].display_name);
+ break;
+ case InputSourceType::ALSA_INPUT:
+ alsa_pool.serialize_device(device_spec.index, device_spec_proto);
+ break;
+ }
+}
+
void AudioMixer::set_input_mapping(const InputMapping &new_input_mapping)
{
lock_guard<timed_mutex> lock(audio_mutex);
return alsa_pool.get_card_state(index);
}
+ // See comments on ALSAPool::create_dead_card().
+ DeviceSpec create_dead_card(const std::string &name, const std::string &info, unsigned num_channels)
+ {
+ unsigned dead_card_index = alsa_pool.create_dead_card(name, info, num_channels);
+ return DeviceSpec{InputSourceType::ALSA_INPUT, dead_card_index};
+ }
+
void set_display_name(DeviceSpec device_spec, const std::string &name);
+ // Note: The card should be held (currently this isn't enforced, though).
+ void serialize_device(DeviceSpec device_spec, DeviceSpecProto *device_spec_proto);
+
void set_input_mapping(const InputMapping &input_mapping);
InputMapping get_input_mapping() const;
--- /dev/null
+#include <stdio.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include <google/protobuf/text_format.h>
+#include <google/protobuf/io/zero_copy_stream.h>
+#include <google/protobuf/io/zero_copy_stream_impl.h>
+
+#include "audio_mixer.h"
+#include "input_mapping.h"
+#include "state.pb.h"
+
+using namespace std;
+using namespace google::protobuf;
+
+bool save_input_mapping_to_file(const map<DeviceSpec, DeviceInfo> &devices, const InputMapping &input_mapping, const string &filename)
+{
+ InputMappingProto mapping_proto;
+ {
+ map<DeviceSpec, unsigned> used_devices;
+ for (const InputMapping::Bus &bus : input_mapping.buses) {
+ if (!used_devices.count(bus.device)) {
+ used_devices.emplace(bus.device, used_devices.size());
+ global_audio_mixer->serialize_device(bus.device, mapping_proto.add_device());
+ }
+
+ BusProto *bus_proto = mapping_proto.add_bus();
+ bus_proto->set_name(bus.name);
+ bus_proto->set_device_index(used_devices[bus.device]);
+ bus_proto->set_source_channel_left(bus.source_channel[0]);
+ bus_proto->set_source_channel_right(bus.source_channel[1]);
+ }
+ }
+
+ // Save to disk. We use the text format because it's friendlier
+ // for a user to look at and edit.
+ int fd = open(filename.c_str(), O_WRONLY | O_TRUNC | O_CREAT, 0666);
+ if (fd == -1) {
+ perror(filename.c_str());
+ return false;
+ }
+ io::FileOutputStream output(fd); // Takes ownership of fd.
+ if (!TextFormat::Print(mapping_proto, &output)) {
+ // TODO: Don't overwrite the old file (if any) on error.
+ output.Close();
+ return false;
+ }
+
+ output.Close();
+ return true;
+}
+
+bool load_input_mapping_from_file(const map<DeviceSpec, DeviceInfo> &devices, const string &filename, InputMapping *new_mapping)
+{
+ // Read and parse the protobuf from disk.
+ int fd = open(filename.c_str(), O_RDONLY);
+ if (fd == -1) {
+ perror(filename.c_str());
+ return false;
+ }
+ io::FileInputStream input(fd); // Takes ownership of fd.
+ InputMappingProto mapping_proto;
+ if (!TextFormat::Parse(&input, &mapping_proto)) {
+ input.Close();
+ return false;
+ }
+ input.Close();
+
+ // Map devices in the proto to our current ones:
+
+ // Get a list of all active devices.
+ set<DeviceSpec> remaining_devices;
+ for (const auto &device_spec_and_info : devices) {
+ remaining_devices.insert(device_spec_and_info.first);
+ }
+
+ // Now look at every device in the serialized protobuf and try to map
+ // it to one device we haven't taken yet. This isn't a full maximal matching,
+ // but it's good enough for our uses.
+ vector<DeviceSpec> device_mapping;
+ for (unsigned device_index = 0; device_index < unsigned(mapping_proto.device_size()); ++device_index) {
+ const DeviceSpecProto &device_proto = mapping_proto.device(device_index);
+ switch (device_proto.type()) {
+ case DeviceSpecProto::SILENCE:
+ device_mapping.push_back(DeviceSpec{InputSourceType::SILENCE, 0});
+ break;
+ case DeviceSpecProto::CAPTURE_CARD: {
+ // First see if there's a card that matches on both index and name.
+ DeviceSpec spec{InputSourceType::CAPTURE_CARD, unsigned(device_proto.index())};
+ assert(devices.count(spec));
+ const DeviceInfo &dev = devices.find(spec)->second;
+ if (remaining_devices.count(spec) &&
+ dev.display_name == device_proto.display_name()) {
+ device_mapping.push_back(spec);
+ remaining_devices.erase(spec);
+ goto found_capture_card;
+ }
+
+ // Scan and see if there's a match on name alone.
+ for (const DeviceSpec &spec : remaining_devices) {
+ if (spec.type == InputSourceType::CAPTURE_CARD &&
+ dev.display_name == device_proto.display_name()) {
+ device_mapping.push_back(spec);
+ remaining_devices.erase(spec);
+ goto found_capture_card;
+ }
+ }
+
+ // OK, see if at least the index is free.
+ if (remaining_devices.count(spec)) {
+ device_mapping.push_back(spec);
+ remaining_devices.erase(spec);
+ goto found_capture_card;
+ }
+
+ // Give up.
+ device_mapping.push_back(DeviceSpec{InputSourceType::SILENCE, 0});
+found_capture_card:
+ break;
+ }
+ case DeviceSpecProto::ALSA_INPUT: {
+ // For ALSA, we don't really care about index, but we can use address
+ // in its place.
+
+ // First see if there's a card that matches on name, num_channels and address.
+ for (const DeviceSpec &spec : remaining_devices) {
+ assert(devices.count(spec));
+ const DeviceInfo &dev = devices.find(spec)->second;
+ if (spec.type == InputSourceType::ALSA_INPUT &&
+ dev.alsa_name == device_proto.alsa_name() &&
+ dev.alsa_info == device_proto.alsa_info() &&
+ int(dev.num_channels) == device_proto.num_channels() &&
+ dev.alsa_address == device_proto.address()) {
+ device_mapping.push_back(spec);
+ remaining_devices.erase(spec);
+ goto found_alsa_input;
+ }
+ }
+
+ // Looser check: Ignore the address.
+ for (const DeviceSpec &spec : remaining_devices) {
+ assert(devices.count(spec));
+ const DeviceInfo &dev = devices.find(spec)->second;
+ if (spec.type == InputSourceType::ALSA_INPUT &&
+ dev.alsa_name == device_proto.alsa_name() &&
+ dev.alsa_info == device_proto.alsa_info() &&
+ int(dev.num_channels) == device_proto.num_channels()) {
+ device_mapping.push_back(spec);
+ remaining_devices.erase(spec);
+ goto found_alsa_input;
+ }
+ }
+
+ // OK, so we couldn't map this to a device, but perhaps one is added
+ // at some point in the future through hotplug. Create a dead card
+ // matching this one; right now, it will give only silence,
+ // but it could be replaced with something later.
+ //
+ // NOTE: There's a potential race condition here, if the card
+ // gets inserted while we're doing the device remapping
+ // (or perhaps more realistically, while we're reading the
+ // input mapping from disk).
+ DeviceSpec dead_card_spec;
+ dead_card_spec = global_audio_mixer->create_dead_card(
+ device_proto.alsa_name(), device_proto.alsa_info(), device_proto.num_channels());
+ device_mapping.push_back(dead_card_spec);
+
+found_alsa_input:
+ break;
+ }
+ default:
+ assert(false);
+ }
+ }
+
+ for (const BusProto &bus_proto : mapping_proto.bus()) {
+ if (bus_proto.device_index() < 0 || unsigned(bus_proto.device_index()) >= device_mapping.size()) {
+ return false;
+ }
+ InputMapping::Bus bus;
+ bus.name = bus_proto.name();
+ bus.device = device_mapping[bus_proto.device_index()];
+ bus.source_channel[0] = bus_proto.source_channel_left();
+ bus.source_channel[1] = bus_proto.source_channel_right();
+ new_mapping->buses.push_back(bus);
+ }
+
+ return true;
+}
#define _INPUT_MAPPING_H 1
#include <stdint.h>
+#include <map>
#include <string>
#include <vector>
std::vector<Bus> buses;
};
+bool save_input_mapping_to_file(const std::map<DeviceSpec, DeviceInfo> &devices,
+ const InputMapping &mapping,
+ const std::string &filename);
+bool load_input_mapping_from_file(const std::map<DeviceSpec, DeviceInfo> &devices,
+ const std::string &filename,
+ InputMapping *mapping);
+
#endif // !defined(_INPUT_MAPPING_H)
#include "ui_input_mapping.h"
#include <QComboBox>
+#include <QFileDialog>
+#include <QMessageBox>
using namespace std;
using namespace std::placeholders;
connect(ui->remove_button, &QPushButton::clicked, this, &InputMappingDialog::remove_clicked);
connect(ui->up_button, &QPushButton::clicked, bind(&InputMappingDialog::updown_clicked, this, -1));
connect(ui->down_button, &QPushButton::clicked, bind(&InputMappingDialog::updown_clicked, this, 1));
+ connect(ui->save_button, &QPushButton::clicked, this, &InputMappingDialog::save_clicked);
+ connect(ui->load_button, &QPushButton::clicked, this, &InputMappingDialog::load_clicked);
update_button_state();
connect(ui->table, &QTableWidget::itemSelectionChanged, this, &InputMappingDialog::update_button_state);
ui->table->setRangeSelected(b_sel, true);
}
+void InputMappingDialog::save_clicked()
+{
+ QString filename = QFileDialog::getSaveFileName(this,
+ "Save input mapping", QString(), tr("Mapping files (*.mapping)"));
+ if (!filename.endsWith(".mapping")) {
+ filename += ".mapping";
+ }
+ if (!save_input_mapping_to_file(devices, mapping, filename.toStdString())) {
+ QMessageBox box;
+ box.setText("Could not save mapping to '" + filename + "'. Check that you have the right permissions and try again.");
+ box.exec();
+ }
+}
+
+void InputMappingDialog::load_clicked()
+{
+ QString filename = QFileDialog::getOpenFileName(this,
+ "Load input mapping", QString(), tr("Mapping files (*.mapping)"));
+ InputMapping new_mapping;
+ if (!load_input_mapping_from_file(devices, filename.toStdString(), &new_mapping)) {
+ QMessageBox box;
+ box.setText("Could not load mapping from '" + filename + "'. Check that the file exists, has the right permissions and is valid.");
+ box.exec();
+ return;
+ }
+
+ mapping = new_mapping;
+ devices = global_audio_mixer->get_devices(); // New dead cards may have been made.
+ fill_ui_from_mapping(mapping);
+}
+
void InputMappingDialog::update_button_state()
{
ui->add_button->setDisabled(mapping.buses.size() >= MAX_BUSES);
void add_clicked();
void remove_clicked();
void updown_clicked(int direction);
+ void save_clicked();
+ void load_clicked();
void update_button_state();
Ui::InputMappingDialog *ui;
// held forever).
InputMapping old_mapping;
- std::map<DeviceSpec, DeviceInfo> devices;
-
+ std::map<DeviceSpec, DeviceInfo> devices; // Needs no lock, accessed only on the UI thread.
AudioMixer::state_changed_callback_t saved_callback;
};
--- /dev/null
+// Used to serialize state between runs. Currently only audio input mappings,
+// but in theory we could do the entire mix, video inputs, etc.
+
+syntax = "proto2";
+
+// Similar to DeviceSpec, but only devices that are used are stored,
+// and contains additional information that will help us try to map
+// to the right device even if the devices have moved around.
+message DeviceSpecProto {
+ // Members from DeviceSpec itself.
+ enum InputSourceType { SILENCE = 0; CAPTURE_CARD = 1; ALSA_INPUT = 2; };
+ optional InputSourceType type = 1;
+ optional int32 index = 2;
+
+ // Additional information.
+ optional string display_name = 3;
+ optional string alsa_name = 4; // Only for ALSA devices.
+ optional string alsa_info = 5; // Only for ALSA devices.
+ optional int32 num_channels = 6; // Only for ALSA devices.
+ optional string address = 7; // Only for ALSA devices.
+}
+
+// Corresponds to InputMapping::Bus.
+message BusProto {
+ optional string name = 1;
+ optional int32 device_index = 2; // Index into the "devices" array.
+ optional int32 source_channel_left = 3;
+ optional int32 source_channel_right = 4;
+}
+
+// Corresponds to InputMapping.
+message InputMappingProto {
+ repeated DeviceSpecProto device = 1;
+ repeated BusProto bus = 2;
+}
</widget>
</item>
<item>
- <layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,0,1,0,0,1,0">
+ <layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,0,0,0,1,0,0,1,0">
<item>
<widget class="QPushButton" name="add_button">
<property name="sizePolicy">
</property>
</widget>
</item>
- <item>
- <spacer name="horizontalSpacer_2">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
<item>
<widget class="QPushButton" name="up_button">
<property name="maximumSize">
</property>
</widget>
</item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="save_button">
+ <property name="text">
+ <string>&Save…</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="load_button">
+ <property name="text">
+ <string>&Load…</string>
+ </property>
+ </widget>
+ </item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">