iOS/iPhone/iPad/watchOS/tvOS/MacOSX/Android プログラミング, Objective-C, Cocoa, Swiftなど
ヒレガス本『Cocoa Programming for OS X (5th Edition)』のサンプルThermostatをSwiftUIで実装してみた。
このサンプルは、KVCとKVO、Bindingsが題材になったものだが、SwiftUIの仕組み自体がこの機能を活用したものなので、今まで学習した内容で対応できた。
struct ContentView: View {
@State private var internalTemperature: Double = 68.0
@State private var isOn = true
var body: some View {
HStack {
VStack {
Slider(value: $internalTemperature, in: 0 ... 212, step: 1.0)
.disabled(!isOn)
Text(String(format: "%.0lf", internalTemperature))
}
.padding()
VStack {
Spacer()
Button("Warmer") {
internalTemperature += 1.0
}
.disabled(!isOn)
Spacer()
Button("Cooler") {
internalTemperature += -1.0
}
.disabled(!isOn)
Spacer()
Button("Power") {
isOn = !isOn
}
.buttonStyle(.borderless)
}
.padding()
}.padding()
}
}
サンプルは縦方向のスライダーだったが、SwiftUIのSliderは横方向のみなので、ここは諦めて横方向のスライダーとした。
ヒレガス本『Cocoa Programming for OS X (5th Edition)』のTableViewのサンプルをSwiftUIで実装してみた。
前回のSpeakLineはテキスト読み上げに利用する声をコードで求めていたが、今回のサンプルでは現在利用できる言語に対応した声から選択できるものとなる。
以下が、現在の言語で利用できる声のリストを返す関数。
func getVoices() -> [AVSpeechSynthesisVoice] {
let locale = Locale(identifier:Locale.preferredLanguages[0])
let voices = AVSpeechSynthesisVoice.speechVoices().filter { $0.language == locale.identifier }
return voices
}
得られた声のリストでListを作成する。
struct ContentView: View {
@State private var text = ""
@ObservedObject private var speaker: Speaker = .init()
private let voices = getVoices()
@State var selectedVoice: AVSpeechSynthesisVoice?
var body: some View {
VStack {
HStack {
ZStack(alignment: .topLeading) {
TextEditor(text: $text)
// prompt
if text.isEmpty {
Text("Enter text to be spoken...") .foregroundColor(Color.gray)
.padding(.horizontal, 6)
}
}
List(voices, id: \.self, selection: $selectedVoice) { voice in
Text(voice.name)
}
}
HStack {
Spacer()
Button("Stop") {
print("stop button clicked")
speaker.stop()
}.disabled(!speaker.isSpeaking)
Button("Speak") {
if text.isEmpty {
print("string from TextEditor is empty")
} else {
print("string is \(text)")
if let voice = selectedVoice {
print("voice is \(voice)")
speaker.speak(text, voice: voice)
} else {
speaker.speak(text)
}
}
}.disabled(speaker.isSpeaking)
}
}
.padding()
}
}
読み上げクラスも、声が渡された場合に対応。
final class Speaker: NSObject, AVSpeechSynthesizerDelegate, ObservableObject {
func speak(_ text: String, voice: AVSpeechSynthesisVoice) {
let utterance = AVSpeechUtterance(string: text)
utterance.voice = voice
speechSynth.speak(utterance)
}
}
ヒレガス本『Cocoa Programming for OS X (5th Edition)』のサンプルSpeakLineをSwiftUIで実装してみた。
テキスト領域に入力された文言を喋るという内容だ。今回のポイントは、マルチプラットフォームなテキスト領域の利用と、マルチプラットフォームなテキスト読み上げ機能の利用とデリゲートの仕組みをSwiftUIでどう利用するかだ。
テキスト領域は、TextFieldを利用するのが適切だが、固定した行数の領域を設定するのが難しかったため、TextEditorを利用した。ただ、TextEditorはプロンプトの機能がないので、それっぽく実装してみたが、テキストの入力が完了しないと消えないとイマイチな挙動となってしまった。
struct ContentView: View {
@State private var text = ""
var body: some View {
VStack {
ZStack(alignment: .topLeading) {
TextEditor(text: $text)
// prompt
if text.isEmpty {
Text("Enter text to be spoken...") .foregroundColor(Color.gray)
.padding(.horizontal, 6)
}
}
HStack {
Spacer()
Button("Stop") {
print("stop button clicked")
}
Button("Speak") {
if text.isEmpty {
print("string from TextEditor is empty")
} else {
print("string is \(text)")
}
}
}
}
.padding()
}
}
2つのStopボタンとSpeakボタンで制御するのだが、喋っている時はSpeakボタンは押下できない。黙っている時はStopボタンは押下できないとしたのだが、SwiftUIは監視するステートに変化がないと描画が更新されないのと、テキストの読み上げ状況はAVSpeechSynthesizerDelegateで受け取るため、そのためのクラスを用意した。
final class Speaker: NSObject, AVSpeechSynthesizerDelegate, ObservableObject {
@Published var isSpeaking: Bool = false
private let speechSynth = AVSpeechSynthesizer()
override init() {
super.init()
speechSynth.delegate = self
}
func speak(_ text: String) {
let utterance = AVSpeechUtterance(string: text)
let locale = Locale(identifier:Locale.preferredLanguages[0])
print("locale is \(locale)")
let code = locale.languageCode!
print("code is \(code)")
utterance.voice = AVSpeechSynthesisVoice(language: code)
speechSynth.speak(utterance)
}
func stop() {
speechSynth.stopSpeaking(at: AVSpeechBoundary.immediate)
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didStart utterance: AVSpeechUtterance) {
isSpeaking = true
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
isSpeaking = false
}
}
アプリケーションのローカライズを行っていないため、言語コードが"en-JP"とおかしなことになったため、少し工夫している。
let utterance = AVSpeechUtterance(string: text)
let locale = Locale(identifier:Locale.preferredLanguages[0])
let code = locale.languageCode!
utterance.voice = AVSpeechSynthesisVoice(language: code)
読み上げ状況に合わせてボタンの有効/無効が切り替わるようにした。
struct ContentView: View {
@State private var text = ""
@ObservedObject private var speaker: Speaker = .init()
var body: some View {
VStack {
ZStack(alignment: .topLeading) {
TextEditor(text: $text)
// prompt
if text.isEmpty {
Text("Enter text to be spoken...") .foregroundColor(Color.gray)
.padding(.horizontal, 6)
}
}
HStack {
Spacer()
Button("Stop") {
print("stop button clicked")
speaker.stop()
}.disabled(!speaker.isSpeaking)
Button("Speak") {
if text.isEmpty {
print("string from TextEditor is empty")
} else {
print("string is \(text)")
speaker.speak(text)
}
}.disabled(speaker.isSpeaking)
}
}
.padding()
}
}
ヒレガス本『Cocoa Programming for OS X (5th Edition)』のサンプルRGBWellをSwiftUIで実装してみた。
NSColorWellの操作で得られた色のRGBをNSSliderの値に反映し、NSSliderの操作で変更したRGBをNSColorWellに変異するというサンプルだ。
AppKitとUIKitでUI部品のクラスに差異があるが、SwiftUIではColorPickerやSlider、Colorと共通のクラスとなっている。ただし、Colorの内部の色情報はmacOSではNSColor、iOSではUIColorと差異があるのでRGBを値として取り出す場合は、macOSとiOSで異なる型になるようにする必要があった。
#if os(iOS)
typealias MyColor = UIColor
#elseif os(macOS)
typealias MyColor = NSColor
#else
#error("your os is not supported")
#endif
extension MyColor {
var rgba: (red: Double, green: Double, blue: Double, alpha: Double) {
var red: CGFloat = 0
var green: CGFloat = 0
var blue: CGFloat = 0
var alpha: CGFloat = 0
getRed(&red, green: &green, blue: &blue, alpha: &alpha)
return (Double(red), Double(green), Double(blue), Double(alpha))
}
}
extension Color {
var rgbValues:(red: Double, green: Double, blue: Double){
let rgba = MyColor(self).rgba
return (rgba.red, rgba.green, rgba.blue)
}
}
ColorPickerで得られるのはColor構造体だ。ColorPickerによって変更が発生したイベントを受け取るために@Observableのクラスを用意した。
@Observable
final class MyData {
var color = Color(.sRGB, red: 0.98, green: 0.9, blue: 0.2)
var red = 0.98
var green = 0.9
var blue = 0.2
}
ただし、Color構造体は更新不可なので、RGBスライダーで値が変更されても、ColorPickerが利用しているColorの値は更新できないので、スライダーによる変更は諦めた。
struct ContentView: View {
@State private var myData = MyData()
var body: some View {
HStack {
ColorPicker("", selection: $myData.color)
VStack {
HStack {
Text("R")
Slider(
value: $myData.red,
in: 0.0...1.0
).disabled(true)
}
HStack {
Text("G")
Slider(
value: $myData.green,
in: 0.0...1.0
).disabled(true)
}
HStack {
Text("B")
Slider(
value: $myData.blue,
in: 0.0...1.0
).disabled(true)
}
}
.padding()
}.padding().onAppear {
trackingColor(myData)
}
}
nonisolated private func trackingColor(_ myData: MyData) {
withObservationTracking {
_ = myData.color
} onChange: {
let rgb = myData.color.rgbValues
myData.red = rgb.red
myData.green = rgb.green
myData.blue = rgb.blue
trackingColor(myData)
}
}
}
ヒレガス本では、
let newColor = NSColor(calibratedRed: CGFloat(r),
green: CGFloat(g),
blue: CGFloat(b),
alpha: CGFloat(a))
colorWell.color = newColor
とNSColorWellのcolorを更新することで値が変更していたが、これは少々トリッキーなコードだがサンプルだったので、と考えて、SwiftUIで同じことを行うことは諦めた。
SwiftUIの学習のため、ヒレガス本『Cocoa Programming for OS X (5th Edition)』のサンプルをSwiftUIで実装していく。
第一弾はボタンと押すとテキスト領域にパスワードを生成して表示するアプリだ。
書籍ではnibまたはStoryboardと独自のWindowControllerまたはViewControllerを関連づけて、ボタン押下イベントを受け取りテキスト領域にパスワードを表示されていたが、これをSwiftUIで組んだ。
"GeneratePassword.swift"。基本的に書籍のサンプルのまま。
import Foundation
private let characters = Array("0123456789abcdefghijklmnopqrstuvwxyz" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ")
func generateRandomString(length: Int) -> String {
var string = ""
for index in 0.. Character {
let index = Int(arc4random_uniform(UInt32(characters.count)))
let character = characters[index]
return character
}
”ContentView”。ここに画面の宣言とイベントに対する操作を実装する。
import SwiftUI
struct ContentView: View {
@State private var text: String = ""
var body: some View {
VStack {
Text(text)
Button("Generate Password") {
let length = 8
let password = generateRandomString(length: length)
text = password
}
}
.padding()
}
}
#Preview {
ContentView()
}
生成したパスワードを表示するテキスト領域とパスワード生成ボタンを縦に並べている。
テキスト領域のクラスは Cocoa (AppKit) と CocoaTouch (UIKIt) で異なっているが、SwiftUIは両者のどちらかに引きずられるのではなくで、新たの機能に合わせて用意しているようだ。
アプリ内課金のレシートを自分で解析する方法を調べていたら、公式ドキュメントにIO Kitを使ったMACアドレスを取り出すコードが説明されていたので、面白いので、Swiftで実装したコマンドラインのプログラムとして動かしてみた。
import IOKit
import Foundation
// Returns an object with a +1 retain count; the caller needs to release.
func io_service(named name: String, wantBuiltIn: Bool) -> io_service_t? {
let default_port = kIOMainPortDefault
var iterator = io_iterator_t()
defer {
if iterator != IO_OBJECT_NULL {
IOObjectRelease(iterator)
}
}
// CFMutableDictionaryRef IOBSDNameMatching(mach_port_t mainPort, uint32_t options, const char *bsdName);
guard let matchingDict = IOBSDNameMatching(default_port, 0, name),
// kern_return_t IOServiceGetMatchingServices(mach_port_t mainPort, CFDictionaryRef matching, io_iterator_t *existing);
IOServiceGetMatchingServices(default_port,
matchingDict as CFDictionary,
&iterator) == KERN_SUCCESS,
iterator != IO_OBJECT_NULL
else {
return nil
}
// io_object_t IOIteratorNext(io_iterator_t iterator);
var candidate = IOIteratorNext(iterator)
while candidate != IO_OBJECT_NULL {
// CFTypeRef IORegistryEntryCreateCFProperty(io_registry_entry_t entry, CFStringRef key, CFAllocatorRef allocator, IOOptionBits options);
if let cftype = IORegistryEntryCreateCFProperty(candidate,
"IOBuiltin" as CFString,
kCFAllocatorDefault,
0) {
let isBuiltIn = cftype.takeRetainedValue() as! CFBoolean
if wantBuiltIn == CFBooleanGetValue(isBuiltIn) {
return candidate
}
}
IOObjectRelease(candidate)
candidate = IOIteratorNext(iterator)
}
return nil
}
func copy_mac_address() -> CFData? {
// Prefer built-in network interfaces.
// For example, an external Ethernet adaptor can displace
// the built-in Wi-Fi as en0.
guard let service = io_service(named: "en0", wantBuiltIn: true)
?? io_service(named: "en1", wantBuiltIn: true)
?? io_service(named: "en0", wantBuiltIn: false)
else { return nil }
defer { IOObjectRelease(service) }
// CFTypeRef IORegistryEntrySearchCFProperty(io_registry_entry_t entry, const io_name_t plane, CFStringRef key, CFAllocatorRef allocator, IOOptionBits options);
if let cftype = IORegistryEntrySearchCFProperty(
service,
kIOServicePlane,
"IOMACAddress" as CFString,
kCFAllocatorDefault,
IOOptionBits(kIORegistryIterateRecursively | kIORegistryIterateParents)) {
return (cftype as! CFData)
}
return nil
}
func hexaDecimalString(_ cfData: CFData) -> String {
let data: Data = cfData as Data;
let value = data.map {
String(format: "%.2hhx", $0)
}
.joined(separator: ":")
return value
}
if let cfData = copy_mac_address() {
print("\(cfData)")
print("\(hexaDecimalString(cfData))")
}
公共交通の標準データフォーマット General Transit Feed Specification (GTFS) を可視化してみます。
HomebrewでDuckDBをインストール。
$ brew install duckdb
岡山県の宇野バス(宇野自動車)のGTFSをダウンロード。
$ curl -L -o uno-GTFS.zip "https://api-public.odpt.org/api/v4/files/odpt/UnoBus/AllLines.zip?date=20231208"
ダウンロードしたファイルを解凍。
$ unzip uno-GTFS.zip -d uno-GTFS
内容を確認。
$ ls -l uno-GTFS
agency.txt 運行会社情報
calendar.txt 運行日情報
calendar_dates.txt 運行日付情報
fare_attributes.txt 運賃情報
fare_rules.txt 運賃ルール情報
feed_info.txt フィード情報
routes.txt 路線情報
shapes.txt 路線形状情報
stops.txt 停留所情報
stop_times.txt 時刻表情報
translations.txt
trips.txt 便情報
DuckDBを起動。
$ duckdb ViewGTFS.duckdb
v1.1.2 f680b7d08f
Enter ".help" for usage hints.
D
Spartial Spartial Extensionを有効にする。
D INSTALL spatial;
D LOAD spatial;
D
停留所情報と路線形状情報、便情報を取り込む。
D CREATE OR REPLACE TABLE stops AS SELECT * FROM read_csv('uno-GTFS/stops.txt');
D CREATE OR REPLACE TABLE shapes AS SELECT * FROM read_csv('uno-GTFS/shapes.txt');
D CREATE OR REPLACE TABLE trips AS SELECT * FROM read_csv('uno-GTFS/trips.txt');
D
内容を確認。
D SELECT * FROM stops LIMIT 5;
┌─────────┬───────────┬──────────────────────┬───────────┬──────────────────┬──────────────────┬─────────┬────────────────────────────────────────┬───────────────┬───────────────┬────────────────┐
│ stop_id │ stop_code │ stop_name │ stop_desc │ stop_lat │ stop_lon │ zone_id │ stop_url │ location_type │ platform_code │ parent_station │
│ varchar │ varchar │ varchar │ varchar │ double │ double │ varchar │ varchar │ int64 │ int64 │ varchar │
├─────────┼───────────┼──────────────────────┼───────────┼──────────────────┼──────────────────┼─────────┼────────────────────────────────────────┼───────────────┼───────────────┼────────────────┤
│ 2_01 │ │ 岡山駅 │ │ 34.6649511659916 │ 133.918614140014 │ 2_01 │ https://www.unobus.co.jp/bustei/2#2_01 │ 0 │ 11 │ │
│ 2_04 │ │ 岡山駅 │ │ 34.665088 │ 133.918707 │ 2_04 │ https://www.unobus.co.jp/bustei/2#2_04 │ 0 │ 12 │ │
│ 2_11 │ │ 岡山駅 │ │ 34.6656276536051 │ 133.918707018287 │ 2_11 │ https://www.unobus.co.jp/bustei/2#2_11 │ 0 │ 1 │ │
│ 3_03 │ │ 柳川西(北側)東岡山 │ │ 34.665708 │ 133.923902 │ 3_03 │ https://www.unobus.co.jp/bustei/3#3_03 │ 0 │ │ │
│ 4_01 │ │ 岡山駅前・ドレミの街 │ │ 34.665691 │ 133.921284 │ 4_01 │ https://www.unobus.co.jp/bustei/4#4_01 │ 0 │ 6 │ │
└─────────┴───────────┴──────────────────────┴───────────┴──────────────────┴──────────────────┴─────────┴────────────────────────────────────────┴───────────────┴───────────────┴────────────────┘
D SELECT * FROM shapes LIMIT 5;
┌──────────┬──────────────────┬──────────────────┬───────────────────┐
│ shape_id │ shape_pt_lat │ shape_pt_lon │ shape_pt_sequence │
│ int64 │ double │ double │ int64 │
├──────────┼──────────────────┼──────────────────┼───────────────────┤
│ 1041 │ 34.6649511659916 │ 133.918614140014 │ 1 │
│ 1041 │ 34.664688828413 │ 133.918397814614 │ 2 │
│ 1041 │ 34.6647661581233 │ 133.918196648608 │ 3 │
│ 1041 │ 34.6656285584571 │ 133.918808190945 │ 4 │
│ 1041 │ 34.665691 │ 133.921284 │ 5 │
└──────────┴──────────────────┴──────────────────┴───────────────────┘
D SELECT * FROM trips LIMIT 5;
┌────────────────┬────────────┬────────────────────────┬─────────────────────────┬──────────┬─────────────────┬──────────────┬──────────┐
│ route_id │ service_id │ trip_id │ trip_headsign │ block_id │ trip_short_name │ direction_id │ shape_id │
│ varchar │ varchar │ varchar │ varchar │ varchar │ varchar │ varchar │ int64 │
├────────────────┼────────────┼────────────────────────┼─────────────────────────┼──────────┼─────────────────┼──────────────┼──────────┤
│ ネオ瀬戸線_A │ 平日 │ 平日_05時40分_系統1642 │ 瀬戸駅(岡山駅 経由) │ │ │ │ 1642 │
│ 林野線_A │ 平日 │ 平日_06時00分_系統1332 │ 表町BC(新道河本 経由) │ │ │ │ 1332 │
│ 林野線_A │ 平日 │ 平日_06時00分_系統1182 │ 表町BC(新道河本 経由) │ │ │ │ 1182 │
│ ネオポリス線_A │ 平日 │ 平日_06時05分_系統1282 │ 表町BC(下市・中 経由) │ │ │ │ 1282 │
│ ネオポリス線_A │ 平日 │ 平日_06時10分_系統1632 │ 表町BC(下市・西 経由) │ │ │ │ 1632 │
└────────────────┴────────────┴────────────────────────┴─────────────────────────┴──────────┴─────────────────┴──────────────┴──────────┘
D
Kepler.gl向けのデータに変換する。
D COPY (
SELECT * FROM stops
) TO 'uno-GTFS_stops.csv' WITH (FORMAT CSV, HEADER);
D COPY (
SELECT * FROM trips
LEFT JOIN (
SELECT
shape_id,
{
"type": 'LineString',
"coordinates": list([shape_pt_lon,shape_pt_lat])
}::JSON AS "geometry"
FROM shapes GROUP BY shape_id
) AS shapes ON trips.shape_id = shapes.shape_id
) TO 'uno-GTFS_trips.csv' WITH (FORMAT CSV, HEADER);
D
https://kepler.gl/demo/ をWebブラウザで開く。
先ほど出力した uno-GTFS_stops.csv と uno-GTFS_trips.csvをDrag&Dropする。
停留所と路線形状が表示されましたが、地図が表示されていませんね。
シグナルはソフトウェア割り込みで、ある条件が生起したことをプロセスに通知する仕組みです。
シグナルを扱う最も簡素な関数はsignal.hで定義されているsignal関数です。
void (*signal(int sig, void (*handler)(int)))(int)
4.4BSDでは信頼できるsigaction関数を利用すべきですが、signal関数はプラットフォームに依存しないのと、単純ですので、signal関数を使っています。
以下はシグナルの例です。
SIGABRT | 異常終了。abortなど。 |
SIGFPE | 異常算術エラー。ゼロ割算やオーバーフローなど。 |
SIGILL | 不当な関数イメージ。不当命令など。 |
SIGINT | 対話的なアテンション。割り込みなど。 |
SIGSEGV | 不当な記憶アクセス。メモリ領域外へのアクセスなど。 |
SIGTERM | プログラムに対する終了要求。 |
簡単な例です。
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
static void onintr(int);
int main(int argc, char *argv[])
{
if (signal(SIGINT, SIG_IGN) != SIG_IGN)
signal(SIGINT, onintr);
for (;;)
sleep(1);
exit(0);
}
static void onintr(int signo)
{
if (signo == SIGINT)
printf("received SIGINT\n");
exit(1 );
}
これをビルドして実行すると無限ループするので、Ctrl+Cで中断させる。するとSIGINTを受け取って、コンソールにreceived SIGINTが印字される。
$ cc onintr.c -o onintr
$ ./onintr
^Creceived SIGINT
$
JNI (Java Native Interface) は、Java仮想マシン (JVM) で実行されるJavaコードと、C言語などのネイティブ・プログラミング言語のコードを相互運用できるようにするためのインタフェースです。
ざっくりと説明すると、以下のことができるようになります。
AndroidでCやC++のコードを使えるようにするツールセットです。
現在デフォルトのビルドツールはCMakeで、以前は、ndk-buildでした。古い資料はndk-buildを想定した説明になっていますので、CMakeに読み替えることになります。
以下のC言語の関数が、demoライブラリモジュールに用意されているものとします。
JNIEXPORT void JNICALL Java_com_example_Demo_printString(JNIEnv *env, jobject obj, jstring javaString)
{
const char *nativeString = (*env)->GetStringUTFChars(env, javaString, NULL);
printf("%s\n", nativeString);
(*env)->ReleaseStringUTFChars(env, javaString, nativeString);
}
以下のJavaコードから呼び出します。
package com.example;
public class Demo {
static {
System.loadLibrary("demo");
}
public static native printString(String s);
public static void main(String[] args) {
printString("hello, world");
}
public static void dump() {
System.out.println("hello, world");
}
}
C言語の関数が、JavaのクラスDemoのメソッドとして扱われています。
JNIEnvへのポインタを取得する。
JavaVM *g_vm;
JNIEnv *env = NULL;
g_vm->AttachCurrentThread((void **)&env, NULL);
Javaコードを呼び出す。
jclass jcDemo = env->FindClass("com/example/Demo");
jfieldID jfDemoDump = env->GetStaticFieldID(jcDemo, "dump", "(V)V");
env->CallStaticVoidMethod(jcDemo, jfDemoDump);
env->DeleteLocalRef(jcDemo);
デタッチする。
g_vm->DetachCurrentThread();
文章読み上げの機能を利用するためには、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と異なり、声色毎という方向での充実ではなさそうですね。