Skip to content

Commit 8d2a03d

Browse files
authored
プルリクエスト #1 のマージ
v0.1.1
2 parents 160f521 + 347336c commit 8d2a03d

File tree

27 files changed

+329
-46
lines changed

27 files changed

+329
-46
lines changed

.github/workflows/publish.yaml

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,10 @@ jobs:
3939
shell: bash
4040
run: |
4141
mv voicepeakcw4j-speech/build/libs/* asset/
42-
- name: Detect asset files
43-
shell: bash
44-
run: |
45-
echo "asset_path=asset/$(ls asset)" >> $GITHUB_ENV
46-
echo "asset_name=$(ls asset)" >> $GITHUB_ENV
4742
- name: Upload artifacts
4843
uses: actions/upload-artifact@v4 # https://github.com/actions/upload-artifact
4944
with:
50-
name: ${{ env.asset_name }}
51-
path: ${{ env.asset_path }}
45+
path: asset/**/*
5246
compression-level: 0
5347
release:
5448
runs-on: ubuntu-latest

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# VOICEPEAK コマンドラインラッパー for Java
22

3-
VOICEPEAKは、[株式会社AHS](https://www.ah-soft.com/)が開発した入力文字読み上げソフトで[Dreamtonics社](https://dreamtonics.com/synthesizerv/)が開発した高品質なAI音声合成エンジン「Syllaflow」を搭載しています。
3+
VOICEPEAKは、[株式会社AHS](https://www.ah-soft.com/)が展開する入力文字読み上げソフトで[Dreamtonics社](https://dreamtonics.com/synthesizerv/)が開発した高品質なAI音声合成エンジン「Syllaflow」を搭載しています。
44

55
2024年8月現在、最新バージョンである「VOICEPEAK 1.2.11」では、コマンドラインから実行して任意のテキストをもとに音声を生成することができます。
66
```
@@ -114,7 +114,7 @@ implementation group: 'io.github.k7t3', name: 'voicepeakcw4j-speech', version: '
114114

115115
現在、VOICEPEAKコマンドライン実行で処理できる文字数には140文字までの制限がありますが、このライブラリはその制限を隠ぺいしています。
116116

117-
140文字を超える文字列がパラメータに設定される場合、言語仕様に基づいて最大限文脈を破綻させないように維持しながら複数の文字列に分割し、その文字列の分割個数に基づいてVOICEPEAKプロセスを繰り返し実行します。このように、長い文字列であっても一度の実行でシームレスに読み上げることができます。
117+
140文字を超える文字列がパラメータに設定される場合、言語に基づいて最大限文脈を破綻させないように維持しながら複数の文字列に分割し、その数だけVOICEPEAKプロセスを繰り返し実行します。こうすることで、長い文字列であっても一度の実行でシームレスに読み上げることができます。
118118

119119
また、現在のVOICEPEAKのバージョンにはコマンドラインの並列実行に関しても制約があります。このライブラリの観測できない場所からプロセスが実行されていると(ターミナルなど)、正常に動作しない可能性があります。
120120

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ subprojects {
4141

4242
pom {
4343
name = "$project.name"
44-
description = 'This library is a command line wrapper for the VOICEPEAK developed by AHS Co.Ltd.'
44+
description = 'This library is a command line wrapper for the VOICEPEAK published by AHS Co.Ltd.'
4545
url = 'https://github.com/k7t3/VOICEPEAKCommandLineWrapper4J'
4646

4747
licenses {

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
voicepeakcw4jVersion=0.1.0
1+
voicepeakcw4jVersion=0.1.1-SNAPSHOT
22

33
javaVersion=21
44
junitVersion=5.10.0

voicepeakcw4j-speech/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ plugins {
55
}
66

77
dependencies {
8-
implementation project(":voicepeakcw4j")
8+
api project(":voicepeakcw4j")
99

1010
testImplementation platform("org.junit:junit-bom:$junitVersion")
1111
testImplementation 'org.junit.jupiter:junit-jupiter'
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright 2024 k7t3
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.github.k7t3.voicepeakcw4j.speech;
18+
19+
import javax.sound.sampled.*;
20+
import java.util.Arrays;
21+
import java.util.List;
22+
import java.util.Objects;
23+
24+
/**
25+
* VOICEPEAKで生成したオーディオファイルを再生できるデバイス
26+
*/
27+
public class AudioDevice {
28+
29+
/**
30+
* VOICEPEAKのコマンドライン実行で生成されるWAVEフォーマット
31+
*/
32+
private static final AudioFormat VOICEPEAK_DEFAULT_FORMAT = new AudioFormat(
33+
AudioFormat.Encoding.PCM_SIGNED,
34+
48000,
35+
16,
36+
1,
37+
2,
38+
48000,
39+
false
40+
);
41+
42+
private final Mixer mixer;
43+
44+
private AudioDevice(Mixer mixer) {
45+
this.mixer = mixer;
46+
}
47+
48+
SourceDataLine getSourceDataLine() {
49+
var info = new DataLine.Info(SourceDataLine.class, VOICEPEAK_DEFAULT_FORMAT);
50+
try {
51+
return (SourceDataLine) mixer.getLine(info);
52+
} catch (LineUnavailableException e) {
53+
throw new RuntimeException(e);
54+
}
55+
}
56+
57+
/**
58+
* 既定のデバイスを返す
59+
* <p>
60+
* 既定のデバイスが読み上げ音声を再生できないときはnullを返す
61+
* </p>
62+
* @return 既定のデバイス、あるいはそのデバイスが再生できないときはnull
63+
*/
64+
public static AudioDevice getDefaultDevice() {
65+
try {
66+
var sourceDataLineInfo = new DataLine.Info(SourceDataLine.class, VOICEPEAK_DEFAULT_FORMAT);
67+
var mixer = AudioSystem.getMixer(null);
68+
if (mixer.isLineSupported(sourceDataLineInfo)) {
69+
return new AudioDevice(mixer);
70+
}
71+
} catch (IllegalArgumentException ignored) {
72+
// 対応するミキサーがないときにIllegalArgumentExceptionがスローされるため無視する
73+
return null;
74+
}
75+
return null;
76+
}
77+
78+
/**
79+
* 読み上げ音声を再生可能なデバイスのリストを返す
80+
* @return 読み上げ音声を再生可能なデバイスのリスト
81+
*/
82+
public static List<AudioDevice> getAvailableDevices() {
83+
var sourceDataLineInfo = new DataLine.Info(SourceDataLine.class, VOICEPEAK_DEFAULT_FORMAT);
84+
return Arrays.stream(AudioSystem.getMixerInfo())
85+
.map(AudioSystem::getMixer)
86+
.filter(m -> m.isLineSupported(sourceDataLineInfo))
87+
.map(AudioDevice::new)
88+
.toList();
89+
}
90+
91+
@Override
92+
public boolean equals(Object o) {
93+
if (this == o) return true;
94+
if (!(o instanceof AudioDevice that)) return false;
95+
var info = mixer.getMixerInfo();
96+
var oi = that.mixer.getMixerInfo();
97+
return Objects.equals(info.getName(), oi.getName())
98+
&& Objects.equals(info.getDescription(), oi.getDescription())
99+
&& Objects.equals(info.getVendor(), oi.getVendor())
100+
&& Objects.equals(info.getVersion(), oi.getVersion());
101+
}
102+
103+
@Override
104+
public int hashCode() {
105+
return Objects.hashCode(mixer);
106+
}
107+
108+
@Override
109+
public String toString() {
110+
return mixer.getMixerInfo().toString();
111+
}
112+
}

voicepeakcw4j-speech/src/main/java/io/github/k7t3/voicepeakcw4j/speech/AudioPlayer.java

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,20 @@ class AudioPlayer implements AutoCloseable {
3939
private volatile boolean closed = false;
4040
private volatile boolean initialized = false;
4141

42+
private final AudioDevice audioDevice;
43+
4244
private SourceDataLine line;
4345
private FloatControl volumeControl;
4446

45-
public AudioPlayer() {
47+
public AudioPlayer(AudioDevice audioDevice) {
48+
this.audioDevice = audioDevice;
4649
}
4750

4851
/**
49-
* 0.0 ~ 1.0
52+
* 0.0 ~ 2.0
53+
* <p>
54+
* 1/10000倍から2倍まで
55+
* </p>
5056
*
5157
* @param volumeRate ボリュームの比率
5258
*/
@@ -60,14 +66,20 @@ public void requestCancel() {
6066
cancelRequest.set(true);
6167
}
6268

63-
private void initialize(AudioFormat format) {
69+
private void initialize() {
70+
if (audioDevice == null) {
71+
LOGGER.log(System.Logger.Level.ERROR, "audio device can not play audio");
72+
close();
73+
return;
74+
}
75+
6476
try {
6577

6678
initialized = true;
6779

68-
// オーディオフォーマットに対応したラインを取得
69-
line = AudioSystem.getSourceDataLine(format);
70-
line.open(format);
80+
// オーディオデバイスのラインを取得
81+
line = audioDevice.getSourceDataLine();
82+
line.open();
7183
line.start();
7284

7385
// ラインが対応している場合はそのコントロールを使用
@@ -82,7 +94,7 @@ private void initialize(AudioFormat format) {
8294
}
8395

8496
} catch (LineUnavailableException e) {
85-
LOGGER.log(System.Logger.Level.ERROR, "audio format is not supported " + format.toString());
97+
LOGGER.log(System.Logger.Level.ERROR, "audio format is not supported");
8698
close();
8799
}
88100
}
@@ -93,7 +105,13 @@ private void setVolumeIfRequested(FloatControl volumeControl) {
93105
var volumeRate = volumeQueue.poll();
94106
if (volumeRate == null) return;
95107

96-
var decibel = (volumeRate <= 0.0f) ? -80.0f : 20 * (float) Math.log10(volumeRate);
108+
// logの0は不定なので下限を設ける
109+
volumeRate = Math.clamp(volumeRate, volumeControl.getMinimum(), volumeControl.getMaximum());
110+
111+
// 音圧のデシベルは10のべき乗ごとに20倍になるらしい
112+
// https://www.mgco.jp/magazine/plan/mame/b_others/2001/
113+
var decibel = (float) (20 * Math.log10(volumeRate));
114+
97115
volumeControl.setValue(decibel);
98116
LOGGER.log(System.Logger.Level.INFO, "set decibel " + decibel);
99117
}
@@ -107,8 +125,7 @@ public void play(Path audioFile) throws IOException, UnsupportedAudioFileExcepti
107125
try (var input = AudioSystem.getAudioInputStream(new BufferedInputStream(Files.newInputStream(audioFile)))) {
108126

109127
if (!initialized) {
110-
var audioFormat = input.getFormat();
111-
initialize(audioFormat);
128+
initialize();
112129
}
113130

114131
// オーディオを読み込んでラインに書き込む

voicepeakcw4j-speech/src/main/java/io/github/k7t3/voicepeakcw4j/speech/DefaultVPSpeech.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@
2121

2222
import java.util.List;
2323

24-
public class DefaultVPSpeech implements VPSpeech {
24+
class DefaultVPSpeech implements VPSpeech {
2525

2626
private final VPExecutable executable;
2727

2828
private final VPClient client;
2929

30-
public DefaultVPSpeech(VPExecutable executable) {
30+
DefaultVPSpeech(VPExecutable executable) {
3131
this.executable = executable;
3232
this.client = VPClient.create(executable);
3333
}

voicepeakcw4j-speech/src/main/java/io/github/k7t3/voicepeakcw4j/speech/SentenceSplitter.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,17 @@ public class SentenceSplitter {
3434

3535
private final Locale locale;
3636

37+
/**
38+
* コンストラクタ
39+
* @param locale 解釈するロケール
40+
*/
3741
public SentenceSplitter(Locale locale) {
3842
this.locale = locale == null ? Locale.getDefault() : locale;
3943
}
4044

45+
/**
46+
* コンストラクタ
47+
*/
4148
public SentenceSplitter() {
4249
this(null);
4350
}
@@ -96,10 +103,9 @@ private List<String> breakFragments(String text, int maxLength) {
96103

97104
// 句読点・空白のときに要素とする
98105
if (!Character.isLetter(word.codePointAt(0))) {
99-
// 改行文字は追加しない
100-
if (!"\n".equals(word)) {
101-
builder.append(word);
102-
}
106+
107+
builder.append(word);
108+
103109
if (!builder.isEmpty()) {
104110
list.add(builder.toString());
105111
builder = new StringBuilder();

0 commit comments

Comments
 (0)