├── .clang-format ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── bin └── main.js ├── index.js ├── lib ├── cli_main.js ├── ebyroid.js ├── mini_server.js ├── module_def.js ├── semaphore.js ├── voiceroid.js └── wave_object.js ├── npm-shrinkwrap.json ├── package.json ├── src ├── api_adapter.cc ├── api_adapter.h ├── api_settings.cc ├── api_settings.h ├── ebyroid.cc ├── ebyroid.h ├── ebyutil.h └── module_main.cc └── test └── test_run.js /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: Google 4 | AccessModifierOffset: -1 5 | AlignAfterOpenBracket: Align 6 | AlignConsecutiveAssignments: false 7 | AlignConsecutiveDeclarations: false 8 | AlignEscapedNewlines: Right 9 | AlignOperands: true 10 | AlignTrailingComments: true 11 | AllowAllParametersOfDeclarationOnNextLine: false 12 | AllowShortBlocksOnASingleLine: false 13 | AllowShortCaseLabelsOnASingleLine: false 14 | AllowShortFunctionsOnASingleLine: Inline 15 | AllowShortIfStatementsOnASingleLine: true 16 | AllowShortLoopsOnASingleLine: true 17 | AlwaysBreakAfterDefinitionReturnType: None 18 | AlwaysBreakAfterReturnType: None 19 | AlwaysBreakBeforeMultilineStrings: false 20 | AlwaysBreakTemplateDeclarations: true 21 | BinPackArguments: false 22 | BinPackParameters: false 23 | BraceWrapping: 24 | AfterClass: false 25 | AfterControlStatement: false 26 | AfterEnum: false 27 | AfterFunction: false 28 | AfterNamespace: false 29 | AfterObjCDeclaration: false 30 | AfterStruct: false 31 | AfterUnion: false 32 | AfterExternBlock: false 33 | BeforeCatch: false 34 | BeforeElse: false 35 | IndentBraces: false 36 | SplitEmptyFunction: true 37 | SplitEmptyRecord: true 38 | SplitEmptyNamespace: true 39 | BreakBeforeBinaryOperators: None 40 | BreakBeforeBraces: Attach 41 | BreakBeforeInheritanceComma: false 42 | BreakBeforeTernaryOperators: true 43 | BreakConstructorInitializersBeforeComma: false 44 | BreakConstructorInitializers: BeforeColon 45 | BreakAfterJavaFieldAnnotations: false 46 | BreakStringLiterals: true 47 | ColumnLimit: 100 48 | CommentPragmas: '^ IWYU pragma:' 49 | CompactNamespaces: false 50 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 51 | ConstructorInitializerIndentWidth: 4 52 | ContinuationIndentWidth: 4 53 | Cpp11BracedListStyle: true 54 | DerivePointerAlignment: false 55 | DisableFormat: false 56 | ExperimentalAutoDetectBinPacking: false 57 | FixNamespaceComments: true 58 | ForEachMacros: 59 | - foreach 60 | - Q_FOREACH 61 | - BOOST_FOREACH 62 | IncludeBlocks: Preserve 63 | IncludeCategories: 64 | - Regex: '^' 65 | Priority: 2 66 | - Regex: '^<.*\.h>' 67 | Priority: 1 68 | - Regex: '^<.*' 69 | Priority: 2 70 | - Regex: '.*' 71 | Priority: 3 72 | IncludeIsMainRegex: '([-_](test|unittest))?$' 73 | IndentCaseLabels: true 74 | IndentPPDirectives: None 75 | IndentWidth: 2 76 | IndentWrappedFunctionNames: false 77 | JavaScriptQuotes: Leave 78 | JavaScriptWrapImports: true 79 | KeepEmptyLinesAtTheStartOfBlocks: false 80 | MacroBlockBegin: '' 81 | MacroBlockEnd: '' 82 | MaxEmptyLinesToKeep: 1 83 | NamespaceIndentation: None 84 | ObjCBlockIndentWidth: 2 85 | ObjCSpaceAfterProperty: false 86 | ObjCSpaceBeforeProtocolList: false 87 | PenaltyBreakAssignment: 2 88 | PenaltyBreakBeforeFirstCallParameter: 1 89 | PenaltyBreakComment: 300 90 | PenaltyBreakFirstLessLess: 120 91 | PenaltyBreakString: 1000 92 | PenaltyExcessCharacter: 1000000 93 | PenaltyReturnTypeOnItsOwnLine: 200 94 | PointerAlignment: Left 95 | ReflowComments: true 96 | SortIncludes: true 97 | SortUsingDeclarations: true 98 | SpaceAfterCStyleCast: true 99 | SpaceAfterTemplateKeyword: true 100 | SpaceBeforeAssignmentOperators: true 101 | SpaceBeforeParens: ControlStatements 102 | SpaceInEmptyParentheses: false 103 | SpacesBeforeTrailingComments: 2 104 | SpacesInAngles: false 105 | SpacesInContainerLiterals: true 106 | SpacesInCStyleCastParentheses: false 107 | SpacesInParentheses: false 108 | SpacesInSquareBrackets: false 109 | Standard: Auto 110 | TabWidth: 8 111 | UseTab: Never 112 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [{*.cc,*.h}] 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [{*.js,*.json}] 14 | indent_style = space 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "extends": [ 6 | "airbnb-base", 7 | "plugin:node/recommended", 8 | "plugin:prettier/recommended" 9 | ], 10 | "globals": { 11 | "Atomics": "readonly", 12 | "SharedArrayBuffer": "readonly" 13 | }, 14 | "parserOptions": { 15 | "ecmaVersion": 2019 16 | }, 17 | "rules": { 18 | "prettier/prettier": [ 19 | "error", 20 | { 21 | "singleQuote": true, 22 | "trailingComma": "es5", 23 | "endOfLine": "lf" 24 | } 25 | ], 26 | "no-bitwise": "off" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Managed 2 | build/ 3 | pack/ 4 | dll/ 5 | .vscode/ 6 | *.wav 7 | *.conf.json 8 | 9 | ## Auto generated 10 | 11 | # Logs 12 | logs 13 | *.log 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | lerna-debug.log* 18 | 19 | # Diagnostic reports (https://nodejs.org/api/report.html) 20 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 21 | 22 | # Runtime data 23 | pids 24 | *.pid 25 | *.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | lib-cov 30 | 31 | # Coverage directory used by tools like istanbul 32 | coverage 33 | *.lcov 34 | 35 | # nyc test coverage 36 | .nyc_output 37 | 38 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 39 | .grunt 40 | 41 | # Bower dependency directory (https://bower.io/) 42 | bower_components 43 | 44 | # node-waf configuration 45 | .lock-wscript 46 | 47 | # Dependency directories 48 | node_modules/ 49 | jspm_packages/ 50 | 51 | # TypeScript v1 declaration files 52 | typings/ 53 | 54 | # TypeScript cache 55 | *.tsbuildinfo 56 | 57 | # Optional npm cache directory 58 | .npm 59 | 60 | # Optional eslint cache 61 | .eslintcache 62 | 63 | # Microbundle cache 64 | .rpt2_cache/ 65 | .rts2_cache_cjs/ 66 | .rts2_cache_es/ 67 | .rts2_cache_umd/ 68 | 69 | # Optional REPL history 70 | .node_repl_history 71 | 72 | # Output of 'npm pack' 73 | *.tgz 74 | 75 | # Yarn Integrity file 76 | .yarn-integrity 77 | 78 | # dotenv environment variables file 79 | .env 80 | .env.test 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | 85 | # Next.js build output 86 | .next 87 | 88 | # Nuxt.js build / generate output 89 | .nuxt 90 | dist 91 | 92 | # Gatsby files 93 | .cache/ 94 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 95 | # https://nextjs.org/blog/next-9-1#public-directory-support 96 | # public 97 | 98 | # vuepress build output 99 | .vuepress/dist 100 | 101 | # Serverless directories 102 | .serverless/ 103 | 104 | # FuseBox cache 105 | .fusebox/ 106 | 107 | # DynamoDB Local files 108 | .dynamodb/ 109 | 110 | # TernJS port file 111 | .tern-port 112 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16.4) 2 | 3 | # oops! 4 | # set(CMAKE_GENERATOR_PLATFORM Win32) 5 | 6 | # WTF: https://gitlab.kitware.com/cmake/cmake/issues/19409 7 | set(CMAKE_GENERATOR_PLATFORM Win32 CACHE INTERNAL "") 8 | 9 | # Dumb options for dumb visual studio compiler 10 | add_compile_options(/utf-8 /std:c++17) 11 | 12 | # Project Name 13 | project(ebyroid) 14 | 15 | # Build a shared library named after the project from the files in `src/` 16 | file(GLOB SOURCE_FILES "src/*.cc" "src/*.h") 17 | add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES} ${CMAKE_JS_SRC}) 18 | 19 | # Gives our library file a .node extension without any "lib" prefix 20 | set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node") 21 | 22 | # Essential include files to build a node addon, 23 | # You should add this line in every CMake.js based project 24 | target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_JS_INC}) 25 | 26 | # Essential library files to link to a node addon 27 | # You should add this line in every CMake.js based project 28 | target_link_libraries(${PROJECT_NAME} ${CMAKE_JS_LIB}) 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kinas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ebyroid 2 | Ebyroid is a node native addon for `VOICEROID+` and `VOICEROID2` supports.\ 3 | It provides access from Node.js Javascript to VOICEROIDs through N-API and native codes, which is fast.\ 4 | We also provide a standalone HTTP server as Win32 executable so you can avoid [a certain difficult problem](https://github.com/nanokina/ebyroid#why-do-i-have-to-use-32-bit-node). 5 | 6 | ### Supported VOICEROIDs 7 | - `VOICEROID2` 8 | - `VOICEROID+` (partially) 9 | - Tohoku Zunko, Tohoku KiritanEx, Kotonoha Akane, Kotonoha Aoi 10 | 11 | ### Requirements (as npm package) 12 | - Windows (10 or Server recommended) 13 | - **Node.js for Windows x86 (win32-ia32)** - `^12.13.1` 14 | - [CMake for Windows](https://cmake.org/download/) - `^3.16.4` 15 | - MSVC `^2017`, or just get the latest [Visual Studio Community](https://visualstudio.microsoft.com/ja/free-developer-offers/) if you aren't certain what it would mean 16 | - Powershell `^3` 17 | - Voiceroid libraries installed with a valid and legitimate license 18 | 19 | ### Requirements (as a standalone server) 20 | - Windows (10 or Server recommended) 21 | - Voiceroid libraries installed with a valid and legitimate license 22 | 23 | ## Install 24 | ### npm package 25 | After you fulfilled the requirements above: 26 | ``` 27 | npm i ebyroid 28 | ``` 29 | 30 | ### standalone server 31 | Go to [Releases](https://github.com/nanokina/ebyroid/releases) and download the latest `ebyroid-v*.zip`. Then unzip it to wherever you want (e.g. `C:\ebyroid`). 32 | 33 | 34 | ## Usage 35 | ### npm package 36 | 37 | ```js 38 | const { Ebyroid, Voiceroid } = require('ebyroid'); 39 | 40 | const akari = new Voiceroid('akari-chan', 'C:\\Program Files (x86)\\AHS\\VOICEROID2', 'akari_44'); 41 | const kiritan = new Voiceroid('kiritan-chan', 'C:\\Program Files (x86)\\AHS\\VOICEROID+\\KiritanEX', 'kiritan_22'); 42 | 43 | const ebyroid = new Ebyroid(akari, kiritan); 44 | 45 | ebyroid.use('akari-chan'); 46 | 47 | async function main() { 48 | const pcm1 = await ebyroid.convert('こんにちは'); 49 | const pcm2 = await ebyroid.convertEx('東京特許許可局東京特きょきょきゃこく', 'kiritan-chan'); 50 | // and your code goes here... 51 | } 52 | 53 | main(); 54 | ``` 55 | 56 | ### standalone server 57 | 58 | on CMD 59 | 60 | ``` 61 | C:\ebyroid> ebyroid.exe configure 62 | C:\ebyroid> ebyroid.exe start --port 3333 63 | ``` 64 | 65 | on Powershell 66 | 67 | ``` 68 | PS C:\ebyroid> ./ebyroid configure 69 | PS C:\ebyroid> ./ebyroid start --port 4567 70 | ``` 71 | 72 | 73 | ## API Endpoints of Standalone Server 74 | 75 | ### `GET /api/v1/audiostream` 76 | 77 | #### query parameters 78 | 79 | | id | type | required | desc | example | 80 | | :---: | :----: | :------: | :--------------- | :--------------------------- | 81 | | text | string | **yes** | TTS content | `text=今日は%20はじめまして` | 82 | | name | string | no | Voiceroid to use | `name=kiritan-chan` | 83 | 84 | #### response types 85 | 86 | - `200 OK` => `application/octet-stream` 87 | - `4xx` and `5xx` => `application/json` 88 | 89 | #### extra response headers 90 | 91 | | id | value | desc | 92 | | :----------------------------: | :-----------------: | :-------------- | 93 | | Ebyroid-PCM-Sample-Rate | `22050\|44100\|48000` | Samples per sec | 94 | | Ebyroid-PCM-Bit-Depth | `16` | Fixed to 16-bit | 95 | | Ebyroid-PCM-Number-Of-Channels | `1\|2` | Mono or Stereo | 96 | 97 | Note that these headers will be sent only with `200 OK`. 98 | 99 | #### response body 100 | 101 | A byte stream of the [Linear PCM](http://soundfile.sapp.org/doc/WaveFormat/) data.\ 102 | The stream doesn't contain file header bytes since this endpoint is rather for those who want to deal with raw audio data.\ 103 | Use `GET /api/v1/audiofile` instead if you demand `.wav` file. 104 | 105 | ### `GET /api/v1/audiofile` 106 | 107 | #### query parameters 108 | 109 | | id | type | required | desc | example | 110 | | :---: | :----: | :------: | :--------------- | :------------------------- | 111 | | text | string | **yes** | TTS content | `text=今晩は%20さようなら` | 112 | | name | string | no | Voiceroid to use | `name=akane-chan` | 113 | 114 | #### response types 115 | 116 | - `200 OK` => `audio/wav` 117 | - `4xx` and `5xx` => `application/json` 118 | 119 | #### extra response headers 120 | 121 | None. 122 | 123 | #### response body 124 | 125 | Complete data for a `.wav` file.\ 126 | Any modern browser should support either to play or to download it. 127 | 128 | ## FAQ 129 | 130 | ### Why do I have to use 32-bit node? 131 | 132 | Because Ebyroid is a native addon that interacts with VOICEROID's 32-bit native code.\ 133 | When you need to interact with 32-bit code, it is necessary that your code also runs in 32-bit. 134 | 135 | ### How do I switch 64-bit node and 32-bit node? 136 | 137 | Use [nvm](https://github.com/coreybutler/nvm-windows). Also, consider using Ebyroid's standalone server. 138 | 139 | ### VOICEROID+ support looks poor, why? 140 | 141 | As of `VOICEROID2`, the software design is sophisticated and there's only one executable while voice library varies.\ 142 | That's why `VOICEROID2` is fully supported. Just making a support for the one executable works fine. 143 | 144 | In contrast, every `VOICEROID+` has its own executable, which means I need to write individual support for each library.\ 145 | And I just ran out of money after buying 4 of them. 146 | 147 | I'd appreciate your support as in making a pull request, opening an issue or emailing me. 148 | 149 | ### Does Ebyroid support concurrency? 150 | 151 | Yes. It sticks to asynchronous operation as hard as I can do in native code so as not to break Node's concept.\ 152 | That results in Ebyroid being able to process `^100RPS` when the CPU is fast enough. 153 | 154 | That said, however, some operations like switching voiceroid may acquire the inter-thread lock and take a couple of hundreds of millis (200ms-400ms practically) solely by itself. Be aware that frequent occurrence of such events may lead to slow the whole app. 155 | 156 | 157 | ## License 158 | MIT. See LICENSE. 159 | 160 | ## Disclaimer 161 | Since Ebyroid is merely an ad-hoc, non-profit, open-source and free library that interacts with VOICEROID, it doesn't contain any data with commercial value nor has any intent to exploit. Ebyroid only uses information loaded to RAM in human-readable format, such as a string, considered to be public, as means of Fair Use of public information. 162 | 163 | I will never be responsible for any consequences in connection with any use of Ebyroid or anyone that uses Ebyroid. I will only be concerned with calls of the US federal courts through Github, Inc. thus any other call and request from other authority or a company shall be ignored and immediately removed. 164 | 165 | FAIR-USE COPYRIGHT DISCLAIMER 166 | 167 | Copyright Disclaimer Under Section 107 of the Copyright Act 1976, allowance is made for "fair use" for purposes such as criticism, commenting, news reporting, teaching, scholarship, and research. Fair use is a use permitted by copyright statute that might otherwise be infringing. Non-profit, educational or personal use tips the balance in favor of fair use. 168 | 169 | ## catgirl 170 | 171 | ![ebyroid](https://user-images.githubusercontent.com/24854132/76497659-c90dc080-647e-11ea-9249-33bcc8d74f64.png) 172 | 173 | ### sister project 174 | 175 | - [Hanako](https://www.github.com/Ebycow/hanako) - Discord TTS Bot for Wide-Use 176 | -------------------------------------------------------------------------------- /bin/main.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../lib/cli_main')(); 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const Ebyroid = require('./lib/ebyroid'); 2 | const Voiceroid = require('./lib/voiceroid'); 3 | const MiniServer = require('./lib/mini_server'); 4 | const WaveObject = require('./lib/wave_object'); 5 | 6 | module.exports = { 7 | Ebyroid, 8 | Voiceroid, 9 | MiniServer, 10 | WaveObject, 11 | }; 12 | -------------------------------------------------------------------------------- /lib/cli_main.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const inquirer = require('inquirer'); 5 | const root = require('yargs'); 6 | const Ebyroid = require('./ebyroid'); 7 | const Voiceroid = require('./voiceroid'); 8 | const MiniServer = require('./mini_server'); 9 | 10 | /** @typedef {root.Argv<{}>} Yargs */ 11 | /** @typedef {{ [key in keyof root.Arguments<{}>]: Arguments<{}>[key] }} Argv */ 12 | 13 | function defaultPath(version) { 14 | return version === 'VOICEROID+' 15 | ? 'C:\\Program Files (x86)\\AHS\\VOICEROID+' 16 | : 'C:\\Program Files (x86)\\AHS\\VOICEROID2'; 17 | } 18 | 19 | function validateVoiceroidPath(vrpath, { version }) { 20 | const fsopt = { withFileTypes: true }; 21 | try { 22 | let files; 23 | if (version === 'VOICEROID+') { 24 | files = fs 25 | .readdirSync(vrpath, fsopt) 26 | .map(dirent => 27 | dirent.isDirectory() && dirent.name !== 'VOICEROID2' 28 | ? fs.readdirSync(path.join(vrpath, dirent.name), fsopt) 29 | : [] 30 | ) 31 | .flat() 32 | .filter(dirent => dirent.isFile()); 33 | } else { 34 | files = fs 35 | .readdirSync(vrpath, { withFileTypes: true }) 36 | .filter(dirent => dirent.isFile()); 37 | } 38 | return files.some(file => file.name === 'aitalked.dll') 39 | ? true 40 | : `That is not a valid ${version} directory.`; 41 | } catch (e) { 42 | if (e.code === 'ENOENT') { 43 | return 'There is no such a directory.'; 44 | } 45 | return `Something went wrong. Possibly an expception related to file access (code=${e.code})`; 46 | } 47 | } 48 | 49 | function configureObject(answers) { 50 | const o = {}; 51 | o.version = answers.version; 52 | o.name = answers.name; 53 | 54 | const base = answers.usesDefaultPath 55 | ? defaultPath(answers.version) 56 | : answers.customPath; 57 | o.baseDirPath = 58 | o.version === 'VOICEROID+' ? path.join(base, answers.vpDirName) : base; 59 | 60 | o.voiceDirName = answers.voiceDirName; 61 | 62 | return o; 63 | } 64 | 65 | /** @param {Argv} argv */ 66 | function configure(argv) { 67 | const names = new Map(); 68 | const questions = [ 69 | { 70 | type: 'list', 71 | name: 'version', 72 | message: 'Which version of VOICEROID do you use?', 73 | choices: ['VOICEROID+', 'VOICEROID2'], 74 | }, 75 | { 76 | type: 'confirm', 77 | name: 'usesDefaultPath', 78 | message: answers => 79 | `Is your VOICEROID installed in "${defaultPath(answers.version)}"?`, 80 | when: answers => { 81 | const vrpath = defaultPath(answers.version); 82 | return typeof validateVoiceroidPath(vrpath, answers) !== 'string'; 83 | }, 84 | }, 85 | { 86 | type: 'input', 87 | name: 'customPath', 88 | message: 'In which directory is it installed?', 89 | when: answers => !answers.usesDefaultPath, 90 | filter: value => path.normalize(value), 91 | validate: validateVoiceroidPath, 92 | }, 93 | { 94 | type: 'list', 95 | name: 'vpDirName', 96 | message: 'Which VOICEROID+ do you like to use?', 97 | choices: answers => { 98 | const base = answers.usesDefaultPath 99 | ? defaultPath(answers.version) 100 | : answers.customPath; 101 | return fs 102 | .readdirSync(base, { withFileTypes: true }) 103 | .filter( 104 | dirent => dirent.isDirectory() && !dirent.name.startsWith('.') 105 | ) 106 | .map(dirent => dirent.name); 107 | }, 108 | when: answers => answers.version === 'VOICEROID+', 109 | }, 110 | { 111 | type: 'list', 112 | name: 'voiceDirName', 113 | message: 'Which voice library do you like to use?', 114 | choices: answers => { 115 | const base = answers.usesDefaultPath 116 | ? defaultPath(answers.version) 117 | : answers.customPath; 118 | const voiceBase = 119 | answers.version === 'VOICEROID+' 120 | ? path.join(base, answers.vpDirName, 'voice') 121 | : path.join(base, 'Voice'); 122 | return fs 123 | .readdirSync(voiceBase, { withFileTypes: true }) 124 | .filter( 125 | dirent => dirent.isDirectory() && !dirent.name.startsWith('.') 126 | ) 127 | .map(dirent => dirent.name); 128 | }, 129 | }, 130 | { 131 | type: 'input', 132 | name: 'name', 133 | message: 'Give an unique name to this voiceroid settings!', 134 | validate: name => { 135 | if (name.length < 1) { 136 | return 'Name cannot be empty.'; 137 | } 138 | if (names.has(name)) { 139 | return 'That name is already used.'; 140 | } 141 | names.set(name, true); 142 | return true; 143 | }, 144 | }, 145 | { 146 | type: 'confirm', 147 | name: 'continue', 148 | message: 'Add one more settings for another voiceroid?', 149 | default: false, 150 | }, 151 | ]; 152 | 153 | function ask(arr = []) { 154 | return inquirer.prompt(questions).then(answers => { 155 | arr.push(answers); 156 | if (answers.continue) { 157 | console.log(`---------- No.${arr.length + 1} ----------`); 158 | return ask(arr); 159 | } 160 | return Promise.resolve(arr.map(configureObject)); 161 | }); 162 | } 163 | 164 | function finalAsk(objects) { 165 | return inquirer 166 | .prompt([ 167 | { 168 | type: 'list', 169 | name: 'which', 170 | message: 'Which one of the voiceroids should be used as default?', 171 | choices: objects.map(o => o.name), 172 | }, 173 | ]) 174 | .then(answers => { 175 | const od = objects.find(o => o.name === answers.which); 176 | od.default = true; 177 | return Promise.resolve(objects); 178 | }); 179 | } 180 | 181 | function writeJson(objects) { 182 | console.log(`Writing to "${argv.output}"...\n`); 183 | const json = JSON.stringify(objects, null, 2); 184 | fs.writeFileSync(argv.output, json); 185 | return Promise.resolve(); 186 | } 187 | 188 | return ask() 189 | .then(finalAsk) 190 | .then(writeJson) 191 | .catch(e => console.error('\nSorry, an error occured!', e)); 192 | } 193 | 194 | function toVR(o) { 195 | return new Voiceroid(o.name, o.baseDirPath, o.voiceDirName); 196 | } 197 | 198 | /** @param {Argv} argv */ 199 | function start(argv) { 200 | console.log('Loading config from JSON file...'); 201 | const json = fs.readFileSync(argv.config, 'utf8'); 202 | const objects = JSON.parse(json); 203 | const vrs = objects.map(toVR); 204 | console.log( 205 | 'Loaded', 206 | vrs.length, 207 | 'voiceroid(s):', 208 | vrs.map(v => v.name).join(', ') 209 | ); 210 | const ebyroid = new Ebyroid(...vrs); 211 | const defname = objects.find(o => o.default).name; 212 | ebyroid.use(defname); 213 | console.log(`Use "${defname}" as default...`); 214 | const mini = new MiniServer(ebyroid); 215 | console.log(`Starting up the server, with port ${argv.port}...`); 216 | mini.start(argv.port); 217 | console.log(`Server started! - http://localhost:${argv.port}/`); 218 | return 0; 219 | } 220 | 221 | const c = { 222 | command: 'configure', 223 | desc: 'create a configuration file', 224 | 225 | /** @param {Yargs} yargs */ 226 | builder(yargs) { 227 | return yargs 228 | .option('output', { 229 | alias: 'o', 230 | describe: 'specify a path to output config file', 231 | default: './ebyroid.conf.json', 232 | }) 233 | .normalize('output') 234 | .demandOption('output'); 235 | }, 236 | 237 | handler: configure, 238 | }; 239 | 240 | const s = { 241 | command: 'start', 242 | desc: 'start the audiostream server', 243 | 244 | /** @param {Yargs} yargs */ 245 | builder(yargs) { 246 | return yargs 247 | .option('config', { 248 | alias: 'c', 249 | describe: 'provide a path to config file', 250 | default: './ebyroid.conf.json', 251 | }) 252 | .option('port', { 253 | alias: 'p', 254 | describe: 'specify a port to listen', 255 | default: 4090, 256 | }) 257 | .normalize('config') 258 | .number('port') 259 | .demandOption('config'); 260 | }, 261 | 262 | handler: start, 263 | }; 264 | 265 | function main() { 266 | const m = [ 267 | 'For more specific details:', 268 | ' ebyroid configure --help', 269 | ' ebyroid start --help', 270 | '', 271 | 'Or just try:', 272 | ' ebyroid configure && ebyroid start', 273 | ]; 274 | return root 275 | .scriptName('ebyroid') 276 | .command(c.command, c.desc, c.builder, c.handler) 277 | .command(s.command, s.desc, s.builder, s.handler) 278 | .demandCommand(1, m.join('\n')) 279 | .help().argv; 280 | } 281 | 282 | module.exports = main; 283 | -------------------------------------------------------------------------------- /lib/ebyroid.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert').strict; 2 | const iconv = require('iconv-lite'); 3 | const debug = require('debug')('ebyroid'); 4 | /** @type {import("./module_def")} */ 5 | const native = require('../dll/ebyroid.node'); // eslint-disable-line node/no-unpublished-require 6 | const Semaphore = require('./semaphore'); 7 | const WaveObject = require('./wave_object'); 8 | 9 | /** @typedef {import("./module_def").NativeOptions} NativeOptions */ 10 | 11 | /** @typedef {import("./voiceroid")} Voiceroid */ 12 | 13 | // shift-jis 14 | const SHIFT_JIS = 'shiftjis'; 15 | 16 | /** 17 | * Class-wise global semaphore object. 18 | * 19 | * @type {Semaphore} 20 | */ 21 | const semaphore = new Semaphore(2); 22 | 23 | /** 24 | * @type {Voiceroid[]} 25 | */ 26 | const fifo = []; 27 | 28 | /** 29 | * The voiceroid that is currently used **in the native library**. 30 | * 31 | * @type {Voiceroid?} 32 | */ 33 | let current = null; 34 | 35 | /** 36 | * @type {Ebyroid?} 37 | */ 38 | let singleton = null; 39 | 40 | /** 41 | * @param {Ebyroid} self 42 | */ 43 | function validateOpCall(self) { 44 | assert(self.using !== null, 'operation calls must be called after .use()'); 45 | assert(current !== null, 'ebyroid native module must have been initialized'); 46 | } 47 | 48 | /** 49 | * @param {Voiceroid} vr 50 | * @param {Error} err 51 | * @returns {Voiceroid} 52 | */ 53 | function errorroid(vr, err) { 54 | // FIXME more graceful way to handle errors? 55 | return Object.assign(Object.create(Object.getPrototypeOf(vr)), vr, { 56 | baseDirPath: 'error', 57 | voiceDirName: err.message, 58 | }); 59 | } 60 | 61 | /** 62 | * @returns {Voiceroid?} 63 | */ 64 | function lastRegistered() { 65 | const vr = fifo[fifo.length - 1]; 66 | return vr || null; 67 | } 68 | 69 | /** 70 | * @param {Voiceroid} vr 71 | */ 72 | function register(vr) { 73 | fifo.push(vr); 74 | debug('(registered) fifo = %d', fifo.length); 75 | } 76 | 77 | /** 78 | * @param {Voiceroid} vr 79 | */ 80 | function unregister(vr) { 81 | assert(vr === fifo[0], 'unregisteration is equivalent to fifo.shift()'); 82 | fifo.shift(); 83 | debug('(unregistered) fifo = %d', fifo.length); 84 | } 85 | 86 | /** 87 | * @param {Voiceroid} vr 88 | * @returns {boolean} 89 | */ 90 | function needsLibraryReload(vr) { 91 | const lastreg = lastRegistered(); 92 | const comparison = lastreg || current; 93 | return !vr.usesSameLibrary(comparison); 94 | } 95 | 96 | /** 97 | * @this Ebyroid 98 | * @param {string} text 99 | * @param {Voiceroid} vr 100 | * @returns {Promise} 101 | */ 102 | async function internalConvertF(text, vr) { 103 | const buffer = iconv.encode(text, SHIFT_JIS); 104 | await semaphore.acquire(); 105 | 106 | assert(vr.usesSameLibrary(current), 'it must not need to reload'); 107 | 108 | // TODO setup full options here 109 | const options = { 110 | needs_reload: false, 111 | volume: vr.outputVolume, 112 | }; 113 | 114 | return new Promise((resolve, reject) => 115 | native.convert(buffer, options, (err, pcmOut) => { 116 | current = vr; 117 | semaphore.release(); 118 | if (err) { 119 | reject(err); 120 | } else { 121 | resolve(new WaveObject(pcmOut, vr.outputSampleRate)); 122 | } 123 | }) 124 | ); 125 | } 126 | 127 | /** 128 | * Ebyroid class provides an access to the native VOICEROID+/VOICEROID2 libraries. 129 | */ 130 | class Ebyroid { 131 | /** 132 | * Construct an Ebyroid instance. 133 | * 134 | * @param {...Voiceroid} voiceroids voiceroids to use. 135 | */ 136 | constructor(...voiceroids) { 137 | assert(voiceroids.length > 0, 'at least one voiceroid must be given'); 138 | debug('voiceroids = %O', voiceroids); 139 | 140 | /** 141 | * voiceroids to use. 142 | * 143 | * @type {Map} 144 | */ 145 | this.voiceroids = new Map(); 146 | voiceroids.forEach(vr => this.voiceroids.set(vr.name, vr)); 147 | 148 | /** 149 | * the voiceroid currently used **by this instance**. 150 | * 151 | * @type {Voiceroid?} 152 | */ 153 | this.using = null; 154 | } 155 | 156 | /** 157 | * Let ebyroid use a specific voiceroid library. 158 | * Distinctively, this operation may take a few seconds to complete when called first time. 159 | * 160 | * @param {string} voiceroidName a name identifier of the voiceroid to use 161 | * @example 162 | * const yukari = new Voiceroid('Yukari-chan', 'C:\\Program Files (x86)\\AHS\\VOICEROID2', 'yukari_44'); 163 | * const kiritan = new Voiceroid('Kiritan-chan', 'C:\\Program Files (x86)\\AHS\\VOICEROID+\\KiritanEX', 'kiritan_22'); 164 | * const ebyroid = new Ebyroid(yukari, kiritan); 165 | * ebyroid.use('Kiritan-chan'); 166 | */ 167 | use(voiceroidName) { 168 | const vr = this.voiceroids.get(voiceroidName); 169 | if (!vr) { 170 | throw new Error(`Could not find a voiceroid by name "${voiceroidName}".`); 171 | } 172 | 173 | if (current === null) { 174 | debug('call init. voiceroid = %O', vr); 175 | try { 176 | native.init(vr.baseDirPath, vr.voiceDirName, vr.outputVolume); 177 | } catch (err) { 178 | // eslint-disable-next-line no-console 179 | console.error('Failed to initialize ebyroid native module', err); 180 | throw err; 181 | } 182 | current = vr; 183 | } 184 | debug('use %s', vr.name); 185 | this.using = vr; 186 | } 187 | 188 | /** 189 | * Convert text to a PCM buffer. 190 | * When a voiceroid corresponding to the given voiceroidName uses a different voice library than 191 | * the one currently used in the native library, it will acquire a mutex lock and reload native library. 192 | * In which case it may block all of other requests for fair amount of time like a two or three seconds. 193 | * See {@link Voiceroid} for further details. 194 | * 195 | * @param {string} text Raw utf-8 text to convert 196 | * @param {string} voiceroidName a name identifier of the voiceroid to use 197 | * @returns {Promise} object that consists of a raw PCM buffer and format information 198 | */ 199 | async convertEx(text, voiceroidName) { 200 | if (this.using === null) { 201 | // only when a user called this method without calling .use() once 202 | this.use(voiceroidName); 203 | } 204 | validateOpCall(this); 205 | 206 | const vr = this.voiceroids.get(voiceroidName); 207 | if (!vr) { 208 | throw new Error(`Could not find a voiceroid by name "${voiceroidName}".`); 209 | } 210 | 211 | if (!needsLibraryReload(vr)) { 212 | debug('convertEx() delegates to internalConvertF %s', vr.name); 213 | return internalConvertF.call(this, text, vr); 214 | } 215 | 216 | const buffer = iconv.encode(text, SHIFT_JIS); 217 | 218 | debug('register %s', vr.name); 219 | register(vr); 220 | 221 | debug('waiting for a lock'); 222 | await semaphore.lock(); 223 | debug('got a lock'); 224 | 225 | assert(!vr.usesSameLibrary(current), 'it must need to reload'); 226 | 227 | /** @type {NativeOptions} */ 228 | const options = { 229 | needs_reload: true, 230 | base_dir: vr.baseDirPath, 231 | voice: vr.voiceDirName, 232 | volume: vr.outputVolume, 233 | }; 234 | 235 | return new Promise((resolve, reject) => 236 | native.convert(buffer, options, (err, pcmOut) => { 237 | debug('unregister %s', vr.name); 238 | unregister(vr); 239 | if (err) { 240 | current = errorroid(vr, err); 241 | reject(err); 242 | debug('unlock with error %O', err); 243 | setImmediate(() => semaphore.unlock()); 244 | } else { 245 | current = vr; 246 | debug('unlock'); 247 | semaphore.unlock(); 248 | resolve(new WaveObject(pcmOut, vr.outputSampleRate)); 249 | } 250 | }) 251 | ); 252 | } 253 | 254 | /** 255 | * Convert text to a PCM buffer. Prefer using this method whenever you can. 256 | * 257 | * @param {string} text Raw utf-8 text to convert 258 | * @returns {Promise} object that consists of a raw PCM buffer and format information 259 | */ 260 | convert(text) { 261 | validateOpCall(this); 262 | if (needsLibraryReload(this.using)) { 263 | debug('convert() escalates to convertEx()'); 264 | return this.convertEx(text, this.using.name); 265 | } 266 | return internalConvertF.call(this, text, this.using); 267 | } 268 | 269 | /** 270 | * (Not Recommended) Compile text to an certain intermediate representation called 'AI Kana' that VOICEROID uses internally. 271 | * This method exists only to gratify your curiosity. No other use for it. 272 | * 273 | * @param {string} rawText Raw utf-8 text to reinterpret into 'AI Kana' representation 274 | * @returns {Promise} AI Kana representation of the text 275 | */ 276 | async rawApiCallTextToKana(rawText) { 277 | validateOpCall(this); 278 | const buffer = iconv.encode(rawText, SHIFT_JIS); 279 | await semaphore.acquire(); 280 | 281 | return new Promise((resolve, reject) => { 282 | native.reinterpret(buffer, {}, (err, output) => { 283 | semaphore.release(); 284 | if (err) { 285 | reject(err); 286 | } else { 287 | const utf8text = iconv.decode(output, SHIFT_JIS); 288 | resolve(utf8text); 289 | } 290 | }); 291 | }); 292 | } 293 | 294 | /** 295 | * (Not Recommended) Read out the given text __written in an intermediate representation called 'AI Kana'__ that VOICEROID uses internally. 296 | * This method exists only to gratify your curiosity. No other use for it. 297 | * 298 | * @param {string} aiKana AI Kana representation to read out 299 | * @returns {Promise} object that consists of a raw PCM buffer and format information 300 | */ 301 | async rawApiCallAiKanaToSpeech(aiKana) { 302 | validateOpCall(this); 303 | const buffer = iconv.encode(aiKana, SHIFT_JIS); 304 | await semaphore.acquire(); 305 | 306 | return new Promise((resolve, reject) => { 307 | native.speech(buffer, {}, (err, pcmOut) => { 308 | semaphore.release(); 309 | if (err) { 310 | reject(err); 311 | } else { 312 | resolve(new WaveObject(pcmOut, current.baseSampleRate)); 313 | } 314 | }); 315 | }); 316 | } 317 | 318 | /** 319 | * Supportive static method for the case in which you like to use it as singleton. 320 | * 321 | * @param {Ebyroid} instance instance to save as singleton 322 | */ 323 | static setInstance(instance) { 324 | singleton = instance; 325 | } 326 | 327 | /** 328 | * Supportive static method for the case in which you like to use it as singleton. 329 | * 330 | * @returns {Ebyroid?} the singleton instance (if set) 331 | */ 332 | static getInstance() { 333 | return singleton; 334 | } 335 | } 336 | 337 | module.exports = Ebyroid; 338 | -------------------------------------------------------------------------------- /lib/mini_server.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const semver = require('semver'); 3 | 4 | /** @typedef {import('./wave_object')} WaveObject */ 5 | /** @typedef {import('./ebyroid')} Ebyroid */ 6 | 7 | function unused(...x) { 8 | return x; 9 | } 10 | 11 | /** 12 | * @param {http.ServerResponse} res 13 | * @param {number} code 14 | * @param {string} message 15 | */ 16 | function error4x(res, code, message) { 17 | const json = JSON.stringify({ error: message }); 18 | const headers = { 19 | 'Content-Type': 'application/json; charset=utf-8', 20 | 'Content-Length': json.length, 21 | }; 22 | res.writeHead(code, headers); 23 | res.write(json); 24 | res.end(); 25 | } 26 | 27 | /** 28 | * check if invalid Sec-Fetch-Dest 29 | * 30 | * @this MiniServer 31 | * @param {http.IncomingMessage} req 32 | * @param {http.ServerResponse} res 33 | * @param {string} minetype 34 | * @returns {boolean} true if killed 35 | */ 36 | function killWrongSFD(req, res, minetype) { 37 | const sfd = req.headers['sec-fetch-dest']; 38 | if (!sfd) { 39 | return false; 40 | } 41 | 42 | const invalids = ['document']; 43 | if (!invalids.includes(sfd)) { 44 | return false; 45 | } 46 | 47 | // a dummy response to the dickhead browser that sends us the same fucking request twice 48 | const headers = { 49 | 'Content-Type': minetype, 50 | 'Content-Length': 0, 51 | }; 52 | res.writeHead(200, headers); 53 | res.end(); 54 | return true; 55 | } 56 | 57 | /** 58 | * @param {http.IncomingMessage} req 59 | * @param {http.ServerResponse} res 60 | * @param {string} ecode 61 | * @param {string} emessage 62 | */ 63 | function error500(res, ecode, emessage) { 64 | const json = JSON.stringify({ 65 | error: 'internal server error', 66 | code: ecode, 67 | message: emessage, 68 | }); 69 | const headers = { 70 | 'Content-Type': 'application/json; charset=utf-8', 71 | 'Content-Length': json.length, 72 | }; 73 | res.writeHead(500, headers); 74 | res.write(json); 75 | res.end(); 76 | } 77 | 78 | function ok(res) { 79 | const json = JSON.stringify({ status: 'ok' }); 80 | const headers = { 81 | 'Content-Type': 'application/json; charset=utf-8', 82 | 'Content-Length': json.length, 83 | }; 84 | res.writeHead(200, headers); 85 | res.write(json); 86 | res.end(); 87 | } 88 | 89 | /** 90 | * @this MiniServer 91 | * @param {http.IncomingMessage} req 92 | * @param {http.ServerResponse} res 93 | * @param {URLSearchParams} params 94 | */ 95 | async function onGetAudioStreamF(req, res, params) { 96 | unused(req); 97 | const text = params.get('text'); 98 | if (!text) { 99 | return error4x(res, 400, 'text was not given'); 100 | } 101 | try { 102 | /** @type {WaveObject} */ let pcm; 103 | const name = params.get('name'); 104 | if (name && name !== this.defaultName) { 105 | pcm = await this.ebyroid.convertEx(text, name); 106 | } else { 107 | pcm = await this.ebyroid.convert(text); 108 | } 109 | const buffer = Buffer.from(pcm.data.buffer); 110 | const headers = { 111 | 'Content-Type': 'application/octet-stream', 112 | 'Content-Length': buffer.byteLength, 113 | 'Ebyroid-PCM-Sample-Rate': pcm.sampleRate, 114 | 'Ebyroid-PCM-Bit-Depth': pcm.bitDepth, 115 | 'Ebyroid-PCM-Number-Of-Channels': pcm.numChannels, 116 | }; 117 | res.writeHead(200, headers); 118 | res.write(buffer); 119 | res.end(); 120 | return Promise.resolve(); 121 | } catch (e) { 122 | return error500(res, e.code, e.message); 123 | } 124 | } 125 | 126 | /** 127 | * @this MiniServer 128 | * @param {http.ServerResponse} res 129 | * @param {URLSearchParams} params 130 | */ 131 | async function onGetAudioFileF(req, res, params) { 132 | if (killWrongSFD(req, res, 'audio/wav')) { 133 | return Promise.resolve(); 134 | } 135 | 136 | const text = params.get('text'); 137 | if (!text) { 138 | return error4x(res, 400, 'text was not given'); 139 | } 140 | try { 141 | /** @type {WaveObject} */ let pcm; 142 | const name = params.get('name'); 143 | if (name && name !== this.defaultName) { 144 | pcm = await this.ebyroid.convertEx(text, name); 145 | } else { 146 | pcm = await this.ebyroid.convert(text); 147 | } 148 | const dataBuffer = Buffer.from(pcm.data.buffer); 149 | const headerBuffer = pcm.waveFileHeader(); 150 | const waveBuffer = Buffer.concat([headerBuffer, dataBuffer]); 151 | const headers = { 152 | 'Content-Type': 'audio/wav', 153 | 'Content-Length': waveBuffer.byteLength, 154 | }; 155 | res.writeHead(200, headers); 156 | res.write(waveBuffer); 157 | res.end(); 158 | return Promise.resolve(); 159 | } catch (e) { 160 | return error500(res, e.code, e.message); 161 | } 162 | } 163 | 164 | /** 165 | * @this MiniServer 166 | * @param {http.IncomingMessage} req 167 | * @param {http.ServerResponse} res 168 | */ 169 | async function onRequestF(req, res) { 170 | if (req.method !== 'GET') { 171 | return error4x(res, 400, 'bad request'); 172 | } 173 | 174 | const url = new URL(req.url, `http://${req.headers.host}/`); 175 | if (!url.pathname.startsWith(this.basePath)) { 176 | return error4x(res, 404, 'not found'); 177 | } 178 | 179 | const pathname = url.pathname.slice(this.basePath.length); 180 | switch (pathname) { 181 | case '': 182 | case '/': 183 | return ok(res); 184 | case '/audiostream': 185 | return onGetAudioStreamF.call(this, req, res, url.searchParams); 186 | case '/audiofile': 187 | return onGetAudioFileF.call(this, req, res, url.searchParams); 188 | default: 189 | return error4x(res, 404, 'not found'); 190 | } 191 | } 192 | 193 | /** 194 | * Ebyroid's built-in audiostream server 195 | */ 196 | class MiniServer { 197 | /** 198 | * @param {Ebyroid} ebyroid 199 | * @param {number} maxHeaderSize requires Node v13.3.0 or higher 200 | */ 201 | constructor(ebyroid, maxHeaderSize = 65536) { 202 | this.ebyroid = ebyroid; 203 | this.basePath = '/api/v1'; 204 | 205 | let options = {}; 206 | if (semver.gte(process.version, '13.3.0')) { 207 | // Setting maxHeaderSize on runtime was added in v13.3.0 208 | // https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V13.md#13.3.0 209 | options = { maxHeaderSize }; 210 | } 211 | this.server = http.createServer(options, onRequestF.bind(this)); 212 | } 213 | 214 | /** 215 | * @param {number} port 216 | */ 217 | start(port) { 218 | this.defaultName = this.ebyroid.using.name; 219 | this.server.listen(port); 220 | } 221 | } 222 | 223 | module.exports = MiniServer; 224 | -------------------------------------------------------------------------------- /lib/module_def.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | /* eslint-disable no-unused-vars */ 3 | 4 | // a silly js file that declares interface of the native module. 5 | // this file should not be required from anywhere, 6 | // with an exception for JSDoc only imports. 7 | 8 | /** 9 | * @typedef NativeOptions 10 | * @type {object} 11 | * @property {boolean} needs_reload determines if native addon needs to reload the VOICEROID library 12 | * @property {string?} base_dir a path in which VOICEROID is installed 13 | * @property {string?} voice a directory name where the voice library files are at 14 | * @property {number?} volume desired output volume ranged from 0.0 to 5.0 15 | */ 16 | 17 | /** 18 | * Native ebyroid module's type interface. 19 | */ 20 | class NativeModule { 21 | /** 22 | * call convert 23 | * 24 | * @param {Buffer} input ShiftJIS bytecodes 25 | * @param {NativeOptions} options options to determine whether to reload or not 26 | * @param {function(Error,Int16Array):void} callback result is an array of 16bit PCM data 27 | * @abstract 28 | */ 29 | convert(input, options, callback) { 30 | throw new Error('not implemented'); 31 | } 32 | 33 | /** 34 | * call reinterpret 35 | * 36 | * @param {Buffer} input ShiftJIS bytecodes 37 | * @param {object} dummy the passing value should always be `{}` 38 | * @param {function(Error,Buffer)} callback result is a buffer of ShiftJIS bytecodes of AI Kana 39 | * @abstract 40 | */ 41 | reinterpret(input, dummy, callback) { 42 | throw new Error('not implemented'); 43 | } 44 | 45 | /** 46 | * call speech 47 | * 48 | * @param {Buffer} input ShiftJIS bytecodes of AI Kana 49 | * @param {object} dummy the passing value should always be `{}` 50 | * @param {function(Error,Int16Array)} callback result is an array of 16bit PCM data 51 | * @abstract 52 | */ 53 | speech(input, dummy, callback) { 54 | throw new Error('not implemented'); 55 | } 56 | } 57 | 58 | module.exports = NativeModule; 59 | -------------------------------------------------------------------------------- /lib/semaphore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Loose implementation for Semaphore and Mutex at the same time -ish class. 3 | * Because the native library of VOICEROID is not capable of processing more than two requests concurrently. 4 | */ 5 | class Semaphore { 6 | constructor(max) { 7 | this.max = max; 8 | this.stone = 0; 9 | this.waitings = []; 10 | } 11 | 12 | acquire() { 13 | if (this.stone < this.max) { 14 | this.stone += 1; 15 | return new Promise(resolve => resolve()); 16 | } 17 | return new Promise(resolve => { 18 | this.waitings.push({ resolve }); 19 | }); 20 | } 21 | 22 | release() { 23 | this.stone -= 1; 24 | if (this.waitings.length > 0) { 25 | this.stone += 1; 26 | const firstOne = this.waitings.shift(); 27 | firstOne.resolve(); 28 | } 29 | } 30 | 31 | lock() { 32 | if (this.stone === 0) { 33 | this.stone = this.max; 34 | return Promise.resolve(); 35 | } 36 | return new Promise(resolve => { 37 | let towait = this.stone; 38 | this.stone = this.max; 39 | const dec = () => { 40 | towait -= 1; 41 | if (towait === 0) { 42 | resolve(); 43 | } 44 | }; 45 | for (let i = 0; i < towait; i += 1) { 46 | this.waitings.push({ resolve: dec }); 47 | } 48 | }); 49 | } 50 | 51 | unlock() { 52 | for (let i = this.stone; i > 0; i -= 1) { 53 | this.release(); 54 | } 55 | } 56 | } 57 | 58 | module.exports = Semaphore; 59 | -------------------------------------------------------------------------------- /lib/voiceroid.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert').strict; 2 | const debug = require('debug')('ebyroid'); 3 | 4 | /** @typedef {import('./ebyroid')} Ebyroid */ 5 | 6 | function sanitizePath(path) { 7 | if ([...path].some(c => c.charCodeAt(0) > 127)) { 8 | throw new Error( 9 | 'The path to VOICEROID directory may not contain any non-ascii character.' 10 | ); 11 | } 12 | if (path.endsWith('\\')) { 13 | return path.slice(0, path.length - 1); 14 | } 15 | return path; 16 | } 17 | 18 | function guessVersion(name) { 19 | if (name.endsWith('_22')) { 20 | return 'VOICEROID+'; 21 | } 22 | if (name.endsWith('_44')) { 23 | return 'VOICEROID2'; 24 | } 25 | throw new Error( 26 | 'Could not infer VOICEROID version. Make sure the given voice directory name is appropriate.' 27 | ); 28 | } 29 | 30 | function sanitizeName(name) { 31 | if (name.endsWith('_22')) { 32 | const supports = ['kiritan', 'zunko', 'akane', 'aoi']; 33 | if (supports.some(s => name.startsWith(s))) { 34 | return name; 35 | } 36 | const names = supports.join('", "'); 37 | throw new Error( 38 | `An unsopported VOICEROID+ library was given.\nEbyroid currently supports "${names}".\nWant your favorite library to get supported? Please open an issue or make a pull request!` 39 | ); 40 | } else if (name.endsWith('_44')) { 41 | return name; 42 | } else { 43 | throw new Error('unreachable'); 44 | } 45 | } 46 | 47 | function sanitizeVolume(volume) { 48 | if (typeof volume === 'undefined') { 49 | return 2.2; 50 | } 51 | if (typeof volume === 'number' && volume <= 5.0 && volume >= 0.0) { 52 | return volume; 53 | } 54 | throw new RangeError('options.volume should range from 0.0 to 5.0'); 55 | } 56 | 57 | function sanitizeSampleRate(sampleRate, version) { 58 | if (typeof sampleRate === 'undefined') { 59 | return version === 'VOICEROID+' ? 22050 : 44100; 60 | } 61 | if ( 62 | typeof sampleRate === 'number' && 63 | [22050, 44100, 48000].includes(sampleRate) 64 | ) { 65 | return sampleRate; 66 | } 67 | throw new TypeError( 68 | 'options.sampleRate should be one of 22050, 44100 or 48000' 69 | ); 70 | } 71 | 72 | function sanitizeChannels(channels) { 73 | if (typeof channels === 'undefined') { 74 | return 1; 75 | } 76 | if (typeof channels === 'number' && channels === 1 && channels === 2) { 77 | return channels; 78 | } 79 | throw new TypeError('options.channels should be 1 or 2'); 80 | } 81 | 82 | /** 83 | * Configurative options for a Voiceroid. 84 | * Note that variety of these values never affects Ebyroid on decision of exclusive reloading of voice libraries. 85 | * 86 | * @typedef VoiceroidOptions 87 | * @type {object} 88 | * @property {number} [volume=2.2] desired output volume (from 0.0 to 5.0) with 2.2 recommended. 89 | * @property {(22050|44100|48000)} [sampleRate=(22050|44100)] desired sample-rate of output PCM. VOICEROID+ defaults to 22050, and VOICEROID2 does to 44100. if a higher rate than default is given, Ebyroid will resample (upconvert) it to the rate. 90 | * @property {(1|2)} [channels=1] desired number of channels of output PCM. 1 stands for Mono, and 2 does for Stereo. since VOICEROID's output is always Mono, Ebyroid will manually interleave it when you set channels to 2. 91 | */ 92 | 93 | /** 94 | * Voiceroid data class contains necessary information to load the native library. 95 | * Note that the name identitier and optional settings never affect Ebyroid on its determination of whether to reload native libraries or not whereas the other params do. 96 | */ 97 | class Voiceroid { 98 | /** 99 | * Construct a Voiceroid data object. 100 | * 101 | * @param {string} name an unique, identifiable name for this object. 102 | * @param {string} baseDirPath a path in which your VOICEROID's `.exe` is installed. 103 | * @param {string} voiceDirName a voice library dir, like `zunko_22` or `yukari_44`. 104 | * @param {VoiceroidOptions} [options={}] optional settings for this voiceroid. 105 | * @example 106 | * const yukari = new Voiceroid('Yukari-chan', 'C:\\Program Files (x86)\\AHS\\VOICEROID2', 'yukari_44'); 107 | * const kiritan = new Voiceroid('Kiritan-chan', 'C:\\Program Files (x86)\\AHS\\VOICEROID+\\KiritanEX', 'kiritan_22'); 108 | * const ebyroid = new Ebyroid(yukari, kiritan); 109 | * ebyroid.use('Yukari-chan'); 110 | */ 111 | constructor(name, baseDirPath, voiceDirName, options = {}) { 112 | assert(typeof name === 'string' && name.length > 0); 113 | assert(typeof baseDirPath === 'string' && baseDirPath.length > 0); 114 | assert(typeof voiceDirName === 'string' && voiceDirName.length > 0); 115 | assert(typeof options === 'object'); 116 | debug( 117 | 'name=%s path=%s voice=%s options=%o', 118 | name, 119 | baseDirPath, 120 | voiceDirName, 121 | options 122 | ); 123 | 124 | /** 125 | * the identifier of this object. {@link Ebyroid.use} takes this value as an argument. 126 | * @type {string} 127 | * @readonly 128 | */ 129 | this.name = name; 130 | 131 | /** 132 | * the path in which `VOICEROID.exe` or `VoiceroidEditor.exe` is installed 133 | * @type {string} 134 | * @readonly 135 | */ 136 | this.baseDirPath = sanitizePath(baseDirPath); 137 | 138 | /** 139 | * the version of the library 140 | * @type {"VOICEROID+"|"VOICEROID2"} 141 | * @readonly 142 | */ 143 | this.version = guessVersion(voiceDirName); 144 | 145 | /** 146 | * the name of the directory where the voice library files are installed 147 | * @type {string} 148 | * @readonly 149 | */ 150 | this.voiceDirName = sanitizeName(voiceDirName); 151 | 152 | /** 153 | * desired output volume 154 | * @type {number} 155 | * @readonly 156 | */ 157 | this.outputVolume = sanitizeVolume(options.volume); 158 | 159 | /** 160 | * desired sample-rate of output PCM 161 | * @type {22050|44100|48000} 162 | * @readonly 163 | */ 164 | this.outputSampleRate = sanitizeSampleRate( 165 | options.sampleRate, 166 | this.version 167 | ); 168 | 169 | /** 170 | * desired number of channels of output PCM 171 | * @type {1|2} 172 | * @readonly 173 | */ 174 | this.outputChannels = sanitizeChannels(options.channels); 175 | 176 | /** 177 | * the library's output sample-rate in Hz 178 | * @type {22050|44100} 179 | * @readonly 180 | */ 181 | this.baseSampleRate = this.version === 'VOICEROID+' ? 22050 : 44100; 182 | 183 | debug('setup voiceroid object %O', this); 184 | } 185 | 186 | /** 187 | * Examine the equality. 188 | * 189 | * @param {Voiceroid} that the object that this instance examines equality with. 190 | * @returns {boolean} 191 | */ 192 | equals(that) { 193 | if (!(that instanceof Voiceroid)) { 194 | return false; 195 | } 196 | return ( 197 | this.name === that.name && 198 | this.version === that.version && 199 | this.baseDirPath === that.baseDirPath && 200 | this.voiceDirName === that.voiceDirName && 201 | this.outputVolume === that.outputVolume && 202 | this.outputSampleRate === that.outputSampleRate && 203 | this.outputChannels === that.outputChannels 204 | ); 205 | } 206 | 207 | /** 208 | * Check if this and that are using same native library. 209 | * 210 | * @param {Voiceroid} that the object that this instance examines equality with. 211 | * @returns {boolean} 212 | */ 213 | usesSameLibrary(that) { 214 | if (!(that instanceof Voiceroid)) { 215 | return false; 216 | } 217 | return ( 218 | this.baseDirPath === that.baseDirPath && 219 | this.voiceDirName === that.voiceDirName 220 | ); 221 | } 222 | } 223 | 224 | module.exports = Voiceroid; 225 | -------------------------------------------------------------------------------- /lib/wave_object.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert').strict; 2 | 3 | // byte op helpers 4 | function toUint32LE(uint32) { 5 | const b32 = Buffer.alloc(4); 6 | b32.writeUInt32LE(uint32, 0); 7 | return b32; 8 | } 9 | 10 | function toSampleRateLE(sampleRateUint32, weight) { 11 | const b32 = Buffer.alloc(4); 12 | b32.writeUInt32LE(sampleRateUint32 * weight, 0); 13 | return b32; 14 | } 15 | 16 | /** 17 | * Conversion result object that contains a PCM data and format information. 18 | */ 19 | class WaveObject { 20 | /** 21 | * @param {Int16Array} data 16bit PCM data 22 | * @param {number} sampleRate sample-rate of the data (Hz) 23 | */ 24 | constructor(data, sampleRate) { 25 | /** 26 | * an array of signed 16bit integer values which represents 16bit Linear PCM data 27 | * @type {Int16Array} 28 | */ 29 | this.data = data; 30 | /** 31 | * the bit depth of PCM data. this value is fixed to 16 unless any VOICEROID comes to support 24bit depth someday. 32 | * @type {16} 33 | */ 34 | this.bitDepth = 16; 35 | /** 36 | * the sample-rate (samples per second) of PCM data. 22050Hz for VOICEROID+ and 44100Hz for VOICEROID2. 37 | * That's for now. Ebyroid's resampling feature will come soon. 38 | * @type {22050|44100} 39 | */ 40 | this.sampleRate = sampleRate; 41 | /** 42 | * the number of channels in PCM data. 1 for Mono, 2 for Stereo. It's fixed to 1 due to VOICEROID's nature. 43 | * That's for now. Ebyroid soon will provide an option to interleave data into Stereo. 44 | * @type {1|2} 45 | */ 46 | this.numChannels = 1; 47 | } 48 | 49 | /** 50 | * @returns {Buffer} the wave file header bytes corresponding to this object 51 | */ 52 | waveFileHeader() { 53 | const theRIFF = new Uint8Array([0x52, 0x49, 0x46, 0x46]); 54 | const fileSize = toUint32LE(this.data.byteLength + 36); // 36 = headers(44) - RIFF(4) - this(4) 55 | const theWAVE = new Uint8Array([0x57, 0x41, 0x56, 0x45]); 56 | const theFmt = new Uint8Array([0x66, 0x6d, 0x74, 0x20]); 57 | const fmtSizeLE = new Uint8Array([0x10, 0x00, 0x00, 0x00]); 58 | const fmtCodeLE = new Uint8Array([0x01, 0x00]); 59 | const numChannelsLE = new Uint8Array([this.numChannels, 0x00]); 60 | const sampleRateLE = toSampleRateLE(this.sampleRate, 1); 61 | const bytesPerSecLE = toSampleRateLE(this.sampleRate, this.numChannels); 62 | const byteAlignmentLE = new Uint8Array([this.numChannels * 2, 0x00]); 63 | const bitsPerSampleLE = new Uint8Array([0x10, 0x00]); 64 | const theData = new Uint8Array([0x64, 0x61, 0x74, 0x61]); 65 | const dataSize = toUint32LE(this.data.byteLength); 66 | const header = Buffer.concat([ 67 | theRIFF, 68 | fileSize, 69 | theWAVE, 70 | theFmt, 71 | fmtSizeLE, 72 | fmtCodeLE, 73 | numChannelsLE, 74 | sampleRateLE, 75 | bytesPerSecLE, 76 | byteAlignmentLE, 77 | bitsPerSampleLE, 78 | theData, 79 | dataSize, 80 | ]); 81 | assert(header.byteLength === 44, `wave header must have just 44 bytes`); 82 | return header; 83 | } 84 | } 85 | 86 | module.exports = WaveObject; 87 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ebyroid", 3 | "version": "0.2.0", 4 | "description": "Node native addon for VOICEROID+ and VOICEROID2", 5 | "author": "Kinas (https://github.com/nanokina)", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/nanokina/ebyroid.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/nanokina/ebyroid/issues" 12 | }, 13 | "homepage": "https://github.com/nanokina/ebyroid#readme", 14 | "keywords": [ 15 | "VOICEROID", 16 | "VOICEROID+", 17 | "VOICEROID2", 18 | "Discord", 19 | "Dispeak", 20 | "TTS", 21 | "Text To Speech" 22 | ], 23 | "main": "index.js", 24 | "files": [ 25 | "CMakeLists.txt", 26 | "lib", 27 | "src", 28 | "bin" 29 | ], 30 | "bin": { 31 | "ebyroid": "./bin/main.js" 32 | }, 33 | "type": "commonjs", 34 | "engines": { 35 | "node": ">=12.13.1" 36 | }, 37 | "os": [ 38 | "win32" 39 | ], 40 | "arch": [ 41 | "ia32" 42 | ], 43 | "license": "MIT", 44 | "dependencies": { 45 | "cmake-js": "^6.0.0", 46 | "debug": "^4.1.1", 47 | "iconv-lite": "^0.5.1", 48 | "inquirer": "^7.0.6", 49 | "npm-run-all": "^4.1.5", 50 | "semver": "^7.1.3", 51 | "yargs": "^15.3.0" 52 | }, 53 | "scripts": { 54 | "install": "run-p build:release", 55 | "prestart": "@powershell -Command if(-not(Test-Path ebyroid.conf.json)) { node ./bin/main.js configure }", 56 | "start": "@powershell -Command node ./bin/main.js start", 57 | "test:run": "@powershell -Command $env:DEBUG='*';node ./test/test_run", 58 | "build:debug": "run-s build:clean build:prepare build:debug:compile build:debug:copy", 59 | "build:debug:copy": "@powershell -Command Copy-Item ./build/debug/ebyroid.node -Destination dll", 60 | "build:debug:compile": "cmake-js -D compile", 61 | "build:release": "run-s build:clean build:prepare build:release:compile build:release:copy", 62 | "build:release:copy": "@powershell -Command Copy-Item ./build/release/ebyroid.node -Destination dll", 63 | "build:release:compile": "cmake-js compile", 64 | "build:clean": "run-s build:clean:*", 65 | "build:clean:node": "@powershell -Command if(Test-Path ./dll/ebyroid.node) { Remove-Item ./dll/ebyroid.node }", 66 | "build:clean:folder": "@powershell -Command if(Test-Path build) { Remove-Item -Recurse build }", 67 | "build:prepare": "@powershell -Command if(-not(Test-Path dll)) { New-Item -Path . -Name dll -ItemType directory }", 68 | "pack:debug": "run-s build:debug pack:clean pack:pkg pack:copy", 69 | "pack:release": "run-s build:release pack:clean pack:pkg pack:copy", 70 | "pack:copy": "@powershell -Command Copy-Item ./dll/ebyroid.node -Destination pack", 71 | "pack:clean": "@powershell -Command if (Test-Path pack) { Remove-Item -Recurse pack }", 72 | "pack:pkg": "pkg --options max-http-header-size=65536 --targets node12-win-x86 --out-path pack ." 73 | }, 74 | "cmake-js": { 75 | "runtime": "node", 76 | "runtimeVersion": "12.13.1", 77 | "arch": "ia32" 78 | }, 79 | "husky": { 80 | "hooks": { 81 | "pre-commit": "lint-staged" 82 | } 83 | }, 84 | "lint-staged": { 85 | "*.js": [ 86 | "eslint --fix" 87 | ] 88 | }, 89 | "devDependencies": { 90 | "eslint": "^6.8.0", 91 | "eslint-config-airbnb-base": "^14.0.0", 92 | "eslint-config-prettier": "^6.10.0", 93 | "eslint-plugin-import": "^2.20.1", 94 | "eslint-plugin-node": "^11.0.0", 95 | "eslint-plugin-prettier": "^3.1.2", 96 | "husky": "^4.2.3", 97 | "lint-staged": "^10.0.8", 98 | "pkg": "^4.4.4", 99 | "prettier": "^1.19.1", 100 | "wavefile": "^11.0.0" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/api_adapter.cc: -------------------------------------------------------------------------------- 1 | #include "api_adapter.h" 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include "ebyutil.h" 8 | 9 | namespace ebyroid { 10 | 11 | namespace { 12 | 13 | template 14 | inline T LoadProc(const HINSTANCE& handle, const char* proc_name) { 15 | FARPROC proc = GetProcAddress(handle, proc_name); 16 | if (proc == nullptr) { 17 | FreeLibrary(handle); 18 | char m[64]; 19 | std::snprintf(m, 64, "Could not find '%s' in the library.", proc_name); 20 | throw std::runtime_error(m); 21 | } 22 | return reinterpret_cast(proc); 23 | } 24 | 25 | } // namespace 26 | 27 | ApiAdapter* ApiAdapter::Create(const char* base_dir, const char* dll_path) { 28 | if (BOOL ok = SetDllDirectoryA(base_dir); !ok) { 29 | char m[128]; 30 | std::snprintf(m, 31 | 128, 32 | "SetDllDirectory failed with code %d (Check out the voiceroid path setting)", 33 | GetLastError()); 34 | throw new std::runtime_error(m); 35 | } 36 | 37 | HINSTANCE handle = LoadLibraryA(dll_path); 38 | if (handle == nullptr) { 39 | char m[128]; 40 | std::snprintf(m, 41 | 128, 42 | "LoadLibrary failed with code %d (Check out the voiceroid path setting)", 43 | GetLastError()); 44 | throw new std::runtime_error(m); 45 | } 46 | 47 | if (BOOL ok = SetDllDirectoryA(nullptr); !ok) { 48 | // this should not be so critical 49 | Eprintf("SetDllDirectoryA(NULL) failed with code %d", GetLastError()); 50 | Eprintf("albeit the program will go on ignoring this error."); 51 | } 52 | 53 | ApiAdapter* adapter = new ApiAdapter(handle); 54 | adapter->init_ = LoadProc(handle, "_AITalkAPI_Init@4"); 55 | adapter->end_ = LoadProc(handle, "_AITalkAPI_End@0"); 56 | adapter->voice_load_ = LoadProc(handle, "_AITalkAPI_VoiceLoad@4"); 57 | adapter->voice_clear_ = LoadProc(handle, "_AITalkAPI_VoiceClear@0"); 58 | adapter->set_param_ = LoadProc(handle, "_AITalkAPI_SetParam@4"); 59 | adapter->get_param_ = LoadProc(handle, "_AITalkAPI_GetParam@8"); 60 | adapter->lang_load_ = LoadProc(handle, "_AITalkAPI_LangLoad@4"); 61 | adapter->text_to_kana_ = LoadProc(handle, "_AITalkAPI_TextToKana@12"); 62 | adapter->close_kana_ = LoadProc(handle, "_AITalkAPI_CloseKana@8"); 63 | adapter->get_kana_ = LoadProc(handle, "_AITalkAPI_GetKana@20"); 64 | adapter->text_to_speech_ = LoadProc(handle, "_AITalkAPI_TextToSpeech@12"); 65 | adapter->close_speech_ = LoadProc(handle, "_AITalkAPI_CloseSpeech@8"); 66 | adapter->get_data_ = LoadProc(handle, "_AITalkAPI_GetData@16"); 67 | 68 | return adapter; 69 | } 70 | 71 | ApiAdapter::~ApiAdapter() { 72 | if (BOOL result = FreeLibrary(dll_instance_); !result) { 73 | Eprintf("FreeLibrary(HMODULE) failed. Though the program will go on, may lead to fatal error."); 74 | } 75 | } 76 | 77 | ResultCode ApiAdapter::Init(TConfig* config) { 78 | return init_(config); 79 | } 80 | 81 | ResultCode ApiAdapter::End() { 82 | return end_(); 83 | } 84 | 85 | ResultCode ApiAdapter::VoiceLoad(const char* voice_name) { 86 | return voice_load_(voice_name); 87 | } 88 | 89 | ResultCode ApiAdapter::VoiceClear() { 90 | return voice_clear_(); 91 | } 92 | 93 | ResultCode ApiAdapter::SetParam(IntPtr p_param) { 94 | return set_param_(p_param); 95 | } 96 | 97 | ResultCode ApiAdapter::GetParam(IntPtr p_param, uint32_t* size) { 98 | return get_param_(p_param, size); 99 | } 100 | 101 | ResultCode ApiAdapter::LangLoad(const char* dir_lang) { 102 | return lang_load_(dir_lang); 103 | } 104 | 105 | ResultCode ApiAdapter::TextToKana(int32_t* job_id, TJobParam* param, const char* text) { 106 | return text_to_kana_(job_id, param, text); 107 | } 108 | 109 | ResultCode ApiAdapter::CloseKana(int32_t job_id, int32_t use_event) { 110 | return close_kana_(job_id, use_event); 111 | } 112 | 113 | ResultCode ApiAdapter::GetKana(int32_t job_id, 114 | char* text_buf, 115 | uint32_t len_buf, 116 | uint32_t* size, 117 | uint32_t* pos) { 118 | return get_kana_(job_id, text_buf, len_buf, size, pos); 119 | } 120 | 121 | ResultCode ApiAdapter::TextToSpeech(int32_t* job_id, TJobParam* param, const char* text) { 122 | return text_to_speech_(job_id, param, text); 123 | } 124 | 125 | ResultCode ApiAdapter::CloseSpeech(int32_t job_id, int32_t use_event) { 126 | return close_speech_(job_id, use_event); 127 | } 128 | 129 | ResultCode ApiAdapter::GetData(int32_t job_id, int16_t* raw_buf, uint32_t len_buf, uint32_t* size) { 130 | return get_data_(job_id, raw_buf, len_buf, size); 131 | } 132 | 133 | } // namespace ebyroid 134 | -------------------------------------------------------------------------------- /src/api_adapter.h: -------------------------------------------------------------------------------- 1 | #ifndef API_ADAPTER_H 2 | #define API_ADAPTER_H 3 | 4 | #include 5 | 6 | // forward-declaration to avoid including Windows.h in header 7 | #ifndef _WINDEF_ 8 | struct HINSTANCE__; 9 | typedef HINSTANCE__* HINSTANCE; 10 | #endif 11 | 12 | namespace ebyroid { 13 | 14 | static constexpr int32_t kMaxVoiceName = 80; 15 | 16 | static constexpr int32_t kControlLength = 12; 17 | 18 | static constexpr int32_t kConfigRawbufSize = 0x158880; 19 | 20 | static constexpr int32_t kLenSeedValue = 0; 21 | 22 | enum EventReasonCode : uint32_t { 23 | TEXTBUF_FULL = 0x00000065, 24 | TEXTBUF_FLUSH = 0x00000066, 25 | TEXTBUF_CLOSE = 0x00000067, 26 | RAWBUF_FULL = 0x000000C9, 27 | RAWBUF_FLUSH = 0x000000CA, 28 | RAWBUF_CLOSE = 0x000000CB, 29 | PH_LABEL = 0x0000012D, 30 | BOOKMARK = 0x0000012E, 31 | AUTOBOOKMARK = 0x0000012F 32 | }; 33 | 34 | enum ExtendFormat : uint32_t { 35 | NONE = 0, 36 | JEITA_RUBY = 1, 37 | AUTO_BOOKMARK = 16, 38 | BOTH = JEITA_RUBY | AUTO_BOOKMARK 39 | }; 40 | 41 | enum JobInOut : uint32_t { 42 | IOMODE_PLAIN_TO_WAVE = 11, 43 | IOMODE_AIKANA_TO_WAVE = 12, 44 | IOMODE_JEITA_TO_WAVE = 13, 45 | IOMODE_PLAIN_TO_AIKANA = 21, 46 | IOMODE_AIKANA_TO_JEITA = 32 47 | }; 48 | 49 | enum ResultCode : int32_t { 50 | ERR_USERDIC_NOENTRY = -1012, 51 | ERR_USERDIC_LOCKED = -1011, 52 | ERR_COUNT_LIMIT = -1004, 53 | ERR_READ_FAULT = -1003, 54 | ERR_PATH_NOT_FOUND = -1002, 55 | ERR_FILE_NOT_FOUND = -1001, 56 | ERR_OUT_OF_MEMORY = -206, 57 | ERR_JOB_BUSY = -203, 58 | ERR_INVALID_JOBID = -202, 59 | ERR_TOO_MANY_JOBS = -201, 60 | ERR_LICENSE_REJECTED = -102, 61 | ERR_LICENSE_EXPIRED = -101, 62 | ERR_LICENSE_ABSENT = -100, 63 | ERR_INSUFFICIENT = -20, 64 | ERR_NOT_LOADED = -11, 65 | ERR_NOT_INITIALIZED = -10, 66 | ERR_WAIT_TIMEOUT = -4, 67 | ERR_INVALID_ARGUMENT = -3, 68 | ERR_UNSUPPORTED = -2, 69 | ERR_INTERNAL_ERROR = -1, 70 | ERR_SUCCESS = 0, 71 | ERR_ALREADY_INITIALIZED = 10, 72 | ERR_ALREADY_LOADED = 11, 73 | ERR_PARTIALLY_REGISTERED = 21, 74 | ERR_NOMORE_DATA = 204 75 | }; 76 | 77 | enum StatusCode : int32_t { 78 | STAT_WRONG_STATE = -1, 79 | STAT_INPROGRESS = 10, 80 | STAT_STILL_RUNNING = 11, 81 | STAT_DONE = 12 82 | }; 83 | 84 | typedef void* IntPtr; 85 | typedef int(__stdcall* ProcTextBuf)(EventReasonCode reason_code, int32_t job_id, IntPtr user_data); 86 | typedef int(__stdcall* ProcRawBuf)(EventReasonCode reason_code, 87 | int32_t job_id, 88 | uint64_t tick, 89 | IntPtr user_data); 90 | typedef int(__stdcall* ProcEventTTS)(EventReasonCode reason_code, 91 | int32_t job_id, 92 | uint64_t tick, 93 | const char* name, 94 | IntPtr user_data); 95 | 96 | #pragma pack(push, 1) 97 | struct TTtsParam { 98 | uint32_t size; 99 | ProcTextBuf proc_text_buf; 100 | ProcRawBuf proc_raw_buf; 101 | ProcEventTTS proc_event_tts; 102 | uint32_t len_text_buf_bytes; 103 | uint32_t len_raw_buf_bytes; 104 | float volume; 105 | int32_t pause_begin; 106 | int32_t pause_term; 107 | ExtendFormat extend_format; 108 | char voice_name[kMaxVoiceName]; 109 | struct TJeitaParam { 110 | char female_name[kMaxVoiceName]; 111 | char male_name[kMaxVoiceName]; 112 | int32_t pause_middle; 113 | int32_t pause_long; 114 | int32_t pause_sentence; 115 | char control[kControlLength]; 116 | }; 117 | TJeitaParam jeita; 118 | uint32_t num_speakers; 119 | int32_t __reserved__; 120 | struct TSpeakerParam { 121 | char voice_name[kMaxVoiceName]; 122 | float volume; 123 | float speed; 124 | float pitch; 125 | float range; 126 | int32_t pause_middle; 127 | int32_t pause_long; 128 | int32_t pause_sentence; 129 | char style_rate[kMaxVoiceName]; 130 | }; 131 | TSpeakerParam speaker[1]; 132 | }; 133 | #pragma pack(pop) 134 | 135 | #pragma pack(push, 1) 136 | struct TJobParam { 137 | JobInOut mode_in_out; 138 | IntPtr user_data; 139 | }; 140 | #pragma pack(pop) 141 | 142 | #pragma pack(push, 1) 143 | struct TConfig { 144 | uint32_t hz_voice_db; 145 | const char* dir_voice_dbs; 146 | uint32_t msec_timeout; 147 | const char* path_license; 148 | const char* code_auth_seed; 149 | uint32_t len_auth_seed; 150 | }; 151 | #pragma pack(pop) 152 | 153 | class ApiAdapter { 154 | public: 155 | ApiAdapter(const ApiAdapter&) = delete; 156 | ApiAdapter(ApiAdapter&&) = delete; 157 | ~ApiAdapter(); 158 | 159 | static ApiAdapter* Create(const char* base_dir, const char* dll_path); 160 | 161 | ResultCode Init(TConfig* config); 162 | ResultCode End(); 163 | ResultCode SetParam(IntPtr p_param); 164 | ResultCode GetParam(IntPtr p_param, uint32_t* size); 165 | ResultCode LangLoad(const char* dir_lang); 166 | ResultCode VoiceLoad(const char* voice_name); 167 | ResultCode VoiceClear(); 168 | ResultCode TextToKana(int32_t* job_id, TJobParam* param, const char* text); 169 | ResultCode CloseKana(int32_t job_id, int32_t use_event = 0); 170 | ResultCode GetKana(int32_t job_id, 171 | char* text_buf, 172 | uint32_t len_buf, 173 | uint32_t* size, 174 | uint32_t* pos); 175 | ResultCode TextToSpeech(int32_t* job_id, TJobParam* param, const char* text); 176 | ResultCode CloseSpeech(int32_t job_id, int32_t use_event = 0); 177 | ResultCode GetData(int32_t job_id, int16_t* raw_buf, uint32_t len_buf, uint32_t* size); 178 | 179 | private: 180 | ApiAdapter(HINSTANCE dll_instance) : dll_instance_(dll_instance) {} 181 | 182 | typedef ResultCode(__stdcall* ApiInit)(TConfig*); 183 | typedef ResultCode(__stdcall* ApiEnd)(void); 184 | typedef ResultCode(__stdcall* ApiSetParam)(IntPtr); 185 | typedef ResultCode(__stdcall* ApiGetParam)(IntPtr, uint32_t*); 186 | typedef ResultCode(__stdcall* ApiLangLoad)(const char*); 187 | typedef ResultCode(__stdcall* ApiVoiceLoad)(const char*); 188 | typedef ResultCode(__stdcall* ApiVoiceClear)(void); 189 | typedef ResultCode(__stdcall* ApiTextToKana)(int32_t*, TJobParam*, const char*); 190 | typedef ResultCode(__stdcall* ApiCloseKana)(int32_t, int32_t); 191 | typedef ResultCode(__stdcall* ApiGetKana)(int32_t, char*, uint32_t, uint32_t*, uint32_t*); 192 | typedef ResultCode(__stdcall* ApiTextToSpeech)(int32_t*, TJobParam*, const char*); 193 | typedef ResultCode(__stdcall* ApiCloseSpeech)(int32_t, int32_t); 194 | typedef ResultCode(__stdcall* ApiGetData)(int32_t, int16_t*, uint32_t, uint32_t*); 195 | 196 | HINSTANCE dll_instance_ = nullptr; 197 | 198 | ApiInit init_; 199 | ApiEnd end_; 200 | ApiVoiceLoad voice_load_; 201 | ApiVoiceClear voice_clear_; 202 | ApiSetParam set_param_; 203 | ApiGetParam get_param_; 204 | ApiLangLoad lang_load_; 205 | ApiTextToKana text_to_kana_; 206 | ApiCloseKana close_kana_; 207 | ApiGetKana get_kana_; 208 | ApiTextToSpeech text_to_speech_; 209 | ApiCloseSpeech close_speech_; 210 | ApiGetData get_data_; 211 | }; 212 | 213 | } // namespace ebyroid 214 | 215 | #endif // API_ADAPTER_H 216 | -------------------------------------------------------------------------------- /src/api_settings.cc: -------------------------------------------------------------------------------- 1 | #include "api_settings.h" 2 | 3 | #include 4 | 5 | #include "ebyutil.h" 6 | 7 | namespace ebyroid { 8 | 9 | using std::string; 10 | 11 | Settings SettingsBuilder::Build() { 12 | Settings settings; 13 | string dll_path = base_dir_ + kWinDelimit + kDllFilename; 14 | string license_path = base_dir_ + kWinDelimit + kLicFilename; 15 | std::strcpy(settings.base_dir, base_dir_.c_str()); 16 | std::strcpy(settings.voice_name, voice_name_.c_str()); 17 | std::strcpy(settings.dll_path, dll_path.c_str()); 18 | std::strcpy(settings.license_path, license_path.c_str()); 19 | 20 | Dprintf("SettingBuilder\nbase_dir=%s\ndll_path=%s\nlicense_path=%s\nvoice_name=%s", 21 | settings.base_dir, 22 | settings.dll_path, 23 | settings.license_path, 24 | settings.voice_name); 25 | 26 | if (voice_name_.find("_22") != string::npos) { 27 | // this means the given library is VOICEROID+ 28 | settings.frequency = kFrequency22; 29 | 30 | string voice_dir = base_dir_ + kWinDelimit + "voice"; 31 | string language_dir = base_dir_ + kWinDelimit + "lang"; 32 | std::strcpy(settings.voice_dir, voice_dir.c_str()); 33 | std::strcpy(settings.language_dir, language_dir.c_str()); 34 | if (voice_name_ == "kiritan_22") { 35 | settings.seed = EBY_SEED_B; 36 | } else if (voice_name_ == "zunko_22") { 37 | settings.seed = EBY_SEED_C; 38 | } else if (voice_name_ == "akane_22") { 39 | settings.seed = EBY_SEED_D; 40 | } else if (voice_name_ == "aoi_22") { 41 | settings.seed = EBY_SEED_E; 42 | } else { 43 | char m[64]; 44 | std::snprintf(m, 64, "Unsupported VOICEROID+ library '%s' was given.", settings.voice_name); 45 | throw new std::runtime_error(m); 46 | } 47 | } else { 48 | // this means it is either VOICEROID2 or an unexpected library 49 | // try to setup as VOICEROID2 anyways 50 | settings.frequency = kFrequency44; 51 | 52 | string voice_dir = base_dir_ + kWinDelimit + "Voice"; 53 | string language_dir = base_dir_ + kWinDelimit + "Lang" + kWinDelimit + "standard"; 54 | std::strcpy(settings.voice_dir, voice_dir.c_str()); 55 | std::strcpy(settings.language_dir, language_dir.c_str()); 56 | settings.seed = EBY_SEED_A; 57 | } 58 | 59 | Dprintf("SettingBuilder\nfrequency=%d\nlanguage_dir=%s\nvoice_dir=%s", 60 | settings.frequency, 61 | settings.language_dir, 62 | settings.voice_dir); 63 | 64 | return std::move(settings); 65 | } 66 | 67 | } // namespace ebyroid 68 | -------------------------------------------------------------------------------- /src/api_settings.h: -------------------------------------------------------------------------------- 1 | #ifndef API_SETTINGS_H 2 | #define API_SETTINGS_H 3 | 4 | #include 5 | 6 | #include 7 | 8 | namespace ebyroid { 9 | 10 | static constexpr size_t kMaxPathSize = 0xFF; 11 | static constexpr int32_t kFrequency44 = 0xAC44; 12 | static constexpr int32_t kFrequency22 = 0x5622; 13 | static constexpr char* kDllFilename = "aitalked.dll"; 14 | static constexpr char* kLicFilename = "aitalk.lic"; 15 | static constexpr char* kWinDelimit = "\\"; 16 | 17 | struct Settings { 18 | char base_dir[kMaxPathSize]; 19 | char dll_path[kMaxPathSize]; 20 | char voice_dir[kMaxPathSize]; 21 | char voice_name[16]; 22 | char language_dir[kMaxPathSize]; 23 | char license_path[kMaxPathSize]; 24 | const char* seed; 25 | uint32_t frequency; 26 | }; 27 | 28 | class SettingsBuilder { 29 | public: 30 | SettingsBuilder(const std::string& base_dir, const std::string& voice_name) 31 | : base_dir_(base_dir), voice_name_(voice_name) {} 32 | 33 | Settings Build(); 34 | 35 | private: 36 | std::string base_dir_; 37 | std::string voice_name_; 38 | }; 39 | 40 | } // namespace ebyroid 41 | 42 | #endif // API_SETTINGS_H 43 | -------------------------------------------------------------------------------- /src/ebyroid.cc: -------------------------------------------------------------------------------- 1 | #include "ebyroid.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | #include "api_adapter.h" 11 | #include "api_settings.h" 12 | #include "ebyutil.h" 13 | 14 | namespace ebyroid { 15 | 16 | using std::string, std::vector, std::function, std::pair; 17 | 18 | namespace { 19 | 20 | ApiAdapter* NewAdapter(const string&, const string&, float); 21 | int __stdcall HiraganaCallback(EventReasonCode, int32_t, IntPtr); 22 | int __stdcall SpeechCallback(EventReasonCode, int32_t, uint64_t, IntPtr); 23 | inline pair WithDirecory(const char* dir, function(void)> yield); 24 | 25 | } // namespace 26 | 27 | Ebyroid::~Ebyroid() { 28 | delete api_adapter_; 29 | } 30 | 31 | Ebyroid* Ebyroid::Create(const string& base_dir, const string& voice, float volume) { 32 | ApiAdapter* adapter = NewAdapter(base_dir, voice, volume); 33 | Ebyroid* ebyroid = new Ebyroid(adapter); 34 | return ebyroid; 35 | } 36 | 37 | int Ebyroid::Hiragana(const unsigned char* inbytes, unsigned char** outbytes, size_t* outsize) { 38 | Response* const response = new Response(api_adapter_); 39 | 40 | TJobParam param; 41 | param.mode_in_out = IOMODE_PLAIN_TO_AIKANA; 42 | param.user_data = response; 43 | 44 | char eventname[32]; 45 | std::sprintf(eventname, "TTKLOCK:%p", response); 46 | 47 | HANDLE event = CreateEventA(NULL, TRUE, FALSE, eventname); 48 | 49 | int32_t job_id; 50 | if (ResultCode result = api_adapter_->TextToKana(&job_id, ¶m, (const char*) inbytes); 51 | result != ERR_SUCCESS) { 52 | delete response; 53 | ResetEvent(event); 54 | CloseHandle(event); 55 | static constexpr char* format = "TextToKana failed with the result code %d\n" 56 | "Given inbytes: %s"; 57 | char m[0xFFFF]; 58 | std::snprintf(m, 0xFFFF, format, result, inbytes); 59 | throw std::runtime_error(m); 60 | } 61 | 62 | WaitForSingleObject(event, INFINITE); 63 | ResetEvent(event); 64 | CloseHandle(event); 65 | 66 | // finalize 67 | if (ResultCode result = api_adapter_->CloseKana(job_id); result != ERR_SUCCESS) { 68 | delete response; 69 | throw std::runtime_error("wtf"); 70 | } 71 | 72 | // write to output memory 73 | vector buffer = response->End(); 74 | *outsize = buffer.size(); 75 | *outbytes = (unsigned char*) malloc(buffer.size() + 1); 76 | std::copy(buffer.begin(), buffer.end(), *outbytes); 77 | *(*outbytes + buffer.size()) = '\0'; 78 | 79 | delete response; 80 | return 0; 81 | } 82 | 83 | int Ebyroid::Speech(const unsigned char* inbytes, 84 | int16_t** outbytes, 85 | size_t* outsize, 86 | uint32_t mode) { 87 | Response* const response = new Response(api_adapter_); 88 | 89 | TJobParam param; 90 | param.mode_in_out = mode == 0u ? IOMODE_AIKANA_TO_WAVE : (JobInOut) mode; 91 | param.user_data = response; 92 | 93 | char eventname[32]; 94 | sprintf(eventname, "TTSLOCK:%p", response); 95 | HANDLE event = CreateEventA(NULL, TRUE, FALSE, eventname); 96 | 97 | int32_t job_id; 98 | if (ResultCode result = api_adapter_->TextToSpeech(&job_id, ¶m, (const char*) inbytes); 99 | result != ERR_SUCCESS) { 100 | delete response; 101 | ResetEvent(event); 102 | CloseHandle(event); 103 | static constexpr char* format = "TextToSpeech failed with the result code %d\n" 104 | "Given inbytes: %s"; 105 | char m[0xFFFF]; 106 | std::snprintf(m, 0xFFFF, format, result, inbytes); 107 | throw std::runtime_error(m); 108 | } 109 | 110 | WaitForSingleObject(event, INFINITE); 111 | ResetEvent(event); 112 | CloseHandle(event); 113 | 114 | // finalize 115 | if (ResultCode result = api_adapter_->CloseSpeech(job_id); result != ERR_SUCCESS) { 116 | delete response; 117 | throw std::runtime_error("wtf"); 118 | } 119 | 120 | // write to output memory 121 | vector buffer = response->End16(); 122 | *outsize = buffer.size() * 2; // sizeof(int16_t) == 2 123 | *outbytes = (int16_t*) malloc(buffer.size() * 2 + 1); 124 | std::copy(buffer.begin(), buffer.end(), *outbytes); 125 | *((char*) *outbytes + (buffer.size() * 2)) = '\0'; 126 | 127 | delete response; 128 | return 0; 129 | } 130 | 131 | int Ebyroid::Convert(const ConvertParams& params, 132 | const unsigned char* inbytes, 133 | int16_t** outbytes, 134 | size_t* outsize) { 135 | if (params.needs_reload) { 136 | delete api_adapter_; 137 | api_adapter_ = NewAdapter(params.base_dir, params.voice, params.volume); 138 | } 139 | 140 | return Speech(inbytes, outbytes, outsize, IOMODE_PLAIN_TO_WAVE); 141 | }; 142 | 143 | void Response::Write(char* bytes, uint32_t size) { 144 | buffer_.insert(std::end(buffer_), bytes, bytes + size); 145 | } 146 | 147 | void Response::Write16(int16_t* shorts, uint32_t size) { 148 | buffer_16_.insert(std::end(buffer_16_), shorts, shorts + size); 149 | } 150 | 151 | vector Response::End() { 152 | return std::move(buffer_); 153 | } 154 | 155 | vector Response::End16() { 156 | return std::move(buffer_16_); 157 | } 158 | 159 | namespace { 160 | 161 | ApiAdapter* NewAdapter(const string& base_dir, const string& voice, float volume) { 162 | SettingsBuilder builder(base_dir, voice); 163 | Settings settings = builder.Build(); 164 | 165 | ApiAdapter* adapter = ApiAdapter::Create(settings.base_dir, settings.dll_path); 166 | 167 | TConfig config; 168 | config.hz_voice_db = settings.frequency; 169 | config.msec_timeout = 1000; 170 | config.path_license = settings.license_path; 171 | config.dir_voice_dbs = settings.voice_dir; 172 | config.code_auth_seed = settings.seed; 173 | config.len_auth_seed = kLenSeedValue; 174 | 175 | if (ResultCode result = adapter->Init(&config); result != ERR_SUCCESS) { 176 | delete adapter; 177 | string message = "API initialization failed with code "; 178 | message += std::to_string(result); 179 | throw std::runtime_error(message); 180 | } 181 | 182 | auto [is_error, what] = WithDirecory(settings.base_dir, [adapter, settings]() { 183 | if (ResultCode result = adapter->LangLoad(settings.language_dir); result != ERR_SUCCESS) { 184 | char m[64]; 185 | std::snprintf(m, 64, "API LangLoad failed (could not load language) with code %d", result); 186 | return pair(true, string(m)); 187 | } 188 | return pair(false, string()); 189 | }); 190 | if (is_error) { 191 | delete adapter; 192 | throw std::runtime_error(what); 193 | } 194 | 195 | if (ResultCode result = adapter->VoiceLoad(settings.voice_name); result != ERR_SUCCESS) { 196 | delete adapter; 197 | string message = "API Load Voice failed (Could not load voice data) with code "; 198 | message += std::to_string(result); 199 | throw std::runtime_error(message); 200 | } 201 | 202 | uint32_t param_size = 0; 203 | if (ResultCode result = adapter->GetParam((void*) 0, ¶m_size); 204 | result != ERR_INSUFFICIENT) { // NOTE: Code -20 is expected here 205 | delete adapter; 206 | string message = "API Get Param failed (Could not acquire the size) with code "; 207 | message += std::to_string(result); 208 | throw std::runtime_error(message); 209 | } 210 | 211 | char* param_buffer = new char[param_size]; 212 | TTtsParam* param = (TTtsParam*) param_buffer; 213 | param->size = param_size; 214 | if (ResultCode result = adapter->GetParam(param, ¶m_size); result != ERR_SUCCESS) { 215 | delete[] param_buffer; 216 | delete adapter; 217 | string message = "API Get Param failed with code "; 218 | message += std::to_string(result); 219 | throw std::runtime_error(message); 220 | } 221 | param->extend_format = BOTH; 222 | param->proc_text_buf = HiraganaCallback; 223 | param->proc_raw_buf = SpeechCallback; 224 | param->proc_event_tts = nullptr; 225 | param->len_raw_buf_bytes = kConfigRawbufSize; 226 | param->volume = volume; 227 | param->speaker[0].volume = 1.0; 228 | 229 | if (ResultCode result = adapter->SetParam(param); result != ERR_SUCCESS) { 230 | delete[] param_buffer; 231 | delete adapter; 232 | string message = "API Set Param failed with code "; 233 | message += std::to_string(result); 234 | throw std::runtime_error(message); 235 | } 236 | 237 | delete[] param_buffer; 238 | 239 | return adapter; 240 | } 241 | 242 | int __stdcall HiraganaCallback(EventReasonCode reason_code, int32_t job_id, IntPtr user_data) { 243 | Response* const response = (Response*) user_data; 244 | ApiAdapter* api_adapter = response->api_adapter(); 245 | 246 | if (reason_code != TEXTBUF_FULL && reason_code != TEXTBUF_FLUSH && reason_code != TEXTBUF_CLOSE) { 247 | // unexpected: may possibly lead to memory leak 248 | return 0; 249 | } 250 | 251 | static constexpr int kBufferSize = 0x1000; 252 | char* buffer = new char[kBufferSize]; 253 | while (true) { 254 | uint32_t size, pos; 255 | if (ResultCode result = api_adapter->GetKana(job_id, buffer, kBufferSize, &size, &pos); 256 | result != ERR_SUCCESS) { 257 | break; 258 | } 259 | response->Write(buffer, size); 260 | if (kBufferSize > size) { 261 | break; 262 | } 263 | } 264 | delete[] buffer; 265 | 266 | if (reason_code == TEXTBUF_CLOSE) { 267 | char eventname[32]; 268 | sprintf(eventname, "TTKLOCK:%p", response); 269 | HANDLE event = OpenEventA(EVENT_ALL_ACCESS, FALSE, eventname); 270 | SetEvent(event); 271 | } 272 | return 0; 273 | } 274 | 275 | int __stdcall SpeechCallback(EventReasonCode reason_code, 276 | int32_t job_id, 277 | uint64_t tick, 278 | IntPtr user_data) { 279 | Response* const response = (Response*) user_data; 280 | ApiAdapter* api_adapter = response->api_adapter(); 281 | 282 | if (reason_code != RAWBUF_FULL && reason_code != RAWBUF_FLUSH && reason_code != RAWBUF_CLOSE) { 283 | // unexpected: may possibly lead to memory leak 284 | return 0; 285 | } 286 | 287 | static constexpr int kBufferSize = 0xFFFF; 288 | int16_t* buffer = new int16_t[kBufferSize]; 289 | while (true) { 290 | uint32_t size, pos; 291 | if (ResultCode result = api_adapter->GetData(job_id, buffer, kBufferSize, &size); 292 | result != ERR_SUCCESS) { 293 | break; 294 | } 295 | response->Write16(buffer, size); 296 | if (kBufferSize > size) { 297 | break; 298 | } 299 | } 300 | delete[] buffer; 301 | 302 | if (reason_code == RAWBUF_CLOSE) { 303 | char eventname[32]; 304 | sprintf(eventname, "TTSLOCK:%p", response); 305 | HANDLE event = OpenEventA(EVENT_ALL_ACCESS, FALSE, eventname); 306 | SetEvent(event); 307 | } 308 | return 0; 309 | } 310 | 311 | inline pair WithDirecory(const char* dir, function(void)> yield) { 312 | static constexpr size_t kErrMax = 64 + MAX_PATH; 313 | char org[MAX_PATH]; 314 | if (DWORD result = GetCurrentDirectoryA(MAX_PATH, org); result == 0) { 315 | char m[64]; 316 | std::snprintf(m, 64, "Could not get the current directory.\n\tErrorNo = %d", GetLastError()); 317 | return pair(true, string(m)); 318 | } 319 | if (BOOL result = SetCurrentDirectoryA(dir); !result) { 320 | char m[kErrMax]; 321 | std::snprintf(m, 322 | kErrMax, 323 | "Could not change directory.\n\tErrorNo = %d\n\tTarget path: %s", 324 | GetLastError(), 325 | dir); 326 | return pair(true, string(m)); 327 | } 328 | auto [is_error, what] = yield(); 329 | if (BOOL result = SetCurrentDirectoryA(org); !result && !is_error) { 330 | char m[kErrMax]; 331 | std::snprintf(m, 332 | kErrMax, 333 | "Could not change directory.\n\tErrorNo = %d\n\tTarget path: %s", 334 | GetLastError(), 335 | org); 336 | return pair(true, string(m)); 337 | } 338 | if (is_error) { 339 | return pair(true, what); 340 | } 341 | return pair(false, string()); 342 | } 343 | 344 | } // namespace 345 | 346 | } // namespace ebyroid 347 | -------------------------------------------------------------------------------- /src/ebyroid.h: -------------------------------------------------------------------------------- 1 | #ifndef EBYROID_H 2 | #define EBYROID_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace ebyroid { 9 | 10 | // forward-declaration to avoid including api_adapter.h 11 | class ApiAdapter; 12 | 13 | struct ConvertParams { 14 | bool needs_reload; 15 | char* base_dir; 16 | char* voice; 17 | float volume; 18 | }; 19 | 20 | class Ebyroid { 21 | public: 22 | Ebyroid(const Ebyroid&) = delete; 23 | Ebyroid(Ebyroid&&) = delete; 24 | ~Ebyroid(); 25 | 26 | static Ebyroid* Create(const std::string& base_dir, const std::string& voice, float volume); 27 | int Hiragana(const unsigned char* inbytes, unsigned char** outbytes, size_t* outsize); 28 | int Speech(const unsigned char* inbytes, int16_t** outbytes, size_t* outsize, uint32_t mode = 0u); 29 | int Convert(const ConvertParams& params, 30 | const unsigned char* inbytes, 31 | int16_t** outbytes, 32 | size_t* outsize); 33 | 34 | private: 35 | Ebyroid(ApiAdapter* api_adapter) : api_adapter_(api_adapter) {} 36 | ApiAdapter* api_adapter_; 37 | }; 38 | 39 | class Response { 40 | public: 41 | Response(ApiAdapter* adapter) : api_adapter_(adapter) {} 42 | void Write(char* bytes, uint32_t size); 43 | void Write16(int16_t* shorts, uint32_t size); 44 | std::vector End(); 45 | std::vector End16(); 46 | ApiAdapter* api_adapter() { return api_adapter_; }; 47 | 48 | private: 49 | ApiAdapter* api_adapter_; 50 | std::vector buffer_; 51 | std::vector buffer_16_; 52 | }; 53 | 54 | } // namespace ebyroid 55 | 56 | #endif // EBYROID_H 57 | -------------------------------------------------------------------------------- /src/ebyutil.h: -------------------------------------------------------------------------------- 1 | #ifndef EBYUTIL_H 2 | #define EBYUTIL_H 3 | 4 | #ifdef _DEBUG 5 | #include "assert.h" 6 | #define e_assert(expr) assert(expr) 7 | #define en_assert(expr) assert(expr) 8 | #else 9 | #define e_assert(expr) \ 10 | do { \ 11 | if (!(expr)) { \ 12 | napi_throw_error(env, "EBY001", "assertion e_assert(" #expr ") failed."); \ 13 | return; \ 14 | }; \ 15 | } while (0) 16 | #define en_assert(expr) \ 17 | do { \ 18 | if (!(expr)) { \ 19 | napi_throw_error(env, "EBY001", "assertion en_assert(" #expr ") failed."); \ 20 | return NULL; \ 21 | }; \ 22 | } while (0) 23 | #endif 24 | 25 | #ifdef _DEBUG 26 | #define Dprintf(a, ...) \ 27 | do { \ 28 | printf("\x1b[1;36m[C++ DEBUG]\x1b[0m " a "\n", __VA_ARGS__); \ 29 | } while (0) 30 | #else 31 | #define Dprintf(a, ...) \ 32 | do { \ 33 | } while (0) 34 | #endif 35 | 36 | #define Eprintf(a, ...) \ 37 | do { \ 38 | fprintf(stderr, "\x1b[1;31m[ERROR]\x1b[0m " a "\n", __VA_ARGS__); \ 39 | } while (0) 40 | 41 | #define _____coffee(milk) #milk 42 | #define _____applepie(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) \ 43 | _____coffee(j##a##l##f##k##n##i##g##h##t##s##q##r##c##o##d##e##b##m##p) 44 | 45 | #define EBY_SEED_A _____applepie(R, 2, p, b, H, J, I, W, A, O, C, X, a, 6, D, l, K, D, U, A) 46 | #define EBY_SEED_B _____applepie(b, R, Q, 6, 1, D, e, Y, 7, q, 1, I, E, A, T, 4, Q, q, P, a) 47 | #define EBY_SEED_C _____applepie(q, M, t, d, C, N, e, l, 8, N, 1, K, C, 4, m, U, O, 2, u, p) 48 | #define EBY_SEED_D _____applepie(2, Y, Z, 0, c, w, 2, N, 1, b, 7, 4, b, D, T, M, K, 8, 1, 5) 49 | #define EBY_SEED_E _____applepie(D, j, Y, l, J, 1, Z, 5, g, L, 7, h, C, E, X, m, V, 7, S, g) 50 | 51 | #endif // EBYUTIL_H 52 | -------------------------------------------------------------------------------- /src/module_main.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include "ebyroid.h" 6 | #include "ebyutil.h" 7 | 8 | using ebyroid::Ebyroid, ebyroid::ConvertParams; 9 | 10 | typedef struct { 11 | Ebyroid* ebyroid; 12 | } module_context; 13 | 14 | typedef enum { WORK_HIRAGANA, WORK_SPEECH, WORK_CONVERT } work_type; 15 | 16 | typedef struct { 17 | work_type worktype; 18 | unsigned char* input; 19 | size_t input_size; 20 | void* output; 21 | size_t output_size; 22 | napi_async_work self; 23 | napi_ref javascript_callback_ref; 24 | char* error_message; 25 | size_t error_size; 26 | ConvertParams* convert_params; 27 | } work_data; 28 | 29 | static module_context* module; 30 | 31 | static void async_work_on_execute(napi_env env, void* data) { 32 | int result; 33 | napi_status status; 34 | work_data* work = (work_data*) data; 35 | 36 | switch (work->worktype) { 37 | case WORK_HIRAGANA: 38 | try { 39 | unsigned char* out; 40 | result = module->ebyroid->Hiragana(work->input, &out, &work->output_size); 41 | work->output = out; 42 | } catch (std::exception& e) { 43 | Eprintf("(Ebyroid::Hiragana) %s", e.what()); 44 | size_t size = strlen(e.what()); 45 | char* what = (char*) malloc(size + 1); 46 | strcpy(what, e.what()); 47 | work->error_message = what; 48 | work->error_size = size; 49 | } 50 | break; 51 | case WORK_SPEECH: 52 | try { 53 | int16_t* out; 54 | result = module->ebyroid->Speech(work->input, &out, &work->output_size); 55 | work->output = out; 56 | } catch (std::exception& e) { 57 | Eprintf("(Ebyroid::Speech) %s", e.what()); 58 | size_t size = strlen(e.what()); 59 | char* what = (char*) malloc(size + 1); 60 | strcpy(what, e.what()); 61 | work->error_message = what; 62 | work->error_size = size; 63 | } 64 | break; 65 | case WORK_CONVERT: 66 | try { 67 | int16_t* out; 68 | result = 69 | module->ebyroid->Convert(*work->convert_params, work->input, &out, &work->output_size); 70 | work->output = out; 71 | } catch (std::exception& e) { 72 | Eprintf("(Ebyroid::Convert) %s", e.what()); 73 | size_t size = strlen(e.what()); 74 | char* what = (char*) malloc(size + 1); 75 | strcpy(what, e.what()); 76 | work->error_message = what; 77 | work->error_size = size; 78 | } 79 | break; 80 | } 81 | } 82 | 83 | static void async_work_on_complete(napi_env env, napi_status work_status, void* data) { 84 | static const size_t RETVAL_SIZE = 2; 85 | napi_status status; 86 | napi_value retval[RETVAL_SIZE]; 87 | napi_value callback, undefined, null_value; 88 | work_data* work = (work_data*) data; 89 | 90 | // prepare JS 'undefined' value 91 | status = napi_get_undefined(env, &undefined); 92 | e_assert(status == napi_ok); 93 | 94 | // prepare JS 'null' value 95 | status = napi_get_null(env, &null_value); 96 | e_assert(status == napi_ok); 97 | 98 | if (work_status == napi_cancelled) { 99 | static const char* what = "N-API async work has been cancelled"; 100 | napi_value message, error_object; 101 | status = napi_create_string_utf8(env, what, strlen(what), &message); 102 | e_assert(status == napi_ok); 103 | status = napi_create_error(env, NULL, message, &error_object); 104 | e_assert(status == napi_ok); 105 | 106 | retval[0] = error_object; 107 | retval[1] = null_value; 108 | goto DO_FINALLY; 109 | } 110 | 111 | if (work->error_message) { 112 | napi_value message, error_object; 113 | status = napi_create_string_utf8(env, work->error_message, work->error_size, &message); 114 | e_assert(status == napi_ok); 115 | status = napi_create_error(env, NULL, message, &error_object); 116 | e_assert(status == napi_ok); 117 | 118 | retval[0] = error_object; 119 | retval[1] = null_value; 120 | goto DO_FINALLY; 121 | } 122 | 123 | napi_value return_value; 124 | switch (work->worktype) { 125 | case WORK_HIRAGANA: 126 | // convert output bytes to node buffer 127 | status = napi_create_buffer_copy(env, work->output_size, work->output, NULL, &return_value); 128 | e_assert(status == napi_ok); 129 | break; 130 | case WORK_SPEECH: 131 | case WORK_CONVERT: 132 | // convert output bytes to int16array 133 | // create underlying arraybuffer 134 | void* node_memory; 135 | napi_value array_buffer; 136 | status = napi_create_arraybuffer(env, work->output_size, &node_memory, &array_buffer); 137 | e_assert(status == napi_ok); 138 | // copy data to arraybuffer and create int16array 139 | memcpy(node_memory, work->output, work->output_size); 140 | e_assert(work->output_size % 2 == 0); 141 | status = napi_create_typedarray( 142 | env, napi_int16_array, work->output_size / 2, array_buffer, 0, &return_value); 143 | e_assert(status == napi_ok); 144 | break; 145 | } 146 | retval[0] = null_value; 147 | retval[1] = return_value; 148 | 149 | DO_FINALLY: 150 | // acquire the javascript callback function 151 | status = napi_get_reference_value(env, work->javascript_callback_ref, &callback); 152 | e_assert(status == napi_ok); 153 | 154 | // actually call the javascript callback function 155 | status = napi_call_function(env, undefined, callback, RETVAL_SIZE, retval, NULL); 156 | e_assert(status == napi_ok || status == napi_pending_exception); 157 | 158 | // decrement the reference count of the function 159 | // ... means it will be GC'd 160 | uint32_t refs; 161 | status = napi_reference_unref(env, work->javascript_callback_ref, &refs); 162 | e_assert(status == napi_ok && refs == 0); 163 | 164 | // now neko work is done so we delete the work object 165 | status = napi_delete_async_work(env, work->self); 166 | e_assert(status == napi_ok); 167 | 168 | // and manually allocated recources 169 | free(work->input); 170 | free(work->output); 171 | free(work->error_message); 172 | if (work->convert_params) { 173 | free(work->convert_params->base_dir); 174 | free(work->convert_params->voice); 175 | free(work->convert_params); 176 | } 177 | free(work); 178 | } 179 | 180 | static napi_value do_async_work(napi_env env, napi_callback_info info, work_type worktype) { 181 | napi_status status; 182 | napi_valuetype valuetype; 183 | 184 | size_t argc = 3; 185 | napi_value argv[3]; 186 | status = napi_get_cb_info(env, info, &argc, argv, NULL, NULL); 187 | en_assert(status == napi_ok); 188 | 189 | // first arg must be buffer 190 | bool is_buffer; 191 | status = napi_is_buffer(env, argv[0], &is_buffer); 192 | en_assert(status == napi_ok && is_buffer == true); 193 | 194 | // second arg must be object 195 | status = napi_typeof(env, argv[1], &valuetype); 196 | en_assert(status == napi_ok && valuetype == napi_object); 197 | 198 | // third arg must be function 199 | status = napi_typeof(env, argv[2], &valuetype); 200 | en_assert(status == napi_ok && valuetype == napi_function); 201 | 202 | // fetch buffer data 203 | unsigned char* node_buffer_data; 204 | size_t node_buffer_size; 205 | status = napi_get_buffer_info(env, argv[0], (void**) &node_buffer_data, &node_buffer_size); 206 | 207 | // allocate 208 | unsigned char* buffer = (unsigned char*) malloc(node_buffer_size + 1); 209 | memcpy(buffer, node_buffer_data, node_buffer_size); 210 | *(buffer + node_buffer_size) = '\0'; 211 | 212 | // check if the object arg is for params 213 | bool is_param; 214 | status = napi_has_named_property(env, argv[1], "needs_reload", &is_param); 215 | en_assert(status == napi_ok); 216 | 217 | // build ConvertParam if it's for param 218 | ConvertParams* params = NULL; 219 | if (is_param) { 220 | napi_value value; 221 | params = (ConvertParams*) malloc(sizeof(*params)); 222 | 223 | // fetch .needs_reload boolean 224 | status = napi_get_named_property(env, argv[1], "needs_reload", &value); 225 | en_assert(status == napi_ok); 226 | status = napi_get_value_bool(env, value, ¶ms->needs_reload); 227 | en_assert(status == napi_ok); 228 | 229 | if (params->needs_reload) { 230 | size_t bufsize; 231 | 232 | // fetch .base_dir string 233 | char* base_dir; 234 | status = napi_get_named_property(env, argv[1], "base_dir", &value); 235 | en_assert(status == napi_ok); 236 | status = napi_get_value_string_utf8(env, value, NULL, NULL, &bufsize); 237 | en_assert(status == napi_ok); 238 | base_dir = (char*) malloc(bufsize + 1); 239 | status = napi_get_value_string_utf8(env, value, base_dir, bufsize + 1, NULL); 240 | en_assert(status == napi_ok); 241 | params->base_dir = base_dir; 242 | 243 | // fetch .voice string 244 | char* voice; 245 | status = napi_get_named_property(env, argv[1], "voice", &value); 246 | en_assert(status == napi_ok); 247 | status = napi_get_value_string_utf8(env, value, NULL, NULL, &bufsize); 248 | en_assert(status == napi_ok); 249 | voice = (char*) malloc(bufsize + 1); 250 | status = napi_get_value_string_utf8(env, value, voice, bufsize + 1, NULL); 251 | en_assert(status == napi_ok); 252 | params->voice = voice; 253 | 254 | // fetch .volume float 255 | double volume; 256 | status = napi_get_named_property(env, argv[1], "volume", &value); 257 | en_assert(status == napi_ok); 258 | status = napi_get_value_double(env, value, &volume); 259 | en_assert(status == napi_ok); 260 | params->volume = (float) volume; 261 | 262 | } else { 263 | params->base_dir = NULL; 264 | params->voice = NULL; 265 | } 266 | } 267 | 268 | char* workname; 269 | switch (worktype) { 270 | case WORK_HIRAGANA: 271 | workname = "Input Text Reinterpretor"; 272 | break; 273 | case WORK_SPEECH: 274 | workname = "Reinterpreted Text To PCM Converter"; 275 | break; 276 | case WORK_CONVERT: 277 | workname = "Raw Text To PCM Converter"; 278 | break; 279 | } 280 | 281 | // create name identifier 282 | napi_value async_work_name; 283 | status = napi_create_string_utf8(env, workname, NAPI_AUTO_LENGTH, &async_work_name); 284 | en_assert(status == napi_ok); 285 | 286 | // create reference for the callback fucntion 287 | // because it otherwise will soon get GC'd 288 | napi_ref callback_ref; 289 | status = napi_create_reference(env, argv[2], 1, &callback_ref); 290 | en_assert(status == napi_ok); 291 | 292 | // create working data 293 | work_data* work = (work_data*) malloc(sizeof(*work)); 294 | work->input = buffer; 295 | work->input_size = node_buffer_size; 296 | work->javascript_callback_ref = callback_ref; 297 | work->worktype = worktype; 298 | work->output = NULL; 299 | work->error_message = NULL; 300 | work->convert_params = params; 301 | 302 | // create async work object 303 | status = napi_create_async_work( 304 | env, NULL, async_work_name, async_work_on_execute, async_work_on_complete, work, &work->self); 305 | en_assert(status == napi_ok); 306 | 307 | // queue the async work 308 | status = napi_queue_async_work(env, work->self); 309 | en_assert(status == napi_ok); 310 | 311 | return NULL; 312 | } 313 | 314 | // 315 | // JS Signature: 316 | // convert(inbytes: Buffer, options: object, done: function(err, pcm: Int16Array) -> none) -> none 317 | // 318 | static napi_value export_func_convert(napi_env env, napi_callback_info info) { 319 | return do_async_work(env, info, WORK_CONVERT); 320 | } 321 | 322 | // 323 | // JS Signature: 324 | // speech(inbytes: Buffer, options={}, done: function(err, pcm: Int16Array) -> none) -> none 325 | // 326 | static napi_value export_func_speech(napi_env env, napi_callback_info info) { 327 | return do_async_work(env, info, WORK_SPEECH); 328 | } 329 | 330 | // 331 | // JS Signature: 332 | // reinterpret(inbytes: Buffer, options={}, done: function(err, outbytes: Buffer) -> none) -> none 333 | // 334 | static napi_value export_func_reinterpret(napi_env env, napi_callback_info info) { 335 | return do_async_work(env, info, WORK_HIRAGANA); 336 | } 337 | 338 | // 339 | // JS Signature: init(baseDir: string, voice: string, volume: number) -> none 340 | // 341 | static napi_value export_func_init(napi_env env, napi_callback_info info) { 342 | if (module->ebyroid == NULL) { 343 | return NULL; 344 | } 345 | 346 | napi_status status; 347 | 348 | size_t argc = 3; 349 | napi_value argv[3]; 350 | status = napi_get_cb_info(env, info, &argc, argv, NULL, NULL); 351 | en_assert(status == napi_ok); 352 | 353 | napi_valuetype valuetype; 354 | status = napi_typeof(env, argv[0], &valuetype); 355 | en_assert(status == napi_ok && valuetype == napi_string); 356 | 357 | status = napi_typeof(env, argv[1], &valuetype); 358 | en_assert(status == napi_ok && valuetype == napi_string); 359 | 360 | status = napi_typeof(env, argv[2], &valuetype); 361 | en_assert(status == napi_ok && valuetype == napi_number); 362 | 363 | // fetch necessary buffer size 364 | size_t size; 365 | status = napi_get_value_string_utf8(env, argv[0], NULL, 0, &size); 366 | // NOTE: 'size' doesn't count the NULL character of the end, it seems 367 | en_assert(status == napi_ok); 368 | 369 | // allocate 370 | char* install_dir_buffer = (char*) malloc(size + 1); 371 | status = napi_get_value_string_utf8(env, argv[0], install_dir_buffer, size + 1, NULL); 372 | en_assert(status == napi_ok); 373 | 374 | // fetch necessary buffer size 375 | status = napi_get_value_string_utf8(env, argv[1], NULL, 0, &size); 376 | en_assert(status == napi_ok); 377 | 378 | // allocate 379 | char* voice_dir_buffer = (char*) malloc(size + 1); 380 | status = napi_get_value_string_utf8(env, argv[1], voice_dir_buffer, size + 1, NULL); 381 | en_assert(status == napi_ok); 382 | 383 | // fetch volume 384 | double volume; 385 | status = napi_get_value_double(env, argv[2], &volume); 386 | en_assert(status == napi_ok); 387 | 388 | // initialize ebyroid 389 | try { 390 | module->ebyroid = Ebyroid::Create(install_dir_buffer, voice_dir_buffer, (float) volume); 391 | } catch (std::exception& e) { 392 | const char* location = "(ebyroid::Ebyroid::Create)"; 393 | napi_fatal_error(location, strlen(location), e.what(), strlen(e.what())); 394 | } 395 | 396 | // finalize ebyroid in the cleanup hook 397 | status = napi_add_env_cleanup_hook(env, [](void* arg) { delete module->ebyroid; }, NULL); 398 | en_assert(status == napi_ok); 399 | 400 | free(install_dir_buffer); 401 | free(voice_dir_buffer); 402 | 403 | return NULL; 404 | } 405 | 406 | static napi_value module_main(napi_env env, napi_value exports) { 407 | napi_property_descriptor props[] = { 408 | {"speech", NULL, export_func_speech, NULL, NULL, NULL, napi_enumerable, NULL}, 409 | {"reinterpret", NULL, export_func_reinterpret, NULL, NULL, NULL, napi_enumerable, NULL}, 410 | {"convert", NULL, export_func_convert, NULL, NULL, NULL, napi_enumerable, NULL}, 411 | {"init", NULL, export_func_init, NULL, NULL, NULL, napi_enumerable, NULL}, 412 | }; 413 | 414 | napi_status status = napi_define_properties(env, exports, sizeof(props) / sizeof(*props), props); 415 | en_assert(status == napi_ok); 416 | 417 | module = (module_context*) malloc(sizeof(*module)); 418 | 419 | // clean heap in the cleanup hook 420 | status = napi_add_env_cleanup_hook(env, [](void* arg) { free(module); }, NULL); 421 | en_assert(status == napi_ok); 422 | 423 | return exports; 424 | } 425 | 426 | NAPI_MODULE(ebyroid, module_main) 427 | -------------------------------------------------------------------------------- /test/test_run.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const fs = require('fs'); 3 | const { WaveFile } = require('wavefile'); 4 | const Ebyroid = require('../lib/ebyroid'); 5 | const Voiceroid = require('../lib/voiceroid'); 6 | 7 | const a = new Voiceroid( 8 | 'kiri', 9 | 'C:\\Program Files (x86)\\AHS\\VOICEROID+\\KiritanEX', 10 | 'kiritan_22' 11 | ); 12 | const b = new Voiceroid( 13 | 'akarin', 14 | 'C:\\Program Files (x86)\\AHS\\VOICEROID2', 15 | 'akari_44' 16 | ); 17 | const ebyroid = new Ebyroid(a, b); 18 | ebyroid.use('kiri'); 19 | 20 | async function main() { 21 | await new Promise(r => { 22 | setTimeout(() => r(), 5000); 23 | }); 24 | 25 | new Array(100).fill(true).forEach((_, i) => { 26 | ebyroid 27 | .rawApiCallTextToKana('東京特許許可局許可局長') 28 | .then(x => console.error(`reinterpret ${i}: ${x}`)); 29 | }); 30 | 31 | new Array(100).fill(true).forEach((_, i) => { 32 | ebyroid 33 | .rawApiCallAiKanaToSpeech('アリガト') 34 | .then(x => console.log(`speech ${i}: ${x.data.length}`)); 35 | }); 36 | 37 | new Array(100).fill(true).forEach((_, i) => { 38 | ebyroid 39 | .convert('あああああああああああああああああああああ') 40 | .then(x => console.log(`convert ${i}: ${x.data.length}`)); 41 | }); 42 | 43 | const voices = ['kiri', 'akarin']; 44 | 45 | new Array(300).fill(true).forEach((_, i) => { 46 | const name = voices[(Math.random() * 2) | 0]; 47 | ebyroid 48 | .convertEx('どうか助けて下さい。', name) 49 | .then(x => console.log(`convertEx ${i}: ${x.data.length}`)); 50 | }); 51 | } 52 | 53 | main(); 54 | 55 | setTimeout(async () => { 56 | const waveObject = await ebyroid.convert( 57 | '私がシュリンプちゃんです。またの名を海老といいます。伊勢海老じゃないよ' 58 | ); 59 | const wav = new WaveFile(); 60 | wav.fromScratch(1, waveObject.sampleRate, '16', waveObject.data); 61 | const oho = (Math.random() * 100) | 0; 62 | fs.writeFileSync(`TEST${oho}.wav`, wav.toBuffer()); 63 | }, 10000); 64 | --------------------------------------------------------------------------------