はじめに
iOSアプリの「中野くん」を偶然にみて、面白いと思っていました。似たようなアプリを作ってみようと思ったら、面白い「音」がなくて作れなさそうな悲しい状況でした。そこで、音を集めづつ遊べるアプリを作ってみました。
作ったモノ
全体構成
簡単に説明するとProcessingで音を集めて、音をBluemixに渡します。また、iOSアプリが常にBluemix上のデータの更新を確認し、更新したら更新した分をBluemixよりとってくることをやっています。 本文は「Processing側」、「Bluemix側」、「iOSアプリ側」に分けて紹介します。
利用イメージ
1.Processingで音声を録音します。
2.Processingの案内によりiOSアプリに移動します。
3.iOSアプリで、自分が作った音で音楽を作ります。
Processing側
面白い音を出せるのは子供たちだと判断し、今回音の提供者を子供に選定しました。子供が楽しめながら、目的達成する方法を考えたら、やはり、ロボットと話せすように作ると楽しめると判断し、Watson要素を追加しました。
動作の流れ
1.Watsonから挨拶及びやることの説明
2.練習
3.Watsonが質問を出し、子供はそれに答える。×3
4.終了の挨拶
1.Watsonから挨拶及びやることの説明
壁に写すつもりでしたので、背景を黒にしました。
ここでのポイント:
Watsonが喋ってるように見せるため、Watsonの画像が音に応じて大きさを変更するようにしました。
...
class watson{
public Minim minim;
public AudioInput in;
private float centerx;//中央のX座標
private float centery;//中央のY座標
private float centerxSize; //中央にいる時のdefault幅
private float centerySize;//中央にいる時のdefault高さ
private float centerxMax;//中央にいる時のMax幅
private float centeryMax;//中央にいるときのMax高さ
public void init(){
minim = new Minim(this);
in = minim.getLineIn(Minim.MONO, 512);
//マイクによってAudioInputの設定方法が異なりますので、ご注意ください。
//正しくない設定方法にした場合音を取れない場合もあります。
centerx = width/2;
centery = height/4;
centerxSize = 400;
centerySize = 400;
centerxMax = 500;
centeryMax = 500;
}
public void stayInCenter(){
float x = centerx;
float y = centery;
for(int i =0; i< 10; i++){
//1Frameで10回画像を更新するように設定しました。これより増やすこともできますが、PCへの負担が大きいので、おすすめしません。
if((centerxSize+in.left.get(i)*300<centerxMax) && (centerySize+in.left.get(i)*300 < centeryMax))
display(x,y,(centerxSize+in.left.get(i)*300),(centerySize+in.left.get(i)*300));
else display(x,y,centerxMax,centeryMax);
}
}
/**
* x , y は画像の表示位置
* sizex , sizeyは画像のサイズ
*/
protected void display(float x, float y, float sizex, float sizey){
showBackground();
pushMatrix();
translate(x,y);
imageMode(CENTER);
image(watsonimg, 0, 0 ,sizex,sizey);
popMatrix();
}
}
2.練習
これより、問題に集中してもらいたかったので、Watsonは問題が終わるまでは端っこに置きました。練習を入れた理由は最初みんな恥ずかしがりなので、より大きい声を出してもらうために設定しました。
ここもポイント:
虹色のバーは音の反応し、Siriみたいな動きをします。(これを見ることで、子供たちはもっとやる気出せました。)
FFTについての詳細の説明はリンク参考してください。
FFT fft;
int BUFSIZE = 512;
float[] rot = new float[BUFSIZE];
float[] rotSpeed = new float[BUFSIZE];
float getBand;
int distance =0;
float changesize =0.2;
int dist =16; //すべてのポイントが丸になるときれいに見えないので、丸くなるポイントに間隔を設定しました。
int rectime;
void setupFFT(){
fft = new FFT(in.bufferSize(), in.sampleRate());
}
void showFFT(){
colorMode(HSB, 900, 100, 100, 100);
// FFT 実行
fft.forward(in.mix);
// FFTのスペクトラムの幅を変数に保管します
int specSize = fft.specSize();
//0
for (int i = 0; i <512; i++)//明るい色のみ取りたかったので、512に設定しました。
{
// 線を描く位置に応じて、色相を変化させます。
float h1 = map(i, 0, specSize/changesize, 0, width);
// 線の色を設定します。
stroke(h1, 80, 80, 80);
// 線を描く x を、スペクトラム幅に応じた位置として取得します
float x = map(i+distance, 0, specSize/changesize, width/4, width*1.4);
fill(h1,70,70,70);
ellipseMode(CENTER);
if(i%dist == 0){
if(i>200 && i< 300){ //真ん中の丸がより大きい方がきれいに見えたので、真ん中の丸の大きさの調整するコードです
if(in.left.get(i)*300 <80)
ellipse(x,height-100,in.left.get(i)*300+1,in.left.get(i)*300+1);
else
ellipse(x,height-100,80,80);
}
else{
if(in.left.get(i)*300 <80)
ellipse(x,height-100,in.left.get(i)*200+1,in.left.get(i)*200+1);
else
ellipse(x,height-100,40,40); //あまりにも大きくなるより、限界を作るとよりきれいに見えます
}
}else{
ellipse(x,height-100,1,1);
}
}
}
3.Watsonが質問を出し、子供はそれに答える。×3
ここでは「練習」と同じことをやっています。iOSアプリと画像を一致させるために、各画像に事前に番号をつけて、その番号も一緒にBluemixに送ります。
ここでのポイント1:
録音するファイルで音がある部分を取ること、面倒なので、今回は質問に答えられる時間制限を設けました。
コードはほとんど参考でしたので、今回はリンクのみにします。
参考:リンク
ここでもポイント2:
最終目的はここで集めた音をiOSアプリで遊ぶことですが、Uploadにかなり時間かかりますので、出来上がった都度ファイルを送った方が良いと判断しました。しかし、そのまま送るとしばらく停止してしまうことが発生しましたので、Threadを新たにforkして送ってみました。
public class UploadThread implements Runnable {
Thread thread;
String url; //upload先
String filepath; //uploadするファイルパス
String question; //回答した質問の番号
String childnum; //子供番号
public UploadThread(String url, String filepath, String geo,String name,String question,String childnum) {
this.url = url;
this.filepath = filepath;
this.question = question;
this.childnum = childnum;
}
public void start()
{
thread = new Thread(this);
thread.start();
}
public void run(){
if(!Upload(this.url,this.filepath,this.question,this.childnum)){
println("失敗しちゃった。ごめんね。");
}
}
protected boolean Upload(String url, String filepath,String question,String childnum) {
try {
DefaultHttpClient httpClient = new DefaultHttpClient();
HttpPost httpPost = new HttpPost( url );
File upfile = new File( filepath );
MultipartEntity mentity = new MultipartEntity();
mentity.addPart("question",new StringBody(question));
mentity.addPart("childNo",new StringBody(childnum));
mentity.addPart("file", new FileBody(upfile));
httpPost.setEntity(mentity);
HttpResponse response = httpClient.execute( httpPost );
HttpEntity entity = response.getEntity();
println("----------------------------------------");
println( response.getStatusLine() );
println("----------------------------------------");
if ( entity != null ) entity.writeTo( System.out );
if ( entity != null ) entity.consumeContent();
httpClient.getConnectionManager().shutdown();
}
catch(Exception e) {
e.printStackTrace();
return false;
}
return true;
}
}
4.終了挨拶
ここでは、最初動きをつくりましたが、やはり動画の方がスムーズでしたので、動画を流しました。
↓画面イメージ
ここまでのまとめ
紹介した技術は以下のようです。
1.音声に応じて画像のサイズ変え方を紹介しました。
2.音声をSiriみたいに認識するように作りました。
3.ProcessingでThreadの使い方を紹介しました。
Bluemix側
Buemix側にはNodeJSのアプリをCloudantにバインドしデプロイしました。Cloudantサービスを利用してファイルのやりとりしてみましたが、予想外いいパフォーマンスが出て、今後も利用できそうがきがしました。
前提
Bluemixアカウントを持っていること。Cloudant NoSQL DBサービスが作れること。
動作の流れ
1.ProcessingからのPOSTリクエストを受け取る。
2.受け取った音声ファイルをCloudantにinsertしダウンロードできるURLを受け取る
3.iOSアプリからのGETリクエストを受け取ると手順2で受け取ったURLをレスポンスする
ここでのポイント
このアプリを設計する時、最初はアプリのCacheに一時的にファイルを保存して、iOSアプリに渡すつもりでしたが、大量なリクエストが発生するとパンクしてしまいますので、Bluemixのパフォーマンスを信頼してみました。その結果、ファイルのやりとりがかなりいいパフォーマンスでした。ぜひ試してみてください。
また、Cloudantを利用することで、DBの定義を直したいと思ってもすぐに対応ができるということに感動しました。
Bluemixを利用するとSTT(Speech to Text)とTTS(Text To Speech)サービスが無料で使えますので、ProcessingでWatsonの話声はすべてTTSで作りました。他にもConversation(Chatbot )サービスも利用できます。今回も最初利用したかったですが、学習に少し時間かかりますので、今回は諦めました。サービスを利用して見たら、かなりいい感じでしたので、ぜひ利用してみてください。
参考リンク
iOSアプリ側
iOSアプリが今回のメインの部分です。
アプリの使い方、動作の流れの順で説明します。
アプリの使い方
画面の下に色んな画像がありますが、それぞれの画像が収集した音声ファイルと紐付いています。矢印(>>>)は最も右側に移動するためのボタンです。新しくダウンロードされた音声ファイルが最も右側に入りますが、子供たちが早く自分が作った音が見つかるように作ったボタンです。
画面上の半分に丸が8つありますが、これは下の画像を載せる部分です。最大8つまで載せれるようにしました。また、ハチもいますが、ハチが画像ファイルの上を通ると音がなる仕組みになっています。(Processing側でハチで誘導していたので、それの引き継ぎでまたハチを使いました。)
動作の流れ
1.起動時にローカルのファイルシステムより、画像ファイルと音声ファイルを読み込みます
2.自動にBluemixにGetリクエストを送り、新たな音声ファイルがあるかを確認し、あった場合にダウンロードリンクを取得 します
3.画像を丸の上に乗せ音楽を作ります
4.ダウンロードリンクが3つ揃ったらダウンロードを開始し、ダウンロードが終わったら画面がクリアされます。(次の子供が来る合図です。)
5.2−4を繰り返します
1.起動時にローカルのファイルシステムより、画像ファイルと音声ファイルを読み込みます
最初にことアプリを利用する人には音3つしかないとかわいそうだから、事前に音を作ったのもあり、他の人が作った音も利用できるようにするために、ローカルカラ音声ファイルを読み込めるようにしました。
ここではすべてのコードを紹介するより、ローカルに既存に存在していたファイルを読み込む時と、ダウンロードしたファイルを読み込む時の違いをコードで説明します。
ローカルファイルを読み込む時
let audioPath = Bundle.main.path(forResource: file_path, ofType:"wav")!
let audioUrl = URL(fileURLWithPath: audioPath)
...
ダウンロードしたファイルを読み込む時
if let dir = FileManager.default.urls( for: .documentDirectory, in: .userDomainMask ).first {
let path_file_name = dir.appendingPathComponent( file_name )
...
}
2.自動にBluemixにGetリクエストを送り、新たな音声ファイルがあるかを確認し、あった場合にダウンロードします
ここで少し苦労した?部分はCloudantからの返事はすべてJSON形式になっていて、SwiftでJSONファイルの中身を取るのに、Javascriptより全然不便だったことでした。
そこで、まずSwiftyJSONライブラリーを導入しました。
このライブラリーを導入して、以下のように書きました
var newChildSoundFileNames: [String] = [] //ダウンロードするファイルのURL
var newChildQuestionIndexes: [Int] = [] //ダウンロードした音声ファイルに紐付く画像ファイル番号
func downloadAudio(){
print("start audio download task ...")
request.get(url: url, completionHandler: { data, response, error in
if data != nil {
let json = JSON(data:data!)
if (json["data1"] == nil || json["data2"] == nil || json["data3"] == nil){
print("data will soon coming!!")
} else {
print(json)
let urls: [String] = [String(describing: json["data1"]["url"]), String(describing: json["data2"]["url"]), String(describing: json["data3"]["url"])]
let qnos: [String] = [String(describing: json["data1"]["value"]["question"]), String(describing: json["data2"]["value"]["question"]),
String(describing: json["data3"]["value"]["question"])]
if String(describing: json["data1"]["value"]["childNo"]) == currentChildNo {
print("wait for new data ")
} else {
print("go into the bed")
newChildSoundFileNames = urls
newChildQuestionIndexes = [Int(qnos[0])!, Int(qnos[1])!, Int(qnos[2])!]
lastChildNo = currentChildNo
currentChildNo = String(describing: json["data1"]["value"]["childNo"])
self.prepareForNextChild()
}}
}
})
}
func prepareForNextChild(){
let currentChildFileUrls = newChildSoundFileNames
let currentChildQuestionIndexes = newChildQuestionIndexes
for i in (0 ..< 3).reversed(){
let fileManager = FileManager.default
let url:URL = URL(string: serverUrl + currentChildFileUrls[i])!
var downloadFileName = String(describing: url).substring(from: (String(describing: url).range(of: "audioFiles_")?.lowerBound)!)
let session = URLSession(configuration: .default)
do {
let documentDirectory = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor:nil, create:false)
let filePath = documentDirectory.appendingPathComponent(downloadFileName)
print(filePath)
let task = session.dataTask(with: url) {(data, response, error) in //ファイルをダウンロードするタスク
print("inside session")
print(downloadFileName)
do{
try data?.write(to: filePath, options: .atomic)
DispatchQueue.main.async {
self.appendSound(file:String(describing: downloadFileName), question:currentChildQuestionIndexes[i]) //音声ファイルを追加する
}
preparedForNextChild = 1 //ファイルのダウンロードができたら画面クリアのflagを1に
print(self.soundFileNames)
} catch {
print("inside error")
print(error)
}
}
task.resume()
} catch {
}
}
}
3.画像を丸の上に乗せ、音楽を作ります
ダウンロードしたファイルを丸の上に載せた時、音楽の流し方を紹介します
前提
最初8つの丸を定義する時、事前にそれと紐付くAudioPlayerを定義する。
var audioPlayers:[AVAudioPlayer]? = []
func makeAudioPlayer(idx:Int, file:String) { //idx:載せた丸の番号 file:ファイルパス
print("load audioplayer from document")
let file_name = file
if let dir = FileManager.default.urls( for: .documentDirectory, in: .userDomainMask ).first {
let path_file_name = dir.appendingPathComponent( file_name )
do {
if FileManager.default.fileExists(atPath: path_file_name.path) {
var audioError:NSError?
do {
if(idx >= (audioPlayers?.count)!) {
audioPlayers?.append(try AVAudioPlayer(contentsOf: path_file_name, fileTypeHint: "wav"))
print("append audio player to audioplayers")
} else {
audioPlayers![idx] = (try AVAudioPlayer(contentsOf: path_file_name, fileTypeHint: "wav"))
print("replace audioplayer [\(idx)]")
}
} catch let error as NSError {
audioError = error
}
// エラーが起きたとき
if let error = audioError {
print("Error \(error.localizedDescription)")
}
audioPlayers![idx].volume = 1.0
audioPlayers![idx].enableRate = true
audioPlayers![idx].rate = Float(soundRate)
audioPlayers![idx].delegate = self
audioPlayers![idx].prepareToPlay()
}
} catch let error as NSError {
//エラー処理
print(error)
}
}
}
4.ダウンロードリンクが3つ揃ったらダウンロードを開始し、ダウンロードが終わったら画面がクリアされます。
クリアの部分は比較的に簡単ですので、紹介を省略します。
感想
今回のアプリを作って感想は、まずProcessing開発の自由度がかなり高いです。あんまりにも高くて細かい動きまで考えないと行けないことです。逆にiOSはやはり開発している方が多いからかもしれませんが、ほんとにたくさんの機能を提供しています。わかりやすく開発にかかった時間を説明すると、Processingの開発に1週間かかり、iOSアプリの開発に1日かかりました。
終わりに
いつもやってる仕事と違うことやるのが、今回始めてでした。昔色々な新しいことを試すことが好きな先輩から「仕事しかできない”バカ”にならないように気をつけてね」と言われたことがあります。どこからスタートしようかと考えて、Developer Communityを探すことにしました。今回作ったモノもそのコミュニティの方とアイディア出しから、開発まで一緒にやって来ました。もし Developer Communityに興味ありましたら、Innvattyを見てみてください。