/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/***************************************************************************
* drumgizmoc.cc
*
* Thu Sep 16 10:23:22 CEST 2010
* Copyright 2010 Bent Bisballe Nyeng
* deva@aasimon.org
****************************************************************************/
/*
* This file is part of DrumGizmo.
*
* DrumGizmo is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* DrumGizmo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with DrumGizmo; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
*/
#include <iostream>
#include <config.h>
#include <getoptpp.hpp>
#include <string.h>
#include <fstream>
#include <sstream>
#include <chrono>
#include <thread>
#include <wordexp.h>
#include <hugin.hpp>
#include "drumgizmo.h"
#include "enginefactory.h"
#include "bytesizeparser.h"
#include "event.h"
#include "nolocale.h"
struct ParmToken
{
std::string key;
std::string value;
};
static std::string version()
{
std::ostringstream output;
output << "DrumGizmo v" << VERSION << std::endl;
return output.str();
}
static std::string copyright()
{
std::ostringstream output;
output << "Copyright (C) 2008-2018 Bent Bisballe Nyeng - Aasimon.org.\n";
output << "This is free software. You may redistribute copies of it under the terms ";
output << "of\n";
output << "the GNU Lesser General Public License <http://www.gnu.org/licenses/gpl.html>.\n";
output << "There is NO WARRANTY, to the extent permitted by law.\n";
output << "\n";
output << "Written by Bent Bisballe Nyeng (deva@aasimon.org)\n";
return output.str();
}
static std::string usage(const std::string& name)
{
std::ostringstream output;
output <<
"==============================================================================\n"
"This is the command line version of DrumGizmo. The graphical user interface is\n"
"only available in the plugins.\n"
"==============================================================================\n"
"\n"
"Usage: " << name << " [options] drumkitfile\n"
"Options:\n"
" -a, --async-load Load drumkit in the background and start the"
" engine immediately.\n"
" -b --bleed Set and enable master bleed\n"
// stddev
// velocity-attack
// velocity-release
// tightness
// timing-regain
// laidback
" -i, --inputengine dummy|test|jackmidi|midifile Use said event "
"input engine.\n"
" -I, --inputparms parmlist\n"
" Set input engine parameters.\n"
" -o, --outputengine dummy|alsa|jackaudio|wavfile Use said audio "
"output engine.\n"
" -O, --outputparms parmlist Set output engine parameters.\n"
" -e, --endpos Number of samples to process, -1: infinite.\n"
#ifndef DISABLE_HUGIN
" -D, --debug ddd Enable debug messages on 'ddd' see hugin "
"documentation for details\n"
#endif /*DISABLE_HUGIN*/
" -r, --no-resampling Disable resampling.\n"
" -s, --streaming Enable streaming.\n"
" -S, --streamingparms parmlist\n"
" Streaming options.\n"
" -t, --timing-humanizer\n"
" Enable moving of notes in time. Note adds latency to the output so do not\n"
" use this with a real-time drumkit.\n"
" -T, --timing-humanizerparms parmlist\n"
" Timing humanizer options.\n"
" -x, --velocity-humanizer\n"
" Enables adapting input velocities to make it sound more realistic.\n"
" -X, --velocity-humanizerparms parmlist\n"
" Velocity humanizer options.\n"
" -p, --parameters parmlist\n"
" Parameters for sample selection algorithm."
" -v, --version Print version information and exit.\n"
" -h, --help Print this message and exit.\n"
"\n"
"Input engine parameters:\n"
" jackmidi: midimap=<midimapfile>\n"
" midifile: file=<midifile>, speed=<tempo> (default 1.0),\n"
" track=<miditrack> (default -1, all tracks)\n"
" midimap=<midimapfile>, loop=<true|false>\n"
" ossmidi: midimap=<midimapfile>, dev=<device> (default '/dev/midi')\n"
" test: p=<hit_propability> (default 0.1)\n"
" instr=<instrument> (default -1, random instrument)\n"
" len=<seconds> (default -1, forever)\n"
" dummy:\n"
"\n"
"Output engine parameters:\n"
" alsa: dev=<device> (default 'default'), frames=<frames> (default "
"32)\n"
" srate=<samplerate> (default 441000)\n"
" oss: dev=<device> (default '/dev/dsp'), srate=<samplerate>,\n"
" max_fragments=<number> (default 4, see man page for more info),\n"
" fragment_size=<selector> (default 8, see man page for more info)\n"
" wavfile: file=<filename> (default 'output'), srate=<samplerate> "
"(default 44100)\n"
" jackaudio:\n"
" dummy:\n"
"\n"
"Streaming parameters:\n"
" limit: Limit the amount of preloaded drumkit data to the size\n"
//" chunk_size: chunk size in k,M,G\n"
"\n"
"Timing humanizer parameters:\n"
" laidback: Move notes ahead or behind in time in ms [+/-100].\n"
" tightness: Control the tightness of the drummer. [0,1]\n"
" regain: Control how fast the drummer catches up the timing. [0,1]\n"
"\n"
"Velocity humanizer parameters:\n"
" attack: How quickly the velocity gets reduced when playing fast notes.\n"
" Lower values result in faster velocity reduction.\n"
" release: How quickly the drummer regains the velocity\n"
" when there are spaces between the notes.\n"
" Lower values result in faster regain. [0,1]\n"
" stddev: The standard-deviation of the velocity humanization.\n"
" Higher value makes it more likely that a sample further\n"
" away from the input velocity will be played. [0,4.5]\n"
"\n"
"Sample selection parameters:\n"
" close: The importance given to choosing a sample close to\n"
" the actual velocity value (after humanization). [0,1]\n"
" diverse: The importance given to choosing samples\n"
" which haven't been played recently. [0,1]\n"
" random: The amount of randomness added. [0,1]\n"
"\n";
return output.str();
}
std::vector<ParmToken> parseParameters(std::string &parms)
{
std::vector<ParmToken> result;
std::string parm;
std::string val;
bool inval = false;
wordexp_t exp_result;
for(size_t i = 0; i < parms.size(); ++i)
{
if(parms[i] == ',')
{
int error = wordexp(val.data(), &exp_result, 0);
if(error)
{
std::cerr << "Wrong argument: ";
std::cerr << parm << " = " << val << std::endl;
exit(1);
}
result.push_back({parm, exp_result.we_wordv[0]});
parm = "";
val = "";
inval = false;
continue;
}
if(parms[i] == '=')
{
inval = true;
continue;
}
if(inval)
{
val += parms[i];
}
else
{
parm += parms[i];
}
}
if(parm != "")
{
int error = wordexp(val.data(), &exp_result, 0);
if(error)
{
std::cerr << "Wrong argument: ";
std::cerr << parm << " = " << val << std::endl;
exit(1);
}
result.push_back({parm, exp_result.we_wordv[0]});
}
return result;
}
int main(int argc, char* argv[])
{
EngineFactory factory;
Settings settings;
bool async = false;
dg::Options opt;
int endpos = -1;
std::string kitfile;
std::unique_ptr<AudioInputEngine> ie;
std::unique_ptr<AudioOutputEngine> oe;
std::string hugin_filter;
unsigned int hugin_flags = 0;
#ifndef DISABLE_HUGIN
hugin_flags = HUG_FLAG_DEFAULT;
#endif /*DISABLE_HUGIN*/
// if no arguments are passed, just print usage and exit
if (argc == 1)
{
std::cout << version();
std::cout << usage(argv[0]);
exit(0);
}
opt.add("async-load", no_argument, 'a', [&]() {
async = true;
});
opt.add("bleed", required_argument, 'B', [&]() {
float bleed = atof_nol(optarg);
settings.enable_bleed_control = true;
settings.master_bleed = bleed;
});
opt.add("inputengine", required_argument, 'i', [&]() {
std::string engine = optarg;
if(engine == "help")
{
std::cout << "Available Input Engines = { ";
for(auto const& name : factory.getInputEngines())
{
std::cout << name << " ";
}
std::cout << "}\n";
exit(0);
}
if(engine == "")
{
std::cerr << "Missing input engine" << std::endl;;
exit(1);
}
ie = factory.createInput(engine);
if(ie == NULL)
{
std::cerr << "Invalid input engine: " << engine << std::endl;
exit(1);
}
});
opt.add("inputparms", required_argument, 'I', [&]() {
std::string parms = optarg;
auto tokens = parseParameters(parms);
for(auto& token : tokens)
{
ie->setParm(token.key, token.value);
}
});
opt.add("outputengine", required_argument, 'o', [&]() {
std::string engine = optarg;
if(engine == "help")
{
std::cout << "Available Output Engines = { ";
for(auto const& name : factory.getOutputEngines())
{
std::cout << name << " ";
}
std::cout << " }\n";
exit(0);
}
if(engine == "")
{
std::cerr << "Missing output engine" << std::endl;
exit(1);
}
oe = factory.createOutput(engine);
if(ie == NULL)
{
std::cerr << "Invalid output engine: " << engine << std::endl;;
exit(1);
}
});
opt.add("outputparms", required_argument, 'O', [&]() {
std::string parms = optarg;
auto tokens = parseParameters(parms);
for(auto& token : tokens)
{
oe->setParm(token.key, token.value);
}
});
opt.add("endpos", required_argument, 'e', [&]() {
try
{
endpos = std::stoi(optarg);
}
catch(...)
{
std::cerr << "Invalid endpos size " << optarg << std::endl;
}
});
#ifndef DISABLE_HUGIN
opt.add("debug", required_argument, 'D', [&]() {
hugin_flags |= HUG_FLAG_USE_FILTER;
hugin_filter = optarg;
});
#endif /*DISABLE_HUGIN*/
opt.add("no-resampling", no_argument, 'r', [&]() {
settings.enable_resampling = false;
});
opt.add("streaming", no_argument, 's', [&]() {
settings.disk_cache_enable = true;
});
opt.add("streamingparms", required_argument, 'S', [&]() {
std::string parms = optarg;
auto tokens = parseParameters(parms);
for(auto& token : tokens)
{
if(token.key == "limit")
{
settings.disk_cache_upper_limit = byteSizeParser(token.value);
if(settings.disk_cache_upper_limit == 0)
{
std::cerr << "Invalid argument for streamparms limit: " <<
token.value << std::endl;
exit(1);
}
}
//else if(token.key == "chunk_size")
//{
// settings.disk_cache_chunk_size = byteSizeParser(token.value);
// if(settings.disk_cache_chunk_size == 0)
// {
// std::cerr << "Invalid argument for streamparms chunk_size: " <<
// token.value << std::endl;
// exit(1);
// }
// }
else
{
std::cerr << "Unknown streamingparms argument " << token.key <<
std::endl;
exit(1);
}
}
});
// Default is to disable timing humanizer
settings.enable_latency_modifier.store(false);
opt.add("timing-humanizer", no_argument, 't', [&]() {
settings.enable_latency_modifier.store(true);
});
opt.add("timing-humanizerparms", required_argument, 'T', [&]() {
std::string parms = optarg;
auto tokens = parseParameters(parms);
for(auto& token : tokens)
{
if(token.key == "laidback")
{
// Input range [+/- 100] ms
auto val = atof_nol(token.value.data());
if(val < -100.0 || val > 100.0)
{
std::cerr << "laidback range is +/- 100 and is in ms.\n";
exit(1);
}
settings.latency_laid_back_ms.store(val);
}
else if(token.key == "tightness")
{
// Input range [0, 1]
auto val = atof_nol(token.value.data());
if(val < 0.0 || val > 1.0)
{
std::cerr << "tightness range is [0, 1].\n";
exit(1);
}
settings.latency_stddev.store((-1.0 * val + 1.0) * 20.0);
}
else if(token.key == "regain")
{
auto val = atof_nol(token.value.data());
if(val < 0.0 || val > 1.0)
{
std::cerr << "regain range is [0, 1].\n";
exit(1);
}
settings.latency_regain.store(val);
}
else
{
std::cerr << "Unknown timing-humanizerparms argument " << token.key << std::endl;
exit(1);
}
}
});
// Default is to disable the velocity humanizer
settings.enable_velocity_modifier.store(false);
opt.add("velocity-humanizer", no_argument, 'x', [&]() {
settings.enable_velocity_modifier.store(true);
});
opt.add("velocity-humanizerparms", required_argument, 'X', [&]() {
std::string parms = optarg;
auto tokens = parseParameters(parms);
for(auto& token : tokens)
{
if (token.key == "attack")
{
auto val = atof_nol(token.value.data());
if(val < 0 || val > 1.0)
{
std::cerr << "attack range is [0, 1].\n";
exit(1);
}
settings.velocity_modifier_weight.store(val);
}
else if (token.key == "release")
{
auto val = atof_nol(token.value.data());
if(val < 0 || val > 1.0)
{
std::cerr << "release range is [0, 1].\n";
exit(1);
}
settings.velocity_modifier_falloff.store(val);
}
else if(token.key == "stddev")
{
auto val = atof_nol(token.value.data());
if(val < 0 || val > 4.5)
{
std::cerr << "stddev range is [0, 4.5].\n";
exit(1);
}
settings.velocity_stddev.store(val);
}
else
{
std::cerr << "Unknown velocity-humanizerparms argument " << token.key << std::endl;
exit(1);
}
}
});
opt.add("parameters", required_argument, 'p', [&]() {
std::string parms = optarg;
auto tokens = parseParameters(parms);
for(auto& token : tokens)
{
if(token.key == "close")
{
auto val = atof_nol(token.value.data());
if(val < 0 || val > 1)
{
std::cerr << "close range is [0, 1].\n";
exit(1);
}
settings.sample_selection_f_close.store(val);
}
else if(token.key == "diverse")
{
auto val = atof_nol(token.value.data());
if(val < 0 || val > 1)
{
std::cerr << "diverse range is [0, 1].\n";
exit(1);
}
settings.sample_selection_f_diverse.store(val);
}
else if(token.key == "random")
{
auto val = atof_nol(token.value.data());
if(val < 0 || val > 1)
{
std::cerr << "random range is [0, 1].\n";
exit(1);
}
settings.sample_selection_f_random.store(val);
}
else
{
std::cerr << "Unknown parameters argument " << token.key << std::endl;
exit(1);
}
}
});
opt.add("version", no_argument, 'v', [&]() {
std::cout << version();
std::cout << copyright();
exit(0);
});
opt.add("help", no_argument, 'h', [&]() {
std::cout << version();
std::cout << usage(argv[0]);
exit(0);
});
if(!opt.process(argc, argv))
{
return 1;
}
hug_status_t status = hug_init(hugin_flags, HUG_OPTION_FILTER,
hugin_filter.data(), HUG_OPTION_END);
if(status != HUG_STATUS_OK)
{
std::cerr << "Error: " << status << std::endl;
return 1;
}
DEBUG(drumgizmo, "Debug enabled.");
if(opt.arguments().empty())
{
std::cerr << "Missing kitfile." << std::endl;
std::cerr << usage(argv[0]) << std::endl;
return 1;
}
else if(opt.arguments().size() > 1)
{
std::cerr << "Can only handle a single kitfile." << std::endl;
std::cerr << usage(argv[0]) << std::endl;
return 1;
}
else
{
kitfile = opt.arguments()[0];
std::ifstream tmp_kit(kitfile);
if(!tmp_kit.good())
{
std::cerr << "Can not open " << kitfile << std::endl;
return 1;
}
}
if(ie == nullptr)
{
std::cerr << "No input engine selected.\n";
std::cout << usage(argv[0]);
return 1;
}
if(oe == nullptr)
{
std::cerr << "No output engine selected.\n";
std::cout << usage(argv[0]);
return 1;
}
std::cout << "Using kitfile: " << kitfile << std::endl;
DrumGizmo gizmo(settings, *oe.get(), *ie.get());
gizmo.setFrameSize(oe->getBufferSize());
settings.drumkit_file.store(kitfile);
settings.reload_counter++;
std::cout << "Loading drumkit, this may take a while:" << std::endl;
if(!async)
{
while(settings.drumkit_load_status.load() != LoadStatus::Done)
{
std::this_thread::sleep_for(std::chrono::milliseconds(10));
int total = settings.number_of_files.load();
int loaded = settings.number_of_files_loaded.load();
std::cout << "\r" << loaded << " of " << total << std::flush;
if(settings.drumkit_load_status.load() == LoadStatus::Error)
{
std::cout << "\nFailed to load " << kitfile << std::endl;
return 1;
}
if(loaded == total)
{
//break;
}
}
std::cout << "\ndone" << std::endl;
}
gizmo.setSamplerate(oe->getSamplerate());
oe->onLatencyChange(gizmo.getLatency());
if(!gizmo.init())
{
std::cout << "Failed init engine." << std::endl;
return 1;
}
size_t pos = 0;
size_t nsamples = oe->getBufferSize();
sample_t *samples = (sample_t *)malloc(nsamples * sizeof(sample_t));
gizmo.setFrameSize(oe->getBufferSize());
ie->start();
oe->start();
while(gizmo.run(pos, samples, nsamples) == true)
{
pos += nsamples;
if((endpos != -1) && (pos >= (size_t)endpos))
{
break;
}
}
ie->stop();
oe->stop();
free(samples);
std::cout << "Quit." << std::endl;
hug_close();
return 0;
}