diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2672c29 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - '**' + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version-file: "go.mod" + + - run: go install golang.org/x/tools/cmd/goimports@latest + - run: go install github.com/go-critic/go-critic/cmd/gocritic@latest + + - name: pre-commit + uses: pre-commit/action@v3.0.0 + + test: + needs: lint + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version-file: "go.mod" + + - run: go test ./... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dd62e37 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +model +voicevox_core diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..1380870 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,8 @@ +repos: + - repo: https://github.com/aethiopicuschan/pre-commit-golang + rev: c17f835cf9f04b8b5ed1c1f7757cedc6728d8a21 + hooks: + - id: go-fmt + - id: go-imports + - id: go-critic + - id: go-mod-tidy diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..64aeaac --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 aethiopicuschan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ad8627c --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +# nanoda + +[![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen?style=flat-square)](/LICENSE) +[![Go Reference](https://pkg.go.dev/badge/github.com/aethiopicuschan/nanoda.svg)](https://pkg.go.dev/github.com/aethiopicuschan/nanoda) +[![CI](https://github.com/aethiopicuschan/nanoda/actions/workflows/ci.yml/badge.svg)](https://github.com/aethiopicuschan/nanoda/actions/workflows/ci.yml) + +nanodaは[VOICEVOX CORE](https://github.com/VOICEVOX/voicevox_core)の動的ライブラリをGolangから叩くためのライブラリです。`cgo`ではなく[ebitengine/purego](https://github.com/ebitengine/purego/)を利用しているため、簡単に使用することが可能です。 + +## VOICEVOXについて + +サポートするVOICEVOX COREのバージョンは `0.15` としており、開発は `0.15.0-preview.13` を元にしています。 + +nanoda自体は[MITライセンス](/LICENSE)ですが、利用に際してはVOICEVOXやOpenJTalkの利用規約に則る必要があることに注意してください。 + +## 使い方 + +```sh +go get github.com/aethiopicuschan/nanoda@latest +``` + +もっとも簡単な例は以下のようになります。 + +```go +v, _ := nanoda.NewVoicevox("voicevox_core/libvoicevox_core.dylib", "voicevox_core/open_jtalk_dic_utf_8-1.11", "voicevox_core/model") +s, _ := v.NewSynthesizer() +s.LoadAllModels() +wav, _ := s.Tts("ずんだもんなのだ!", 3) +defer wav.Close() +f, _ := os.Create("output.wav") +defer f.Close() +io.Copy(f, wav) +``` + +その他 `examples` ディレクトリにサンプルコードを置いていますので、ご活用ください。 + +## 動作環境 + +``` +GOARCH='arm64' +GOOS='darwin' +``` + +でのみ確認しています。 + +## 開発方針 + +以下の理由からなるべくnanoda側で処理を受け持ったり抽象化したりして機能を提供することを目指しています。 + +- 使いやすさの向上 +- メモリまわりの安全性要件の確保 +- VOICEVOXとアプリケーション間の密結合を避け、APIの変更等に強くする + +## テスト + +TODOです。ありません。 + +## 対応状況 + +以下は内部的に利用している関数のリストであり、必ずしも一致する形で公開されているわけではありません。 + +- [x] voicevox_create_supported_devices_json +- [x] voicevox_error_result_to_message +- [x] voicevox_get_version +- [x] voicevox_json_free +- [ ] voicevox_make_default_initialize_options +- [ ] voicevox_make_default_synthesis_options +- [ ] voicevox_make_default_tts_options +- [x] voicevox_open_jtalk_rc_delete +- [x] voicevox_open_jtalk_rc_new +- [x] voicevox_open_jtalk_rc_use_user_dict +- [x] voicevox_synthesizer_create_accent_phrases +- [x] voicevox_synthesizer_create_accent_phrases_from_kana +- [x] voicevox_synthesizer_create_audio_query +- [x] voicevox_synthesizer_create_audio_query_from_kana +- [x] voicevox_synthesizer_create_metas_json +- [x] voicevox_synthesizer_delete +- [x] voicevox_synthesizer_is_gpu_mode +- [x] voicevox_synthesizer_is_loaded_voice_model +- [x] voicevox_synthesizer_load_voice_model +- [x] voicevox_synthesizer_new_with_initialize +- [x] voicevox_synthesizer_replace_mora_data +- [x] voicevox_synthesizer_replace_mora_pitch +- [x] voicevox_synthesizer_replace_phoneme_length +- [x] voicevox_synthesizer_synthesis +- [x] voicevox_synthesizer_tts +- [x] voicevox_synthesizer_tts_from_kana +- [x] voicevox_synthesizer_unload_voice_model +- [x] voicevox_user_dict_add_word +- [x] voicevox_user_dict_delete +- [x] voicevox_user_dict_import +- [x] voicevox_user_dict_load +- [x] voicevox_user_dict_new +- [x] voicevox_user_dict_remove_word +- [x] voicevox_user_dict_save +- [x] voicevox_user_dict_to_json +- [x] voicevox_user_dict_update_word +- [ ] voicevox_user_dict_word_make +- [x] voicevox_voice_model_delete +- [x] voicevox_voice_model_get_metas_json +- [x] voicevox_voice_model_id +- [x] voicevox_voice_model_new_from_path +- [x] voicevox_wav_free diff --git a/accent_phrases.go b/accent_phrases.go new file mode 100644 index 0000000..4819fba --- /dev/null +++ b/accent_phrases.go @@ -0,0 +1,55 @@ +package nanoda + +import ( + "encoding/json" + "unsafe" + + "github.com/aethiopicuschan/nanoda/internal/strings" +) + +// モーラ(子音+母音) +type Mora struct { + Text string `json:"text"` + Consonant *string `json:"consonant"` + ConsonantLength *float64 `json:"consonant_length"` + Vowel string `json:"vowel"` + VowelLength float64 `json:"vowel_length"` + Pitch float64 `json:"pitch"` +} + +// アクセント句 +type AccentPhrase struct { + Moras []Mora `json:"moras"` + Accent int `json:"accent"` + PauseMora *Mora `json:"pause_mora"` + IsInterrogative bool `json:"is_interrogative"` +} + +// アクセント句の配列を生成する +func (s *Synthesizer) createAccentPhrases(text string, styleID StyleId, enableKana bool) (a []AccentPhrase, err error) { + var ptr *byte + var code ResultCode + if enableKana { + code = s.v.voicevoxSynthesizerCreateAccentPhrasesFromKana(s.synthesizer, text, styleID, uintptr(unsafe.Pointer(&ptr))) + } else { + code = s.v.voicevoxSynthesizerCreateAccentPhrases(s.synthesizer, text, styleID, uintptr(unsafe.Pointer(&ptr))) + } + if code != VOICEVOX_RESULT_OK { + err = s.v.newError(code) + return + } + defer s.v.voicevoxJsonFree(uintptr(unsafe.Pointer(ptr))) + j := strings.GoString(ptr) + err = json.Unmarshal([]byte(j), &a) + return +} + +// アクセント句の配列を生成する +func (s *Synthesizer) CreateAccentPhrases(text string, styleID StyleId) (a []AccentPhrase, err error) { + return s.createAccentPhrases(text, styleID, false) +} + +// アクセント句の配列を生成する(AquesTalk風記法) +func (s *Synthesizer) CreateAccentPhrasesFromKana(text string, styleID StyleId) (a []AccentPhrase, err error) { + return s.createAccentPhrases(text, styleID, true) +} diff --git a/audio_query.go b/audio_query.go new file mode 100644 index 0000000..555b483 --- /dev/null +++ b/audio_query.go @@ -0,0 +1,51 @@ +package nanoda + +import ( + "encoding/json" + "unsafe" + + "github.com/aethiopicuschan/nanoda/internal/strings" +) + +// 音声合成用のクエリ +type AudioQuery struct { + AccentPhrases []AccentPhrase `json:"accent_phrases"` + SpeedScale float64 `json:"speed_scale"` + PitchScale float64 `json:"pitch_scale"` + IntonationScale float64 `json:"intonation_scale"` + VolumeScale float64 `json:"volume_scale"` + PrePhonemeLength float64 `json:"pre_phoneme_length"` + PostPhonemeLength float64 `json:"post_phoneme_length"` + OutputSamplingRate int `json:"output_sampling_rate"` + OutputStereo bool `json:"output_stereo"` + Kana string `json:"kana"` +} + +// 音声合成用のクエリを作成する +func (s *Synthesizer) createAudioQuery(text string, styleID StyleId, enableKana bool) (a AudioQuery, err error) { + var ptr *byte + var code ResultCode + if enableKana { + code = s.v.voicevoxSynthesizerCreateAudioQueryFromKana(s.synthesizer, text, styleID, uintptr(unsafe.Pointer(&ptr))) + } else { + code = s.v.voicevoxSynthesizerCreateAudioQuery(s.synthesizer, text, styleID, uintptr(unsafe.Pointer(&ptr))) + } + if code != VOICEVOX_RESULT_OK { + err = s.v.newError(code) + return + } + defer s.v.voicevoxJsonFree(uintptr(unsafe.Pointer(ptr))) + j := strings.GoString(ptr) + err = json.Unmarshal([]byte(j), &a) + return +} + +// 音声合成用のクエリを生成する +func (s *Synthesizer) CreateAudioQuery(text string, styleID StyleId) (a AudioQuery, err error) { + return s.createAudioQuery(text, styleID, false) +} + +// 音声合成用のクエリを生成する(AquesTalk風記法) +func (s *Synthesizer) CreateAudioQueryFromKana(text string, styleID StyleId) (a AudioQuery, err error) { + return s.createAudioQuery(text, styleID, true) +} diff --git a/error.go b/error.go new file mode 100644 index 0000000..2e78067 --- /dev/null +++ b/error.go @@ -0,0 +1,19 @@ +package nanoda + +import "fmt" + +type Error struct { + Code ResultCode + Msg string +} + +func (e Error) Error() string { + return fmt.Sprintf("%d: %s", e.Code, e.Msg) +} + +func (v *Voicevox) newError(code ResultCode) error { + return Error{ + Code: code, + Msg: v.voicevoxErrorResultToMessage(code), + } +} diff --git a/examples/audioquery/main.go b/examples/audioquery/main.go new file mode 100644 index 0000000..9d1f9d8 --- /dev/null +++ b/examples/audioquery/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "io" + "os" + + "github.com/aethiopicuschan/nanoda" +) + +func main() { + v, _ := nanoda.NewVoicevox("voicevox_core/libvoicevox_core.dylib", "voicevox_core/open_jtalk_dic_utf_8-1.11", "voicevox_core/model") + s, _ := v.NewSynthesizer() + s.LoadModelsFromStyleId(3) + + aq, _ := s.CreateAudioQuery("2倍速ずんだもんなのだ!", 3) + aq.SpeedScale = 2.0 + + wav, _ := s.Synthesis(aq, 3) + defer wav.Close() + f, _ := os.Create("output.wav") + defer f.Close() + io.Copy(f, wav) +} diff --git a/examples/tts/main.go b/examples/tts/main.go new file mode 100644 index 0000000..8f16f2b --- /dev/null +++ b/examples/tts/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "io" + "os" + + "github.com/aethiopicuschan/nanoda" +) + +func main() { + v, _ := nanoda.NewVoicevox("voicevox_core/libvoicevox_core.dylib", "voicevox_core/open_jtalk_dic_utf_8-1.11", "voicevox_core/model") + s, _ := v.NewSynthesizer() + s.LoadModelsFromStyleId(3) + wav, _ := s.Tts("ずんだもんなのだ!", 3) + defer wav.Close() + f, _ := os.Create("output.wav") + defer f.Close() + io.Copy(f, wav) +} diff --git a/examples/userdict/main.go b/examples/userdict/main.go new file mode 100644 index 0000000..78fefe1 --- /dev/null +++ b/examples/userdict/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "io" + "os" + + "github.com/aethiopicuschan/nanoda" +) + +func main() { + v, _ := nanoda.NewVoicevox("voicevox_core/libvoicevox_core.dylib", "voicevox_core/open_jtalk_dic_utf_8-1.11", "voicevox_core/model") + + ud := v.NewUserDict() + w := nanoda.NewWord("開始めいッ", "ハジメイッ") + ud.AddWord(w) + ud.Use() + + s, _ := v.NewSynthesizer() + s.LoadModelsFromStyleId(3) + + wav, _ := s.Tts("開始めいッ!", 3) + defer wav.Close() + f, _ := os.Create("output.wav") + defer f.Close() + io.Copy(f, wav) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1177a30 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module github.com/aethiopicuschan/nanoda + +go 1.21.3 + +require ( + github.com/ebitengine/purego v0.5.0 + github.com/google/uuid v1.3.1 +) + +require golang.org/x/sys v0.13.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3c2278b --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/ebitengine/purego v0.5.0 h1:JrMGKfRIAM4/QVKaesIIT7m/UVjTj5GYhRSQYwfVdpo= +github.com/ebitengine/purego v0.5.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/strings/strings.go b/internal/strings/strings.go new file mode 100644 index 0000000..763f442 --- /dev/null +++ b/internal/strings/strings.go @@ -0,0 +1,28 @@ +package strings + +import ( + "unsafe" +) + +// 終端文字を付けてポインタにして返す +func CString(s string) uintptr { + bytes := []byte(s) + if len(bytes) == 0 || bytes[len(bytes)-1] != 0 { + bytes = append(bytes, 0) + } + return *(*uintptr)(unsafe.Pointer(&bytes)) +} + +// 先頭のアドレスから0が出るまでの文字列を取得する +func GoString(ptr *byte) string { + length := 0 + for *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(ptr)) + uintptr(length))) != 0 { + length++ + } + return unsafe.String((*byte)(unsafe.Pointer(ptr)), length) +} + +// 先頭のアドレスから0が出るまでの文字列を取得する +func GoStringFromUintptr(ptr uintptr) string { + return GoString((*byte)(unsafe.Pointer(ptr))) +} diff --git a/meta.go b/meta.go new file mode 100644 index 0000000..74acb21 --- /dev/null +++ b/meta.go @@ -0,0 +1,66 @@ +package nanoda + +import "sort" + +// スタイルのID +type StyleId uint32 + +// 話者のID +type SpeakerId string + +// スタイル情報 +type Style struct { + Name string `json:"name"` + Id StyleId `json:"id"` +} + +// 内部用のメタ情報 +type meta struct { + Name string `json:"name"` + Styles []Style `json:"styles"` + Version string `json:"version"` + SpeakerUuid SpeakerId `json:"speaker_uuid"` +} + +// キャラクター単位のメタ情報 +type Meta struct { + Name string + Styles []Style + SpeakerId SpeakerId +} + +func (v *Voicevox) getMetas() (m []meta) { + for _, vvm := range v.vvms { + m = append(m, vvm.metas...) + } + return +} + +// VVMごとのメタ情報をキャラクター単位にマージする +func (v *Voicevox) sortMetas(ms []meta) []Meta { + m := map[SpeakerId][]meta{} + for _, meta := range ms { + m[meta.SpeakerUuid] = append(m[meta.SpeakerUuid], meta) + } + metas := []Meta{} + for _, v := range m { + styles := []Style{} + for _, meta := range v { + styles = append(styles, meta.Styles...) + } + // スタイル情報をID順にソートする + sort.Slice(styles, func(i, j int) bool { + return styles[i].Id < styles[j].Id + }) + metas = append(metas, Meta{ + Name: v[0].Name, + Styles: styles, + SpeakerId: v[0].SpeakerUuid, + }) + } + // メタ情報をスタイルのID順にソートする + sort.Slice(metas, func(i, j int) bool { + return metas[i].Styles[0].Id < metas[j].Styles[0].Id + }) + return metas +} diff --git a/register.go b/register.go new file mode 100644 index 0000000..273cae6 --- /dev/null +++ b/register.go @@ -0,0 +1,47 @@ +package nanoda + +import ( + "github.com/ebitengine/purego" +) + +// 関数の紐付けを行う +func (v *Voicevox) register() { + purego.RegisterLibFunc(&v.voicevoxCreateSupportedDevicesJson, v.core, "voicevox_create_supported_devices_json") + purego.RegisterLibFunc(&v.voicevoxErrorResultToMessage, v.core, "voicevox_error_result_to_message") + purego.RegisterLibFunc(&v.voicevoxGetVersion, v.core, "voicevox_get_version") + purego.RegisterLibFunc(&v.voicevoxJsonFree, v.core, "voicevox_json_free") + purego.RegisterLibFunc(&v.voicevoxSynthesizerCreateAccentPhrases, v.core, "voicevox_synthesizer_create_accent_phrases") + purego.RegisterLibFunc(&v.voicevoxSynthesizerCreateAccentPhrasesFromKana, v.core, "voicevox_synthesizer_create_accent_phrases_from_kana") + purego.RegisterLibFunc(&v.voicevoxSynthesizerCreateAudioQuery, v.core, "voicevox_synthesizer_create_audio_query") + purego.RegisterLibFunc(&v.voicevoxSynthesizerCreateAudioQueryFromKana, v.core, "voicevox_synthesizer_create_audio_query_from_kana") + purego.RegisterLibFunc(&v.voicevoxSynthesizerCreateMetasJson, v.core, "voicevox_synthesizer_create_metas_json") + purego.RegisterLibFunc(&v.voicevoxOpenJtalkRcDelete, v.core, "voicevox_open_jtalk_rc_delete") + purego.RegisterLibFunc(&v.voicevoxOpenJtalkRcNew, v.core, "voicevox_open_jtalk_rc_new") + purego.RegisterLibFunc(&v.voicevoxOpenJtalkRcUseUserDict, v.core, "voicevox_open_jtalk_rc_use_user_dict") + purego.RegisterLibFunc(&v.voicevoxSynthesizerDelete, v.core, "voicevox_synthesizer_delete") + purego.RegisterLibFunc(&v.voicevoxSynthesizerIsGpuMode, v.core, "voicevox_synthesizer_is_gpu_mode") + purego.RegisterLibFunc(&v.voicevoxSynthesizerIsLoadedVoiceModel, v.core, "voicevox_synthesizer_is_loaded_voice_model") + purego.RegisterLibFunc(&v.voicevoxSynthesizerLoadVoiceModel, v.core, "voicevox_synthesizer_load_voice_model") + purego.RegisterLibFunc(&v.voicevoxSynthesizerNewWithInitialize, v.core, "voicevox_synthesizer_new_with_initialize") + purego.RegisterLibFunc(&v.voicevoxSynthesizerReplaceMoraData, v.core, "voicevox_synthesizer_replace_mora_data") + purego.RegisterLibFunc(&v.voicevoxSynthesizerReplaceMoraPitch, v.core, "voicevox_synthesizer_replace_mora_pitch") + purego.RegisterLibFunc(&v.voicevoxSynthesizerReplacePhonemeLength, v.core, "voicevox_synthesizer_replace_phoneme_length") + purego.RegisterLibFunc(&v.voicevoxSynthesizerSynthesis, v.core, "voicevox_synthesizer_synthesis") + purego.RegisterLibFunc(&v.voicevoxSynthesizerTts, v.core, "voicevox_synthesizer_tts") + purego.RegisterLibFunc(&v.voicevoxSynthesizerTtsFromKana, v.core, "voicevox_synthesizer_tts_from_kana") + purego.RegisterLibFunc(&v.voicevoxSynthesizerUnloadVoiceModel, v.core, "voicevox_synthesizer_unload_voice_model") + purego.RegisterLibFunc(&v.voicevoxUserDictAddWord, v.core, "voicevox_user_dict_add_word") + purego.RegisterLibFunc(&v.voicevoxUserDictDelete, v.core, "voicevox_user_dict_delete") + purego.RegisterLibFunc(&v.voicevoxUserDictImport, v.core, "voicevox_user_dict_import") + purego.RegisterLibFunc(&v.voicevoxUserDictLoad, v.core, "voicevox_user_dict_load") + purego.RegisterLibFunc(&v.voicevoxUserDictNew, v.core, "voicevox_user_dict_new") + purego.RegisterLibFunc(&v.voicevoxUserDictRemoveWord, v.core, "voicevox_user_dict_remove_word") + purego.RegisterLibFunc(&v.voicevoxUserDictSave, v.core, "voicevox_user_dict_save") + purego.RegisterLibFunc(&v.voicevoxUserDictToJson, v.core, "voicevox_user_dict_to_json") + purego.RegisterLibFunc(&v.voicevoxUserDictUpdateWord, v.core, "voicevox_user_dict_update_word") + purego.RegisterLibFunc(&v.voicevoxVoiceModelDelete, v.core, "voicevox_voice_model_delete") + purego.RegisterLibFunc(&v.voicevoxVoiceModelGetMetasJson, v.core, "voicevox_voice_model_get_metas_json") + purego.RegisterLibFunc(&v.voicevoxVoiceModelId, v.core, "voicevox_voice_model_id") + purego.RegisterLibFunc(&v.voicevoxVoiceModelNewFromPath, v.core, "voicevox_voice_model_new_from_path") + purego.RegisterLibFunc(&v.voicevoxWavFree, v.core, "voicevox_wav_free") +} diff --git a/replace.go b/replace.go new file mode 100644 index 0000000..c531ecf --- /dev/null +++ b/replace.go @@ -0,0 +1,78 @@ +package nanoda + +import ( + "encoding/json" + "fmt" + "unsafe" + + "github.com/aethiopicuschan/nanoda/internal/strings" +) + +// アクセント句の再生成時に指定するオプション +type replaceOptions struct { + replaceMoraPitch bool + replacePhonemeLength bool +} + +// 音高を生成しなおす +func withReplaceMoraPitch() func(*replaceOptions) { + return func(o *replaceOptions) { + o.replaceMoraPitch = true + } +} + +// 音素長を生成しなおす +func withReplacePhonemeLength() func(*replaceOptions) { + return func(o *replaceOptions) { + o.replacePhonemeLength = true + } +} + +// アクセント句の配列を指定されたスタイルで再生成する +func (s *Synthesizer) replace(ap []AccentPhrase, styleID StyleId, options ...func(*replaceOptions)) (a []AccentPhrase, err error) { + opt := replaceOptions{} + for _, o := range options { + o(&opt) + } + var ptr *byte + var code ResultCode + jB, err := json.Marshal(ap) + if err != nil { + return + } + j := string(jB) + switch { + case opt.replaceMoraPitch && opt.replacePhonemeLength: + code = s.v.voicevoxSynthesizerReplaceMoraData(s.synthesizer, j, styleID, uintptr(unsafe.Pointer(&ptr))) + case opt.replaceMoraPitch: + code = s.v.voicevoxSynthesizerReplaceMoraPitch(s.synthesizer, j, styleID, uintptr(unsafe.Pointer(&ptr))) + case opt.replacePhonemeLength: + code = s.v.voicevoxSynthesizerReplacePhonemeLength(s.synthesizer, j, styleID, uintptr(unsafe.Pointer(&ptr))) + default: + err = fmt.Errorf("options are required") + return + } + if code != VOICEVOX_RESULT_OK { + err = s.v.newError(code) + return + } + defer s.v.voicevoxJsonFree(uintptr(unsafe.Pointer(ptr))) + j = strings.GoString(ptr) + err = json.Unmarshal([]byte(j), &a) + return +} + +// アクセント句の配列を指定されたスタイルで再生成する +func (s *Synthesizer) Replace(ap []AccentPhrase, styleID StyleId) (a []AccentPhrase, err error) { + return s.replace(ap, styleID, withReplaceMoraPitch(), withReplacePhonemeLength()) +} + +// アクセント句の配列を指定されたスタイルで再生成する(音高のみ) +func (s *Synthesizer) ReplaceOnlyMoraPitch(ap []AccentPhrase, styleID StyleId) (a []AccentPhrase, err error) { + return s.replace(ap, styleID, withReplaceMoraPitch()) +} + +// アクセント句の配列を指定されたスタイルで再生成する(音素長のみ) +func (s *Synthesizer) ReplaceOnlyPhonemeLength(ap []AccentPhrase, styleID StyleId) (a []AccentPhrase, err error) { + return s.replace(ap, styleID, withReplacePhonemeLength()) +} diff --git a/result_code.go b/result_code.go new file mode 100644 index 0000000..3d572a8 --- /dev/null +++ b/result_code.go @@ -0,0 +1,30 @@ +package nanoda + +// 処理結果を示す結果コード +type ResultCode int32 + +const ( + VOICEVOX_RESULT_OK ResultCode = 0 // 成功 + VOICEVOX_RESULT_NOT_LOADED_OPENJTALK_DICT_ERROR ResultCode = 1 // open_jtalk辞書ファイルが読み込まれていない + VOICEVOX_RESULT_GET_SUPPORTED_DEVICES_ERROR ResultCode = 3 // サポートされているデバイス情報取得に失敗した + VOICEVOX_RESULT_GPU_SUPPORT_ERROR ResultCode = 4 // GPUモードがサポートされていない + VOICEVOX_RESULT_STYLE_NOT_FOUND_ERROR ResultCode = 6 // スタイルIDに対するスタイルが見つからなかった + VOICEVOX_RESULT_MODEL_NOT_FOUND_ERROR ResultCode = 7 // 音声モデルIDに対する音声モデルが見つからなかった + VOICEVOX_RESULT_INFERENCE_ERROR ResultCode = 8 // 推論に失敗した + VOICEVOX_RESULT_EXTRACT_FULL_CONTEXT_LABEL_ERROR ResultCode = 11 // コンテキストラベル出力に失敗した + VOICEVOX_RESULT_INVALID_UTF8_INPUT_ERROR ResultCode = 12 // 無効なutf8文字列が入力された + VOICEVOX_RESULT_PARSE_KANA_ERROR ResultCode = 13 // AquesTalk風記法のテキストの解析に失敗した + VOICEVOX_RESULT_INVALID_AUDIO_QUERY_ERROR ResultCode = 14 // 無効なAudioQuery + VOICEVOX_RESULT_INVALID_ACCENT_PHRASE_ERROR ResultCode = 15 // 無効なAccentPhrase + VOICEVOX_RESULT_OPEN_ZIP_FILE_ERROR ResultCode = 16 // ZIPファイルを開くことに失敗した + VOICEVOX_RESULT_READ_ZIP_ENTRY_ERROR ResultCode = 17 // ZIP内のファイルが読めなかった + VOICEVOX_RESULT_MODEL_ALREADY_LOADED_ERROR ResultCode = 18 // すでに読み込まれている音声モデルを読み込もうとした + VOICEVOX_RESULT_STYLE_ALREADY_LOADED_ERROR ResultCode = 26 // すでに読み込まれているスタイルを読み込もうとした + VOICEVOX_RESULT_INVALID_MODEL_DATA_ERROR ResultCode = 27 // 無効なモデルデータ + VOICEVOX_RESULT_LOAD_USER_DICT_ERROR ResultCode = 20 // ユーザー辞書を読み込めなかった + VOICEVOX_RESULT_SAVE_USER_DICT_ERROR ResultCode = 21 // ユーザー辞書を書き込めなかった + VOICEVOX_RESULT_USER_DICT_WORD_NOT_FOUND_ERROR ResultCode = 22 // ユーザー辞書に単語が見つからなかった + VOICEVOX_RESULT_USE_USER_DICT_ERROR ResultCode = 23 // OpenJTalkのユーザー辞書の設定に失敗した + VOICEVOX_RESULT_INVALID_USER_DICT_WORD_ERROR ResultCode = 24 // ユーザー辞書の単語のバリデーションに失敗した + VOICEVOX_RESULT_INVALID_UUID_ERROR ResultCode = 25 // UUIDの変換に失敗した +) diff --git a/supported_devices.go b/supported_devices.go new file mode 100644 index 0000000..3340599 --- /dev/null +++ b/supported_devices.go @@ -0,0 +1,11 @@ +package nanoda + +/* +利用可能なデバイスの情報。 +あくまでVOICEVOX COREライブラリが対応しているかどうかであることに注意すること。 +*/ +type SupportedDevices struct { + Cpu bool `json:"cpu"` + Cuda bool `json:"cuda"` + Dml bool `json:"dml"` +} diff --git a/synthesizer.go b/synthesizer.go new file mode 100644 index 0000000..f3f2e91 --- /dev/null +++ b/synthesizer.go @@ -0,0 +1,181 @@ +package nanoda + +import ( + "encoding/json" + "io" + "unsafe" + + "github.com/aethiopicuschan/nanoda/internal/strings" +) + +// ハードウェアアクセラレーションモードを設定する設定値 +type AccelerationMode int32 + +const ( + ACCELERATION_MODE_AUTO AccelerationMode = iota // 実行環境に合った適切なハードウェアアクセラレーションモードを選択する + ACCELERATION_MODE_CPU // ハードウェアアクセラレーションモードを"CPU"に設定する + ACCELERATION_MODE_GPU // ハードウェアアクセラレーションモードを"GPU"に設定する +) + +// シンセナイザの作成時に指定するオプション +type SynthesizerOption struct { + accelerationMode AccelerationMode + cpuNumThreads uint16 +} + +// ハードウェアアクセラレーションモードを設定する +func WithAccelerationMode(mode AccelerationMode) func(*SynthesizerOption) { + return func(o *SynthesizerOption) { + o.accelerationMode = mode + } +} + +// CPU利用数を設定する 0の場合は環境に合わせてCPUが利用される +func WithCpuNumThreads(num uint16) func(*SynthesizerOption) { + return func(o *SynthesizerOption) { + o.cpuNumThreads = num + } +} + +// 音声シンセナイザ +// Voicevox.NewSynthesizerで作成する +type Synthesizer struct { + v *Voicevox + synthesizer uintptr +} + +// シンセナイザを作成する +func (v *Voicevox) NewSynthesizer(options ...func(*SynthesizerOption)) (s Synthesizer, err error) { + s.v = v + opt := SynthesizerOption{ + accelerationMode: ACCELERATION_MODE_AUTO, + cpuNumThreads: 0, + } + for _, o := range options { + o(&opt) + } + code := v.voicevoxSynthesizerNewWithInitialize(v.openJtalkRc, *(*uintptr)(unsafe.Pointer(&opt)), uintptr(unsafe.Pointer(&s.synthesizer))) + if code != VOICEVOX_RESULT_OK { + err = v.newError(code) + } + return +} + +// このSynthesizerを閉じる +func (s *Synthesizer) Close() { + s.v.voicevoxSynthesizerDelete(s.synthesizer) +} + +// 現在読み込んでいる音声モデルのメタ情報を取得する +func (s *Synthesizer) GetMetas() (metas []Meta, err error) { + ptr := s.v.voicevoxSynthesizerCreateMetasJson(s.synthesizer) + defer s.v.voicevoxJsonFree(ptr) + j := strings.GoStringFromUintptr(ptr) + ms := []meta{} + if err = json.Unmarshal([]byte(j), &ms); err != nil { + return + } + metas = s.v.sortMetas(ms) + return +} + +// GPUモードかどうかを判定する +func (s *Synthesizer) IsGpuMode() bool { + return s.v.voicevoxSynthesizerIsGpuMode(s.synthesizer) +} + +// Ttsを用いた音声合成時に指定するオプション +type TtsOptions struct { + // 疑問文の調整を有効にするかどうか + enableInterrogativeUpspeak bool + // AquesTalk風記法を有効にするかどうか + enableKana bool +} + +// 疑問文の調整を有効にする +func WithEnableInterrogativeUpspeak() func(*TtsOptions) { + return func(o *TtsOptions) { + o.enableInterrogativeUpspeak = true + } +} + +// AquesTalk風記法を有効にする +func WithEnableKana() func(*TtsOptions) { + return func(o *TtsOptions) { + o.enableKana = true + } +} + +// 音声合成を行う +func (s *Synthesizer) Tts(text string, styleID StyleId, options ...func(*TtsOptions)) (io.ReadCloser, error) { + opt := TtsOptions{ + enableInterrogativeUpspeak: false, + } + for _, o := range options { + o(&opt) + } + opt2 := struct { + enableInterrogativeUpspeak bool + }{ + opt.enableInterrogativeUpspeak, + } + + var outputBinarySize uint + var outputWav *uint8 + + var code ResultCode + if opt.enableKana { + code = s.v.voicevoxSynthesizerTtsFromKana(s.synthesizer, text, styleID, *(*uintptr)(unsafe.Pointer(&opt2)), uintptr(unsafe.Pointer(&outputBinarySize)), uintptr(unsafe.Pointer(&outputWav))) + } else { + code = s.v.voicevoxSynthesizerTts(s.synthesizer, text, styleID, *(*uintptr)(unsafe.Pointer(&opt2)), uintptr(unsafe.Pointer(&outputBinarySize)), uintptr(unsafe.Pointer(&outputWav))) + } + if code != VOICEVOX_RESULT_OK { + return nil, s.v.newError(code) + } + raw := unsafe.Slice(outputWav, outputBinarySize) + wav := newWav(raw, func() error { + s.v.voicevoxWavFree(uintptr(unsafe.Pointer(outputWav))) + return nil + }) + return wav, nil +} + +// AudioQueryから音声合成を行う +func (s *Synthesizer) synthesis(aq AudioQuery, styleId StyleId, enableInterrogativeUpspeak bool) (io.ReadCloser, error) { + opt := struct { + enableInterrogativeUpspeak bool + }{ + enableInterrogativeUpspeak, + } + + jB, err := json.Marshal(aq) + if err != nil { + return nil, err + } + j := string(jB) + + var outputBinarySize uint + var outputWav *uint8 + + code := s.v.voicevoxSynthesizerSynthesis(s.synthesizer, j, styleId, *(*uintptr)(unsafe.Pointer(&opt)), uintptr(unsafe.Pointer(&outputBinarySize)), uintptr(unsafe.Pointer(&outputWav))) + + if code != VOICEVOX_RESULT_OK { + return nil, s.v.newError(code) + } + raw := unsafe.Slice(outputWav, outputBinarySize) + wav := newWav(raw, func() error { + s.v.voicevoxWavFree(uintptr(unsafe.Pointer(outputWav))) + return nil + }) + return wav, nil +} + +// AudioQueryから音声合成を行う +func (s *Synthesizer) Synthesis(aq AudioQuery, styleId StyleId) (io.ReadCloser, error) { + return s.synthesis(aq, styleId, true) +} + +// AudioQueryから音声合成を行う(疑問文の調整なし) +func (s *Synthesizer) SynthesisWithoutInterrogativeUpspeak(aq AudioQuery, styleId StyleId) (io.ReadCloser, error) { + return s.synthesis(aq, styleId, false) +} diff --git a/user_dict.go b/user_dict.go new file mode 100644 index 0000000..6bbea5e --- /dev/null +++ b/user_dict.go @@ -0,0 +1,131 @@ +package nanoda + +import ( + "unsafe" + + "github.com/aethiopicuschan/nanoda/internal/strings" + "github.com/google/uuid" +) + +// ユーザ辞書 +// voicevox.NewUserDictで作成する +type UserDict struct { + v *Voicevox + userDict uintptr +} + +// ユーザ辞書を作成する +func (v *Voicevox) NewUserDict() (ud *UserDict) { + ud = &UserDict{ + v: v, + userDict: v.voicevoxUserDictNew(), + } + return +} + +// 単語を追加する +func (ud *UserDict) AddWord(word Word) (id string, err error) { + iw := word.toinner() + idPtr := make([]byte, 16) + code := ud.v.voicevoxUserDictAddWord(ud.userDict, uintptr(unsafe.Pointer(&iw)), *(*uintptr)(unsafe.Pointer(&idPtr))) + if code != VOICEVOX_RESULT_OK { + err = ud.v.newError(code) + return + } + _uuid, err := uuid.FromBytes(idPtr) + if err != nil { + return + } + id = _uuid.String() + return +} + +// 単語を更新する +func (ud *UserDict) UpdateWord(id string, word Word) (err error) { + iw := word.toinner() + _uuid, err := uuid.Parse(id) + if err != nil { + return + } + ptr, err := _uuid.MarshalBinary() + if err != nil { + return + } + code := ud.v.voicevoxUserDictUpdateWord(ud.userDict, *(*uintptr)(unsafe.Pointer(&ptr)), uintptr(unsafe.Pointer(&iw))) + if code != VOICEVOX_RESULT_OK { + err = ud.v.newError(code) + } + return +} + +// 単語を削除する +func (ud *UserDict) RemoveWord(id string) (err error) { + _uuid, err := uuid.Parse(id) + if err != nil { + return + } + ptr, err := _uuid.MarshalBinary() + if err != nil { + return + } + code := ud.v.voicevoxUserDictRemoveWord(ud.userDict, *(*uintptr)(unsafe.Pointer(&ptr))) + if code != VOICEVOX_RESULT_OK { + err = ud.v.newError(code) + } + return +} + +// ユーザ辞書を閉じる +func (ud *UserDict) Close() { + ud.v.voicevoxUserDictDelete(ud.userDict) +} + +// 他のユーザ辞書をインポートする +func (ud *UserDict) Import(other *UserDict) (err error) { + code := ud.v.voicevoxUserDictImport(ud.userDict, other.userDict) + if code != VOICEVOX_RESULT_OK { + err = ud.v.newError(code) + } + return +} + +// 指定されたパスからユーザ辞書を読み込む +func (ud *UserDict) Load(path string) (err error) { + code := ud.v.voicevoxUserDictLoad(ud.userDict, path) + if code != VOICEVOX_RESULT_OK { + err = ud.v.newError(code) + } + return +} + +// ユーザ辞書を指定されたパスに保存する +// 形式はJSONで、文字列として得たい場合はToJsonを使うこと +func (ud *UserDict) Save(path string) (err error) { + code := ud.v.voicevoxUserDictSave(ud.userDict, path) + if code != VOICEVOX_RESULT_OK { + err = ud.v.newError(code) + } + return +} + +// ユーザ辞書を使用する +func (ud *UserDict) Use() (err error) { + code := ud.v.voicevoxOpenJtalkRcUseUserDict(ud.v.openJtalkRc, *(*uintptr)(unsafe.Pointer(&ud.userDict))) + if code != VOICEVOX_RESULT_OK { + err = ud.v.newError(code) + } + return +} + +// ユーザ辞書をJSONとして出力する +func (ud *UserDict) ToJson() (j string, err error) { + var ptr *byte + code := ud.v.voicevoxUserDictToJson(ud.userDict, uintptr(unsafe.Pointer(&ptr))) + if code != VOICEVOX_RESULT_OK { + err = ud.v.newError(code) + return + } + defer ud.v.voicevoxJsonFree(uintptr(unsafe.Pointer(ptr))) + j = strings.GoString(ptr) + return +} diff --git a/voicevox.go b/voicevox.go new file mode 100644 index 0000000..f0e82a4 --- /dev/null +++ b/voicevox.go @@ -0,0 +1,128 @@ +package nanoda + +import ( + "encoding/json" + "fmt" + "unsafe" + + "github.com/aethiopicuschan/nanoda/internal/strings" + "github.com/ebitengine/purego" +) + +// 各種関数やポインタなどを保持する構造体 +type Voicevox struct { + core uintptr + openJtalkRc uintptr + vvms []voicevoxVoiceModel + // 生(?)の関数群 + voicevoxCreateSupportedDevicesJson func(uintptr) ResultCode + voicevoxErrorResultToMessage func(ResultCode) string + voicevoxGetVersion func() string + voicevoxJsonFree func(uintptr) + voicevoxSynthesizerCreateAccentPhrases func(uintptr, string, StyleId, uintptr) ResultCode + voicevoxSynthesizerCreateAccentPhrasesFromKana func(uintptr, string, StyleId, uintptr) ResultCode + voicevoxSynthesizerCreateAudioQuery func(uintptr, string, StyleId, uintptr) ResultCode + voicevoxSynthesizerCreateAudioQueryFromKana func(uintptr, string, StyleId, uintptr) ResultCode + voicevoxSynthesizerCreateMetasJson func(uintptr) uintptr + voicevoxOpenJtalkRcDelete func(uintptr) + voicevoxOpenJtalkRcNew func(string, uintptr) ResultCode + voicevoxOpenJtalkRcUseUserDict func(uintptr, uintptr) ResultCode + voicevoxSynthesizerDelete func(uintptr) + voicevoxSynthesizerIsGpuMode func(uintptr) bool + voicevoxSynthesizerIsLoadedVoiceModel func(uintptr, string) bool + voicevoxSynthesizerLoadVoiceModel func(uintptr, uintptr) ResultCode + voicevoxSynthesizerNewWithInitialize func(uintptr, uintptr, uintptr) ResultCode + voicevoxSynthesizerReplaceMoraData func(uintptr, string, StyleId, uintptr) ResultCode + voicevoxSynthesizerReplaceMoraPitch func(uintptr, string, StyleId, uintptr) ResultCode + voicevoxSynthesizerReplacePhonemeLength func(uintptr, string, StyleId, uintptr) ResultCode + voicevoxSynthesizerSynthesis func(uintptr, string, StyleId, uintptr, uintptr, uintptr) ResultCode + voicevoxSynthesizerTts func(uintptr, string, StyleId, uintptr, uintptr, uintptr) ResultCode + voicevoxSynthesizerTtsFromKana func(uintptr, string, StyleId, uintptr, uintptr, uintptr) ResultCode + voicevoxSynthesizerUnloadVoiceModel func(uintptr, string) ResultCode + voicevoxUserDictAddWord func(uintptr, uintptr, uintptr) ResultCode + voicevoxUserDictDelete func(uintptr) + voicevoxUserDictImport func(uintptr, uintptr) ResultCode + voicevoxUserDictLoad func(uintptr, string) ResultCode + voicevoxUserDictNew func() uintptr + voicevoxUserDictRemoveWord func(uintptr, uintptr) ResultCode + voicevoxUserDictSave func(uintptr, string) ResultCode + voicevoxUserDictToJson func(uintptr, uintptr) ResultCode + voicevoxUserDictUpdateWord func(uintptr, uintptr, uintptr) ResultCode + voicevoxVoiceModelDelete func(uintptr) + voicevoxVoiceModelGetMetasJson func(uintptr) string + voicevoxVoiceModelId func(uintptr) string + voicevoxVoiceModelNewFromPath func(string, uintptr) ResultCode + voicevoxWavFree func(uintptr) +} + +// 必要なパスを引数に取り、Voicevoxのインスタンスを生成する +func NewVoicevox(corePath string, openJtalkPath string, modelPath string) (v *Voicevox, err error) { + c, err := purego.Dlopen(corePath, purego.RTLD_NOW|purego.RTLD_GLOBAL) + if err != nil { + return + } + v = &Voicevox{ + core: c, + } + // 関数の紐付けを行う + v.register() + // OpenJtalkRcを構築する + code := v.voicevoxOpenJtalkRcNew(openJtalkPath, uintptr(unsafe.Pointer(&v.openJtalkRc))) + if code != VOICEVOX_RESULT_OK { + err = v.newError(code) + return + } + // VVMファイルを読み込む + v.vvms, err = v.loadVVMs(modelPath) + // エラーがあった場合、もろもろを破棄する + if err != nil { + v.voicevoxOpenJtalkRcDelete(v.openJtalkRc) + v.deleteVVMs() + } + return +} + +// 利用可能なデバイスの情報を取得する +func (v *Voicevox) SupportedDevices() (sd SupportedDevices, err error) { + var ptr *byte + code := v.voicevoxCreateSupportedDevicesJson(uintptr(unsafe.Pointer(&ptr))) + defer v.voicevoxJsonFree(uintptr(unsafe.Pointer(ptr))) + if code != VOICEVOX_RESULT_OK { + err = v.newError(code) + return + } + j := strings.GoString(ptr) + err = json.Unmarshal([]byte(j), &sd) + return +} + +// 結果コードからメッセージを取得する +func (v *Voicevox) GetMessageFromResult(code ResultCode) string { + return v.voicevoxErrorResultToMessage(code) +} + +// voicevoxのバージョンを取得する +func (v *Voicevox) GetVersion() string { + return v.voicevoxGetVersion() +} + +// メタ情報の一覧を取得する +func (v *Voicevox) GetMetas() []Meta { + return v.sortMetas(v.getMetas()) +} + +/* +スタイルの一覧を取得する +ここで取得されるスタイルのNameは「ずんだもん(あまあま)」のようなフォーマットになる +*/ +func (v *Voicevox) GetStyles() []Style { + metas := v.sortMetas(v.getMetas()) + styles := []Style{} + for _, meta := range metas { + for _, style := range meta.Styles { + style.Name = fmt.Sprintf("%s(%s)", meta.Name, style.Name) + styles = append(styles, style) + } + } + return styles +} diff --git a/vvm.go b/vvm.go new file mode 100644 index 0000000..dc43472 --- /dev/null +++ b/vvm.go @@ -0,0 +1,197 @@ +package nanoda + +import ( + "encoding/json" + "fmt" + "os" + "path" + "unsafe" +) + +type voicevoxVoiceModel struct { + ptr uintptr + id string + metas []meta +} + +// vvmファイルを読み込み構築する +func (v *Voicevox) loadVVMs(modelPath string) (vvms []voicevoxVoiceModel, err error) { + files, err := os.ReadDir(modelPath) + for _, file := range files { + if file.IsDir() { + continue + } + if path.Ext(file.Name()) != ".vvm" { + continue + } + fp := path.Join(modelPath, file.Name()) + vvm := voicevoxVoiceModel{} + if code := v.voicevoxVoiceModelNewFromPath(fp, uintptr(unsafe.Pointer(&vvm.ptr))); code != VOICEVOX_RESULT_OK { + err = fmt.Errorf("%s: %s", fp, v.GetMessageFromResult(code)) + return + } + vvm.id = v.voicevoxVoiceModelId(vvm.ptr) + raw := v.voicevoxVoiceModelGetMetasJson(vvm.ptr) + if err = json.Unmarshal([]byte(raw), &vvm.metas); err != nil { + return + } + vvms = append(vvms, vvm) + } + return +} + +// vvmを破棄する +func (v *Voicevox) deleteVVMs() { + for _, vvm := range v.vvms { + v.voicevoxVoiceModelDelete(vvm.ptr) + } + v.vvms = nil +} + +type findOptions struct { + all bool + byStyleId bool + bySpeakerId bool + styleId StyleId + speakerId SpeakerId +} + +func withFindAll() func(*findOptions) { + return func(o *findOptions) { + o.all = true + } +} + +func withFindByStyleId(styleId StyleId) func(*findOptions) { + return func(o *findOptions) { + o.byStyleId = true + o.styleId = styleId + } +} + +func withFindBySpeakerId(speakerId SpeakerId) func(*findOptions) { + return func(o *findOptions) { + o.bySpeakerId = true + o.speakerId = speakerId + } +} + +// 音声モデルを逆引きする +func (v *Voicevox) findVVMs(options ...func(*findOptions)) (vvms []voicevoxVoiceModel, err error) { + opt := findOptions{} + for _, o := range options { + o(&opt) + } + for _, vvm := range v.vvms { + if opt.all { + vvms = append(vvms, vvm) + continue + } + for _, meta := range vvm.metas { + if opt.bySpeakerId && meta.SpeakerUuid == opt.speakerId { + vvms = append(vvms, vvm) + continue + } + for _, style := range meta.Styles { + if opt.byStyleId && style.Id == opt.styleId { + vvms = append(vvms, vvm) + return + } + } + } + } + if len(vvms) == 0 { + err = fmt.Errorf("not found") + } + return +} + +// 音声モデルを読み込む(内部用) +func (s *Synthesizer) load(vvms []voicevoxVoiceModel) (err error) { + for _, vvm := range vvms { + loaded := s.v.voicevoxSynthesizerIsLoadedVoiceModel(s.synthesizer, vvm.id) + if !loaded { + code := s.v.voicevoxSynthesizerLoadVoiceModel(s.synthesizer, vvm.ptr) + if code != VOICEVOX_RESULT_OK { + err = s.v.newError(code) + return + } + } + } + return +} + +// スタイルIDを元にして音声モデルを読み込む +func (s *Synthesizer) LoadModelsFromStyleId(styleId StyleId) (err error) { + vvms, err := s.v.findVVMs(withFindByStyleId(styleId)) + if err != nil { + return + } + err = s.load(vvms) + return +} + +// 話者IDを元にして音声モデルを読み込む +func (s *Synthesizer) LoadModelsFromSpeakerId(speakerId SpeakerId) (err error) { + vvms, err := s.v.findVVMs(withFindBySpeakerId(speakerId)) + if err != nil { + return + } + err = s.load(vvms) + return +} + +// すべての音声モデルを読み込む +func (s *Synthesizer) LoadAllModels() (err error) { + vvms, err := s.v.findVVMs(withFindAll()) + if err != nil { + return + } + err = s.load(vvms) + return +} + +// 音声モデルをアンロードする(内部用) +func (s *Synthesizer) unload(vvms []voicevoxVoiceModel) (err error) { + for _, vvm := range vvms { + loaded := s.v.voicevoxSynthesizerIsLoadedVoiceModel(s.synthesizer, vvm.id) + if loaded { + code := s.v.voicevoxSynthesizerUnloadVoiceModel(s.synthesizer, vvm.id) + if code != VOICEVOX_RESULT_OK { + err = s.v.newError(code) + return + } + } + } + return +} + +// スタイルIDを元にして音声モデルをアンロードする +func (s *Synthesizer) UnloadModelsFromStyleId(styleId StyleId) (err error) { + vvms, err := s.v.findVVMs(withFindByStyleId(styleId)) + if err != nil { + return + } + err = s.unload(vvms) + return +} + +// 話者IDを元にして音声モデルをアンロードする +func (s *Synthesizer) UnloadModelsFromSpeakerId(speakerId SpeakerId) (err error) { + vvms, err := s.v.findVVMs(withFindBySpeakerId(speakerId)) + if err != nil { + return + } + err = s.unload(vvms) + return +} + +// すべての音声モデルをアンロードする +func (s *Synthesizer) UnloadAllModels() (err error) { + vvms, err := s.v.findVVMs(withFindAll()) + if err != nil { + return + } + err = s.unload(vvms) + return +} diff --git a/wav.go b/wav.go new file mode 100644 index 0000000..d9dedd0 --- /dev/null +++ b/wav.go @@ -0,0 +1,28 @@ +package nanoda + +import ( + "bytes" + "io" +) + +// 出力されたwavファイルを表す構造体 +// io.ReadCloserを実装している +type Wav struct { + reader io.Reader + close func() error +} + +func newWav(raw []byte, close func() error) *Wav { + return &Wav{ + reader: bytes.NewReader(raw), + close: close, + } +} + +func (w *Wav) Read(p []byte) (n int, err error) { + return w.reader.Read(p) +} + +func (w *Wav) Close() error { + return w.close() +} diff --git a/word.go b/word.go new file mode 100644 index 0000000..ecade54 --- /dev/null +++ b/word.go @@ -0,0 +1,80 @@ +package nanoda + +import "github.com/aethiopicuschan/nanoda/internal/strings" + +// 内部用の単語 +type innerWord struct { + surface uintptr + pronunciation uintptr + accentType uintptr + wordType int32 + priority uint32 +} + +// 単語の種類 +type WordType int32 + +const ( + VOICEVOX_USER_DICT_WORD_TYPE_PROPER_NOUN WordType = iota // 固有名詞 + VOICEVOX_USER_DICT_WORD_TYPE_COMMON_NOUN // 一般名詞 + VOICEVOX_USER_DICT_WORD_TYPE_VERB // 動詞 + VOICEVOX_USER_DICT_WORD_TYPE_ADJECTIVE // 形容詞 + VOICEVOX_USER_DICT_WORD_TYPE_SUFFIX // 接尾辞 +) + +// 単語 +type Word struct { + Surface string // 表記 + Pronunciation string // 読み + AccentType uint64 // アクセント型(音が下がる場所を指す) + WordType WordType // 単語の種類 + Priority uint32 // 優先度(0〜10までの整数) +} + +func (w *Word) toinner() innerWord { + return innerWord{ + surface: strings.CString(w.Surface), + pronunciation: strings.CString(w.Pronunciation), + accentType: uintptr(w.AccentType), + wordType: int32(w.WordType), + priority: w.Priority, + } +} + +// NewWordを呼ぶ際にアクセント型を指定する +func WithAccentType(at uint64) func(*Word) { + return func(w *Word) { + w.AccentType = at + } +} + +// NewWordを呼ぶ際に単語の種類を指定する +func WithWordType(wt WordType) func(*Word) { + return func(w *Word) { + w.WordType = wt + } +} + +// NewWordを呼ぶ際に単語の種類を指定する +func WithPriority(p uint32) func(*Word) { + return func(w *Word) { + w.Priority = p + } +} + +// 単語を作成する +// 辞書に登録するには別途AddWordを呼び出す必要がある +func NewWord(surface, pronunciation string, options ...func(*Word)) (w Word) { + w = Word{ + Surface: surface, + Pronunciation: pronunciation, + AccentType: 0, + WordType: 0, + Priority: 5, + } + for _, o := range options { + o(&w) + } + + return +}