WebAPIで学位記を読む

NBUゆるゆるかれんだー Advent Calendar 2020の5日目の記事が埋まっていなかったので穴埋め用に2020.3頃に取り組んでたネタを記事に書き残しておく。

名古屋文理大学は2020年年度の卒業式と卒業パーティーはコロナ禍により中止となった。例年通りなら情報メディア学科の卒業生はFLOSホールにて卒業式開始前のホームルームで長谷川学部長が学位記を読み上げ一人一人に手渡していたが残念ながらそれもなくなり、郵送で送付というさみしい形となった。

リモート卒業式の企画も無かったのでセルフ卒業式用に学位記読み上げサービスでも作ろうかと思った次第。

3月中は私もリモートワークで卒業式までには出来上がるだろうと作業を自宅で進めたが結局間に合わず、Twitterにネタとしてリンクを投稿したにとどまっている。

学位記読み上げサービス ver. 1

http://kobashi.nagoya-bunri.ac.jp/forked-web-speech-api-speechsynthesis/dist/

テキストボックスにデフォルトで入力されている文面は名古屋文理大学の情報メディア学科の学位記の文面です。適当に書き換えても上手くいくと思います。

handclapボタンを押すと各種拍手を再生します。少人数から大人数までの3種類の拍手を用意しました。

拍手を連打しても音割れしない様にコンプレッサーをかけてます。

作り方

codepenというHTMLとCSSおよびJSのコードを試すことができるサイトで作りました。こんな感じで作ってました。

そう言えば2019年度までのプログラミング入門(2020年度では扱わず)では第11回にProcessing.jsでこんなことやってたのだった。

その際にデモ用にcodepenでこんなのを作ってた(注意:音が出ます。キーを押したりクリックした座標に応じて音色が変化します。WebAudio APIによる音声合成。)

効果音ファイルの置き場所問題

codepenは有料プランにユーザアカウントをアップグレードしないとデータファイルを使うことが出来ません(サーバにデータファイルを置けない)。

文面の読み上げはWebSpeech APIで行うので音声ファイルは不要ですが、読み上げ終了後の「拍手の演出」は効果音の音源ファイルをWebAudio APIで再生しています。単発のClap音にランダムなタイミングと高さを加えてJavaScriptで再生して色々なパターンの拍手をコードで生成したかったからです。

そこでVer.1ではCodePenにサービス用のコードとデータを置くことを諦めて、CodePenのコードを情報メディア学科でレンタルしているサーバにコピーしてデータファイルと共に置くことにしました。

http://kobashi.nagoya-bunri.ac.jp/

レンタルサーバ1台にサブドメインを複数設定して教員毎のWeb領域を持たせている。

情報メディア学科の一部の先生方(吉田先生と吉川先生と青山先生辺り、プロジェクト運営とか外部向け企画用かな)が利用しています。私はほとんど使っていないのですが、こんなネタとかを置いて試すときに便利ですね。

CodePenのJSコードからレンタルサーバの音声データだけを利用すればよいように思うかもしれませんが、XSS(クロスサイトスクリプティング)のセキュリティ対応のため通常は外部サーバのコンテンツにアクセスできないようになっています。サーバの設定で許可すればいいのですがCodePenでは無理です。

Ver.1のレシピ

http://kobashi.nagoya-bunri.ac.jp/forked-web-speech-api-speechsynthesis/dist/index.html

テキストエリアやボタンをFORMで配置する。昔ながらの作り方。

jQueryを使用している。理由は参考にしたサイトで使っていたから。ずいぶんと久しぶりに使った。15年ぶりくらい。

http://kobashi.nagoya-bunri.ac.jp/forked-web-speech-api-speechsynthesis/dist/script.js

参考サイトをググって2つほど見つけて混ぜた結果。

http://kobashi.nagoya-bunri.ac.jp/forked-web-speech-api-speechsynthesis/dist/style.css

ヤル気の無いCSS。何もしていない。Ver.2でMaterializeを使ってレスポンシブその他に直した。

http://kobashi.nagoya-bunri.ac.jp/data/clap02_fade.wav

フリーの効果音をどこかで拾ってきた。どこかは忘れた。


参考サイト

Webページでブラウザの音声合成機能を使おう - Web Speech API Speech Synthesis

Web Audio API を使用したゲーム用音声の開発

CSS フレームワーク Materialize でセレクトボックスの表示を動的に変更する方法 (Ver.2でMaterializeに苦戦したときに助かった)

学位記読み上げサービス ver. 2

https://codepen.io/_kobashi-kaz/full/XWbPPPx

iPadやWindowsパソコン、Androidスマフォで動作を確認しました。ブラウザはChromeでないと動かないかも。Safariでも試した記憶はあります。

読み上げ音声の種類と声質はプラットフォームに依存します。男性・女性など切り替えが出来たり出来なかったりします。

Ver.1が残念な見栄えだったのでCSS FrameworkをググってMaterializeというものを適用してみました。あまり流行ってないかな。横方向が12個のグリッドレイアウトを基本とするデザインです。もちろんレスポンシブです。この講義用サイトも多分マテリアルデザイン。


参考サイト

マテリアルデザインをサクッと実装できるCSSフレームワーク6選【2017年版】

Ver.2のレシピ

CodePenで実装しました。

https://codepen.io/_kobashi-kaz/details/XWbPPPx

HTMLとかCSSとかJSのコードはリンク先をご覧ください。

おまけ: CodePenの無料プランでデータファイルを扱う裏技

Ver.1では効果音データファイルをCodePenで置くことは諦めましたが、工夫してできるようにしてみました。

解説

JSファイルの中にとんでもなく長い1行が有ります。この行は285255文字も有ります。

const base64 = "UklGRp5DAwBXQVZFZm10IBAAAAABAAEARKwAA 長すぎるので中略 A=";

これは効果音ファイル clap02_fade.wav をBASE64というコードに変換したものです。画像や音声などのバイナリ形式のファイルは通常はテキストエディタで文字のように編集は出来ません。BASE64はWebページのHTMLやCSSの埋め込み画像データやメールの添付ファイルのデータ化で利用されています。ここでは音声データをJSコードに埋め込むためにBASE64文字列化しています。

次の2行は、文字列化したデータを元のバイナリデータに戻すためのコードです。色々ググった果てにこうなりました。なんでこうなったか記憶があやふやですが解説も書いておきます。MP3やWAVファイルならファイルローダが自動的に音声バイナリデータにしてくれるので楽ですね。

const audioArrayBuffer = Uint8Array.from(atob(base64).split(""), e => e.charCodeAt(0)).buffer;


context.decodeAudioData(audioArrayBuffer, audioBuffer => {

ctx.buffers = [audioBuffer];

});

1行目の内容:

atob関数でBASE64文字列をASCII 文字列 にします。まだ元のバイナリデータではありません。

ASCII 文字列をsplitで一文字ずつに分解して文字列の配列に変換します。文字の配列ではなくまだ文字列の配列です。

JSのUint8Arrayクラスのfromメソッド(ビルダー)で文字列の配列から数値配列オブジェクトに変換します。その際にcharCodeAtで文字列を数値に変換しています。

最後に Uint8Arrayオブジェクトの配列構造bufferから符号なし8ビット配列を取得します。

2行目の内容:

1行目で変換してできた数値(符号なし8ビット)の配列を次のコードでオーディオ用のデータに変換します。

ブラウザのWebAudio環境と接続した contextから decodeAudioData メソッドを使用してaudioArrayBufferからaudioBufferに変換します。

変換して出来たオーディオバッファを ctx.buffers に配列に入れて格納していますが、これは元にしたコードが複数の音源ファイルに対応していたからです。


上のコードはSafariがJavaScriptのPromise構文に対応していない様子なので古い構文で書いてあります。Chromeなら以下のコードで動きます。

const audioArrayBuffer = Uint8Array.from(atob(base64).split(""), e => e.charCodeAt(0)).buffer;


context.decodeAudioData(audioArrayBuffer).then(audioBuffer => {

ctx.buffers = [audioBuffer];

});

Promise構文は読みやすいですね(thenの部分)。


参照

https://qiita.com/PlanetMeron/items/2905e2d0aa7fe46a36d4

このようなサイトでファイルをBASE64文字列に変換できる