〜譜面管理/譜めくりアプリ〜 Scolip
リピートマークとかのたびに譜面めくるの面倒くさいなぁと思って作ってみました!
経緯
私、趣味でギターなどの演奏をするのですが、自分で使う譜面管理ツールが欲しいなと以前から構想していました。
特に面倒なのが「ページを行ったり来たりするたびに紙をめくらなければならない」という点。
演奏中だととにかくリピートマークとか面倒くさい。。
これは「デジタル譜面で指定した順序でめくれたら便利だろう!」というコンセプトのもと、開発を始めました。
小さいコンセプトだけど便利になるかもしれない!のがとっつきやすくてとても良いのではないかと。
そこから始めて、他にも便利にできることを思いつきや要望次第で作っていくプロジェクトにしたいなと。
まだ開発中の機能もありますが、大まかにコンセプト部分+αが完成したので、リリースすることにしました!
実際にみなさんに使っていただき、ご要望などいただけたらなとの思いです!
(ちょっと早めにiPadのSafariで使えるようにはしたく頑張っております)
2023年9月1日リリース!
Scolipとは
新しい演奏の体験、Scolipで。
演奏中の中断や気を散らす動作を最小限に。
デジタル譜面の利便性を活かしつつ、その煩わしさを解消します。
デジタルで譜面を管理する利点は数多くあります。
しかし、実際の使用時にはいくつかの問題が浮上します。
- 演奏中にデバイスの操作が必要…
- 目的の演奏箇所がすぐに見つからない…
- 譜面が散逸してしまい、どれがどこにあるか分からない…
Scolipでデジタル譜面の利便性を最大限にしよう!
Scolipの特長
- 譜面の一元管理: どこに何があるか一目瞭然。
- 譜面の即時検索: 目的の譜面にすぐにアクセス。
- 譜面上への番号マーキング: 「次へ」ボタン一つで指定箇所にジャンプ。
- Bluetoothデバイスでの操作: 手を楽器から離すことなくページを操作。
その他、さまざまな機能であなたの楽器練習を完全サポート。
今、新しい演奏体験を。
Scolipで煩わしさから解放され、集中した演奏を実現しましょう!
技術
クライアント処理
- JavaScript(React = Next.js)
- Material UI
webシステムは色々作ってきてますが、今回、完全ステートレス方式やらGCEやら課金システムやらSEO系など、あまり予備知識なかったところまで学習しながらの実装だったので、主にそこら辺に時間掛けた感じです。
あとは、このシステム全体の開発環境整備〜ビルド〜デプロイなど毎回行うタスクをシェルコマンドで完全統一できたのが良かったな。
こういうのやってるとすごく楽しい。
githubActionとかに乗せても良いんだけど、コミットやマージで必ずデプロイするとは限らないのでね。
ゆくゆくはCIの定期的な勉強も兼ねてやるのも良し。
やり途中のこと
他にも、iOSアプリ化のためにSwiftUI触ってたり、マイコン(esp32)からのコマンド受信(WebBluetoothAPI)とかやってたり、風呂敷広げるだけ広げましたが、色々制約があったので、やり途中な感じです笑
とにかくコアコンセプトを作り切ってリリースしよう!と決めるまでに時間食ってしまった。
IoT感出していけるプロジェクトにしたいなー!要望優先順位次第!
リリースまで作ってきたここまでの感想
構想は前からしてましたが、作り始めてから週1〜2日かけて、実装期間として約8ヶ月。
少し個人仕事減らして頑張ってました。
全部一人で開発するのってかなり久々でしたが、昔に比べて精神面のキープが結構きつくなってきたなと、めっちゃ実感してます笑
まず自分一人で初期リリースまでガッツリとしたフォーマットとして実装したかったことと、資金面もリアクションを考慮したいなどもあり。
そういった孤軍奮闘をした月日でした。
これからも続くと思うとちょいと折れそうになりますが、せっかく作ったのでね。まだ頑張りたいと思います。
あとやっぱり私は思いっきり技術屋さんだけなのだなーとも実感。みなさんに知ってもらう方法が全然わからないという笑
作ってる時は楽しいけど、広告するフェーズになった途端に、どうやって良いかわからず。。
仕事って一人では完結しないんだな、誰かと一緒に仕事をするのって大事だなと改めて感じています。(はぁ誰か一緒にやってくれないかなー)
社内文書検索システム(商用利用可)のソースコードを公開します!
Java8のStream覚えるためにネタとしてロト6の過去データから次回当選予想+Linebotで通知やってみた
データを関数型言語(Scala)でごにょごにょする系のことを今仕事で携わらせてもらってるので、Java8のStreamではどうやって書くんだろ?と、ふと興味を持ちました。
ただコピペするだけじゃつまらんということで、何かやる気が出るネタはないかなーと思っていたところ、ありました。宝くじです。
しょっちゅう買ってるんですが、なかなか当たらないので、過去のデータを使って予想とかできないかな?と思い始めたのです!
ここには詳しく書くつもりはないのでひとまず、作ってみたソースを公開してみました!
github.com
ざっくりと流れを書くと、
loto-dataプロジェクト
- みずほ銀行さんのロト6の結果ページをスクレイピングする
- そのデータを軽量のDBであるH2に溜め込んでいく(みずほさんのwebサイトは1年以上前の詳細データがどんどん消えてってしまうため)
- そのデータをjava内で簡単に扱えるようにRepositoryオブジェクトというMapを作る
loto-data-apiプロジェクト
- LineBotからのリクエストやブラウザからのリクエストを受け取れるようにする(こちらのページline-bot-sdk-java で chat bot - Qiitaをとても参考にさせていただきました。)
- リクエストに応じて上記loto-dataライブラリのRepositoryオブジェクトを使って、データを絞ったり加工したりする
- Stream操作を体に染み付くまで嫌という程に使いまくって!予想を行う!
- その結果をLineやブラウザに返す
- これをAWSにデプロイする(Lineがhttpsじゃないとダメなので、Let's Encryptなどを使ってhttps化するのがミソ)
とまぁこんな感じです。
そんなこんなで完成したLinebotの「ロングボ君」でございます!
(※やべーかなーやべーかなー写真とか勝手に使うの)
↓↓↓もしよければ以下のQRコードをLINEで友達追加してみてくだされ↓↓↓
会話で過去結果とか次回予想とかしますよ。
でね、結論としてはね、まじで当たらない笑
良い子はこんなくだらないことに時間を使っちゃダメよ。
引用:https://www.mizuhobank.co.jp/takarakuji/loto/loto6/index.html
JavaScriptで高階関数とかカリー化とか
今まで気にも留めずに書いていたけれど、ふと調べたら、なんとなく面白かったと思い、なんとなく久々に書いてみようと思いました!
高階関数
関数を引数に取る関数。コールバックを引数に取る関数と言った方がわかりやすいか。
以下のフィルター関数みたいに引数に関数をもらって中で処理する奴が高階関数。
function filter(orgList, callback) { var newList = []; for(var i = 0; i < orgList.length; ++i) { if (callback(orgList[i])) { newList.push(orgList[i]); } } return newList; };
これを呼び出すと、
var list1 = filter([1, 5, 8, 6, 2, 4, 7], function(num) { return num > 5; }); console.log(list1);// 8, 6, 7 var list2 = filter([1, 5, 8, 6, 2, 4, 7], function(num) { return num > 3; }); console.log(list2);// 5, 8, 6, 4, 7
こんな感じになる。
なぜ便利なのかというと、配列の中のどれを弾くかという条件に当たる部分が、filter関数から切り離されているため、
・フィルタリング条件を変えるだけで返ってくる新配列を変更できる
・条件部分以外を使い回しできるため、共通化(ソースコードの削減)ができる
ロジックの共有と個別ロジックの注入をOOPではポリモーフィズムで表現したりするが、関数渡しができる言語だと、親子関係がなくてもロジックを注入できる。
カリー化
上記の高階関数のように、手続きの大部分を共通化し、一部を外出しする方法があるが、今度は渡す側の関数もちょっと共通化してソース減らしてみようということになる。
この際、パラメータに応じた関数を返す共通関数を作っておいておけば良さげ。
function over(condition) { return function(num) { return num > condition; }; };
こんな関数を作って、
var overFive = over(5);
としてやると、もちろんoverFiveという変数は関数を格納してる。
なので、
console.log(overFive(3)); // false (3は5より小さいのでoverしてない) console.log(overFive(6)); // true (6は5より大きいのでoverしている)
だし、以下と等価になるわけだ。
console.log(over(5)(3)); // false (3は5より小さいのでoverしてない) console.log(over(5)(6)); // true (6は5より大きいのでoverしている)
動的に関数が作れるので、こんな風に3以上フィルタもすぐ作れる。
var overThree = over(3); console.log(overThree(5)); // true console.log(overThree(2)); // false
このように動的に関数を生み出す関数のこと(関数を作る行為)をカリー化という。
これを使って上記のフィルター関数呼び出しを書き換えると、
var list = filter([1, 5, 8, 6, 2, 4, 7], over(5)); console.log(list); // 8, 6, 7
うおおすげー減ったし、ちょっとソースが文章っぽくなってきた!
おまけ Arrayオブジェクトのprototypeにfilter関数を実装しちゃう
※ 2022年現在すでにArrayにはfilter関数が存在してるので実装不要です。
上記のfilter関数をjsのArrayオブジェクトに拡張して実装するともう少し幸せになれそうなので勢いで書いておく。
上記のfilter関数を以下のように書き換える。
Array.prototype.filter = function(callback) { var newList = []; for(var i = 0; i < this.length; ++i) { if (callback(this[i])) { newList.push(this[i]); } } return newList; };
すると呼び出し部分は、
var list = [1, 5, 8, 6, 2, 4, 7].filter(over(5));// この行注目! console.log(list); // 8, 6, 7
もうこれで完全に文章でしょ処理が。
「配列を、フィルタしてくれ、条件は5以上の数値だけ」と読める。
この辺の技術を体得してソースに表現できるように設計できると神。
最後に全ソース
Array.prototype.filter = function(callback) { var newList = []; for(var i = 0; i < this.length; ++i) { if (callback(this[i])) { newList.push(this[i]); } } return newList; }; function over(condition) { return function(num) { return num > condition; }; }; var list1 = [1, 5, 8, 6, 2, 4, 7].filter(over(5)); console.log(list1); // 8, 6, 7 var list2 = [1, 5, 8, 6, 2, 4, 7].filter(over(3)); console.log(list2); // 5, 8, 6, 4, 7
アプリ紹介 : 簡単シェア文字~複数端末間での楽々テキスト共有アプリ~
前回書いた記事のChannelAPIの機能を使ってアプリを作ってみました。
「 簡単シェア文字~複数端末間での楽々テキスト共有アプリ~」ってアプリです。
webサービスとして、ブラウザからも利用できます!
簡単シェア文字~複数端末間での楽々テキスト共有アプリ~ - Google Play の Android アプリ
パソコンで調べていた気になるお店のURL、スマホに送るときにいちいちメールを立ち上げて。。って面倒だったことはありませんか!?
このアプリは1回設定しておけば自分の持っている端末同士のテキスト情報のやり取りをいつでも簡単に行うことができるアプリです!
[スムーズな起動~簡単シェア]
いつも使う端末にアプリをインストールしておいたり、ブラウザのお気に入りやデスクトップにショートカットを置いていけば、いつでもスピーディに起動~テキスト送信が簡単に行えます!
[データを保存しないから安全かつスピーディ]
送信されたデータはサーバーに保存されません!端末でアプリを落とせばすぐ消去されます!
そのためスピーディな送受信を可能としています!
[シェアされた文字は簡単にコピー]
シェアした文字は他のアプリなどで使うことを想定して、簡単にコピーできる機能を用意!
URLの場合は自動でリンクになるのでタップするだけでブラウザが起動します!
[接続端末の一覧を表示]
現在あなたが接続しているシェア環境(部屋という)に接続している端末の一覧が表示可能!
[ブラウザからもどうぞ!↓↓↓]
アプリでなくてもブラウザ同士でもシェア可能ですよ!
(以下紹介ページにリンクがあります)
http://cosbroths.com/textbroadcaster/cosbroths.com
このアプリでいつでも簡単に文字を共有しましょう!
是非使ってみてくださいm(__)m
GAEjでChannelAPI利用時の端末~サーバー間接続をしっかり管理しよう - その2
さて、前回の記事にて、詳しく紹介することにした「ChannelAPIの接続管理」の詳細を書き始めます。
順を追って行きましょう!
動作サンプルはコチラ
スマホとブラウザだったり、PCブラウザで2つタブを開いたり何でもいいですが、同じ部屋IDを入力して試してみてください!
また、別の部屋に入っている場合はメッセージが届かないことまで確認してもらえると良いです。
※前回も書きましたが、面倒な人は直接ソースを見てもらった方がいいと思います!github.com
ちなみに、サーバーサイドは例によってSpringMVCを使って楽しています。
ではいきます。
A.HTMLファイル(1枚だけ!)
特に難しい記載はないので、特徴的なとこだけ抜粋。
<!-- 1.jQueryは便利だから読み込む --> <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script> <!-- 2.このURLでChannelAPIに必要な接続スクリプトを読み込む --> <script src="/_ah/channel/jsapi"></script> <script src="common.js"></script> <!-- 3.部屋に入るための自作接続クラス --> <script src="room-connector.js"></script> <!-- 4.自作接続クラスを扱うスクリプト --> <script src="index.js"></script>
1.jQuery
特筆すべきもんでもないですが、いろいろ楽になるので読み込みます。
2.jsapi
GAEでサーバーを起動すると"/_ah/channel/jsapi"というURLでChanneAPIへの接続に必要なスクリプトを取得できますので、このように読み込みます。
3.自作接続クラス
特に説明はしませんが、これを使ってサーバー上の部屋との通信を行います。
中身も大したことないのですが、見たい方はソース見てみてくださいね!(prototype使ってないのはすいません汗)
4.それを扱うindex.jsスクリプト
以下に説明します。
※cssはクソほども書いていないので省略!
B.index.jsファイル
$(document).ready(function() { // 1.まずは簡単接続用のクラスを生成! var roomConnector = new RoomConnector(); // 2.端末IDを画面上に表示 $('#device-id').text(roomConnector.getDeviceId()); // 3.サーバーからのプッシュメッセージを受けたときに何をするかをここに記述 roomConnector.setOnMessage(function(data) { if (data.substring(0, 21) === 'joinRoomAndDeviceIds:') { // 4.部屋に接続している端末一覧が送られてきたらそれを画面上に表示 var joinRoomAndDeviceIds = data.substring(21).replaceAll([ '[', ']', ' ' ], '').split(','); $('#join-devices').empty(); joinRoomAndDeviceIds.forEach(function(roomAndDeviceId) { var deviceId = roomAndDeviceId.replaceAll(roomConnector.getRoomId() + ':', ''); $('#join-devices').append('<div>・' + deviceId + '</div>'); }); } else if (data.substring(0, 8) === 'message:') { // 5.メッセージが送られてきたらそれを画面上に表示 var message = data.substring(8); var html = '<div>' + message + ':' + new Date().toLocaleString() + '</div>'; $('#receive').append(html); } }); // 6.部屋に入るボタンを押したとき $('#btn-enter-room').on('click', function() { var roomId = $('#enter-room-id').val(); if (!roomId) { alert('部屋IDが空です'); return; } // 7.セミコロンが部屋IDに指定されているとバグるのでそれを回避 if (roomId.indexOf(':') > -1) { alert('部屋IDにコロン(:)は利用できません'); return; } roomConnector.open(roomId); $('#enter-room').hide(); $('#current-room-id').text(roomId); $('#current-room').show(); }); // 8.送信ボタンを押したとき $('#btn-send').on('click', function() { var message = $('#send').val(); roomConnector.send(message + ':' + roomConnector.getDeviceId()); $('#send').val(''); }); });
1.先ほど読み込んだ自作接続クラスをインスタンス化します。
3.サーバーからメッセージが届いたときの処理を自作接続クラスにセットします。
4.joinRoomAndDeviceIdsという接頭文字で始まるメッセージを受け取ったら、参加者一覧を更新する
5.messageという接頭文字で始まるメッセージを受け取ったら、受信欄にメッセージを追記する
6.部屋に入るボタンを押して初めてメッセージのやり取りが可能になる
8.送信ボタンを押したら自作接続クラスのsendメソッドを呼ぶ
クライアント側のソースは大体こんな感じです!
続いて、サーバーサイド行きます。
サーバーサイドは大きく4つの役割で構成されています。
・Room - 部屋自体を表すオブジェクト
・RoomManager - サーバー上に存在するすべての部屋を把握している奴
・RoomConnectController - 部屋への参加、メッセージの送信を受け付ける奴
・ChannelController - チャンネルの接続やブラウザの切断を検知して適切に部屋参加者を操作する奴(ここ大事!)
てな感じです
C.RoomConnectController.java
package sample; import java.util.Map; import org.springframework.http.MediaType; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; /** * * @author jazzmaster0601 * */ @Controller @RequestMapping("/room-connect") public class RoomConnectController { private String getRoomId(Map<String, String> param) { return param.get("roomId"); } /** * 部屋へ参加させます * @param param * @return */ @RequestMapping(value = "open", method = RequestMethod.POST, produces = MediaType.TEXT_PLAIN_VALUE) @ResponseBody public String open(@RequestBody Map<String, String> param) { String roomId = getRoomId(param); String roomAndDeviceId = param.get("roomAndDeviceId"); return RoomManager.getInstance().tryToOpenRoom(roomId).join(roomAndDeviceId); } /** * 部屋へのメッセージを受け取り参加者に配信します * @param param * @return */ @RequestMapping(value = "send", method = RequestMethod.POST, produces = MediaType.TEXT_PLAIN_VALUE) @ResponseBody public String send(@RequestBody Map<String, String> param) { String roomId = getRoomId(param); String message = param.get("message"); RoomManager.getInstance().getRoom(roomId).sendMessage(message); return "success"; } }
1.部屋への参加
リクエストで飛んできた部屋IDに該当する部屋をなければ作ってそこに「roomAndDeviceId」の端末を参加させます。
2.部屋へのメッセージ配信
リクエストで飛んできた部屋IDに該当する部屋の参加者に、リクエストで飛んできたメッセージをプッシュします。
※roomAndDeviceIdってなんやねん?
→次のChannelControllerの説明で詳しく言います。
D.ChannelController.java
package sample; import java.io.IOException; import javax.servlet.http.HttpServletRequest; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import com.google.appengine.api.channel.ChannelServiceFactory; /** * * @author jazzmaster0601 * */ @Controller @RequestMapping("/_ah/channel") public class ChannelController { private String getRoomAndDeviceId(HttpServletRequest req) throws IOException { return ChannelServiceFactory.getChannelService().parsePresence(req).clientId(); } private String getRoomId(String deviceId) { return deviceId.split(":")[0]; } /** * 部屋への接続が完了したときに呼ばれます * @param req * @return * @throws IOException */ @RequestMapping(value = "connected", method = RequestMethod.POST) @ResponseBody public String connect(HttpServletRequest req) throws IOException { String roomAndDeviceId = getRoomAndDeviceId(req); String roomId = getRoomId(roomAndDeviceId); Room room = RoomManager.getInstance().getRoom(roomId); room.sendJoinRoomAndDeviceIds(); return "success"; } /** * ブラウザが閉じられたときに呼ばれます * @param req * @return * @throws IOException */ @RequestMapping(value = "disconnected", method = RequestMethod.POST) @ResponseBody public String disconnect(HttpServletRequest req) throws IOException { String roomAndDeviceId = getRoomAndDeviceId(req); String roomId = getRoomId(roomAndDeviceId); RoomManager manager = RoomManager.getInstance(); Room room = manager.getRoom(roomId); room.exit(roomAndDeviceId); manager.tryToCloseRoom(roomId); room = manager.getRoom(roomId); if (room != null) { room.sendJoinRoomAndDeviceIds(); } return "success"; } }
まず大事なのが、GAEのChannelAPIは接続が確立したとき、"/_ah/channel/connected"というURLに対して、POSTリクエストが飛ぶような設定が可能です。
その設定方法は、WEB-INF以下のappengine-web.xmlに
<inbound-services> <service>channel_presence</service> </inbound-services>
という記載をすることで動作するようになります。
1.部屋への接続が完了
この際に部屋への参加者が増加したはずなので、参加者全員にそのメッセージを送っています。
2.ブラウザが閉じられたとき
上記XMLの設定をしていると、Channelの接続が切断された際に"/_ah/channel/disconnected"というURLに対して、POSTリクエストが飛ぶようになっています。これを検知して、部屋から適切に参加者を退出させるのです。
そして、参加者が減少したはずなのでその旨を部屋に残っている参加者にメッセージングするのです。
※roomAndDeviceIdにそろそろ答えんかい!
ChannelControllerで処理をする際に、「どの部屋に」送るかという情報が必要なのは分かると思います。
しかし、このリクエストの中では、clientIdという端末のIDしか知ることができません。
そこでセッションを使う方法を考えたのですが、このChannelControllerに対するリクエストはRoomConnectControllerに対するリクエストと別のセッションを作成してしまうようなのです。
さて困ったということで、javascript側で生成できる端末固有のIDに部屋IDも一定のルールで付けてしまえ!という考えの元、roomAndDeviceIdというものにしたわけですね。
これは、
「部屋ID:端末ID」
というただの文字列です。
以上が大体の流れです!
しつこく再度載せておきますね。
ソースはコチラgithub.com
追記:それを踏まえて作ったアプリを紹介します。jazzmaster0601.hatenablog.com
今時いろいろなアプリやサービスがあるので、上記のようなアプリでできることは楽勝なのでしょうが、逆にそれしかできないアプリを超簡素化して作りました。
是非♪
GAEjでChannelAPI利用時の端末~サーバー間接続をしっかり管理しよう - その1
まず、GoogleAppEngine には、いわゆる昨今のWebSocketのようにサーバーからプッシュし、ネット閲覧中の画面を他動的に変更する方法が用意されています。それをChannelAPIといいます。
難しく言いましたが、つまり、ブラウザを立ち上げてそのままほったらかしておいても、サーバー側から何か情報を与えて画面上の情報を変更できる機能を作ることができるのです。
例えば、チャットだったら、ブラウザを立ち上げておけば誰かがメッセージを送信した際に、勝手に画面にそのメッセージが表示されたり、今何か買いたいものを探している人に効率的にマッチした広告を見せてあげたりといったことが可能になるのです。
一番のポイントとしては、サーバーという機械が勝手に何か情報をスマホなどの端末に与えるのではなく、リアルタイムにどこか別の場所にいる何者か人間が情報を与えることができることではないかと思います。
リアルタイムかつインタラクティブにゴージャスな情報を与え合うことができる技術として個人的にはかなり今後イクんじゃないか?と期待しております。(与える側のインセンティブをどうするとかビジネス的な観点はあまり得意でなく考えてないのですが笑)
前置きは長くなりましたが、このWebSocketやChannelAPIなどざっくりいうと、
端末⇔サーバー⇔端末
のように、今どの端末が接続されているかをサーバー自身が管理することで、適切な端末に対して適切なメッセージを送ることができるわけです。
この「サーバーで管理する」という部分、用途には依りますが、個人で実装しなければならないのがとても面倒です!
例えばチャットアプリを作った場合の必要条件は、
・同じ部屋に入った人だけにメッセージが送信されること
→同じ部屋に入っている人をサーバーで管理する必要
・ブラウザを切った場合に部屋から退場されること
→ブラウザがOFFになったという通知をサーバーが適切に受け取ることができること
辺りになると思うのですが、案外これがかったるい!
自分がアプリを作っていたところ、この辺に面倒くささを感じたので、ちょっとサンプルアプリとソースを公開してみようと思ったので、ここにそれを紹介しようと思いました。
次回に順を追って詳しく記載しようと思うのですが、とりあえずソースだけ読みたい場合は、
直でここを見てもらえればと思います!