#include <fstream>
#include <sstream>
#include <vector>
+#include <utility>
+#include <algorithm>
#include <cstdlib>
#include <cmath>
+#include <cassert>
#include <iostream>
+#include <cctype>
+#include <mutex>
extern "C"
{
#include <xdo.h>
#include <pulse/simple.h>
+#include <readline/readline.h>
+#include <readline/history.h>
}
#include "mouse_cursor_tracker.h"
return std::sqrt(sum / count);
}
-MouseCursorTracker::MouseCursorTracker(std::string cfgPath)
+static std::vector<std::string> split(std::string s)
+{
+ std::vector<std::string> v;
+ std::string tmp;
+
+ for (std::size_t i = 0; i < s.length(); i++)
+ {
+ char c = s[i];
+ if (std::isspace(c))
+ {
+ if (tmp != "")
+ {
+ v.push_back(tmp);
+ tmp = "";
+ }
+ }
+ else
+ {
+ tmp += c;
+ }
+ }
+ if (tmp != "")
+ {
+ v.push_back(tmp);
+ }
+ return v;
+}
+
+
+/* Using readline callback functions means that we need to pass
+ * information from our class to the callback, and the only way
+ * to do so is using globals.
+ */
+
+// Taken from https://github.com/eliben/code-for-blog/blob/master/2016/readline-samples/utils.cpp
+static std::string longest_common_prefix(std::string s,
+ const std::vector<std::string>& candidates) {
+ assert(candidates.size() > 0);
+ if (candidates.size() == 1) {
+ return candidates[0];
+ }
+
+ std::string prefix(s);
+ while (true) {
+ // Each iteration of this loop advances to the next location in all the
+ // candidates and sees if they match up to it.
+ size_t nextloc = prefix.size();
+ auto i = candidates.begin();
+ if (i->size() <= nextloc) {
+ return prefix;
+ }
+ char nextchar = (*(i++))[nextloc];
+ for (; i != candidates.end(); ++i) {
+ if (i->size() <= nextloc || (*i)[nextloc] != nextchar) {
+ // Bail out if there's a mismatch for this candidate.
+ return prefix;
+ }
+ }
+ // All candidates have contents[nextloc] == nextchar, so we can safely
+ // extend the prefix.
+ prefix.append(1, nextchar);
+ }
+
+ assert(0 && "unreachable");
+}
+
+std::vector<std::string> commands =
+{
+ "help", "motion", "expression", "set", "clear"
+};
+
+std::vector<std::string> live2dParams =
+{ // https://docs.live2d.com/cubism-editor-manual/standard-parametor-list/?locale=ja
+ "ParamAngleX", "ParamAngleY", "ParamAngleZ",
+ "ParamEyeLOpen", "ParamEyeLSmile", "ParamEyeROpen", "ParamEyeRSmile",
+ "ParamEyeBallX", "ParamEyeBallY", "ParamEyeBallForm",
+ "ParamBrowLY", "ParamBrowRY", "ParamBrowLX", "ParamBrowRX",
+ "ParamBrowLAngle", "ParamBrowRAngle", "ParamBrowLForm", "ParamBrowRForm",
+ "ParamMouthForm", "ParamMouthOpenY", "ParamTere",
+ "ParamBodyAngleX", "ParamBodyAngleY", "ParamBodyAngleZ", "ParamBreath",
+ "ParamArmLA", "ParamArmRA", "ParamArmLB", "ParamArmRB",
+ "ParamHandL", "ParamHandR",
+ "ParamHairFront", "ParamHairSide", "ParamHairBack", "ParamHairFluffy",
+ "ParamShoulderY", "ParamBustX", "ParamBustY",
+ "ParamBaseX", "ParamBaseY"
+};
+std::vector<std::pair<std::string, int> > MCT_motions;
+std::vector<std::string> MCT_expressions;
+std::map<std::string, double> *MCT_overrideMap;
+
+static char **cliCompletionFunction(const char *textCStr, int start, int end)
+{
+ // Reference: https://github.com/eliben/code-for-blog/blob/master/2016/readline-samples/readline-complete-subcommand.cpp
+ rl_attempted_completion_over = 1;
+
+ std::string line(rl_line_buffer);
+
+ std::vector<std::string> cmdline = split(line);
+
+ std::vector<std::string> constructed;
+ std::vector<std::string> *vocab = nullptr;
+
+ if (cmdline.size() == 0 ||
+ (cmdline.size() == 1 && line.back() != ' ') ||
+ cmdline[0] == "help")
+ {
+ vocab = &commands;
+ }
+ else if (cmdline[0] == "motion")
+ {
+ for (auto it = MCT_motions.begin(); it != MCT_motions.end(); ++it)
+ {
+ if ((cmdline.size() == 1 && line.back() == ' ') ||
+ (cmdline.size() == 2 && line.back() != ' '))
+ { // motionGroup
+ {
+ constructed.push_back(it->first);
+ }
+ }
+ else if ((cmdline.size() == 2 && line.back() == ' ') ||
+ (cmdline.size() == 3 && line.back() != ' '))
+ { // motionNumber
+ if (it->first == cmdline[1])
+ {
+ for (int i = 0; i < it->second; i++)
+ {
+ constructed.push_back(std::to_string(i));
+ }
+ break;
+ }
+ }
+ else if (cmdline.size() <= 4)
+ { // priority
+ for (int i = 0; i < 4; i++)
+ {
+ constructed.push_back(std::to_string(i));
+ }
+ }
+ }
+
+ vocab = &constructed;
+ }
+ else if (cmdline[0] == "expression")
+ {
+ vocab = &MCT_expressions;
+ }
+ else if (cmdline[0] == "set")
+ {
+ if ((cmdline.size() % 2 == 0 && line.back() != ' ') ||
+ (cmdline.size() % 2 == 1 && line.back() == ' '))
+ {
+ vocab = &live2dParams;
+ }
+ }
+ else if (cmdline[0] == "clear")
+ {
+ constructed.push_back("all");
+ for (auto const &entry : *MCT_overrideMap)
+ {
+ constructed.push_back(entry.first);
+ }
+ vocab = &constructed;
+ }
+
+ if (!vocab)
+ {
+ return nullptr;
+ }
+
+ std::string text(textCStr);
+ std::vector<std::string> matches;
+ std::copy_if(vocab->begin(), vocab->end(), std::back_inserter(matches),
+ [&text](const std::string &s)
+ {
+ return (s.size() >= text.size() &&
+ s.compare(0, text.size(), text) == 0);
+ });
+
+ if (matches.empty())
+ {
+ return nullptr;
+ }
+
+ char** array =
+ static_cast<char**>(malloc((2 + matches.size()) * sizeof(*array)));
+ array[0] = strdup(longest_common_prefix(text, matches).c_str());
+ size_t ptr = 1;
+ for (const auto& m : matches) {
+ array[ptr++] = strdup(m.c_str());
+ }
+ array[ptr] = nullptr;
+ return array;
+}
+
+MouseCursorTracker::MouseCursorTracker(std::string cfgPath,
+ std::vector<std::pair<std::string, int> > motions,
+ std::vector<std::string> expressions)
: m_stop(false)
{
+ m_motionPriority = MotionPriority::none;
+ m_motionNumber = 0;
+
+
parseConfig(cfgPath);
m_xdo = xdo_new(nullptr);
}
m_getVolumeThread = std::thread(&MouseCursorTracker::audioLoop, this);
+ m_parseCommandThread = std::thread(&MouseCursorTracker::cliLoop, this);
+
+ MCT_motions = motions;
+ MCT_expressions = expressions;
+ MCT_overrideMap = &m_overrideMap;
}
void MouseCursorTracker::audioLoop(void)
delete[] buf;
}
+void MouseCursorTracker::cliLoop(void)
+{
+ rl_catch_signals = 0;
+ rl_attempted_completion_function = cliCompletionFunction;
+ while (!m_stop)
+ {
+ char *buf = readline(">> ");
+
+ if (buf)
+ {
+ std::string cmdline(buf);
+ free(buf);
+ processCommand(cmdline);
+ }
+ else
+ {
+ std::cout << "Exiting CLI loop. Use Ctrl+C to exit the whole process." << std::endl;
+ stop();
+ }
+ }
+}
+
+
+void MouseCursorTracker::processCommand(std::string cmdline)
+{
+ auto cmdSplit = split(cmdline);
+
+ if (cmdSplit.size() > 0)
+ {
+ add_history(cmdline.c_str());
+
+ if (cmdSplit[0] == "help")
+ {
+ if (cmdSplit.size() == 1)
+ {
+ std::cout << "Available commands: motion set clear\n"
+ << "Type \"help <command>\" for more help" << std::endl;
+ }
+ else if (cmdSplit[1] == "motion")
+ {
+ std::cout << "motion <motionGroup> <motionNumber> [<priority>]\n"
+ << "motionGroup: The motion name in the .model3.json file\n"
+ << "motionNumber: The index of this motion in the .model3.json file, 0-indexed\n"
+ << "priority: 0 = none, 1 = idle, 2 = normal, 3 = force (default normal)" << std::endl;
+ }
+ else if (cmdSplit[1] == "expression")
+ {
+ std::cout << "expression <expressionName>\n"
+ << "expressionName: Name of expression in the .model3.json file" << std::endl;
+ }
+ else if (cmdSplit[1] == "set")
+ {
+ std::cout << "set <param1> <value1> [<param2> <value2> ...]\n"
+ << "Set parameter value. Overrides any tracking."
+ << "See live2D documentation for full list of params" << std::endl;
+ }
+ else if (cmdSplit[1] == "clear")
+ {
+ std::cout << "clear <param1> [<param2> ...]\n"
+ << "Clear parameter value. Re-enables tracking if it was overridden by \"set\"\n"
+ << "You can also use \"clear all\" to clear everything" << std::endl;
+ }
+ else
+ {
+ std::cout << "Unrecognized command" << std::endl;
+ }
+ }
+ else if (cmdSplit[0] == "motion")
+ {
+ if (cmdSplit.size() == 3 || cmdSplit.size() == 4)
+ {
+ std::unique_lock<std::mutex> lock(m_motionMutex, std::defer_lock);
+ lock.lock();
+ m_motionGroup = cmdSplit[1];
+ try
+ {
+ m_motionNumber = std::stoi(cmdSplit[2]);
+ if (cmdSplit.size() == 4)
+ {
+ m_motionPriority = static_cast<MotionPriority>(std::stoi(cmdSplit[3]));
+ }
+ else
+ {
+ m_motionPriority = MotionPriority::normal;
+ }
+ }
+ catch (const std::exception &e)
+ {
+ std::cerr << "std::stoi failed" << std::endl;
+ }
+ lock.unlock();
+ }
+ else
+ {
+ std::cerr << "Incorrect command, expecting 2 or 3 arguments" << std::endl;
+ std::cerr << "motion motionGroup motionNumber [motionPriority]" << std::endl;
+ }
+ }
+ else if (cmdSplit[0] == "expression")
+ {
+ if (cmdSplit.size() == 2)
+ {
+ std::unique_lock<std::mutex> lock(m_motionMutex, std::defer_lock);
+ lock.lock();
+ m_expression = cmdSplit[1];
+ lock.unlock();
+ }
+ else
+ {
+ std::cerr << "Incorrect command, expecting 1 argument: expressionName" << std::endl;
+ }
+ }
+ else if (cmdSplit[0] == "set")
+ {
+ if (cmdSplit.size() % 2 != 1)
+ {
+ // "set param1 value1 param2 value2 ..."
+ std::cerr << "Incorrect number of arguments for command 'set'" << std::endl;
+ }
+ for (std::size_t i = 1; i < cmdSplit.size(); i += 2)
+ {
+ try
+ {
+ m_overrideMap[cmdSplit[i]] = std::stod(cmdSplit[i + 1]);
+ }
+ catch (const std::exception &e)
+ {
+ std::cerr << "std::stod failed" << std::endl;
+ }
+
+ std::cerr << "Debug: setting " << cmdSplit[i] << std::endl;
+ }
+ }
+ else if (cmdSplit[0] == "clear")
+ {
+ for (std::size_t i = 1; i < cmdSplit.size(); i++)
+ {
+ if (cmdSplit[i] == "all")
+ {
+ m_overrideMap.clear();
+ break;
+ }
+ std::size_t removed = m_overrideMap.erase(cmdSplit[i]);
+ if (removed == 0)
+ {
+ std::cerr << "Warning: key " << cmdSplit[i] << " not found" << std::endl;
+ }
+ }
+ }
+ else
+ {
+ std::cerr << "Unknown command" << std::endl;
+ }
+ }
+}
+
MouseCursorTracker::~MouseCursorTracker()
{
xdo_free(m_xdo);
m_getVolumeThread.join();
+ m_parseCommandThread.join();
pa_simple_free(m_pulse);
}
m_stop = true;
}
-MouseCursorTracker::Params MouseCursorTracker::getParams(void) const
+MouseCursorTracker::Params MouseCursorTracker::getParams(void)
{
Params params = Params();
- int xOffset = m_curPos.x - m_cfg.middle.x;
- int leftRange = m_cfg.middle.x - m_cfg.left;
- int rightRange = m_cfg.right - m_cfg.middle.x;
+ int xOffset = m_curPos.x - m_cfg.origin.x;
+ int leftRange = m_cfg.origin.x - m_cfg.left;
+ int rightRange = m_cfg.right - m_cfg.origin.x;
if (xOffset > 0) // i.e. to the right
{
- params.faceXAngle = 30.0 * xOffset / rightRange;
+ params.live2d["ParamAngleX"] = 30.0 * xOffset / rightRange;
}
else // to the left
{
- params.faceXAngle = 30.0 * xOffset / leftRange;
+ params.live2d["ParamAngleX"] = 30.0 * xOffset / leftRange;
}
- int yOffset = m_curPos.y - m_cfg.middle.y;
- int topRange = m_cfg.middle.y - m_cfg.top;
- int bottomRange = m_cfg.bottom - m_cfg.middle.y;
+ int yOffset = m_curPos.y - m_cfg.origin.y;
+ int topRange = m_cfg.origin.y - m_cfg.top;
+ int bottomRange = m_cfg.bottom - m_cfg.origin.y;
if (yOffset > 0) // downwards
{
- params.faceYAngle = -30.0 * yOffset / bottomRange;
+ params.live2d["ParamAngleY"] = -30.0 * yOffset / bottomRange;
}
else // upwards
{
- params.faceYAngle = -30.0 * yOffset / topRange;
+ params.live2d["ParamAngleY"] = -30.0 * yOffset / topRange;
}
- params.faceZAngle = 0;
-
- params.leftEyeOpenness = 1;
- params.rightEyeOpenness = 1;
-
params.autoBlink = m_cfg.autoBlink;
params.autoBreath = m_cfg.autoBreath;
- params.randomMotion = m_cfg.randomMotion;
+ params.randomIdleMotion = m_cfg.randomIdleMotion;
params.useLipSync = m_cfg.useLipSync;
- params.mouthForm = m_cfg.mouthForm;
+ params.live2d["ParamMouthForm"] = m_cfg.mouthForm;
if (m_cfg.useLipSync)
{
}
}
+ // Don't block in getParams()
+ std::unique_lock<std::mutex> lock(m_motionMutex, std::try_to_lock);
+ if (lock.owns_lock())
+ {
+ if (m_motionPriority != MotionPriority::none)
+ {
+ params.motionPriority = m_motionPriority;
+ params.motionGroup = m_motionGroup;
+ params.motionNumber = m_motionNumber;
+
+ m_motionPriority = MotionPriority::none;
+ m_motionGroup = "";
+ m_motionNumber = 0;
+ }
+ params.expression = m_expression;
+ m_expression = "";
+ lock.unlock();
+ }
// Leave everything else as zero
+ // Process overrides
+ for (auto const &x : m_overrideMap)
+ {
+ std::string key = x.first;
+ double val = x.second;
+
+ params.live2d[key] = val;
+ }
+
+
return params;
}
throw std::runtime_error("Error parsing autoBreath");
}
}
- else if (paramName == "randomMotion")
+ else if (paramName == "randomIdleMotion")
{
- if (!(ss >> m_cfg.randomMotion))
+ if (!(ss >> m_cfg.randomIdleMotion))
{
- throw std::runtime_error("Error parsing randomMotion");
+ throw std::runtime_error("Error parsing randomIdleMotion");
}
}
else if (paramName == "useLipSync")
throw std::runtime_error("Error parsing screen");
}
}
- else if (paramName == "middle_x")
+ else if (paramName == "origin_x")
{
- if (!(ss >> m_cfg.middle.x))
+ if (!(ss >> m_cfg.origin.x))
{
- throw std::runtime_error("Error parsing middle_x");
+ throw std::runtime_error("Error parsing origin_x");
}
}
- else if (paramName == "middle_y")
+ else if (paramName == "origin_y")
{
- if (!(ss >> m_cfg.middle.y))
+ if (!(ss >> m_cfg.origin.y))
{
- throw std::runtime_error("Error parsing middle_y");
+ throw std::runtime_error("Error parsing origin_y");
}
}
else if (paramName == "top")
m_cfg.sleepMs = 5;
m_cfg.autoBlink = true;
m_cfg.autoBreath = true;
- m_cfg.randomMotion = false;
+ m_cfg.randomIdleMotion = false;
m_cfg.useLipSync = true;
m_cfg.lipSyncGain = 10;
m_cfg.lipSyncCutOff = 0.15;
m_cfg.right = 1919; // These will be the full screen for 1920x1080
m_cfg.screen = 0;
- m_cfg.middle = {1600, 870}; // Somewhere near the bottom right
+ m_cfg.origin = {1600, 870}; // Somewhere near the bottom right
}