iOS/iPhone/iPad/watchOS/tvOS/MacOSX/Android プログラミング, Objective-C, Cocoa, Swiftなど
文章読み上げの機能を利用するためには、AndroidManifest.xmlでの宣言が必要です。
<manifest>
<application>
</application>
<queries>
<intent>
<action android:name="android.intent.action.TTS_SERVICE" />
</intent>
</queries>
</manifest>
Jetpack Compose的なグローバルな変数を保持する方法はあると思いますが、TextToSpeechインスタンスはsetContentの外のメンバーとして保持しています。
class MainActivity : ComponentActivity(), TextToSpeech.OnInitListener {
private var textToSpeech: TextToSpeech? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
this.textToSpeech = TextToSpeech(this, this)
setContent {
TextToSpeechTheme {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
Greeting(this.textToSpeech)
}
}
}
}
onInit()で初期化を
override fun onInit(status: Int) {
if (status == TextToSpeech.SUCCESS) {
val locale = Locale.JAPAN
if (this.textToSpeech!!.isLanguageAvailable(locale) >= TextToSpeech.LANG_AVAILABLE) {
this.textToSpeech!!.language = Locale.JAPAN
}
// this.tts!!.speak("こんにちは", TextToSpeech.QUEUE_FLUSH, null, "utteranceId")
}
}
}
入力フィールドとボタンのコンテンツ。
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Greeting(textToSpeech: TextToSpeech?, modifier: Modifier = Modifier) {
val textValue = rememberSaveable { mutableStateOf("文字列を入力してください。") }
Column {
TextField(
value = textValue.value,
onValueChange = { textValue.value = it },
label = { },
modifier = Modifier.padding(16.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Button(
onClick = {
if (textToSpeech != null) {
textToSpeech!!.speak(
textValue.value,
TextToSpeech.QUEUE_FLUSH,
null,
"utteranceId"
)
}
}
) {
Text("Say")
}
}
}
rememberSaveableで文章を永続的に保持し、入力フィールドの値をtextToSpeechに渡している。
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
TextToSpeechTheme {
Greeting(null)
}
}
以下のデバッグ出力を仕込んで、利用できるヴォイスをダンプしてみた。
this.textToSpeech!!.voices.forEach { voice -> Log.d("MainActivity", voice.toString()) }
量が多いので日本語関連のみ抜き出してみる。
Voice[Name: ja-jp-x-jab-network, locale: ja_JP, quality: 400, latency: 200, requiresNetwork: true, features: [networkTimeoutMs, networkRetriesCount]]
Voice[Name: ja-jp-x-htm-network, locale: ja_JP, quality: 400, latency: 200, requiresNetwork: true, features: [networkTimeoutMs, networkRetriesCount]]
Voice[Name: ja-jp-x-jad-network, locale: ja_JP, quality: 400, latency: 200, requiresNetwork: true, features: [networkTimeoutMs, networkRetriesCount]]
Voice[Name: ja-jp-x-jab-local, locale: ja_JP, quality: 400, latency: 200, requiresNetwork: false, features: [networkTimeoutMs, networkRetriesCount]]
Voice[Name: ja-jp-x-jad-local, locale: ja_JP, quality: 400, latency: 200, requiresNetwork: false, features: [networkTimeoutMs, networkRetriesCount]]
Voice[Name: ja-jp-x-jac-local, locale: ja_JP, quality: 400, latency: 200, requiresNetwork: false, features: [networkTimeoutMs, networkRetriesCount]]
Voice[Name: ja-jp-x-jac-network, locale: ja_JP, quality: 400, latency: 200, requiresNetwork: true, features: [networkTimeoutMs, networkRetriesCount]]
Voice[Name: ja-jp-x-htm-local, locale: ja_JP, quality: 400, latency: 200, requiresNetwork: false, features: [networkTimeoutMs, networkRetriesCount]]
Appleと異なり、声色毎という方向での充実ではなさそうですね。
Macintoshの文章読み上げエンジンの歴史は古く、1984年のMacintosh発表のイベントでMacinTalkを使用して実演されました。このMacinTalkはPlainTalkと総称され、確か、これを利用するために用意されたライブラリがSpeech Managerだったと思います。
この流れからだと思いますが、Mac OS XとなってCocoaのフレームワークとして用意されたのがNSSpeechSynthesizerです。
今回、文章読み上げ(text-to-speech (TTS))を利用しようと調べたのですが、NSSpeechSynthesizerはDeprecatedとなっていました。その後継として用意されたのが、おそらく、AVFoundationに追加されたAVSpeechSynthesizerのようです。
NSSpeechSynthesizerがDeprecatedになったのは、macOSのみだったのをiOSに対応させる際、古いAPIをモダン化するために大幅な変更が発生するので、別のクラスになったのでしょう。
Speech Synthesis フレームワークの利用は簡単で、文章と音声の設定する。
// Create an utterance.
let utterance = AVSpeechUtterance(string: text)
// Configure the utterance.
utterance.rate = 0.57
utterance.pitchMultiplier = 0.8
utterance.postUtteranceDelay = 0.2
utterance.volume = 0.8
// Retrieve the Japanese voice.
let voice = AVSpeechSynthesisVoice(language: "ja-JP")
// Assign the voice to the utterance.
utterance.voice = voice
文章を読み上げる。
// Create a speech synthesizer.
synthesizer = AVSpeechSynthesizer()
// Tell the synthesizer to speak the utterance.
synthesizer!.speak(utterance)
日本語の場合、漢字の読みが文脈によって異なるので、振り仮名をつけたいなどの要望があると思いますが、Speech Synthesis Markup Language (SSML)を利用すれば文章に情報をつけることができます。
<speak>
Hello
<break time="1s"/>
<prosody rate="200%">nice to meet you!</prosody>
</speak>
これをAVSpeechUtteranceのコンストラクタでSSMLだと指定して渡します。
let utterance = AVSpeechUtterance(ssmlRepresentation: ssml)
voiceは以下のコードで一覧が取得できます。
let voices = AVSpeechSynthesisVoice.speechVoices()
print("\(voices)")
ログから、日本語のvoiceを抜き出してみました。
[AVSpeechSynthesisVoice 0x600000934170] Language: ja-JP, Name: Kyoko, Quality: Enhanced [com.apple.voice.enhanced.ja-JP.Kyoko],
[AVSpeechSynthesisVoice 0x600000934400] Language: ja-JP, Name: Otoya, Quality: Enhanced [com.apple.voice.enhanced.ja-JP.Otoya],
[AVSpeechSynthesisVoice 0x60000093bca0] Language: ja-JP, Name: Kyoko, Quality: Default [com.apple.voice.compact.ja-JP.Kyoko],
[AVSpeechSynthesisVoice 0x60000093bef0] Language: ja-JP, Name: Hattori, Quality: Default [com.apple.ttsbundle.siri_Hattori_ja-JP_compact],
[AVSpeechSynthesisVoice 0x600000934330] Language: ja-JP, Name: Otoya, Quality: Default [com.apple.voice.compact.ja-JP.Otoya],
[AVSpeechSynthesisVoice 0x6000009343d0] Language: ja-JP, Name: O-Ren, Quality: Default [com.apple.ttsbundle.siri_O-Ren_ja-JP_compact],
これはシステム設定のシステムの声の内容と一致します。
SwiftとKotlinに関係する情報は、可能な限り一次情報や、信頼できるコミュニティから得るのが良いと思いますが、今年、自分詩人が助けられた情報源についてまとめました。この情報が誰かの役に立てば嬉しいです!
公式からニュースという形で公開されています。自分は、以下をよく利用しています。
プラットフォームの変更内容や、期限を設けられた対応の告知について情報が得られます。
AppleとGoogle Androidの開発サイトです。
Apple Developer Programのメンバーシップには、技術的な質問をするためのTechnical Support Incidents(TSI)が付与されています。TSIは追加で購入できます。
ストア関連については、AppleとGoogleはチャットやメールで質問を受け付けています。営業日/営業時間でしたら、すぐに回答してくれますので、怪しい噂話に踊るぐらいなら、悩んだら、質問を投げることをお勧めします。
WWDCとGoogle I/Oのサイトです。
ウェビナーや1対1のコンサルテーションについて情報が掲載されています。
Play Consoleに関係する変更内容と対応期限については、ポリシー センターのサイトから情報が得られます。
動的に生成されるURLのようですが、以下は、現時点で対応内容と期限がまとめられいるサイトです。
対応内容と期限を説明するウェビナーも開催されていまして、こちらも動的に生成されるURLなので、過去のものとなりますが、最近開催されたサイトのURLです。
様々なコミュニティが存在しますが、自分がよくお邪魔するコミュニティのURLを紹介します。
SlackやDiscodeを利用しているコミュニティもありますので、悩んだら相談してみるのがいいと思います。ただ、善意での対応ですので、自分で調べれば分かることはご自身で解決を。また、解決した場合は、同様に悩んでいる方々に役立つよう、結果を報告されるのが、いいと思います。」
UIはJetpack Compose。
TextToSpeechインスタンスは外のメンバーとして保持し、setContent内がReact的なコンテンツとなるようだ。
class MainActivity : ComponentActivity(), TextToSpeech.OnInitListener {
private var textToSpeech: TextToSpeech? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
this.textToSpeech = TextToSpeech(this, this)
setContent {
TextToSpeechTheme {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
Greeting(this.textToSpeech)
}
}
}
}
onInit()で初期化を
override fun onInit(status: Int) {
if (status == TextToSpeech.SUCCESS) {
val locale = Locale.JAPAN
if (this.textToSpeech!!.isLanguageAvailable(locale) >= TextToSpeech.LANG_AVAILABLE) {
this.textToSpeech!!.language = Locale.JAPAN
}
// this.tts!!.speak("こんにちは", TextToSpeech.QUEUE_FLUSH, null, "utteranceId")
}
}
}
入力フィールドとボタンのコンテンツ。
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Greeting(textToSpeech: TextToSpeech?, modifier: Modifier = Modifier) {
val textValue = rememberSaveable { mutableStateOf("文字列を入力してください。") }
Column {
TextField(
value = textValue.value,
onValueChange = { textValue.value = it },
label = { },
modifier = Modifier.padding(16.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Button(
onClick = {
if (textToSpeech != null) {
textToSpeech!!.speak(
textValue.value,
TextToSpeech.QUEUE_FLUSH,
null,
"utteranceId"
)
}
}
) {
Text("Say")
}
}
}
rememberSaveableで文章を永続的に保持し、入力フィールドの値をtextToSpeechに渡している。
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
TextToSpeechTheme {
Greeting(null)
}
}
文章を読み上げる機能は、過去、様々な年化があったが、これが現在の方法のようだ。
文章と音声の設定する。
// Create an utterance.
let utterance = AVSpeechUtterance(string: text)
// Configure the utterance.
utterance.rate = 0.57
utterance.pitchMultiplier = 0.8
utterance.postUtteranceDelay = 0.2
utterance.volume = 0.8
// Retrieve the Japanese voice.
let voice = AVSpeechSynthesisVoice(language: "ja-JP")
// Assign the voice to the utterance.
utterance.voice = voice
文章を読み上げる。
// Create a speech synthesizer.
synthesizer = AVSpeechSynthesizer()
// Tell the synthesizer to speak the utterance.
synthesizer!.speak(utterance)
これをSwiftUIで利用できるようにする。
struct ContentView: View {
@State var text = ""
@State var synthesizer: AVSpeechSynthesizer?
var body: some View {
VStack {
TextField("Input text", text: $text)
Button(action: {
// Create an utterance.
let utterance = AVSpeechUtterance(string: text)
// Configure the utterance.
utterance.rate = 0.57
utterance.pitchMultiplier = 0.8
utterance.postUtteranceDelay = 0.2
utterance.volume = 0.8
// Retrieve the Japanese voice.
let voice = AVSpeechSynthesisVoice(language: "ja-JP")
// Assign the voice to the utterance.
utterance.voice = voice
// Create a speech synthesizer.
synthesizer = AVSpeechSynthesizer()
// Tell the synthesizer to speak the utterance.
synthesizer!.speak(utterance)
}) {
Text("Say")
}
}
.padding()
}
}
#Preview {
ContentView()
}
文章やAVSpeechSynthesizerのインスタンスは@Stateで永続的に保持し、バインディングでTextFieldの値を渡している。
以下を入力して、"コードを実行"を選択します。
print("計算結果: \(1 + 1)")
コンソールに、以下の文字が表示されます。
計算結果: 2
print()は関数と呼ばれ、括弧内の文字列をコンソールに表示します。
文字列は"と"と囲まれた文言で、文字列中に\()で囲まれた計算式を書きますと、計算結果がコンソールに表示されます。上記では、1 + 1 が計算され、その結果がコンソールに表示されます。
値を格納する変数を使います。以下を入力してください。
let pai = 3.14
var r = 10.0
var area = pai * r * r
r = 20.0
area = pai * r * r
print("円の面積: \(area)")
コードを実行した結果です。
円の面積: 1256.0
変数は宣言してから使えるようになります。letで宣言しますと変更できない定数となります。varで宣言しますと変更できます。
半径10.0の面積を計算したのちに、半径20.0の面積を計算し直して、それをコンソールに表示しています。
未経験者向けのプログラミング入門の記事を投稿していきます。
今やプログラミングは多種多様で何をやるべきか悩みました。息の長い技術は、よりコアなものだと思いますが、環境の用意や、操作方法の習得に時間がかかり、やりたかったプログラミングに到達するまで大変です。いろいろ試して見た結果、ネイティブなものですが、環境や操作は後回しにできる、Swift Playgrounds アプリケーションがベターではないかと考え選択しました。
Swift PlaygroundsはApp Storeアプリケーションから入手できますが、それへの到達も困難な方がいらっしゃると思いますので、Swift Playgrounds サイトを紹介します。こちらから辿って入手してください。
Swift Playgroundsを入手したら、起動してください。"ファイル"メニューの"新しいブック"を選んで、"マイプレイグランド"を作成してください。
"マイプレイグランド"ウィンドウの右下を選択して、結果が表示されるコンソールを開いてください。
左上に"クリックしてコードを入力"を選択すると文字が入力できますので、以下を入力してください。
print("hello, world")
右下の"コードを実行"を選択してください。
右半分のコンソールに、以下の文字が表示されます。
hello, world
macOSとiOSの実行可能コードをパッケージ化する方法にバンドル構造というのがある。内容を簡単に説明すると実行可能コードとリソースをディレクトリは以下に配置し、そのディレクトリをパッケージ化された塊として扱うもので、アプリケーションはApplication Bundle、フレームワークはFramework Bundleと呼ばれる決められた構成となっている。
MyApp.app
|-- MyApp
|-- MyAppIcon.png
|-- MySearchIcon.png
|-- Info.plist
|-- Default.png
|-- MainWindow.nib
|-- Settings.bundle
|-- MySettingsIcon.png
|-- iTunesArtwork
|-- en.lproj
| `-- MyImage.png
`-- fr.lproj
`-- MyImage.png
MyFramework.framework
|-- MyFramework -> Versions/Current/MyFramework
|-- Resources -> Versions/Current/Resources
`-- Versions
|-- A
| |-- MyFramework
| |-- Headers
| | `-- MyHeader.h
| `-- Resources
| |-- English.lproj
| | `-- InfoPlist.strings
| `-- Info.plist
`-- Current -> A
Loadable Bundleは動的にロード可能なパッケージで、.bundle や .plugin 等が suffix として使われている。
MyLoadableBundle.bundle
`-- Contents
|-- Info.plist
|-- MacOS
| `-- MyLoadableBundle
`-- Resources
|-- Lizard.jpg
|-- MyLoadableBundle.icns
|-- en.lproj
| |-- MyLoadableBundle.nib
| `-- InfoPlist.strings
`-- jp.lproj
|-- MyLoadableBundle.nib
`-- InfoPlist.strings
例えば、ローカライズの際に日本以外で全角英数字を使っていいのか疑問に思ったので、全角半角について調べたことを備忘録として残す。
主に米国製のコンピュータを日本で利用するために様々な工夫があったと考えられる。
全角半角のいやらしいところは文字コードなのにグリフであったらり、全角半角は文字としては別なので、英数字を全角で符合してしまうと、変換機能がないと、英数字として扱えない。
Unicodeを調べると、FF00〜FFEFはHalfwidth and Fullwidth Formsのブロックとして決められていて、中国、日本、韓国の、という説明があったので、これらの言語では全角英数字は使えそう。でも、微妙。
Day One Classicという日記アプリを使っていたが、iOSのバージョンが上がって使えなくなったので、このアプリのデータを取り出して、自分の日記アプリの取り込みたいと考え、Day One Classicのデータの書式を調べた。
Journal.dayoneがデータの名前で、これはディレクトリだった。
.
`-- Journal.dayone
|-- entries
| |-- UUID1.doentry
| `-- UUIDn.doentry
`-- photos
|-- UUID1.jpg
`-- UUIDm.jpg
日記の本文はentriesディレクトリ配下に、投稿毎の単位でsuffixが.doentryのファイルに記録されている。日記に写真がある場合はphotosディレクトリ配下に対応する.doentryファイルと同じUUIDでJPEGファイルとして格納されている。
日記の本文はプロパティリストの書式となっていて、NSDictionaryとして読み込むことができる。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Creation Date</key>
<date>2014-01-03T21:40:52Z</date>
<key>Creator</key>
<dict>
<key>Device Agent</key>
<string>iPhone/iPhone5,2</string>
<key>Generation Date</key>
<date>2014-01-03T21:40:52Z</date>
<key>Host Name</key>
<string>iPhone5Black64GB</string>
<key>OS Agent</key>
<string>iOS/7.0.4</string>
<key>Software Agent</key>
<string>Day One iOS/1.12</string>
</dict>
<key>Entry Text</key>
<string>娘と
俺の藤井2014</string>
<key>Location</key>
<dict>
<key>Administrative Area</key>
<string>埼玉県</string>
<key>Country</key>
<string>日本</string>
<key>Latitude</key>
<real>35.904324925538063</real>
<key>Locality</key>
<string>さいたま市 大宮区</string>
<key>Longitude</key>
<real>139.62506669586489</real>
<key>Place Name</key>
<string>下町 1丁目2番</string>
</dict>
<key>Music</key>
<dict>
<key>Track</key>
<string>Weekend Sunshine - Dec 7, 2013</string>
</dict>
<key>Starred</key>
<false/>
<key>Time Zone</key>
<string>Asia/Tokyo</string>
<key>UUID</key>
<string>B2713EC2EAF54B64884E8FF85D20DE5F</string>
</dict>
</plist>
Swiftで読み込むコードを書いてみた。
import Foundation
func dump(url aUrl: URL) {
print("dump(\(aUrl))")
do {
let urls = try FileManager.default.contentsOfDirectory(
at: aUrl,
includingPropertiesForKeys: nil,
options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants])
urls.forEach { url in
print(url)
if url.hasDirectoryPath {
dump(url: url)
} else {
if url.pathExtension == "doentry" {
let entry = NSDictionary(contentsOfFile: url.path)
print("\(String(describing: entry))")
}
}
}
} catch {
print(error.localizedDescription)
}
}
let journalDayonePath: String = "/Users/yukio/Documents/Development/Projects/KeepADiary/temp/Day One/Journal.dayone"
let journalDayoneURL = URL(fileURLWithPath: journalDayonePath)
dump(url: journalDayoneURL)
Xcodeのデバッガで値をダンプして、例えば、日付はNSDateのオブジェクトになっていることが確認できた。