700 lines
17 KiB
C++
700 lines
17 KiB
C++
// ------------------------------------------------------------------------
|
|
// eca-control-base.cpp: Base class providing basic functionality
|
|
// for controlling the ecasound library
|
|
// Copyright (C) 1999-2004,2006,2008,2009 Kai Vehmanen
|
|
//
|
|
// Attributes:
|
|
// eca-style-version: 3 (see Ecasound Programmer's Guide)
|
|
//
|
|
// This program is free software; you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation; either version 2 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program 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 General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program; if not, write to the Free Software
|
|
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
// ------------------------------------------------------------------------
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include <pthread.h>
|
|
#include <signal.h>
|
|
#include <unistd.h>
|
|
|
|
#include <kvu_dbc.h>
|
|
#include <kvu_utils.h>
|
|
#include <kvu_numtostr.h>
|
|
#include <kvu_message_item.h>
|
|
#include <kvu_value_queue.h>
|
|
|
|
#include "eca-engine.h"
|
|
#include "eca-session.h"
|
|
#include "eca-chainsetup.h"
|
|
#include "eca-resources.h"
|
|
#include "eca-control.h"
|
|
|
|
#include "eca-error.h"
|
|
#include "eca-logger.h"
|
|
|
|
/**
|
|
* Import namespaces
|
|
*/
|
|
using std::list;
|
|
using std::string;
|
|
using std::vector;
|
|
|
|
/**
|
|
* Definitions for member functions
|
|
*/
|
|
|
|
/**
|
|
* Helper function for starting the slave thread.
|
|
*/
|
|
void* ECA_CONTROL::start_normal_thread(void *ptr)
|
|
{
|
|
ECA_CONTROL* ctrl_base = static_cast<ECA_CONTROL*>(ptr);
|
|
ctrl_base->engine_pid_rep = getpid();
|
|
DBC_CHECK(ctrl_base->engine_pid_rep >= 0);
|
|
|
|
ECA_LOG_MSG(ECA_LOGGER::system_objects, "Engine thread started with pid: " + kvu_numtostr(ctrl_base->engine_pid_rep));
|
|
|
|
ctrl_base->run_engine();
|
|
|
|
ECA_LOG_MSG(ECA_LOGGER::system_objects,
|
|
"Engine thread " + kvu_numtostr(ctrl_base->engine_pid_rep) + " will exit.\n");
|
|
ctrl_base->engine_pid_rep = -1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Initializes the engine
|
|
*
|
|
* @pre is_connected() == true
|
|
* @pre is_engine_created() != true
|
|
*/
|
|
void ECA_CONTROL::engine_start(void)
|
|
{
|
|
// --------
|
|
DBC_REQUIRE(is_connected() == true);
|
|
DBC_REQUIRE(is_engine_created() != true);
|
|
// --------
|
|
|
|
start_engine_sub(false);
|
|
}
|
|
|
|
/**
|
|
* Start the processing engine
|
|
*
|
|
* @pre is_connected() == true
|
|
* @pre is_running() != true
|
|
* @post is_engine_created() == true
|
|
*
|
|
* @return negative on error, zero on success
|
|
*/
|
|
int ECA_CONTROL::start(void)
|
|
{
|
|
// --------
|
|
DBC_REQUIRE(is_connected() == true);
|
|
DBC_REQUIRE(is_running() != true);
|
|
// --------
|
|
|
|
int result = 0;
|
|
|
|
ECA_LOG_MSG(ECA_LOGGER::subsystems, "Controller/Processing started");
|
|
|
|
if (is_engine_created() != true) {
|
|
/* request_batchmode=false */
|
|
start_engine_sub(false);
|
|
}
|
|
|
|
if (is_engine_created() != true) {
|
|
ECA_LOG_MSG(ECA_LOGGER::info, "Can't start processing: couldn't start engine.");
|
|
result = -1;
|
|
}
|
|
else {
|
|
engine_repp->command(ECA_ENGINE::ep_start, 0.0);
|
|
}
|
|
|
|
// --------
|
|
DBC_ENSURE(result != 0 || is_engine_created() == true);
|
|
// --------
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Starts the processing engine and blocks until
|
|
* processing is finished.
|
|
*
|
|
* @param batchmode if true, runs until finished/stopped state is reached, and
|
|
* then returns; if false, will run infinitely
|
|
*
|
|
* @pre is_connected() == true
|
|
* @pre is_running() != true
|
|
* @post is_finished() == true ||
|
|
* processing_started == true && is_running() != true ||
|
|
* processing_started != true &&
|
|
* (is_engine_created() != true ||
|
|
* is_engine_created() == true &&
|
|
* engine_repp->status() != ECA_ENGINE::engine_status_stopped))
|
|
*
|
|
* @return negative on error, zero on success
|
|
*/
|
|
int ECA_CONTROL::run(bool batchmode)
|
|
{
|
|
// --------
|
|
DBC_REQUIRE(is_connected() == true);
|
|
DBC_REQUIRE(is_running() != true);
|
|
// --------
|
|
|
|
ECA_LOG_MSG(ECA_LOGGER::subsystems, "Controller/Starting batch processing");
|
|
|
|
bool processing_started = false;
|
|
int result = -1;
|
|
|
|
if (is_engine_created() != true) {
|
|
/* request_batchmode=true */
|
|
start_engine_sub(batchmode);
|
|
}
|
|
|
|
if (is_engine_created() != true) {
|
|
ECA_LOG_MSG(ECA_LOGGER::info, "Can't start processing: couldn't start the engine. (2)");
|
|
}
|
|
else {
|
|
engine_repp->command(ECA_ENGINE::ep_start, 0.0);
|
|
|
|
DBC_CHECK(is_finished() != true);
|
|
|
|
result = 0;
|
|
|
|
/* run until processing is finished; in batchmode run forever (or
|
|
* until error occurs) */
|
|
while(is_finished() != true || batchmode != true) {
|
|
|
|
/* sleep for 250ms */
|
|
kvu_sleep(0, 250000000);
|
|
|
|
if (processing_started != true) {
|
|
if (is_running() == true ||
|
|
is_finished() == true ||
|
|
engine_exited_rep.get() == 1) {
|
|
/* make a note that engine state changed to 'running' */
|
|
processing_started = true;
|
|
}
|
|
else if (is_engine_created() == true) {
|
|
if (engine_repp->status() == ECA_ENGINE::engine_status_error) {
|
|
/* not running, so status() is either 'not_ready' or 'error' */
|
|
ECA_LOG_MSG(ECA_LOGGER::info, "Can't start processing: engine startup failed. (3)");
|
|
result = -2;
|
|
break;
|
|
}
|
|
/* other valid state alternatives: */
|
|
DBC_CHECK(engine_repp->status() == ECA_ENGINE::engine_status_stopped ||
|
|
engine_repp->status() == ECA_ENGINE::engine_status_notready);
|
|
}
|
|
else {
|
|
/* ECA_CONTROL_BASE destructor has been run and
|
|
* engine_repp is now 0 (--> is_engine_created() != true) */
|
|
break;
|
|
}
|
|
}
|
|
else {
|
|
/* engine was started succesfully (processing_started == true) */
|
|
if (is_running() != true) {
|
|
/* operation succesfully completed, exit from run() unless
|
|
* infinite operation is requested (batchmode) */
|
|
if (batchmode == true) break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (last_exec_res_rep < 0) {
|
|
/* error occured during processing */
|
|
result = -3;
|
|
}
|
|
|
|
ECA_LOG_MSG(ECA_LOGGER::subsystems,
|
|
std::string("Controller/Batch processing finished (")
|
|
+ kvu_numtostr(result) + ")");
|
|
|
|
// --------
|
|
DBC_ENSURE(is_finished() == true ||
|
|
(processing_started == true && is_running()) != true ||
|
|
(processing_started != true &&
|
|
(is_engine_created() != true ||
|
|
(is_engine_created() == true &&
|
|
engine_repp->status() != ECA_ENGINE::engine_status_stopped))));
|
|
// --------
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Stops the processing engine.
|
|
*
|
|
* @see stop_on_condition()
|
|
*
|
|
* @pre is_engine_created() == true
|
|
* @pre is_running() == true
|
|
* @post is_running() == false
|
|
*/
|
|
void ECA_CONTROL::stop(void)
|
|
{
|
|
// --------
|
|
DBC_REQUIRE(is_engine_created() == true);
|
|
DBC_REQUIRE(is_running() == true);
|
|
// --------
|
|
|
|
ECA_LOG_MSG(ECA_LOGGER::subsystems, "Controller/Processing stopped");
|
|
engine_repp->command(ECA_ENGINE::ep_stop, 0.0);
|
|
|
|
// --------
|
|
// ensure:
|
|
// assert(is_running() == false);
|
|
// -- there's a small timeout so assertion cannot be checked
|
|
// --------
|
|
}
|
|
|
|
/**
|
|
* Stop the processing engine using thread-to-thread condition
|
|
* signaling.
|
|
*
|
|
* @pre is_engine_created() == true
|
|
* @post is_running() == false
|
|
*/
|
|
void ECA_CONTROL::stop_on_condition(void)
|
|
{
|
|
// --------
|
|
DBC_REQUIRE(is_engine_created() == true);
|
|
// --------
|
|
|
|
if (engine_repp->status() != ECA_ENGINE::engine_status_running) return;
|
|
ECA_LOG_MSG(ECA_LOGGER::subsystems, "Controller/Processing stopped (cond)");
|
|
engine_repp->command(ECA_ENGINE::ep_stop, 0.0);
|
|
ECA_LOG_MSG(ECA_LOGGER::system_objects, "Received stop-cond");
|
|
|
|
// --
|
|
// blocks until engine has stopped (or 5 sec has passed);
|
|
engine_repp->wait_for_stop(5);
|
|
|
|
// --------
|
|
DBC_ENSURE(is_running() == false);
|
|
// --------
|
|
}
|
|
|
|
/**
|
|
* Stops the processing engine.
|
|
* Call will block until engine has terminated.
|
|
*/
|
|
void ECA_CONTROL::quit(void) { close_engine(); }
|
|
|
|
/**
|
|
* Stops the processing engine. A thread-safe variant of
|
|
* quit(). Call will not block.
|
|
*
|
|
*/
|
|
void ECA_CONTROL::quit_async(void)
|
|
{
|
|
if (is_engine_running() != true)
|
|
return;
|
|
|
|
engine_repp->command(ECA_ENGINE::ep_exit, 0.0);
|
|
}
|
|
|
|
/**
|
|
* Starts the processing engine.
|
|
*
|
|
* @pre is_connected() == true
|
|
* @pre is_engine_running() != true
|
|
*/
|
|
void ECA_CONTROL::start_engine_sub(bool batchmode)
|
|
{
|
|
// --------
|
|
DBC_REQUIRE(is_connected() == true);
|
|
DBC_REQUIRE(is_engine_running() != true);
|
|
// --------
|
|
|
|
DBC_CHECK(engine_exited_rep.get() != 1);
|
|
|
|
unsigned int p = session_repp->connected_chainsetup_repp->first_selected_chain();
|
|
if (p < session_repp->connected_chainsetup_repp->chains.size())
|
|
session_repp->connected_chainsetup_repp->selected_chain_index_rep = p;
|
|
|
|
if (engine_repp)
|
|
close_engine();
|
|
|
|
DBC_CHECK(is_engine_created() != true);
|
|
engine_repp = new ECA_ENGINE (session_repp->connected_chainsetup_repp);
|
|
DBC_CHECK(is_engine_created() == true);
|
|
|
|
/* to relay the batchmode parameter to created new thread */
|
|
req_batchmode_rep = batchmode;
|
|
|
|
pthread_attr_t th_attr;
|
|
pthread_attr_init(&th_attr);
|
|
int retcode_rep = pthread_create(&th_cqueue_rep,
|
|
&th_attr,
|
|
start_normal_thread,
|
|
static_cast<void *>(this));
|
|
if (retcode_rep != 0) {
|
|
ECA_LOG_MSG(ECA_LOGGER::info, "WARNING: Unable to create a new thread for engine.");
|
|
ECA_ENGINE *engine_tmp = engine_repp;
|
|
engine_repp = 0;
|
|
delete engine_tmp;
|
|
}
|
|
|
|
DBC_ENSURE(is_engine_created() == true);
|
|
}
|
|
|
|
/**
|
|
* Routine used for launching the engine.
|
|
*/
|
|
void ECA_CONTROL::run_engine(void)
|
|
{
|
|
last_exec_res_rep = 0;
|
|
last_exec_res_rep = engine_repp->exec(req_batchmode_rep);
|
|
engine_exited_rep.set(1);
|
|
}
|
|
|
|
/**
|
|
* Closes the processing engine.
|
|
*
|
|
* ensure:
|
|
* is_engine_created() != true
|
|
* is_engine_running() != true
|
|
*/
|
|
void ECA_CONTROL::close_engine(void)
|
|
{
|
|
if (is_engine_created() != true) return;
|
|
|
|
engine_repp->command(ECA_ENGINE::ep_exit, 0.0);
|
|
|
|
ECA_LOG_MSG(ECA_LOGGER::system_objects, "Waiting for engine thread to exit.");
|
|
if (joining_rep != true) {
|
|
joining_rep = true;
|
|
int res = pthread_join(th_cqueue_rep,NULL);
|
|
joining_rep = false;
|
|
ECA_LOG_MSG(ECA_LOGGER::system_objects,
|
|
"pthread_join returned: "
|
|
+ kvu_numtostr(res));
|
|
}
|
|
else {
|
|
DBC_CHECK(engine_pid_rep >= 0);
|
|
int i;
|
|
for (i = 0; i < 30; i++) {
|
|
|
|
if (engine_exited_rep.get() ==1)
|
|
break;
|
|
|
|
/* 100ms sleep */
|
|
kvu_sleep(0, 100000000);
|
|
}
|
|
ECA_LOG_MSG(ECA_LOGGER::info, "WARNING: engine is stuck, sending SIGKILL.");
|
|
DBC_CHECK(engine_pid_rep >= 0);
|
|
/* note: we use SIGKILL as SIGTERM, SIGINT et al are blocked and
|
|
* handled by the watchdog thread */
|
|
pthread_kill(th_cqueue_rep, SIGKILL);
|
|
}
|
|
|
|
if (engine_exited_rep.get() == 1) {
|
|
ECA_LOG_MSG(ECA_LOGGER::system_objects, "Engine thread has exited succesfully.");
|
|
delete engine_repp;
|
|
engine_repp = 0;
|
|
engine_exited_rep.set(0);
|
|
}
|
|
else {
|
|
ECA_LOG_MSG(ECA_LOGGER::info, "WARNING: Problems while shutting down the engine!");
|
|
}
|
|
|
|
// ---
|
|
DBC_ENSURE(is_engine_created() != true);
|
|
DBC_ENSURE(is_engine_running() != true);
|
|
// ---
|
|
}
|
|
|
|
/**
|
|
* Is currently selected chainsetup valid?
|
|
*
|
|
* @pre is_selected()
|
|
*/
|
|
bool ECA_CONTROL::is_valid(void) const
|
|
{
|
|
// --------
|
|
DBC_REQUIRE(is_selected());
|
|
// --------
|
|
|
|
/* use is_valid_for_connection() instead of is_valid() to
|
|
* report any detected errors via the logging subsystem */
|
|
return selected_chainsetup_repp->is_valid_for_connection(true);
|
|
}
|
|
|
|
/**
|
|
* Returns true if active chainsetup exists and is connected.
|
|
*/
|
|
bool ECA_CONTROL::is_connected(void) const
|
|
{
|
|
if (session_repp->connected_chainsetup_repp == 0) {
|
|
return false;
|
|
}
|
|
|
|
return (session_repp->connected_chainsetup_repp->is_valid() &&
|
|
session_repp->connected_chainsetup_repp->is_enabled());
|
|
}
|
|
|
|
/**
|
|
* Returns true if some chainsetup is selected.
|
|
*/
|
|
bool ECA_CONTROL::is_selected(void) const { return selected_chainsetup_repp != 0; }
|
|
|
|
/**
|
|
* Returns true if processing engine is running.
|
|
*/
|
|
bool ECA_CONTROL::is_running(void) const { return (is_engine_created() == true && engine_repp->status() == ECA_ENGINE::engine_status_running); }
|
|
|
|
/**
|
|
* Returns true if engine has finished processing. Engine state is
|
|
* either "finished" or "error".
|
|
*/
|
|
bool ECA_CONTROL::is_finished(void) const
|
|
{
|
|
return (is_engine_created() == true &&
|
|
(engine_repp->status() == ECA_ENGINE::engine_status_finished ||
|
|
engine_repp->status() == ECA_ENGINE::engine_status_error));
|
|
}
|
|
|
|
string ECA_CONTROL::resource_value(const string& key) const
|
|
{
|
|
ECA_RESOURCES ecarc;
|
|
return ecarc.resource(key);
|
|
}
|
|
|
|
/**
|
|
* Returns the length of the selected chainsetup (in samples).
|
|
*
|
|
* @pre is_selected() == true
|
|
*/
|
|
SAMPLE_SPECS::sample_pos_t ECA_CONTROL::length_in_samples(void) const
|
|
{
|
|
// --------
|
|
DBC_REQUIRE(is_selected());
|
|
// --------
|
|
|
|
SAMPLE_SPECS::sample_pos_t cslen = 0;
|
|
if (selected_chainsetup_repp->length_set() == true) {
|
|
cslen = selected_chainsetup_repp->length_in_samples();
|
|
}
|
|
if (selected_chainsetup_repp->max_length_set() == true) {
|
|
cslen = selected_chainsetup_repp->max_length_in_samples();
|
|
}
|
|
|
|
return cslen;
|
|
}
|
|
|
|
/**
|
|
* Returns the length of the selected chainsetup (in seconds).
|
|
*
|
|
* @pre is_selected() == true
|
|
*/
|
|
double ECA_CONTROL::length_in_seconds_exact(void) const
|
|
{
|
|
// --------
|
|
DBC_REQUIRE(is_selected());
|
|
// --------
|
|
|
|
double cslen = 0.0f;
|
|
if (selected_chainsetup_repp->length_set() == true) {
|
|
cslen = selected_chainsetup_repp->length_in_seconds_exact();
|
|
}
|
|
if (selected_chainsetup_repp->max_length_set() == true) {
|
|
cslen = selected_chainsetup_repp->max_length_in_seconds_exact();
|
|
}
|
|
|
|
return cslen;
|
|
}
|
|
|
|
/**
|
|
* Returns the current position of the selected chainsetup (in samples).
|
|
*
|
|
* @pre is_selected() == true
|
|
*/
|
|
SAMPLE_SPECS::sample_pos_t ECA_CONTROL::position_in_samples(void) const
|
|
{
|
|
// --------
|
|
DBC_REQUIRE(is_selected());
|
|
// --------
|
|
|
|
return selected_chainsetup_repp->position_in_samples();
|
|
}
|
|
|
|
/**
|
|
* Returns the current position of the selected chainsetup (in seconds).
|
|
*
|
|
* @pre is_selected() == true
|
|
*/
|
|
double ECA_CONTROL::position_in_seconds_exact(void) const
|
|
{
|
|
// --------
|
|
DBC_REQUIRE(is_selected());
|
|
// --------
|
|
|
|
return selected_chainsetup_repp->position_in_seconds_exact();
|
|
}
|
|
|
|
/**
|
|
* Returns true if engine object has been created.
|
|
* If true, the engine object is available for use, but
|
|
* the related engine thread is not necessarily running.
|
|
*
|
|
* @see is_engine_running()
|
|
*/
|
|
bool ECA_CONTROL::is_engine_created(void) const
|
|
{
|
|
return (engine_repp != 0);
|
|
}
|
|
|
|
/**
|
|
* Returns true if engine is running and ready to receive
|
|
* control commands via the message queue.
|
|
*
|
|
* In practise running means that the engine thread
|
|
* has been created and it is running the exec() method
|
|
* of the engine.
|
|
*
|
|
* @see is_engine_created()
|
|
*/
|
|
bool ECA_CONTROL::is_engine_running(void) const
|
|
{
|
|
bool started = is_engine_created();
|
|
|
|
if (started != true)
|
|
return false;
|
|
|
|
/* note: has been started, but run_engine() has returned */
|
|
if (engine_pid_rep < 0)
|
|
return false;
|
|
|
|
DBC_CHECK(engine_repp != 0);
|
|
if (engine_repp->status() ==
|
|
ECA_ENGINE::engine_status_notready)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Return info about engine status.
|
|
*/
|
|
string ECA_CONTROL::engine_status(void) const
|
|
{
|
|
if (is_engine_created() == true) {
|
|
switch(engine_repp->status()) {
|
|
case ECA_ENGINE::engine_status_running:
|
|
{
|
|
return "running";
|
|
}
|
|
case ECA_ENGINE::engine_status_stopped:
|
|
{
|
|
return "stopped";
|
|
}
|
|
case ECA_ENGINE::engine_status_finished:
|
|
{
|
|
return "finished";
|
|
}
|
|
case ECA_ENGINE::engine_status_error:
|
|
{
|
|
return "error";
|
|
}
|
|
case ECA_ENGINE::engine_status_notready:
|
|
{
|
|
return "not ready";
|
|
}
|
|
default:
|
|
{
|
|
return "unknown status";
|
|
}
|
|
}
|
|
}
|
|
return "not started";
|
|
}
|
|
|
|
void ECA_CONTROL::set_last_string(const list<string>& s)
|
|
{
|
|
string s_rep;
|
|
|
|
DBC_CHECK(s_rep.size() == 0);
|
|
list<string>::const_iterator p = s.begin();
|
|
while(p != s.end()) {
|
|
s_rep += *p;
|
|
++p;
|
|
if (p != s.end()) s_rep += "\n";
|
|
}
|
|
set_last_string(s_rep);
|
|
}
|
|
|
|
void ECA_CONTROL::set_last_string_list(const vector<string>& s)
|
|
{
|
|
last_retval_rep.type = eci_return_value::retval_string_list;
|
|
last_retval_rep.string_list_val = s;
|
|
}
|
|
|
|
void ECA_CONTROL::set_last_string(const string& s)
|
|
{
|
|
last_retval_rep.type = eci_return_value::retval_string;
|
|
last_retval_rep.string_val = s;
|
|
}
|
|
|
|
void ECA_CONTROL::set_last_float(double v)
|
|
{
|
|
last_retval_rep.type = eci_return_value::retval_float;
|
|
last_retval_rep.m.float_val = v;
|
|
}
|
|
|
|
void ECA_CONTROL::set_last_integer(int v)
|
|
{
|
|
last_retval_rep.type = eci_return_value::retval_integer;
|
|
last_retval_rep.m.int_val = v;
|
|
}
|
|
|
|
void ECA_CONTROL::set_last_long_integer(long int v)
|
|
{
|
|
last_retval_rep.type = eci_return_value::retval_long_integer;
|
|
last_retval_rep.m.long_int_val = v;
|
|
}
|
|
|
|
void ECA_CONTROL::set_last_error(const string& s)
|
|
{
|
|
last_retval_rep.type = eci_return_value::retval_error;
|
|
last_retval_rep.string_val = s;
|
|
}
|
|
|
|
string ECA_CONTROL::last_error(void) const
|
|
{
|
|
if (last_retval_rep.type == eci_return_value::retval_error)
|
|
return last_retval_rep.string_val;
|
|
|
|
return string();
|
|
}
|
|
|
|
void ECA_CONTROL::clear_last_values(void)
|
|
{
|
|
ECA_CONTROL_MAIN::clear_return_value(&last_retval_rep);
|
|
}
|
|
|
|
void ECA_CONTROL::set_float_to_string_precision(int precision)
|
|
{
|
|
float_to_string_precision_rep = precision;
|
|
}
|
|
std::string ECA_CONTROL::float_to_string(double n) const
|
|
{
|
|
return kvu_numtostr(n, float_to_string_precision_rep);
|
|
}
|