]> git.sesse.net Git - nageru/blob - input_mapping.cpp
Let settings follow buses when editing the mapping.
[nageru] / input_mapping.cpp
1 #include <stdio.h>
2 #include <fcntl.h>
3 #include <unistd.h>
4
5 #include <google/protobuf/text_format.h>
6 #include <google/protobuf/io/zero_copy_stream.h>
7 #include <google/protobuf/io/zero_copy_stream_impl.h>
8
9 #include "audio_mixer.h" 
10 #include "input_mapping.h"
11 #include "state.pb.h"
12
13 using namespace std;
14 using namespace google::protobuf;
15
16 bool save_input_mapping_to_file(const map<DeviceSpec, DeviceInfo> &devices, const InputMapping &input_mapping, const string &filename)
17 {
18         InputMappingProto mapping_proto;
19         {
20                 map<DeviceSpec, unsigned> used_devices;
21                 for (const InputMapping::Bus &bus : input_mapping.buses) {
22                         if (!used_devices.count(bus.device)) {
23                                 used_devices.emplace(bus.device, used_devices.size());
24                                 global_audio_mixer->serialize_device(bus.device, mapping_proto.add_device());
25                         }
26
27                         BusProto *bus_proto = mapping_proto.add_bus();
28                         bus_proto->set_name(bus.name);
29                         bus_proto->set_device_index(used_devices[bus.device]);
30                         bus_proto->set_source_channel_left(bus.source_channel[0]);
31                         bus_proto->set_source_channel_right(bus.source_channel[1]);
32                 }
33         }
34
35         // Save to disk. We use the text format because it's friendlier
36         // for a user to look at and edit.
37         int fd = open(filename.c_str(), O_WRONLY | O_TRUNC | O_CREAT, 0666);
38         if (fd == -1) {
39                 perror(filename.c_str());
40                 return false;
41         }
42         io::FileOutputStream output(fd);  // Takes ownership of fd.
43         if (!TextFormat::Print(mapping_proto, &output)) {
44                 // TODO: Don't overwrite the old file (if any) on error.
45                 output.Close();
46                 return false;
47         }
48
49         output.Close();
50         return true;
51 }
52
53 bool load_input_mapping_from_file(const map<DeviceSpec, DeviceInfo> &devices, const string &filename, InputMapping *new_mapping)
54 {
55         // Read and parse the protobuf from disk.
56         int fd = open(filename.c_str(), O_RDONLY);
57         if (fd == -1) {
58                 perror(filename.c_str());
59                 return false;
60         }
61         io::FileInputStream input(fd);  // Takes ownership of fd.
62         InputMappingProto mapping_proto;
63         if (!TextFormat::Parse(&input, &mapping_proto)) {
64                 input.Close();
65                 return false;
66         }
67         input.Close();
68
69         // Map devices in the proto to our current ones:
70
71         // Get a list of all active devices.
72         set<DeviceSpec> remaining_devices;
73         for (const auto &device_spec_and_info : devices) {
74                 remaining_devices.insert(device_spec_and_info.first);
75         }
76
77         // Now look at every device in the serialized protobuf and try to map
78         // it to one device we haven't taken yet. This isn't a full maximal matching,
79         // but it's good enough for our uses.
80         vector<DeviceSpec> device_mapping;
81         for (unsigned device_index = 0; device_index < unsigned(mapping_proto.device_size()); ++device_index) {
82                 const DeviceSpecProto &device_proto = mapping_proto.device(device_index);
83                 switch (device_proto.type()) {
84                 case DeviceSpecProto::SILENCE:
85                         device_mapping.push_back(DeviceSpec{InputSourceType::SILENCE, 0});
86                         break;
87                 case DeviceSpecProto::CAPTURE_CARD: {
88                         // First see if there's a card that matches on both index and name.
89                         DeviceSpec spec{InputSourceType::CAPTURE_CARD, unsigned(device_proto.index())};
90                         assert(devices.count(spec));
91                         const DeviceInfo &dev = devices.find(spec)->second;
92                         if (remaining_devices.count(spec) &&
93                             dev.display_name == device_proto.display_name()) {
94                                 device_mapping.push_back(spec);
95                                 remaining_devices.erase(spec);
96                                 goto found_capture_card;
97                         }
98
99                         // Scan and see if there's a match on name alone.
100                         for (const DeviceSpec &spec : remaining_devices) {
101                                 if (spec.type == InputSourceType::CAPTURE_CARD &&
102                                     dev.display_name == device_proto.display_name()) {
103                                         device_mapping.push_back(spec);
104                                         remaining_devices.erase(spec);
105                                         goto found_capture_card;
106                                 }
107                         }
108
109                         // OK, see if at least the index is free.
110                         if (remaining_devices.count(spec)) {
111                                 device_mapping.push_back(spec);
112                                 remaining_devices.erase(spec);
113                                 goto found_capture_card;
114                         }
115
116                         // Give up.
117                         device_mapping.push_back(DeviceSpec{InputSourceType::SILENCE, 0});
118 found_capture_card:
119                         break;
120                 }
121                 case DeviceSpecProto::ALSA_INPUT: {
122                         // For ALSA, we don't really care about index, but we can use address
123                         // in its place.
124
125                         // First see if there's a card that matches on name, num_channels and address.
126                         for (const DeviceSpec &spec : remaining_devices) {
127                                 assert(devices.count(spec));
128                                 const DeviceInfo &dev = devices.find(spec)->second;
129                                 if (spec.type == InputSourceType::ALSA_INPUT &&
130                                     dev.alsa_name == device_proto.alsa_name() &&
131                                     dev.alsa_info == device_proto.alsa_info() &&
132                                     int(dev.num_channels) == device_proto.num_channels() &&
133                                     dev.alsa_address == device_proto.address()) {
134                                         device_mapping.push_back(spec);
135                                         remaining_devices.erase(spec);
136                                         goto found_alsa_input;
137                                 }
138                         }
139
140                         // Looser check: Ignore the address.
141                         for (const DeviceSpec &spec : remaining_devices) {
142                                 assert(devices.count(spec));
143                                 const DeviceInfo &dev = devices.find(spec)->second;
144                                 if (spec.type == InputSourceType::ALSA_INPUT &&
145                                     dev.alsa_name == device_proto.alsa_name() &&
146                                     dev.alsa_info == device_proto.alsa_info() &&
147                                     int(dev.num_channels) == device_proto.num_channels()) {
148                                         device_mapping.push_back(spec);
149                                         remaining_devices.erase(spec);
150                                         goto found_alsa_input;
151                                 }
152                         }
153
154                         // OK, so we couldn't map this to a device, but perhaps one is added
155                         // at some point in the future through hotplug. Create a dead card
156                         // matching this one; right now, it will give only silence,
157                         // but it could be replaced with something later.
158                         //
159                         // NOTE: There's a potential race condition here, if the card
160                         // gets inserted while we're doing the device remapping
161                         // (or perhaps more realistically, while we're reading the
162                         // input mapping from disk).
163                         DeviceSpec dead_card_spec;
164                         dead_card_spec = global_audio_mixer->create_dead_card(
165                                 device_proto.alsa_name(), device_proto.alsa_info(), device_proto.num_channels());
166                         device_mapping.push_back(dead_card_spec);
167
168 found_alsa_input:
169                         break;
170                 }
171                 default:
172                         assert(false);
173                 }
174         }
175
176         for (const BusProto &bus_proto : mapping_proto.bus()) {
177                 if (bus_proto.device_index() < 0 || unsigned(bus_proto.device_index()) >= device_mapping.size()) {
178                         return false;
179                 }
180                 InputMapping::Bus bus;
181                 bus.name = bus_proto.name();
182                 bus.device = device_mapping[bus_proto.device_index()];
183                 bus.source_channel[0] = bus_proto.source_channel_left();
184                 bus.source_channel[1] = bus_proto.source_channel_right();
185                 new_mapping->buses.push_back(bus);
186         }
187
188         return true;
189 }