8ccdb355379d8f8eb2bf9eb8396095859a2edd08
[mouse-tracker-for-cubism.git] / src / mouse_cursor_tracker.cpp
1 /****
2 Copyright (c) 2020 Adrian I. Lam
3
4 Permission is hereby granted, free of charge, to any person obtaining a copy
5 of this software and associated documentation files (the "Software"), to deal
6 in the Software without restriction, including without limitation the rights
7 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 copies of the Software, and to permit persons to whom the Software is
9 furnished to do so, subject to the following conditions:
10
11 The above copyright notice and this permission notice shall be included in all
12 copies or substantial portions of the Software.
13
14 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 SOFTWARE.
21 ****/
22
23 #include <stdexcept>
24 #include <string>
25 #include <chrono>
26 #include <thread>
27 #include <fstream>
28 #include <sstream>
29 #include <vector>
30 #include <cstdlib>
31 #include <cmath>
32
33 #include <iostream>
34
35 extern "C"
36 {
37 #include <xdo.h>
38 #include <pulse/simple.h>
39 }
40 #include "mouse_cursor_tracker.h"
41
42 static double rms(float *buf, std::size_t count)
43 {
44 double sum = 0;
45 for (std::size_t i = 0; i < count; i++)
46 {
47 sum += buf[i] * buf[i];
48 }
49 return std::sqrt(sum / count);
50 }
51
52 MouseCursorTracker::MouseCursorTracker(std::string cfgPath)
53 : m_stop(false)
54 {
55 parseConfig(cfgPath);
56 m_xdo = xdo_new(nullptr);
57
58 const pa_sample_spec ss =
59 {
60 .format = PA_SAMPLE_FLOAT32NE,
61 .rate = 44100,
62 .channels = 2
63 };
64 m_pulse = pa_simple_new(nullptr, "MouseCursorTracker", PA_STREAM_RECORD,
65 nullptr, "LipSync", &ss, nullptr, nullptr, nullptr);
66 if (!m_pulse)
67 {
68 throw std::runtime_error("Unable to create pulse");
69 }
70
71 m_getVolumeThread = std::thread(&MouseCursorTracker::audioLoop, this);
72 }
73
74 void MouseCursorTracker::audioLoop(void)
75 {
76 float *buf = new float[m_cfg.audioBufSize];
77
78 std::size_t audioBufByteSize = m_cfg.audioBufSize * sizeof *buf;
79
80 while (!m_stop)
81 {
82 if (pa_simple_read(m_pulse, buf, audioBufByteSize, nullptr) < 0)
83 {
84 throw std::runtime_error("Unable to get audio data");
85 }
86 m_currentVol = rms(buf, m_cfg.audioBufSize);
87 }
88
89 delete[] buf;
90 }
91
92 MouseCursorTracker::~MouseCursorTracker()
93 {
94 xdo_free(m_xdo);
95 m_getVolumeThread.join();
96 pa_simple_free(m_pulse);
97 }
98
99 void MouseCursorTracker::stop(void)
100 {
101 m_stop = true;
102 }
103
104 MouseCursorTracker::Params MouseCursorTracker::getParams(void) const
105 {
106 Params params = Params();
107
108 int xOffset = m_curPos.x - m_cfg.middle.x;
109 int leftRange = m_cfg.middle.x - m_cfg.left;
110 int rightRange = m_cfg.right - m_cfg.middle.x;
111
112 if (xOffset > 0) // i.e. to the right
113 {
114 params.faceXAngle = 30.0 * xOffset / rightRange;
115 }
116 else // to the left
117 {
118 params.faceXAngle = 30.0 * xOffset / leftRange;
119 }
120
121 int yOffset = m_curPos.y - m_cfg.middle.y;
122 int topRange = m_cfg.middle.y - m_cfg.top;
123 int bottomRange = m_cfg.bottom - m_cfg.middle.y;
124
125 if (yOffset > 0) // downwards
126 {
127 params.faceYAngle = -30.0 * yOffset / bottomRange;
128 }
129 else // upwards
130 {
131 params.faceYAngle = -30.0 * yOffset / topRange;
132 }
133
134 params.faceZAngle = 0;
135
136 params.leftEyeOpenness = 1;
137 params.rightEyeOpenness = 1;
138
139 params.autoBlink = m_cfg.autoBlink;
140 params.autoBreath = m_cfg.autoBreath;
141 params.randomMotion = m_cfg.randomMotion;
142 params.useLipSync = m_cfg.useLipSync;
143
144 params.mouthForm = m_cfg.mouthForm;
145
146 if (m_cfg.useLipSync)
147 {
148 params.lipSyncParam = m_currentVol * m_cfg.lipSyncGain;
149 if (params.lipSyncParam < m_cfg.lipSyncCutOff)
150 {
151 params.lipSyncParam = 0;
152 }
153 else if (params.lipSyncParam > 1)
154 {
155 params.lipSyncParam = 1;
156 }
157 }
158
159
160 // Leave everything else as zero
161
162
163 return params;
164 }
165
166 void MouseCursorTracker::mainLoop(void)
167 {
168 while (!m_stop)
169 {
170 int x;
171 int y;
172 int screenNum;
173
174 xdo_get_mouse_location(m_xdo, &x, &y, &screenNum);
175
176 if (screenNum == m_cfg.screen)
177 {
178 m_curPos.x = x;
179 m_curPos.y = y;
180 }
181 // else just silently ignore for now
182 std::this_thread::sleep_for(std::chrono::milliseconds(m_cfg.sleepMs));
183 }
184 }
185
186 void MouseCursorTracker::parseConfig(std::string cfgPath)
187 {
188 populateDefaultConfig();
189
190 if (cfgPath != "")
191 {
192 std::ifstream file(cfgPath);
193 if (!file)
194 {
195 throw std::runtime_error("Failed to open config file");
196 }
197 std::string line;
198 unsigned int lineNum = 0;
199
200 while (std::getline(file, line))
201 {
202 if (line[0] == '#')
203 {
204 continue;
205 }
206
207 std::istringstream ss(line);
208 std::string paramName;
209
210 if (ss >> paramName)
211 {
212 if (paramName == "sleep_ms")
213 {
214 if (!(ss >> m_cfg.sleepMs))
215 {
216 throw std::runtime_error("Error parsing sleep_ms");
217 }
218 }
219 else if (paramName == "autoBlink")
220 {
221 if (!(ss >> m_cfg.autoBlink))
222 {
223 throw std::runtime_error("Error parsing autoBlink");
224 }
225 }
226 else if (paramName == "autoBreath")
227 {
228 if (!(ss >> m_cfg.autoBreath))
229 {
230 throw std::runtime_error("Error parsing autoBreath");
231 }
232 }
233 else if (paramName == "randomMotion")
234 {
235 if (!(ss >> m_cfg.randomMotion))
236 {
237 throw std::runtime_error("Error parsing randomMotion");
238 }
239 }
240 else if (paramName == "useLipSync")
241 {
242 if (!(ss >> m_cfg.useLipSync))
243 {
244 throw std::runtime_error("Error parsing useLipSync");
245 }
246 }
247 else if (paramName == "lipSyncGain")
248 {
249 if (!(ss >> m_cfg.lipSyncGain))
250 {
251 throw std::runtime_error("Error parsing lipSyncGain");
252 }
253 }
254 else if (paramName == "lipSyncCutOff")
255 {
256 if (!(ss >> m_cfg.lipSyncCutOff))
257 {
258 throw std::runtime_error("Error parsing lipSyncCutOff");
259 }
260 }
261 else if (paramName == "audioBufSize")
262 {
263 if (!(ss >> m_cfg.audioBufSize))
264 {
265 throw std::runtime_error("Error parsing audioBufSize");
266 }
267 }
268 else if (paramName == "mouthForm")
269 {
270 if (!(ss >> m_cfg.mouthForm))
271 {
272 throw std::runtime_error("Error parsing mouthForm");
273 }
274 }
275 else if (paramName == "screen")
276 {
277 if (!(ss >> m_cfg.screen))
278 {
279 throw std::runtime_error("Error parsing screen");
280 }
281 }
282 else if (paramName == "middle_x")
283 {
284 if (!(ss >> m_cfg.middle.x))
285 {
286 throw std::runtime_error("Error parsing middle_x");
287 }
288 }
289 else if (paramName == "middle_y")
290 {
291 if (!(ss >> m_cfg.middle.y))
292 {
293 throw std::runtime_error("Error parsing middle_y");
294 }
295 }
296 else if (paramName == "top")
297 {
298 if (!(ss >> m_cfg.top))
299 {
300 throw std::runtime_error("Error parsing top");
301 }
302 }
303 else if (paramName == "bottom")
304 {
305 if (!(ss >> m_cfg.bottom))
306 {
307 throw std::runtime_error("Error parsing bottom");
308 }
309 }
310 else if (paramName == "left")
311 {
312 if (!(ss >> m_cfg.left))
313 {
314 throw std::runtime_error("Error parsing left");
315 }
316 }
317 else if (paramName == "right")
318 {
319 if (!(ss >> m_cfg.right))
320 {
321 throw std::runtime_error("Error parsing right");
322 }
323 }
324 else
325 {
326 throw std::runtime_error("Unrecognized config parameter");
327 }
328 }
329 }
330 }
331 }
332
333 void MouseCursorTracker::populateDefaultConfig(void)
334 {
335 m_cfg.sleepMs = 5;
336 m_cfg.autoBlink = true;
337 m_cfg.autoBreath = true;
338 m_cfg.randomMotion = false;
339 m_cfg.useLipSync = true;
340 m_cfg.lipSyncGain = 10;
341 m_cfg.lipSyncCutOff = 0.15;
342 m_cfg.audioBufSize = 4096;
343 m_cfg.mouthForm = 0;
344 m_cfg.top = 0;
345 m_cfg.bottom = 1079;
346 m_cfg.left = 0;
347 m_cfg.right = 1919; // These will be the full screen for 1920x1080
348
349 m_cfg.screen = 0;
350 m_cfg.middle = {1600, 870}; // Somewhere near the bottom right
351 }
352