iOS/iPhone/iPad/watchOS/tvOS/MacOSX/Android プログラミング, Objective-C, Cocoa, Swiftなど
公共交通の標準データフォーマット 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と異なり、声色毎という方向での充実ではなさそうですね。
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