Commit | Line | Data |
---|---|---|
126d8fa4 AIL |
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 |