blob: 5d77a116b87424feaf5faa60764af8f7f2c016e5 [file]
/*
* piper.cpp - Speech Dispatcher Piper output module
*
* Copyright (C) 2022 Michael Hansen
* Copyright (C) 2025 Derek L Davies
* Copyright (C) 2026 Sola
*
* This 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 2.1, or (at your option) any later
* version.
*
* This software 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 Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <algorithm>
#include <atomic>
#include <cmath>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include <vector>
#include <piper.h>
#if __has_include(<nlohmann/json.hpp>)
#include <nlohmann/json.hpp>
#elif __has_include(<json.hpp>)
#include <json.hpp>
#endif
#include <rubberband/RubberBandStretcher.h>
#include "spd_audio.h"
#include <speechd_types.h>
#include "module_utils.h"
#define MODULE_NAME "piper"
#define DBG_MODNAME "piper"
#define MODULE_VERSION "0.1"
#define DEBUG_MODULE 5
DECLARE_DEBUG();
namespace piper_module {
using json = nlohmann::json;
using RubberBand::RubberBandStretcher;
MOD_OPTION_1_STR(ModelPath)
MOD_OPTION_1_STR(ConfigPath)
MOD_OPTION_1_STR(SoundIconFolder)
MOD_OPTION_1_STR(ESpeakNGDataDirPath)
static const int bs = 1024;
static piper_synthesizer *synth = NULL;
static json model_config;
static int model_sample_rate = 22050;
static int model_num_speakers = 1;
static std::string model_language = "en_US";
static std::atomic<bool> stop_requested{false};
static int current_speaker_id = 0;
static const char *default_voice_name = NULL;
static SPDVoice **piper_voice_list = NULL;
static RubberBandStretcher::Options initOptions()
{
RubberBandStretcher::Options options = 0;
options |= RubberBandStretcher::OptionProcessRealTime;
options |= RubberBandStretcher::OptionPhaseIndependent;
options |= RubberBandStretcher::OptionThreadingAuto;
options |= RubberBandStretcher::OptionTransientsCrisp;
options |= RubberBandStretcher::OptionDetectorCompound;
return options;
}
static void piper_stretch_and_copy(const int samplerate, const int channels, const float *in_samples, const size_t num_samples, std::vector<int16_t>& sharedAudioBuffer)
{
if (num_samples == 0 || in_samples == NULL) return;
// Peak normalization: Piper neural vocoders output raw float amplitudes
// peaking around 0.4 - 0.6. Normalize to 100% full scale (1.0 / max_sample).
float max_sample = 0.01f;
for (size_t i = 0; i < num_samples; ++i) {
float abs_s = std::abs(in_samples[i]);
if (abs_s > max_sample) {
max_sample = abs_s;
}
}
float norm_factor = (max_sample > 0.01f) ? (1.0f / max_sample) : 1.0f;
// Speed multiplier: map -100..100 onto duration ratio
double speed = 1.0;
if (msg_settings.rate > 0) {
speed = 1.0 + ((double)msg_settings.rate / 50.0);
} else if (msg_settings.rate < 0) {
speed = 1.0 + ((double)msg_settings.rate / 150.0);
}
double ratio = 1.0 / std::max(0.1, speed);
if (ratio < 0.01) ratio = 0.01;
// Pitch shift: map -100 .. 100 onto -10.0 .. 10.0 semitones
double mS = std::clamp((double)msg_settings.pitch, -100.0, 100.0);
double pitchshift = mS / 10.0;
double frequencyshift = 1.0;
if (pitchshift != 0.0) frequencyshift *= std::pow(2.0, pitchshift / 12.0);
// Volume / gain: map SSIP -100..100 onto gain factor (0 is nominal 1.0f gain)
float gain = 1.0f;
if (msg_settings.volume < 0) {
gain = ((float)msg_settings.volume + 100.0f) / 100.0f;
} else if (msg_settings.volume > 0) {
gain = 1.0f + ((float)msg_settings.volume / 100.0f);
}
gain = std::max(0.0f, gain);
// Pre-allocate buffer capacity to avoid multiple dynamic reallocations during stretching
sharedAudioBuffer.reserve(static_cast<size_t>(num_samples * std::max(1.0, ratio) + 1024));
RubberBandStretcher ts(samplerate, channels, initOptions(), ratio, frequencyshift);
ts.setExpectedInputDuration(num_samples);
ts.setMaxProcessSize(bs);
const float sample_scale = norm_factor * 32767.0f;
float cbuf_arr[bs];
float *cbuf_ptrs[1] = { cbuf_arr };
size_t countIn = 0;
while (countIn < num_samples) {
size_t ibSize = std::min((size_t)bs, num_samples - countIn);
for (size_t i = 0; i < ibSize; ++i) {
// Apply peak normalization and scale to 16-bit amplitude range [-32767, 32767]
cbuf_arr[i] = in_samples[countIn + i] * sample_scale;
}
countIn += ibSize;
ts.process(cbuf_ptrs, ibSize, countIn >= num_samples);
int avail;
while ((avail = ts.available()) > 0) {
size_t obSize = std::min((size_t)avail, (size_t)bs);
ts.retrieve(cbuf_ptrs, obSize);
for (size_t i = 0; i < obSize; ++i) {
float value = gain * cbuf_arr[i];
value = std::clamp(value, -32768.0f, 32767.0f);
sharedAudioBuffer.push_back(static_cast<int16_t>(value));
}
}
}
}
static int piper_voice_name_to_speaker_id(const char *voice_name)
{
if (voice_name && model_config.contains("speaker_id_map")) {
auto &spkr_map = model_config["speaker_id_map"];
if (spkr_map.contains(voice_name)) {
return spkr_map[voice_name].get<int>();
}
}
return 0;
}
static SPDVoice **piper_allocate_voice_list()
{
std::filesystem::path modelPath(ModelPath ? ModelPath : "piper");
std::string model_stem = modelPath.stem().string();
if (model_num_speakers > 1 && model_config.contains("speaker_id_map") && !model_config["speaker_id_map"].empty()) {
auto &speakerIdMap = model_config["speaker_id_map"];
int count = speakerIdMap.size();
SPDVoice **result = g_new0(SPDVoice *, count + 1);
SPDVoice **reg_voices = module_list_registered_voices();
int i = 0;
for (auto &speakerItem : speakerIdMap.items()) {
std::string spkr_name = speakerItem.key();
SPDVoice *v = g_new0(SPDVoice, 1);
v->name = g_strdup(spkr_name.c_str());
v->language = g_strdup(model_language.c_str());
const char *var_str = "none";
for (int j = 0; reg_voices && reg_voices[j]; ++j) {
SPDVoice *r_v = reg_voices[j];
if (r_v->name && strcasecmp(v->name, r_v->name) == 0) {
if (r_v->variant && strlen(r_v->variant) > 0) {
var_str = r_v->variant;
}
break;
}
}
v->variant = g_strdup(var_str);
result[i++] = v;
}
result[count] = NULL;
return result;
} else {
SPDVoice **result = g_new0(SPDVoice *, 2);
SPDVoice *v = g_new0(SPDVoice, 1);
v->name = g_strdup(model_stem.c_str());
v->language = g_strdup(model_language.c_str());
v->variant = g_strdup("none");
result[0] = v;
result[1] = NULL;
return result;
}
}
static void piper_free_voice_list()
{
if (piper_voice_list != NULL) {
for (int i = 0; piper_voice_list[i] != NULL; i++) {
g_free(piper_voice_list[i]->name);
g_free(piper_voice_list[i]->language);
g_free(piper_voice_list[i]->variant);
g_free(piper_voice_list[i]);
}
g_free(piper_voice_list);
piper_voice_list = NULL;
}
}
static const char *piper_search_for_sound_icon(const char *icon_name)
{
char *fn = NULL;
if (!SoundIconFolder || strlen(SoundIconFolder) == 0) return fn;
GString *filename = g_string_new(SoundIconFolder);
filename = g_string_append(filename, icon_name);
if (g_file_test(filename->str, G_FILE_TEST_EXISTS)) fn = filename->str;
return g_string_free(filename, (fn == NULL));
}
static void piper_handle_text(const char *data)
{
if (!synth) {
DBG("Synth not initialized");
return;
}
char *cmdInp = (char *) module_strip_ssml(data);
DBG("Input after strip XML: %s", cmdInp);
piper_synthesize_options options = piper_default_synthesize_options(synth);
options.speaker_id = current_speaker_id;
int ret = piper_synthesize_start(synth, cmdInp, &options);
g_free(cmdInp);
if (ret != PIPER_OK) {
DBG("Failed to start synthesis with piper_synthesize_start (error code: %d)", ret);
return;
}
module_report_event_begin();
piper_audio_chunk chunk;
do {
int status = piper_synthesize_next(synth, &chunk);
if (status == PIPER_ERR_GENERIC) {
DBG("piper_synthesize_next failed with PIPER_ERR_GENERIC");
break;
}
if (stop_requested) {
DBG("Stop requested during synthesis");
break;
}
if (chunk.num_samples > 0 && chunk.samples != NULL) {
std::vector<int16_t> chunkAudioBuffer;
piper_stretch_and_copy(chunk.sample_rate, 1, chunk.samples, chunk.num_samples, chunkAudioBuffer);
if (!chunkAudioBuffer.empty() && !stop_requested) {
AudioFormat format = SPD_AUDIO_LE;
AudioTrack track;
track.bits = 16;
track.num_samples = chunkAudioBuffer.size();
track.samples = chunkAudioBuffer.data();
track.num_channels = 1;
track.sample_rate = chunk.sample_rate;
module_tts_output_server(&track, format);
}
}
} while (!chunk.is_last);
module_report_event_end();
}
static void piper_handle_sound_icon(const char *icon_name)
{
char *icon_path = (char *) piper_search_for_sound_icon(icon_name);
if (icon_path != NULL) {
module_report_event_begin();
(void)module_play_file(icon_path);
module_report_icon(icon_path);
module_report_event_end();
g_free(icon_path);
} else {
MSG(3, "Warning: Speaking sound icon name, %s, as a fallback, since audio file can not be found.", icon_name);
piper_handle_text(icon_name);
}
}
static void piper_set_language(char *lang)
{
DBG("%s, lang=%s, voice_type=%d", __FUNCTION__, lang ? lang : "", (int)msg_settings.voice_type);
if (msg_settings.voice_type > 0) {
const char *vname = module_getvoice(lang ? lang : model_language.c_str(), msg_settings.voice_type);
if (vname) {
current_speaker_id = piper_voice_name_to_speaker_id(vname);
return;
}
}
SPDVoice **lst = piper_voice_list;
int index = -1;
if (lang && lst) {
char *dash = strchr(lang, '-');
char *langbase = dash ? g_strndup(lang, dash - lang + 1) : g_strdup_printf("%s-", lang);
for (int i = 0; lst[i]; ++i) {
if (lst[i]->language && !strcasecmp(lst[i]->language, lang)) {
current_speaker_id = piper_voice_name_to_speaker_id(lst[i]->name);
index = i;
break;
}
if (index == -1 && lst[i]->language) {
if (!strncasecmp(lst[i]->language, langbase, strlen(langbase))) {
current_speaker_id = piper_voice_name_to_speaker_id(lst[i]->name);
index = i;
}
}
}
g_free(langbase);
}
if (index == -1) {
if (piper_voice_list && piper_voice_list[0])
current_speaker_id = piper_voice_name_to_speaker_id(piper_voice_list[0]->name);
}
}
static void piper_set_voice_type(SPDVoiceType voice_type)
{
const char *lang = msg_settings.voice.language ? msg_settings.voice.language : model_language.c_str();
const char *vname = module_getvoice(lang, voice_type);
if (vname) {
current_speaker_id = piper_voice_name_to_speaker_id(vname);
} else {
MSG(3, "Warning: No definition of voice type %d, check 'AddVoice' directives in piper.conf file.", (int)voice_type);
}
}
static void piper_set_synthesis_voice(char *voice_name)
{
const char *vname = voice_name ? voice_name : (msg_settings.voice.name ? msg_settings.voice.name : default_voice_name);
current_speaker_id = piper_voice_name_to_speaker_id(vname);
if (current_speaker_id < 0) current_speaker_id = 0;
}
extern "C" {
int module_load(void)
{
INIT_SETTINGS_TABLES();
REGISTER_DEBUG();
MOD_OPTION_1_STR_REG(ModelPath, "");
MOD_OPTION_1_STR_REG(ConfigPath, "");
MOD_OPTION_1_STR_REG(SoundIconFolder, "/usr/share/sounds/sound-icons/");
MOD_OPTION_1_STR_REG(ESpeakNGDataDirPath, "/usr/share/espeak-ng-data/");
module_register_available_voices();
module_register_settings_voices();
return 0;
}
int module_init(char **status_info)
{
module_audio_set_server();
module_audio_init(status_info);
if (!ModelPath || strlen(ModelPath) == 0) {
*status_info = g_strdup("piper: ModelPath is not configured in piper.conf");
return -1;
}
std::string config_path_str;
if (ConfigPath && strlen(ConfigPath) > 0) {
config_path_str = ConfigPath;
} else {
config_path_str = std::string(ModelPath) + ".json";
}
try {
std::ifstream config_file(config_path_str);
if (config_file.is_open()) {
model_config = json::parse(config_file);
if (model_config.contains("audio") && model_config["audio"].contains("sample_rate")) {
model_sample_rate = model_config["audio"]["sample_rate"].get<int>();
}
if (model_config.contains("num_speakers")) {
model_num_speakers = model_config["num_speakers"].get<int>();
}
if (model_config.contains("language") && model_config["language"].contains("code")) {
model_language = model_config["language"]["code"].get<std::string>();
}
} else {
DBG(DBG_MODNAME " Warning: Could not open JSON config file at '%s'", config_path_str.c_str());
}
piper_create_options opts;
piper_init_create_options(&opts);
opts.model_path = ModelPath;
opts.config_path = config_path_str.c_str();
opts.espeak_data_path = (ESpeakNGDataDirPath && strlen(ESpeakNGDataDirPath) > 0) ? ESpeakNGDataDirPath : NULL;
synth = piper_create_with_options(&opts);
if (!synth) {
*status_info = g_strdup("piper: Failed to create piper synthesizer with piper_create_with_options");
return -1;
}
default_voice_name = module_getdefaultvoice();
current_speaker_id = piper_voice_name_to_speaker_id(default_voice_name);
if (current_speaker_id < 0) current_speaker_id = 0;
piper_voice_list = piper_allocate_voice_list();
*status_info = g_strdup(DBG_MODNAME " Initialized successfully.");
}
catch (const json::parse_error& e) {
DBG(DBG_MODNAME " Could not initialize, caught JSON exception: %s", e.what());
*status_info = g_strdup(e.what());
return -1;
}
catch (const std::runtime_error& e) {
DBG(DBG_MODNAME " Could not initialize, caught runtime_error exception: %s", e.what());
*status_info = g_strdup(e.what());
return -1;
}
catch (const std::exception& e) {
DBG(DBG_MODNAME " Could not initialize, caught exception: %s", e.what());
*status_info = g_strdup(e.what());
return -1;
}
return 0;
}
SPDVoice **module_list_voices(void)
{
return piper_voice_list;
}
void module_speak_sync(const char *data, size_t bytes, SPDMessageType msgtype)
{
stop_requested = false;
UPDATE_STRING_PARAMETER(voice.language, piper_set_language);
UPDATE_PARAMETER(voice_type, piper_set_voice_type);
UPDATE_STRING_PARAMETER(voice.name, piper_set_synthesis_voice);
module_speak_ok();
switch (msgtype) {
case SPD_MSGTYPE_CHAR:
case SPD_MSGTYPE_KEY:
case SPD_MSGTYPE_SPELL:
case SPD_MSGTYPE_TEXT:
piper_handle_text(data);
break;
case SPD_MSGTYPE_SOUND_ICON:
piper_handle_sound_icon(data);
break;
}
}
int module_stop(void)
{
stop_requested = true;
return 0;
}
size_t module_pause(void)
{
stop_requested = true;
return 0;
}
int module_close(void)
{
if (synth) {
piper_free(synth);
synth = NULL;
}
piper_free_voice_list();
return 0;
}
} // extern "C"
} // namespace piper_module