Skip to content

Commit 49fab2f

Browse files
authored
Ported C++ CLI executable from legacy Piper repository (#215)
* Ported C++ executable from old repository * Added tests * Added version parsing from setup.py
1 parent ab2e9b6 commit 49fab2f

18 files changed

Lines changed: 938 additions & 2 deletions

libpiper/CMakeLists.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,21 @@ target_link_libraries(piper
147147
onnxruntime
148148
)
149149

150+
# ---- piper exe ---
151+
add_subdirectory(src/main)
152+
153+
# ---- version
154+
file(READ ${CMAKE_SOURCE_DIR}/../setup.py SETUP_PY_CONTENTS)
155+
string(REGEX MATCH "version=\"([0-9]+\\.[0-9]+\\.[0-9]+)\"" _ ${SETUP_PY_CONTENTS})
156+
set(PIPER_VERSION ${CMAKE_MATCH_1})
157+
158+
if(PIPER_VERSION)
159+
message(STATUS "Parsed Piper Version: ${PIPER_VERSION}")
160+
target_compile_definitions(piper PRIVATE PIPER_VERSION="${PIPER_VERSION}")
161+
else()
162+
message(WARNING "Could not parse version from setup.py")
163+
endif()
164+
150165
# ---- install ---
151166

152167
include(GNUInstallDirs)

libpiper/include/piper.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,14 @@ int piper_synthesize_start(piper_synthesizer *synth, const char *text,
225225
EXPORT_SYMBOL
226226
int piper_synthesize_next(piper_synthesizer *synth, piper_audio_chunk *chunk);
227227

228+
229+
/**
230+
* \return piper version
231+
*/
232+
EXPORT_SYMBOL
233+
char const *piper_version(void);
234+
235+
228236
#ifdef __cplusplus
229237
}
230238
#endif

libpiper/src/main/CMakeLists.txt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
cmake_minimum_required(VERSION 3.26)
2+
3+
add_executable(piper_exe main.cpp)
4+
5+
add_library(main_utils
6+
utils/wavfile.cpp
7+
utils/main_utils.cpp
8+
utils/process.cpp
9+
utils/wav_headers.cpp)
10+
11+
target_include_directories(
12+
main_utils
13+
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
14+
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/utils)
15+
16+
target_sources(
17+
main_utils
18+
PUBLIC FILE_SET HEADERS
19+
BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}
20+
FILES utils/wavfile.hpp utils/main_utils.hpp utils/process.hpp
21+
utils/wav_headers.hpp)
22+
23+
target_link_libraries(main_utils PRIVATE piper)
24+
25+
target_link_libraries(piper_exe PRIVATE piper main_utils)
26+
27+
install(
28+
TARGETS piper_exe
29+
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
30+
)

libpiper/src/main/main.cpp

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#include <chrono>
2+
#include <condition_variable>
3+
#include <filesystem>
4+
#include <fstream>
5+
#include <functional>
6+
#include <iostream>
7+
#include <map>
8+
#include <sstream>
9+
#include <stdexcept>
10+
#include <string>
11+
#include <thread>
12+
#include <vector>
13+
14+
#ifdef _MSC_VER
15+
#define WIN32_LEAN_AND_MEAN
16+
#define NOMINMAX
17+
#include <windows.h>
18+
#endif
19+
20+
#ifdef _WIN32
21+
#include <fcntl.h>
22+
#include <io.h>
23+
#endif
24+
25+
#ifdef __APPLE__
26+
#include <mach-o/dyld.h>
27+
#endif
28+
29+
#include "utils/process.hpp"
30+
#include "utils/main_utils.hpp"
31+
32+
#include "json.hpp"
33+
#include "piper.h"
34+
#include "piper_impl.hpp"
35+
36+
using namespace std;
37+
38+
// ----------------------------------------------------------------------------
39+
40+
int main(int argc, char *argv[]) {
41+
piper::RunConfig runConfig;
42+
parseArgs(argc, argv, runConfig);
43+
44+
#ifdef _WIN32
45+
// Required on Windows to show IPA symbols
46+
SetConsoleOutputCP(CP_UTF8);
47+
#endif
48+
piper_synthesizer *piper;
49+
50+
// Get the path to the piper executable so we can locate espeak-ng-data, etc.
51+
// next to it.
52+
#ifdef _MSC_VER
53+
auto exePath = []() {
54+
wchar_t moduleFileName[MAX_PATH] = {0};
55+
GetModuleFileNameW(nullptr, moduleFileName, std::size(moduleFileName));
56+
return filesystem::path(moduleFileName);
57+
}();
58+
#else
59+
#ifdef __APPLE__
60+
auto exePath = []() {
61+
char moduleFileName[PATH_MAX] = {0};
62+
uint32_t moduleFileNameSize = std::size(moduleFileName);
63+
_NSGetExecutablePath(moduleFileName, &moduleFileNameSize);
64+
return filesystem::path(moduleFileName);
65+
}();
66+
#else
67+
auto exePath = filesystem::canonical("/proc/self/exe");
68+
#endif
69+
#endif
70+
71+
if (runConfig.eSpeakDataPath) {
72+
// User provided path
73+
runConfig.eSpeakDataPath = runConfig.eSpeakDataPath.value().string();
74+
} else {
75+
// Assume next to piper executable
76+
runConfig.eSpeakDataPath =
77+
std::filesystem::absolute(
78+
exePath.parent_path().append("espeak-ng-data"))
79+
.string();
80+
}
81+
auto startTime = chrono::steady_clock::now();
82+
piper = piper_create(runConfig.modelPath.string().c_str(),
83+
runConfig.modelConfigPath.string().c_str(),
84+
runConfig.eSpeakDataPath->string().c_str()
85+
);
86+
auto endTime = chrono::steady_clock::now();
87+
88+
piper_synthesize_options options;
89+
options.speaker_id = 0;
90+
options.length_scale = DEFAULT_LENGTH_SCALE;
91+
options.noise_scale = DEFAULT_NOISE_SCALE;
92+
options.noise_w_scale = DEFAULT_NOISE_W_SCALE;
93+
94+
// Speaker ID
95+
if (runConfig.speakerId) {
96+
options.speaker_id = runConfig.speakerId.value();
97+
}
98+
99+
// Scales
100+
if (runConfig.noiseScale) {
101+
options.noise_scale = runConfig.noiseScale.value();
102+
}
103+
104+
if (runConfig.lengthScale) {
105+
options.length_scale = runConfig.lengthScale.value();
106+
}
107+
108+
if (runConfig.noiseW) {
109+
options.noise_w_scale = runConfig.noiseW.value();
110+
}
111+
112+
if (runConfig.outputType == piper::OUTPUT_DIRECTORY) {
113+
runConfig.outputPath = filesystem::absolute(runConfig.outputPath.value());
114+
}
115+
116+
processInputStream(runConfig, piper, &options);
117+
118+
piper_free(piper);
119+
120+
return EXIT_SUCCESS;
121+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
#include "main_utils.hpp"
2+
3+
#include <filesystem>
4+
#include <fstream>
5+
#include <iostream>
6+
#include <optional>
7+
#include <stdexcept>
8+
#include <string>
9+
10+
#include "piper.h"
11+
12+
namespace piper {
13+
14+
void printUsage(char *argv[]) {
15+
std::cerr << std::endl;
16+
std::cerr << "usage: " << argv[0] << " [options]" << std::endl;
17+
std::cerr << std::endl;
18+
std::cerr << "options:" << std::endl;
19+
std::cerr << " -h --help show this message and exit"
20+
<< std::endl;
21+
std::cerr << " -m FILE --model FILE path to onnx model file"
22+
<< std::endl;
23+
std::cerr << " -c FILE --config FILE path to model config file "
24+
"(default: model path + .json)"
25+
<< std::endl;
26+
std::cerr << " -f FILE --output_file FILE path to output WAV file ('-' "
27+
"for stdout)"
28+
<< std::endl;
29+
std::cerr << " -d DIR --output_dir DIR path to output directory "
30+
"(default: cwd)"
31+
<< std::endl;
32+
std::cerr << " -s NUM --speaker NUM id of speaker (default: 0)"
33+
<< std::endl;
34+
std::cerr << " --noise_scale NUM generator noise (default: 0.667)"
35+
<< std::endl;
36+
std::cerr << " --length_scale NUM phoneme length (default: 1.0)"
37+
<< std::endl;
38+
std::cerr << " --noise_w NUM phoneme width noise (default: 0.8)"
39+
<< std::endl;
40+
std::cerr << " --espeak_data DIR path to espeak-ng data directory"
41+
<< std::endl;
42+
std::cerr << " --json-input stdin input is lines of JSON "
43+
"instead of plain text"
44+
<< std::endl;
45+
std::cerr << std::endl;
46+
}
47+
48+
void ensureArg(int argc, char *argv[], int argi) {
49+
if ((argi + 1) >= argc)
50+
{
51+
throw ArgError(std::string("Missing argument for ") + argv[argi]);
52+
}
53+
}
54+
55+
// Parse command-line arguments
56+
void parseArgsLogic(int argc, char *argv[], RunConfig &runConfig) {
57+
std::optional<std::filesystem::path> modelConfigPath;
58+
59+
for (int i = 1; i < argc; i++) {
60+
std::string arg = argv[i];
61+
62+
if (arg == "-m" || arg == "--model") {
63+
ensureArg(argc, argv, i);
64+
runConfig.modelPath = std::filesystem::path(argv[++i]);
65+
} else if (arg == "-c" || arg == "--config") {
66+
ensureArg(argc, argv, i);
67+
modelConfigPath = std::filesystem::path(argv[++i]);
68+
} else if (arg == "-f" || arg == "--output_file" ||
69+
arg == "--output-file") {
70+
ensureArg(argc, argv, i);
71+
std::string filePath = argv[++i];
72+
if (filePath == "-") {
73+
runConfig.outputType = OUTPUT_STDOUT;
74+
runConfig.outputPath = std::nullopt;
75+
} else {
76+
runConfig.outputType = OUTPUT_FILE;
77+
runConfig.outputPath = std::filesystem::path(filePath);
78+
}
79+
} else if (arg == "-d" || arg == "--output_dir" || arg == "--output-dir") {
80+
ensureArg(argc, argv, i);
81+
runConfig.outputType = OUTPUT_DIRECTORY;
82+
runConfig.outputPath = std::filesystem::path(argv[++i]);
83+
} else if (arg == "-s" || arg == "--speaker") {
84+
ensureArg(argc, argv, i);
85+
runConfig.speakerId = std::stol(argv[++i]);
86+
} else if (arg == "--noise_scale" || arg == "--noise-scale") {
87+
ensureArg(argc, argv, i);
88+
runConfig.noiseScale = std::stof(argv[++i]);
89+
} else if (arg == "--length_scale" || arg == "--length-scale") {
90+
ensureArg(argc, argv, i);
91+
runConfig.lengthScale = std::stof(argv[++i]);
92+
} else if (arg == "--noise_w" || arg == "--noise-w") {
93+
ensureArg(argc, argv, i);
94+
runConfig.noiseW = std::stof(argv[++i]);
95+
} else if (arg == "--espeak_data" || arg == "--espeak-data") {
96+
ensureArg(argc, argv, i);
97+
runConfig.eSpeakDataPath = std::filesystem::path(argv[++i]);
98+
} else if (arg == "--json_input" || arg == "--json-input") {
99+
runConfig.jsonInput = true;
100+
} else if (arg == "--version") {
101+
std::cout << piper_version() << std::endl;
102+
exit(0);
103+
} else if (arg == "-h" || arg == "--help") {
104+
printUsage(argv);
105+
exit(0);
106+
}
107+
}
108+
109+
// Verify model file exists
110+
std::ifstream modelFile(runConfig.modelPath.c_str(), std::ios::binary);
111+
if (!modelFile.good()) {
112+
throw std::runtime_error("Model file doesn't exist");
113+
}
114+
115+
if (!modelConfigPath) {
116+
runConfig.modelConfigPath =
117+
std::filesystem::path(runConfig.modelPath.string() + ".json");
118+
} else {
119+
runConfig.modelConfigPath = modelConfigPath.value();
120+
}
121+
122+
// Verify model config exists
123+
std::ifstream modelConfigFile(runConfig.modelConfigPath.c_str());
124+
if (!modelConfigFile.good()) {
125+
throw std::runtime_error("Model config doesn't exist");
126+
}
127+
}
128+
129+
void parseArgs(int argc, char *argv[], RunConfig &runConfig) {
130+
try {
131+
parseArgsLogic(argc, argv, runConfig);
132+
} catch (const ArgError &e) {
133+
std::cerr << e.what() << std::endl;
134+
printUsage(argv);
135+
exit(1);
136+
} catch (const std::exception &e) {
137+
std::cerr << e.what() << std::endl;
138+
exit(1);
139+
}
140+
}
141+
142+
} // namespace piper
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#ifndef PIPER_MAIN_UTILS_HPP
2+
#define PIPER_MAIN_UTILS_HPP
3+
4+
#include <chrono>
5+
#include <filesystem>
6+
#include <optional>
7+
#include <stdexcept>
8+
9+
10+
namespace piper {
11+
12+
enum OutputType { OUTPUT_FILE, OUTPUT_DIRECTORY, OUTPUT_STDOUT };
13+
14+
struct RunConfig {
15+
// Path to .onnx voice file
16+
std::filesystem::path modelPath;
17+
18+
// Path to JSON voice config file
19+
std::filesystem::path modelConfigPath;
20+
21+
// Type of output to produce.
22+
// Default is to write a WAV file in the current directory.
23+
OutputType outputType = OUTPUT_DIRECTORY;
24+
25+
// Path for output
26+
std::optional<std::filesystem::path> outputPath = std::filesystem::path(".");
27+
28+
// Numerical id of the default speaker (multi-speaker voices)
29+
std::optional<int> speakerId;
30+
std::optional<float> noiseScale;
31+
std::optional<float> lengthScale;
32+
std::optional<float> noiseW;
33+
std::optional<std::filesystem::path> eSpeakDataPath;
34+
bool jsonInput = false;
35+
};
36+
37+
struct ArgError : public std::runtime_error {
38+
using std::runtime_error::runtime_error;
39+
};
40+
41+
void printUsage(char *argv[]);
42+
void ensureArg(int argc, char *argv[], int argi);
43+
void parseArgsLogic(int argc, char *argv[], RunConfig &runConfig);
44+
void parseArgs(int argc, char *argv[], RunConfig &runConfig);
45+
46+
} // namespace piper
47+
#endif // PIPER_MAIN_UTILS_HPP

0 commit comments

Comments
 (0)