first commit of recorder

This commit is contained in:
wataru 2023-02-09 03:11:38 +09:00
parent 21e7c9389f
commit 5b85d58b8b
76 changed files with 22745 additions and 156 deletions

9
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,9 @@
{
"files.associations": {
"*.css": "postcss"
},
"workbench.colorCustomizations": {
"tab.activeBackground": "#65952acc"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

View File

@ -83,8 +83,6 @@ PERFORMANCE OF THIS SOFTWARE.
/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*!***********************!*\
!*** ./src/drawer.js ***!
\***********************/

View File

@ -1,153 +0,0 @@
from flask import Flask, request, Markup, abort, jsonify, send_from_directory
from flask_cors import CORS
import logging
from logging.config import dictConfig
import sys, os
import base64
import traceback
DATA_ROOT = "./"
dictConfig({
'version': 1,
'formatters': {'default': {
'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s',
}},
'handlers': {'wsgi': {
'class': 'logging.StreamHandler',
'stream': 'ext://flask.logging.wsgi_errors_stream',
'formatter': 'default'
}},
'root': {
'level': 'DEBUG',
'handlers': ['wsgi']
}
})
app = Flask(__name__)
@app.route("/<path:path>")
def static_dir(path):
return send_from_directory("../docs", path)
@app.route('/', methods=['GET'])
def redirect_to_index():
return send_from_directory("../frontend/dist", 'index.html')
CORS(app, resources={r"/*": {"origins": "*"}})
## !!!!!!!!!!! COLABのプロキシがRoot直下のパスしか通さない??? !!!!!!
## !!!!!!!!!!! Bodyで参照、設定コマンドを代替する。 !!!!!!
@app.route('/api', methods=['POST'])
def api():
try:
print("POST REQUEST PROCESSING....\n")
command = request.json['command']
print(request)
# GET VOICE
if command == "GET_VOICE":
title = request.json['title']
prefix = request.json['prefix']
index = request.json['index']
data_dir = os.path.join(DATA_ROOT, title)
os.makedirs(data_dir,exist_ok=True)
filename = f"{prefix}{index:03}.zip"
fullpath = os.path.join(data_dir, filename)
is_file = os.path.isfile(fullpath)
if is_file == False:
data = {
"message":"NOT_FOUND",
}
return jsonify(data)
f = open(fullpath, 'rb')
data = f.read()
dataBase64 = base64.b64encode(data).decode('utf-8')
data = {
"message":"OK",
"data":dataBase64,
}
return jsonify(data)
# POST VOICE
if command == "POST_VOICE":
title = request.json['title']
prefix = request.json['prefix']
index = request.json['index']
data = base64.b64decode(request.json['data'])
data_dir = os.path.join(DATA_ROOT, title)
os.makedirs(data_dir,exist_ok=True)
filename = f"{prefix}{index:03}.zip"
fullpath = os.path.join(data_dir, filename)
f = open(fullpath, 'wb')
f.write(data)
f.close()
data = {
"message":"OK_TEST"
}
return jsonify(data)
except Exception as e:
print("REQUEST PROCESSING!!!! EXCEPTION!!!", e)
print(traceback.format_exc())
return str(e)
## !!!!!!!!!!! COLABのプロキシがRoot直下のパスしか通さない??? !!!!!!
## !!!!!!!!!!! Bodyで参照、設定コマンドを代替する。 !!!!!!
# @app.route('/api/voice/<string:title>/<string:prefix>/<int:index>', methods=['POST'])
# def post_voice(title, prefix, index):
# try:
# filename = f"{prefix}{index:03}.zip"
# data_dir = os.path.join(DATA_ROOT, title)
# os.makedirs(data_dir,exist_ok=True)
# fullpath = os.path.join(data_dir, filename)
# data = base64.b64decode(request.json['data'])
# f = open(fullpath, 'wb')
# f.write(data)
# f.close()
# data = {
# "message":"OK"
# }
# return jsonify(data)
# except Exception as e:
# print("REQUEST PROCESSING!!!! EXCEPTION!!!", e)
# print(traceback.format_exc())
# return str(e)
# @app.route('/api/voice/<string:title>/<string:prefix>/<int:index>', methods=['GET'])
# def get_voice(title, prefix, index):
# filename = f"{prefix}{index:03}.zip"
# data_dir = os.path.join(DATA_ROOT, title)
# fullpath = os.path.join(data_dir, filename)
# is_file = os.path.isfile(fullpath)
# if is_file == False:
# data = {
# "message":"NOT_FOUND",
# }
# return jsonify(data)
# f = open(fullpath, 'rb')
# data = f.read()
# dataBase64 = base64.b64encode(data).decode('utf-8')
# data = {
# "message":"OK",
# "data":dataBase64,
# }
# return jsonify(data)
if __name__ == '__main__':
args = sys.argv
PORT = args[1]
DATA_ROOT = args[2]
app.logger.info('START APP')
app.run(debug=True, host='0.0.0.0',port=PORT)

18
recorder/.eslintrc.js Normal file
View File

@ -0,0 +1,18 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: ["eslint:recommended", "plugin:react/recommended", "plugin:@typescript-eslint/recommended"],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 13,
sourceType: "module",
},
plugins: ["react", "@typescript-eslint"],
rules: {},
};

3
recorder/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/node_modules
*#
*~

6
recorder/.prettierrc Normal file
View File

@ -0,0 +1,6 @@
{
"tabWidth": 4,
"useTabs": false,
"semi": true,
"printWidth": 360
}

View File

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2012-2022, katspaugh and contributors
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

17427
recorder/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

64
recorder/package.json Normal file
View File

@ -0,0 +1,64 @@
{
"name": "voice-recorder",
"version": "1.0.0",
"description": "",
"main": "backend/dist/index.js",
"scripts": {
"clean": "rimraf ../docs/*",
"webpack": "webpack --config webpack.prod.js",
"build": "run-s clean webpack",
"start": "webpack-dev-server --config webpack.dev.js",
"start:backend": "python3 backend/recorderServer.py 8000",
"copy": "cp -r ./docs/* ../voice-changer/docs/ && cp ./backend/recorderServer.py ../voice-changer/docs/",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/plugin-transform-runtime": "^7.19.6",
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.18.6",
"@types/node": "^18.13.0",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@types/readable-stream": "^2.3.15",
"@types/wavesurfer.js": "^6.0.3",
"autoprefixer": "^10.4.13",
"babel-loader": "^9.1.2",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.7.3",
"html-loader": "^4.2.0",
"html-webpack-plugin": "^5.5.0",
"npm-run-all": "^4.1.5",
"postcss-loader": "^7.0.2",
"postcss-nested": "^6.0.0",
"rimraf": "^4.1.2",
"style-loader": "^3.3.1",
"ts-loader": "^9.4.2",
"typescript": "^4.9.5",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.11.1"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.3.0",
"@fortawesome/free-brands-svg-icons": "^6.3.0",
"@fortawesome/free-regular-svg-icons": "^6.3.0",
"@fortawesome/free-solid-svg-icons": "^6.3.0",
"@fortawesome/react-fontawesome": "^0.2.0",
"amazon-chime-sdk-js": "^3.10.0",
"buffer": "^6.0.3",
"fft.js": "^4.0.4",
"jszip": "^3.10.1",
"localforage": "^1.10.0",
"microphone-stream": "^6.0.1",
"process": "^0.11.10",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^3.1.4",
"readable-stream": "^4.3.0",
"wavesurfer.js": "^6.4.0"
}
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
autoprefixer: {},
"postcss-nested": {},
},
};

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>

After

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-github"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path></svg>

After

Width:  |  Height:  |  Size: 522 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-help-circle"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>

After

Width:  |  Height:  |  Size: 365 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-home"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>

After

Width:  |  Height:  |  Size: 327 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-linkedin"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path><rect x="2" y="9" width="4" height="12"></rect><circle cx="4" cy="4" r="2"></circle></svg>

After

Width:  |  Height:  |  Size: 395 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-twitter"><path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"></path></svg>

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -0,0 +1,28 @@
{
"app_title": "voice-recorder",
"storage_type":"local",
"use_mel_spectrogram":true,
"current_text":"ITA-emotion",
"current_text_index":0,
"sample_rate":48000,
"text": [
{
"title": "ITA-emotion",
"wavPrefix": "emotion",
"file": "./assets/text/ITA_emotion_all.txt",
"file_hira": "./assets/text/ITA_emotion_all_hira.txt"
},
{
"title": "ITA-recitation",
"wavPrefix": "recitation",
"file": "./assets/text/ITA_recitation_all.txt",
"file_hira": "./assets/text/ITA_recitation_all_hira.txt"
},
{
"title": "wagahaiwa",
"wavPrefix": "wagahaiwa",
"file": "./assets/text/wagahaiwa.txt",
"file_hira": "./assets/text/wagahaiwa_hira.txt"
}
]
}

View File

@ -0,0 +1,100 @@
えっ嘘でしょ。
シュヴァイツァーは見習うべき人間です。
デーヴィスさんはとても疲れているように見える。
スティーヴはジェーンから手紙をもらった。
彼女はモーツァルトやベートーヴェンといった、古典派の作曲家が好きだ。
ストラットフォード・オン・エイヴォンは、シェイクスピアの生まれたところですが、毎年多くの観光客が訪れます。
彼はピューマを慣らすのに、大いに骨を折った。
彼が解雇されるとは妙な話だ。
クリスはヴァンパイア・ナイトを倒した。
彼のあだ名は言い得て妙だよね。
イタリア旅行で彼は、いくつか景勝の地として有名な都市、例えば、ナポリやフィレンツェを訪れた。
ゼロという概念は、ヒンドゥー文化に由来している。
そのいたずら娘は何食わぬ顔をした。
スミスさん、ピエール・デュボワをご紹介しますわ。私の親友なの。
どーすんの、このお店。完ッ全に閑古鳥が鳴いちゃってるじゃない。
頂上に着くと皆でヤッホーと叫んだ。
あっベルが鳴ってる。
彼女は彼にぴょこんとおじぎをした。
男子生徒のめいめいが、帽子に学校のバッジをつけています。
ヒューズが飛んだ。
私はポピュラー音楽を聞きたい。
猫はにゃーにゃーと鳴く。
私の一番上の兄が父の名代としてその会合に出席した。
彼は首相にインタビューした。
その会社の急速な成長は、その独特な戦略によるものだった。
私はいつもミネラルウォーターを持ち歩いています。
彼女はハンドバッグを開けて家の鍵を探してみたが、見つからなかった。
彼女はスタッフをまとめていけると思いますか?
牛乳はあなたの体に良いだろう、毎日飲んだほうがいい。
あなたは流感になりかけているか、もっと重い病気かもしれません。
彼女はその事件を、生き生きとした筆致で描写した。
彼は自らの生涯を、インドでの病人の治療に捧げるつもりだ。
奇妙な男で、彼は人から話し掛けられないと口をきかない。
我々はその山脈で土着のガイドを雇った。
彼女は息子に家で行儀よくするように言った。
彼はライフルを拾い上げ、それで標的をねらった。
私はこの本に八百円を払った。
気分が悪くて入院したが、結果的にはたいしたことはなかった。
アフィ狙いの釣り記事ですね。英語関係のコミュのあちこちにマルチポストしています。
トラベラーズチェックを現金に替えてくれるのはこの窓口ですか?
本日の映画は、サウンド・オブ・ミュージックでございます。
概してわれわれ日本人は、外国語を話すのに少し臆病すぎる。
店の人は私のことを知るようになり、私はいつも同じウェイトレスに応対してもらっていた。
この丘からは何百万という星が見える。
彼女は大学に入学したら、親から経済的に独立しようと思っていた。
サブマリンのペリスコープが、水中からにょっきり突き出ていた。
別にブルマに特別な関心があるわけじゃない。
そういうフェティシズムはないと思う。
ウッド夫人が作ってくれるおいしい田舎風の料理を食べたし、ミルクをたくさん飲みました。
私の妻が瓶をわってしまったので、台所の床は牛乳だらけになっている。
喧嘩をしていた二人の子供は、お互いにしかめっ面をして座っていた。
事業を継続しながら、事業が依拠している不動産を、切り売りしていくことなど非現実的なのだ。
しかしペパーバーグは、そのオウムを研究することによって、動物に対する考え方が変わったと言っている。
パックマンがある条件を満たすと、追ってくるモンスターを逆襲して食べることができる。
その数百年後に、フォークが西洋のテーブルに現れることになるが、ただちに受け入れられたわけではなかった。
トップのリーダーは犬の行動学ではアルファと呼ばれ、以下ベータ、ガンマと続きます。
ウィーンまでは歩くとどのくらいかかりますか?
すみません、この辺に詳しくないんです。
目標は授業設計をするときの、学生の思考を触発するメディア教材の選択および、活用方法について理解することである。
とりあえず店の前、掃除しといてくれ。打ち水も頼む。
人々はトーナメントが始まる何ヶ月も前に、これらの入場券を買う。
彼女の魅力は言葉では表現できない、とその芸術家は叫んだ。
事象として簡単なことを、いかにも難しそうに表現する人は、あまり頭がよさそうではない。
デザインも、アーチ型のロゴデザインにより、現代的で登場感、躍動感あるものに仕上げました。
そんなに慌てて運転して、一体どこへ行こうってんだよ。
時間はあるんだから、安全運転してくれよ。
ディスプレイはモニタともいい、コンピュータなどの機器から出力される静止画、または動画の映像信号を表示する機器である。
撃ち合いが少し静まったとき、パパが走ってフラットに行って、私たちにサンドイッチを持ってきてくれたわ。
ギリシャのフットボールの試合では、一方のチームの選手は、相手チームの陣地のラインの向こう側にボールを持ち込もうとしたのです。
泳者のシンディー・ニコラスは、へとへとになって泳ぎ切った後、ドーバーでかろうじて陸に上がってきたが、海峡水泳協会のスポークスマンは、彼女がとても元気であると発表した。
若い男女が人里離れた洋館で恐怖の一夜を過ごすという、ホラーの定番スタイルだ。
一つには、西洋人ではない人々が自分たち独自の文化に誇りを持ち始めてきたためと、また一つには、フォークを使わないそれらの地域は最も高い出生率の地域を抱えているという理由から、このことは当たっている。
私たちは恐怖の中で生きていて、苦しんでいるの。
太陽も花も楽しめないし、私たちの子供時代も楽しめないのよ。
弊社のエンジニアが日本国内で販売されている同様の製品と仕様を比較した結果、非常に競合力があると判断いたしました。
ノースウエスタン大学の研究者、アイリーン・ペパーバーグは、オウムは人の口まねをするだけでなく言葉の意味を学ぶことができることを発見しつつある。
名前をテョといいます。
ぴゅうぴゅう風が吹きこんでくる。
キェー。ギェー。イェーイ。
ひぇーん。びぇーん。ぴぇーん。
プリミェー。ミェルン。ニェン。
ステューデント。ダブルデュアル。
エテュード。エデュケーショナル。サブスティテューション。
ジャデャクシュ。
ブレンドデョート。
イデュスルファーゼ。
あっあの。
いっいえ。
えっえぇ。
おっおい。
んーとね。
アンツィオ。
エンツォ。
カートゥーン。
スィーディー。
ズィーブラ。
デピュティーガバナー。
エリュシオン。
ガリェント。
ラーテャン。

View File

@ -0,0 +1,100 @@
えっうそでしょ。
しゅゔぁいつぁーわみならうべきにんげんです。
でーゔぃすさんわとてもつかれているよーにみえる。
すてぃーゔわじぇーんからてがみをもらった。
かのじょわもーつぁるとやべーとーゔぇんといった、こてんはのさっきょくかがすきだ。
すとらっとふぉーどおんえいゔぉんわ、しぇいくすぴあのうまれたところですが、まいとしおおくのかんこーきゃくがおとずれます。
かれわぴゅーまをならすのに、おおいにほねをおった。
かれがかいこされるとわみょーなはなしだ。
くりすわゔぁんぱいあないとをたおした。
かれのあだなわいいえてみょーだよね。
いたりありょこーでかれわ、いくつかけーしょーのちとしてゆうめーなとし、たとえば、なぽりやふぃれんつぇをおとずれた。
ぜろとゆうがいねんわ、ひんどぅーぶんかにゆらいしている。
そのいたずらむすめわなにくわぬかおをした。
すみすさん、ぴえーるでゅぼわをごしょーかいしますわ。わたしのしんゆうなの。
どーすんの、このおみせ。かんっぜんにかんこどりがないちゃってるじゃない。
ちょーじょーにつくとみんなでやっほーとさけんだ。
あっべるがなってる。
かのじょわかれにぴょこんとおじぎをした。
だんしせーとのめーめーが、ぼーしにがっこーのばっじをつけています。
ひゅーずがとんだ。
わたしわぽぴゅらーおんがくをききたい。
ねこわにゃーにゃーとなく。
わたしのいちばんうえのあにがちちのみょーだいとしてそのかいごーにしゅっせきした。
かれわしゅしょーにいんたびゅーした。
そのかいしゃのきゅうそくなせーちょーわ、そのどくとくなせんりゃくによるものだった。
わたしわいつもみねらるうぉーたーをもちあるいています。
かのじょわはんどばっぐをあけていえのかぎをさがしてみたが、みつからなかった。
かのじょわすたっふをまとめていけるとおもいますか?
ぎゅうにゅうわあなたのからだによいだろー、まいにちのんだほーがいい。
あなたわりゅうかんになりかけているか、もっとおもいびょーきかもしれません。
かのじょわそのじけんを、いきいきとしたひっちでびょーしゃした。
かれわみずからのしょーがいを、いんどでのびょーにんのちりょーにささげるつもりだ。
きみょーなおとこで、かれわひとからはなしかけられないとくちをきかない。
われわれわそのさんみゃくでどちゃくのがいどをやとった。
かのじょわむすこにいえでぎょーぎよくするよーにいった。
かれわらいふるをひろいあげ、それでひょーてきをねらった。
わたしわこのほんにはっぴゃくえんをはらった。
きぶんがわるくてにゅういんしたが、けっかてきにわたいしたことわなかった。
あふぃねらいのつりきじですね。えーごかんけーのこみゅのあちこちにまるちぽすとしています。
とらべらーずちぇっくをげんきんにかえてくれるのわこのまどぐちですか?
ほんじつのえーがわ、さうんどおぶみゅーじっくでございます。
がいしてわれわれにほんじんわ、がいこくごをはなすのにすこしおくびょーすぎる。
みせのひとわわたしのことをしるよーになり、わたしわいつもおなじうぇいとれすにおーたいしてもらっていた。
このおかからわなんびゃくまんとゆうほしがみえる。
かのじょわだいがくににゅうがくしたら、おやからけーざいてきにどくりつしよーとおもっていた。
さぶまりんのぺりすこーぷが、すいちゅうからにょっきりつきでていた。
べつにぶるまにとくべつなかんしんがあるわけじゃない。
そーゆうふぇてぃしずむわないとおもう。
うっどふじんがつくってくれるおいしいいなかふうのりょーりをたべたし、みるくをたくさんのみました。
わたしのつまがびんをわってしまったので、だいどころのゆかわぎゅうにゅうだらけになっている。
けんかをしていたふたりのこどもわ、おたがいにしかめっつらをしてすわっていた。
じぎょーをけーぞくしながら、じぎょーがいきょしているふどーさんを、きりうりしていくことなどひげんじつてきなのだ。
しかしぺぱーばーぐわ、そのおーむをけんきゅうすることによって、どーぶつにたいするかんがえかたがかわったといっている。
ぱっくまんがあるじょーけんをみたすと、おってくるもんすたーをぎゃくしゅうしてたべることができる。
そのすうひゃくねんごに、ふぉーくがせーよーのてーぶるにあらわれることになるが、ただちにうけいれられたわけでわなかった。
とっぷのりーだーわいぬのこーどーがくでわあるふぁとよばれ、いかべーた、がんまとつづきます。
うぃーんまでわあるくとどのくらいかかりますか?
すみません、このへんにくわしくないんです。
もくひょーわじゅぎょーせっけーをするときの、がくせーのしこーをしょくはつするめでぃあきょーざいのせんたくおよび、かつよーほーほーについてりかいすることである。
とりあえずみせのまえ、そーじしといてくれ。うちみずもたのむ。
ひとびとわとーなめんとがはじまるなんかげつもまえに、これらのにゅうじょーけんをかう。
かのじょのみりょくわことばでわひょーげんできない、とそのげーじゅつかわさけんだ。
じしょーとしてかんたんなことを、いかにもむずかしそーにひょーげんするひとわ、あまりあたまがよさそーでわない。
でざいんも、あーちがたのろごでざいんにより、げんだいてきでとーじょーかん、やくどーかんあるものにしあげました。
そんなにあわててうんてんして、いったいどこえいこーってんだよ。
じかんわあるんだから、あんぜんうんてんしてくれよ。
でぃすぷれいわもにたともいい、こんぴゅーたなどのききからしゅつりょくされるせーしが、またわどーがのえーぞーしんごーをひょーじするききである。
うちあいがすこししずまったとき、ぱぱがはしってふらっとにいって、わたしたちにさんどいっちをもってきてくれたわ。
ぎりしゃのふっとぼーるのしあいでわ、いっぽーのちーむのせんしゅわ、あいてちーむのじんちのらいんのむこーがわにぼーるをもちこもーとしたのです。
えーしゃのしんでぃーにこらすわ、へとへとになっておよぎきったあと、どーばーでかろーじてりくにあがってきたが、かいきょーすいえーきょーかいのすぽーくすまんわ、かのじょがとてもげんきであるとはっぴょーした。
わかいだんじょがひとざとはなれたよーかんできょーふのいちやをすごすとゆう、ほらーのてーばんすたいるだ。
ひとつにわ、せーよーじんでわないひとびとがじぶんたちどくじのぶんかにほこりをもちはじめてきたためと、またひとつにわ、ふぉーくをつかわないそれらのちいきわもっともたかいしゅっしょーりつのちいきをかかえているとゆうりゆうから、このことわあたっている。
わたしたちわきょーふのなかでいきていて、くるしんでいるの。
たいよーもはなもたのしめないし、わたしたちのこどもじだいもたのしめないのよ。
へーしゃのえんじにあがにほんこくないではんばいされているどーよーのせーひんとしよーをひかくしたけっか、ひじょーにきょーごーりょくがあるとはんだんいたしました。
のーすうえすたんだいがくのけんきゅうしゃ、あいりーんぺぱーばーぐわ、おーむわひとのくちまねをするだけでなくことばのいみをまなぶことができることをはっけんしつつある。
なまえをてょといいます。
ぴゅうぴゅうかぜがふきこんでくる。
きぇー。ぎぇー。いぇーい。
ひぇーん。びぇーん。ぴぇーん。
ぷりみぇー。みぇるん。にぇん。
すてゅーでんと。だぶるでゅある。
えてゅーど。えでゅけーしょなる。さぶすてぃてゅーしょん。
じゃでゃくしゅ。
ぶれんどでょーと。
いでゅするふぁーぜ。
あっあの。
いっいえ。
えっえぇ。
おっおい。
んーとね。
あんつぃお。
えんつぉ。
かーとぅーん。
すぃーでぃー。
ずぃーぶら。
でぴゅてぃーがばなー。
えりゅしおん。
がりぇんと。
らーてゃん。

View File

@ -0,0 +1,324 @@
女の子がキッキッ嬉しそう。
ツァツォに旅行した。
民衆がテュルリー宮殿に侵入した。
ハイチ共和国でトゥーサンルーヴェルテュールが勝利を収められたのは、実際黄熱病のおかげだった。
レジャンドルは民衆をテュルリー宮殿に招いた。
助言はできないとデュパンは言った。
フランス人シェフと日本人シェフは全然違う。
中国の外交団にアタッシェとして派遣された。
ファシズム勢力との総力戦に臨む。
家具商人のフィシェルは、荷車と仔馬を貸してくれた。
ローカル路線にはファンも多い。
フェイントで相手をかわしてからシュートでフィニッシュした。
1877、プフェファーにより浸透現象が発見された。
揺れるフェリーに乗るのは私にとって苦行です。
ホルロ・アラ・ティタルッフォという特別なお料理も出ました。
笛の音がなるとウサギのキッドが早速ぴょんと跳ねた。
あの旅客は噂のキャフェに行くようです。
目標は一等賞です。
ウサギのキッドは気分よくピョン、またピョンと飛び続けた。
アフタヌーンティーを楽しみましょう。
彼女はティピカルなフェミニストです。
助手たちとミッツィは探している書類を見つけられなかった。
フィレンツェ、パドヴァ、ヴェネツィアはどれもイタリアの都市です。
楽譜に次のように書いてあるのが、エーフェリチェです。
ショペンハウエルとニーチェの哲学書を本棚から取り出した。
早速召使い全員に知らせましょう。
重い綿入を脱いで、あわせに着替える。
ボストンで、とあるチョプスイ屋へ入って夕飯を食った。
ろくすっぽ休憩をとらず働いた。
かつて一人で国府に侵入した。
だが、今日お前がここへ御入来になったのは、どんなご用なのかな?
サブフランチャイザーを増やして目指せ百店舗。
四国でお遍路を行脚しよう。
いつもの通りギャンギャン泣き出しました。
先生は、立ったままニュースを見ていました。
私はギョッと目を見開いた。
友達へニューイヤーカードを送ろう。
家政婦は休みにおしゃれなアウターウェアに身を包み一人で屋台を楽しみました。
ウォッカのお供には塩漬けのきゅうりがあいます。
山の向こうのミュンヒェンの人たちが攻撃をしかけた。
ボスニア国境からの攻撃により、十一月にヴァリェヴォが占領された。
シルヴィウスはデュボアと呼ばれていたフランスのユグノーの家に生まれた。
そのほかに私に出来ることはなかったのです、百合枝は涙声になった。
ガル博士百体近く。
日本政府からの百兆円を超える予算要求。
写経の美しさに私は仰天してしまった。
ソプラノ歌手ポリランダチョは歌劇アイーダの特別名歌手と評判です。
貴方には最初百ポンド渡します。
社長からの指示です。
どうも気まぐれというものは多少メフィスティックなものであるらしい。
蛙がピョコピョコ飛び回っています。
魔境に足を踏み入れる。
ヴァンダーヴォットタイム中は、いわゆるパーティーのようで晴れやかです。
スピリッツとは蒸留酒の事です。
ヌルシアのベネディクトゥスはアポロン神殿を壊し、ベネディクト会の修道院を建てた。
ちょうどそのとき、デストゥパーゴがコップをもって立ちあがりました。
パフィーのグッズが残らず部屋に落ち着いた。
エピファーノフは財布を無くした。
ポピュラーなソフトを使いセキュアな状態を復旧する。
チョコの在庫あったかな?
おめえ、この仕込みにゃあ、どのくれえ時間かかるか知ってっか。
それに、このほうが体のためにゃずっといいんだからね。
夏休みに、トラアヴェミュンへ旅行した。
ここで一緒にウェイクフィールドの叔母を待った。
八つになるウォルターと一緒に出た兄弟がいたが、ウォルターだけ発見された。
最初のジョブはウォーリアがいいと思います。
およそ六百メートル先を右折です。
新店オープンのレセプションに沢山のお客さんを招待した。
脚本作者ピエール・オービュルナンの給仕クレマンが、主人の書斎の戸を大切そうに開いた。
われわれは、天主教徒か長老教会派のもので、天主教徒が多数を占めている。
結局のところお互い五十歩百歩だ。
突拍子もない話だが、決して嘘ではない。
ネットで懸案の解決を目指す。
切れ味鋭いペティナイフは使い勝手が良い。
指をくわえてぴゅーと一声口笛を吹いた。
クレンペ教頭は無骨な男だが、自分の学問の秘密には深く浸っていた。
尻尾ふりたて、ひげくいそらす。
すべての獲物を望みどおりに狙う技術がある。
タコのグニャグニャした感触が嫌だ。
私たちは、抽象的意識的自己を否定することで、本当の自己とは身心一如だということを知る。
鹿島明神が釘で刺し貫いて、魚が動かないようにしている。
私は手始めに、同業者から話を聞く努力をした。
とてもうれしそうにぴょんぴょん跳ねて出ていった。
二分だけのオルガン演奏で終わってしまった。
ニセ教会に騙されるな。
およそ百年前には、薬剤として薬屋で売っていた。
いっそ脚本家を目指した方がいいかとも考えた。
この書物に誤謬があっても、純一でない何ものにもインフェクトしないでしょう。
仏教はインド由来の宗教です。
キャリーバッグは旅行に必須。
本番前はメチャメチャ不安になる。
三角関数においてピュタゴラスの定理は必須です。
著名なラニョン博士が患者と接していた。
おもちゃの刀を持った少年が、お百度石に寄りかかっている。
食料の補給が急務であると伝えられた。
展示会であの作品のみ不評だった。
客人をもてなすのは当然です。
旅行客が楽しめるように工夫しましょう。
こんな冗談のようなニュースはない。
小さい星をたくさん描いた、水飲みグラスはよくある。
柄は、猫の尻尾でもあるように、尖端をぶるぶると震わせながら、動いていく。
開店当初プリンが一番売れていた。
ロナルドホープ大尉が大将のマンションへ急行しました。
日刊センティナル紙のヘプバンです。
この宝石は、ひとつ百万円以上のお値段です。
チョウチョと仲良くなるんだから。
宇宙では、エントロピーは無際限に増大している。
川の中流に集落がある。
是非お話させて頂きたいと思います。
お隣さんが蒟蒻を持っていらっしゃる。
パーティーは楽しむものです。
おてつと大きく書かれた番茶茶碗は、これらの人々の前に置かれた。
私のポケットの中には携帯電話が入っています。
彼は今度は牧場へ行って、沼地で小悪魔の尻尾一つ見つけました。
五行説による占いがあるという情報あり。
テンプル君、既に真逆だと言った。
世界中の様々なモニュメントを訪ね歩いた。
アンソニー・ホプキンスは有名な俳優です。
彼女は出来るだけぴったりと耳をあてて聴きました。
茶一つ参らぬか、まあいいで。
ボヤですんでよかった。
モンタギュ・ゴーシ卿がマンチェスターに来た。
ウィスキーの水割りをガッツリ飲んだ。
これやお祭りを若いものに見せるにゃ持ってこいだ。
私はイメージカラーをピンクに決めた。
ムニャムニャ、もう食べれません。
満洲は雨季以外には雨が少ないと言われているが、わたしが満洲にあるあいだは、大戦中のせいか、ずいぶん雨が多かった。
均一居酒屋では一番売れても二人で八千円くらいだ。
願いをかなえる。
最初辛かったけど、花や園芸が好きだったから、失意が癒やされないこともない。
ペピス爺さんはもう寝るらしい。
直ぐウィルキンソンを呼びに行け。
お昼前ジャスパーさん宅へ再びお邪魔しました。
その竜の百の頭が恐ろしい。
必要なミョウバンの量はプリントに書いてあります。
マリー・ロジェはパヴェサンタンの家を出た。
読み進むにつれ、ますます興味が湧いた。
笑いかけながら一二歩近寄った。
地表を緑化して、温暖化を抑止する能力を強くする。
ハサミでプツッと切った切れ端をペットにあげた。
ホームランを打つ。
プレゼントをギャロウェイさんに渡してください。
ケプラーの法則について直接私に聞いてきた。
ウェンディーズはハンバーガー屋さんです。
しかし氷河はアルプスだけにあるものではない。
この先百年も抹茶は衰退しない。
自分を評価するのは会社であって、行き過ぎた自己表現は失脚につながる。
夜に吹く風のヒュウヒュウという音が私を不安にさせる。
可愛い華奢な女の子。
水中の金魚をすくうためのポイ。
グレンエルギンはウィスキーの蒸留所です。
一寸法師が、ヒョコヒョコと彼の方へ近づいた。
連中はリビングでぺちゃくちゃ喋って、警戒していない。
平一郎はシャツ一枚になって絹物の布団の中へ潜りこんだ。
下京区に引っ越す。
彼の言葉に一種不思議な感覚を覚えた。
この事業所には百人以上の人が勤めています。
わたしの家ばかりでなく、近所の住居といわず、商店といわず、バラックの家々ではみな草花を植えている。
どうせ私は馬の世話をせにゃならんから、外へ行こう。
ヤン・セチャンというお笑い芸人。
男が妙な顔をして、一瞬残忍になった。
普及活動に幻滅した。
雨がぽつぽつ降りだした。
ペリウィンクルやプランティンはブルーアイです。
姫や侍女たちが、キンポウゲやタンポポの花を持って、彼の方へ駆け寄っていった。
皆の協力のおかげで帰郷できた。
ウォルターとウォードが入室すると、ノラが真っ赤になった。
今の持ち札ではあがれずに終わる。
勉強中は話しかけないで。
レパードの花壇が枯れ果てた。
蒸留酒にミョウバンを加える。
取っつきにくい女中が三人いる。
レインボーブリッジは東京の名所。
卑怯者は悪党です。
突然海へ飛び込んだ。
仕事はどっさりとあります。
葉巻パイプはありましたか。
八百屋に行って百円で大根を買った。
般若とは鬼女の能面の事です。
日本へ行くには船か飛行機が必要です。
しかしパーで回るのも難しい。
のぼせないように入浴するにはお湯の量と温度が大事。
鬼太郎くんは冗談半分で盗みに入って怪我をした。
バーニ医師がピシッと答えた。
男の妙な動きが怪しい。
私の病気は先天性なのです。
三国志の関羽という将軍はすごく有名です。
過酷な業務に耐える。
町の女房らしい二人連れが、日傘を持って入ってきた。
名をツァウォツキイといった。
飲み会の参加を拒否した。
夫人が仰天したのも無理はない。
セファドールはめまいを抑える薬です。
明の一訓詁学者は、宋代典籍の一つにあげてある茶せんの形状を思い起こすに苦しんでいる。
深海魚は見た目は悪いがおいしいことが多い。
ニューヨークでイヴニングポストの記事に注目した。
確かに牛乳とコーンフレークの相性は抜群だ。
その一は、明治三十七年の九月八日か九日の夜とおぼえている。
マルメゾンの店主はジェシーとは仲良しだ。
釣竿を肩にかけた処士あり。
最新鋭機に乗り込む。
誰かが後ろへ来て、変な声で叫んだのでぞっとした。
おおでらの石段の前に立ち止まって、その出て来るのを待ちあわせた。
未解決の懸案を持って重役と対峙する。
薄月の光が庭を照らす。
犯人がどっちの部屋へ入ったかわからない。
渓谷から出た氷河が一本に合流する。
ディスカッションを進める。
九頭竜明神を祭るために灯篭をながす。
今回の資料作りは深い思慮を必要としたが、先日やっと終了した。
骨子をしっかりと組み立てる。
気球にのって空を楽しむ。
批評ばかりでなく対案も出すべき。
かれらは幕のあいだに木戸の外を散歩しているのである。
過去の数々の奇病が治るようになりつつある。
購入者はポンプの修理が必要なことに気がついた。
彼自身は、レジ業務につきたいと思っている。
地毛は金色なんです。
鉛筆は折れやすくて不便です。
ヒポクラテスは医学の父と呼ばれます。
ところが商人は、国ざかいのすぐ近くへ住まって、やはり前と同じようにやっています。
今まで明るかった二階の窓は、急にまっくらになってしまいました。
アスファルトに囲まれた中にケヤキの木が一本。
名札を用意する。
マッチョな男性はモテるそう。
社務所の人の話に嘘はなかった。
行楽シーズンの京都は人でいっぱい。
どこからかパチパチと音が聞こえる。
プロ野球はどのチームが優勝するだろう?
しかし、これではまるで私が誘拐しましたと自首して出るようなもので、そんな馬鹿なことをするやつはあるまい。
切望しつつ、主を待つ。
しばらくしてパチパチという音も止んだ。
身分をわきまえず放った狂言。
天を翔ける竜の姿は神秘的だ。
私の精神と一脈相通じるものがあると思いました。
皆も球場に行きましょう。
コペルニクスはポーランドの天文学者です。
海沿いの旅館は眺めがいい。
集中すると周りが見えない。
春木座は今日の本郷座である。
私の手を引っ張るようにして、手のひらへくれました。
北海の荒波は、その氷の絶壁の根を噛んで、激しく飛沫を散らしている。
戦意を喪失させるのが勝利への近道。
がちょうを飼う。
ふぁふぁと笑いながら楽しく手拍子。
従軍記者は大尉相当の待遇を受ける。
茶碗にかかるほど、シャツの袖のふくらかなので、掻き抱く体に茶碗を持った。
色々隠して今日まで犯人と共にいる。
浦子は寝ながら息を引いた。
兄が邪険にされた。
彼は不服そうに呟いた。
摸造品ばかりでなく、本物のドイツ将校や兵卒のヘルメットを売っているのもある。
彼女と一緒にいると落ち着きます。
困ってる人に向けて寄付をしました。
落ち込んでいるのか、うつむいてじっとしている。
改良が進むとパンはどんどんおいしくなる。
こんなことを言いながら、気の短いおじいさんは下駄を突っかけて、そそくさと出て行ってしまった。
彼女と初デートの今日は夢うつつ。
おなじ東京の名をよぶにも、今後はおそらく旧東京と新東京とに区別されるであろう。
腰振りを二分間続ける。
臆病者が逃げ出した。
絵葉書と一緒に銀色のルアーをマッシュに送った。
オムライスにはケチャップが一番。
ストレスは適度に発散しましょう。
この人よりぞ始まりける。
中学生の時、避暑旅行中に体調を崩した。
軍医は病院の門に入るのである。
一日中明るい白夜は、一切太陽が沈まないことで起こります。
もう、あなたにばかりも精一杯、誰にも見せられます体ではないんです。
みんな揃って海に飛び込んだ。
なんだそりゃ、到底無理なお願いだ。
腸チフスは怖い病気。
排球はバレーボールの事です。
マッチを買いに入ったのかな。
盆栽は風情がある。
やがて陪審員は合議をするために法廷を出て行った。
芸術の求める永遠性に疑問を感じる。
聞きつけて、件の嫗、ぶるぶるとかぶりをふった。
キェルツェを通ってドビェに、ザリピェからミェイに行く。
マリアーンスケー・ラーズニェを訪れる。
乳牛を見ながら、レテュの入ったピッツァを食べる。
ウドゥの奏者を施療した。
インスティテュートで、リデュースの話と併せて、ルデュックの話も聞いた。
ギェナーを見てイェーイと叫ぶ。
スィーディーを聞きながら、でゃーこんを食べる。
テョさんはズィーブラを見た。
レヴォリューション。レギュレーション。エデュケーション。
ブレンドデョート。ラーテャン。
あっあの。いっいえ。えっえぇ。おっおい。んーとね。
いぶかしげに見上げた雨雲から、琥珀色のドラゴンがギュンと現れた。
布でギュギュっとヌンチャクを縛る。
服を脱ぐが、いつも上下が逆さまだ。
放課後の音楽室で、高音を頑張って出した。
モゴモゴしながら言うギャグは面白くない。
海水魚の漁業の一環として、稚魚が育てられている。
ムンムンとした熱気に、あの淡水魚もへとへとになっている。
ヘスティア所長は、十二音音楽の作法を知っている。
主催者は、このフェスのキャパが小さいことを、セシルから聞いた。
母は、サフランライスと、さつまいもの入ったシチューと、ポトフをハフハフしながら食べた。
そして、左京と千紗はヘファ駅に着いた。
根本と曽原は主君を批判した。
ケケっと笑いながら、津原はパトカーに乗った。
キュキュッと鳴らした靴でパスを出した。
ティファニーはパピーにムギュッと抱き着きながら、チュチュッとキスをし、センキュと言った。
その義軍は、一ヘクタールほふく前進をした。
へへっと、きゃつは媚びへつらった。
ほとんどの被調査者は、写真を車載した。
補佐が、一酸化炭素中毒になるというハプニングは起きなかった。
スチューデントが被災するファクターを、可能な限り取り払う。
カフェとは、ブレックファストとして、フォカッチャを食べれる場所でもある。
普通、初級者では、高音を伸ばすことはできない。
彼からしたら、左中間から見る景色は貴重だった。
シェパードと同居中に、フォスターはその格付け表を見た。
去々年、虚数とヘ長調について学んだ。
脚立の上でヒュヒューと風が吹くと、彼は背筋を伸ばした。
昼にはペスカトーレを、夜には寿司をパクパク食べた。
ケフィアに関するこの本は、初版では三百部くらいだったが、次から波及的に増加した。
皮膚が私のフェチである。
社販で巨富を築くという、彼の目論見は途中でへし折られた。
左表のとおりの支出になることが、ある意味わが社の社風である。
この古風な酒瓢は故郷のものだ。
そのほつれが腐敗しているというのは、誇張した表現だと思う。
その映画の出演者である彼が、主犯である可能性はフィフティーフィフティーだ。
チュクンの波長は、パツンと共通している。

View File

@ -0,0 +1,324 @@
おんなのこがきっきっうれしそー。
つぁつぉにりょこーした。
みんしゅうがてゅるりーきゅうでんにしんにゅうした。
はいちきょーわこくでとぅーさんるーゔぇるてゅーるがしょーりをおさめられたのわ、じっさいおーねつびょーのおかげだった。
れじゃんどるわみんしゅうをてゅるりーきゅうでんにまねーた。
じょげんわできないとでゅぱんわいった。
ふらんすじんしぇふとにほんじんしぇふわぜんぜんちがう。
ちゅうごくのがいこーだんにあたっしぇとしてはけんされた。
ふぁしずむせーりょくとのそーりょくせんにのぞむ。
かぐしょーにんのふぃしぇるわ、にぐるまとこうまをかしてくれた。
ろーかるろせんにわふぁんもおおい。
ふぇいんとであいてをかわしてからしゅーとでふぃにっしゅした。
せんはっぴゃくななじゅうなな、ぷふぇふぁーによりしんとーげんしょーがはっけんされた。
ゆれるふぇりーにのるのわわたしにとってくぎょーです。
ほるろあらてぃたるっふぉとゆうとくべつなおりょーりもでました。
ふえのおとがなるとうさぎのきっどがさっそくぴょんとはねた。
あのりょきゃくわうわさのきゃふぇにいくよーです。
もくひょーわいっとーしょーです。
うさぎのきっどわきぶんよくぴょん、またぴょんととびつづけた。
あふたぬーんてぃーをたのしみましょー。
かのじょわてぃぴかるなふぇみにすとです。
じょしゅたちとみっつぃわさがしているしょるいをみつけられなかった。
ふぃれんつぇ、ぱどゔぁ、ゔぇねつぃあわどれもいたりあのとしです。
がくふにつぎのよーにかいてあるのが、えーふぇりちぇです。
しょぺんはうえるとにーちぇのてつがくしょをほんだなからとりだした。
さっそくめしつかいぜんいんにしらせましょー。
おもいわたいれをぬいで、あわせにきがえる。
ぼすとんで、とあるちょぷすいやえはいってゆうはんをくった。
ろくすっぽきゅうけーをとらずはたらいた。
かつてひとりでこくふにしんにゅうした。
だが、きょーおまえがここえごじゅらいになったのわ、どんなごよーなのかな?
さぶふらんちゃいざーをふやしてめざせひゃくてんぽ。
しこくでおへんろをあんぎゃしよー。
いつものとおりぎゃんぎゃんなきだしました。
せんせーわ、たったままにゅーすをみていました。
わたしわぎょっとめをみひらいた。
ともだちえにゅーいやーかーどをおくろー。
かせーふわやすみにおしゃれなあうたーうぇあにみをつつみひとりでやたいをたのしみました。
うぉっかのおともにわしおづけのきゅうりがあいます。
やまのむこーのみゅんひぇんのひとたちがこーげきをしかけた。
ぼすにあこっきょーからのこーげきにより、じゅういちがつにゔぁりぇゔぉがせんりょーされた。
しるゔぃうすわでゅぼあとよばれていたふらんすのゆぐのーのいえにうまれた。
そのほかにわたしにできることわなかったのです、ゆりえわなみだごえになった。
がるはかせひゃくたいちかく。
にほんせーふからのひゃくちょーえんをこえるよさんよーきゅう。
しゃきょーのうつくしさにわたしわぎょーてんしてしまった。
そぷらのかしゅぽりらんだちょわかげきあいーだのとくべつめーかしゅとひょーばんです。
あなたにわさいしょひゃくぽんどわたします。
しゃちょーからのしじです。
どーもきまぐれとゆうものわたしょーめふぃすてぃっくなものであるらしい。
かえるがぴょこぴょことびまわっています。
まきょーにあしをふみいれる。
ゔぁんだーゔぉっとたいむちゅうわ、いわゆるぱーてぃーのよーではれやかです。
すぴりっつとわじょーりゅうしゅのことです。
ぬるしあのべねでぃくとぅすわあぽろんしんでんをこわし、べねでぃくとかいのしゅうどーいんをたてた。
ちょーどそのとき、ですとぅぱーごがこっぷをもってたちあがりました。
ぱふぃーのぐっずがのこらずへやにおちついた。
えぴふぁーのふわさいふをなくした。
ぽぴゅらーなそふとをつかいせきゅあなじょーたいをふっきゅうする。
ちょこのざいこあったかな?
おめえ、このしこみにゃあ、どのくれえじかんかかるかしってっか。
それに、このほーがからだのためにゃずっといいんだからね。
なつやすみに、とらあゔぇみゅんえりょこーした。
ここでいっしょにうぇいくふぃーるどのおばをまった。
やっつになるうぉるたーといっしょにでたきょーだいがいたが、うぉるたーだけはっけんされた。
さいしょのじょぶわうぉーりあがいいとおもいます。
およそろっぴゃくめーとるさきをうせつです。
しんてんおーぷんのれせぷしょんにたくさんのおきゃくさんをしょーたいした。
きゃくほんさくしゃぴえーるおーびゅるなんのきゅうじくれまんが、しゅじんのしょさいのとをたいせつそーにひらいた。
われわれわ、てんしゅきょーとかちょーろーきょーかいはのもので、てんしゅきょーとがたすうをしめている。
けっきょくのところおたがいごじっぽひゃっぽだ。
とっぴょーしもないはなしだが、けっしてうそでわない。
ねっとでけんあんのかいけつをめざす。
きれあじするどいぺてぃないふわつかいがってがよい。
ゆびをくわえてぴゅーとひとこえくちぶえをふいた。
くれんぺきょーとーわぶこつなおとこだが、じぶんのがくもんのひみつにわふかくひたっていた。
しっぽふりたて、ひげくいそらす。
すべてのえものをのぞみどおりにねらうぎじゅつがある。
たこのぐにゃぐにゃしたかんしょくがいやだ。
わたしたちわ、ちゅうしょーてきいしきてきじこをひてーすることで、ほんとーのじことわしんじんいちにょだとゆうことをしる。
かしまみょーじんがくぎでさしつらぬいて、さかながうごかないよーにしている。
わたしわてはじめに、どーぎょーしゃからはなしをきくどりょくをした。
とてもうれしそーにぴょんぴょんはねてでていった。
にふんだけのおるがんえんそーでおわってしまった。
にせきょーかいにだまされるな。
およそひゃくねんまえにわ、やくざいとしてくすりやでうっていた。
いっそきゃくほんかをめざしたほーがいいかともかんがえた。
このしょもつにごびゅうがあっても、じゅんいつでないなにものにもいんふぇくとしないでしょー。
ぶっきょーわいんどゆらいのしゅうきょーです。
きゃりーばっぐわりょこーにひっす。
ほんばんまえわめちゃめちゃふあんになる。
さんかくかんすうにおいてぴゅたごらすのてーりわひっすです。
ちょめーならにょんはかせがかんじゃとせっしていた。
おもちゃのかたなをもったしょーねんが、おひゃくどいしによりかかっている。
しょくりょーのほきゅうがきゅうむであるとつたえられた。
てんじかいであのさくひんのみふひょーだった。
きゃくじんをもてなすのわとーぜんです。
りょこーきゃくがたのしめるよーにくふうしましょー。
こんなじょーだんのよーなにゅーすわない。
ちいさいほしをたくさんえがいた、みずのみぐらすわよくある。
えわ、ねこのしっぽでもあるよーに、せんたんをぶるぶるとふるわせながら、うごいていく。
かいてんとーしょぷりんがいちばんうれていた。
ろなるどほーぷたいいがたいしょーのまんしょんえきゅうこーしました。
にっかんせんてぃなるしのへぷばんです。
このほーせきわ、ひとつひゃくまんえんいじょーのおねだんです。
ちょーちょとなかよくなるんだから。
うちゅうでわ、えんとろぴーわむさいげんにぞーだいしている。
かわのちゅうりゅうにしゅうらくがある。
ぜひおはなしさせていただきたいとおもいます。
おとなりさんがこんにゃくをもっていらっしゃる。
ぱーてぃーわたのしむものです。
おてつとおおきくかかれたばんちゃじゃわんわ、これらのひとびとのまえにおかれた。
わたしのぽけっとのなかにわけーたいでんわがはいっています。
かれわこんどわぼくじょーえいって、ぬまちでこあくまのしっぽひとつみつけました。
ごぎょーせつによるうらないがあるとゆうじょーほーあり。
てんぷるくん、すでにまぎゃくだといった。
せかいじゅうのさまざまなもにゅめんとをたずねあるいた。
あんそにーほぷきんすわゆうめーなはいゆうです。
かのじょわできるだけぴったりとみみをあててききました。
ちゃひとつまいらぬか、まあいいで。
ぼやですんでよかった。
もんたぎゅごーしきょーがまんちぇすたーにきた。
うぃすきーのみずわりをがっつりのんだ。
これやおまつりをわかいものにみせるにゃもってこいだ。
わたしわいめーじからーをぴんくにきめた。
むにゃむにゃ、もーたべれません。
まんしゅうわうきいがいにわあめがすくないといわれているが、わたしがまんしゅうにあるあいだわ、たいせんちゅうのせーか、ずいぶんあめがおおかった。
きんいついざかやでわいちばんうれてもふたりではっせんえんくらいだ。
ねがいをかなえる。
さいしょつらかったけど、はなやえんげーがすきだったから、しついがいやされないこともない。
ぺぴすじいさんわもーねるらしい。
すぐうぃるきんそんをよびにいけ。
おひるまえじゃすぱーさんたくえふたたびおじゃましました。
そのりゅうのひゃくのあたまがおそろしい。
ひつよーなみょーばんのりょーわぷりんとにかいてあります。
まりーろじぇわぱゔぇさんたんのいえをでた。
よみすすむにつれ、ますますきょーみがわいた。
わらいかけながらいちにほちかよった。
ちひょーをりょくかして、おんだんかをよくしするのーりょくをつよくする。
はさみでぷつっときったきれはしをぺっとにあげた。
ほーむらんをうつ。
ぷれぜんとをぎゃろうぇいさんにわたしてください。
けぷらーのほーそくについてちょくせつわたしにきいてきた。
うぇんでぃーずわはんばーがーやさんです。
しかしひょーがわあるぷすだけにあるものでわない。
このさきひゃくねんもまっちゃわすいたいしない。
じぶんをひょーかするのわかいしゃであって、いきすぎたじこひょーげんわしっきゃくにつながる。
よるにふくかぜのひゅうひゅうとゆうおとがわたしをふあんにさせる。
かわいいきゃしゃなおんなのこ。
すいちゅうのきんぎょをすくうためのぽい。
ぐれんえるぎんわうぃすきーのじょーりゅうじょです。
いっすんぼーしが、ひょこひょことかれのほーえちかづいた。
れんちゅうわりびんぐでぺちゃくちゃしゃべって、けーかいしていない。
へーいちろーわしゃついちまいになってきぬもののふとんのなかえもぐりこんだ。
しもぎょーくにひっこす。
かれのことばにいっしゅふしぎなかんかくをおぼえた。
このじぎょーしょにわひゃくにんいじょーのひとがつとめています。
わたしのいえばかりでなく、きんじょのじゅうきょといわず、しょーてんといわず、ばらっくのいえいえでわみなくさばなをうえている。
どーせわたしわうまのせわをせにゃならんから、そとえいこー。
やんせちゃんとゆうおわらいげーにん。
おとこがみょーなかおをして、いっしゅんざんにんになった。
ふきゅうかつどーにげんめつした。
あめがぽつぽつふりだした。
ぺりうぃんくるやぷらんてぃんわぶるーあいです。
ひめやじじょたちが、きんぽーげやたんぽぽのはなをもって、かれのほーえかけよっていった。
みんなのきょーりょくのおかげでききょーできた。
うぉるたーとうぉーどがにゅうしつすると、のらがまっかになった。
いまのもちふだでわあがれずにおわる。
べんきょーちゅうわはなしかけないで。
れぱーどのかだんがかれはてた。
じょーりゅうしゅにみょーばんをくわえる。
とっつきにくいじょちゅうがさんにんいる。
れいんぼーぶりっじわとーきょーのめーしょ。
ひきょーものわあくとーです。
とつぜんうみえとびこんだ。
しごとわどっさりとあります。
はまきぱいぷわありましたか。
やおやにいってひゃくえんでだいこんをかった。
はんにゃとわきじょののーめんのことです。
にほんえいくにわふねかひこーきがひつよーです。
しかしぱーでまわるのもむずかしい。
のぼせないよーににゅうよくするにわおゆのりょーとおんどがだいじ。
きたろーくんわじょーだんはんぶんでぬすみにはいってけがをした。
ばーにいしがぴしっとこたえた。
おとこのみょーなうごきがあやしい。
わたしのびょーきわせんてんせーなのです。
さんごくしのかんうとゆうしょーぐんわすごくゆうめーです。
かこくなぎょーむにたえる。
まちのにょーぼーらしいふたりづれが、ひがさをもってはいってきた。
なをつぁうぉつきいといった。
のみかいのさんかをきょひした。
ふじんがぎょーてんしたのもむりわない。
せふぁどーるわめまいをおさえるくすりです。
みんのいちくんこがくしゃわ、そーだいてんせきのひとつにあげてあるちゃせんのけーじょーをおもいおこすにくるしんでいる。
しんかいぎょわみためわわるいがおいしいことがおおい。
にゅーよーくでいゔにんぐぽすとのきじにちゅうもくした。
たしかにぎゅうにゅうとこーんふれーくのあいしょーわばつぐんだ。
そのいちわ、めーじさんじゅうななねんのくがつよーかかここのかのよるとおぼえている。
まるめぞんのてんしゅわじぇしーとわなかよしだ。
つりざおをかたにかけたしょしあり。
さいしんえーきにのりこむ。
だれかがうしろえきて、へんなこえでさけんだのでぞっとした。
おおでらのいしだんのまえにたちどまって、そのでてくるのをまちあわせた。
みかいけつのけんあんをもってじゅうやくとたいじする。
うすづきのひかりがにわをてらす。
はんにんがどっちのへやえはいったかわからない。
けーこくからでたひょーががいっぽんにごーりゅうする。
でぃすかっしょんをすすめる。
くずりゅうみょーじんをまつるためにとーろーをながす。
こんかいのしりょーづくりわふかいしりょをひつよーとしたが、せんじつやっとしゅうりょーした。
こっしをしっかりとくみたてる。
ききゅうにのってそらをたのしむ。
ひひょーばかりでなくたいあんもだすべき。
かれらわまくのあいだにきどのそとをさんぽしているのである。
かこのかずかずのきびょーがなおるよーになりつつある。
こーにゅうしゃわぽんぷのしゅうりがひつよーなことにきがついた。
かれじしんわ、れじぎょーむにつきたいとおもっている。
じげわきんいろなんです。
えんぴつわおれやすくてふべんです。
ひぽくらてすわいがくのちちとよばれます。
ところがしょーにんわ、くにざかいのすぐちかくえすまって、やはりまえとおなじよーにやっています。
いままであかるかったにかいのまどわ、きゅうにまっくらになってしまいました。
あすふぁるとにかこまれたなかにけやきのきがいっぽん。
なふだをよーいする。
まっちょなだんせーわもてるそー。
しゃむしょのひとのはなしにうそわなかった。
こーらくしーずんのきょーとわひとでいっぱい。
どこからかぱちぱちとおとがきこえる。
ぷろやきゅうわどのちーむがゆうしょーするだろー?
しかし、これでわまるでわたしがゆうかいしましたとじしゅしてでるよーなもので、そんなばかなことをするやつわあるまい。
せつぼーしつつ、ぬしをまつ。
しばらくしてぱちぱちとゆうおともやんだ。
みぶんをわきまえずはなったきょーげん。
てんをかけるりゅうのすがたわしんぴてきだ。
わたしのせーしんといちみゃくあいつうじるものがあるとおもいました。
みんなもきゅうじょーにいきましょー。
こぺるにくすわぽーらんどのてんもんがくしゃです。
うみぞいのりょかんわながめがいい。
しゅうちゅうするとまわりがみえない。
はるきざわこんにちのほんごーざである。
わたしのてをひっぱるよーにして、てのひらえくれました。
ほっかいのあらなみわ、そのこおりのぜっぺきのねをかんで、はげしくしぶきをちらしている。
せんいをそーしつさせるのがしょーりえのちかみち。
がちょーをかう。
ふぁふぁとわらいながらたのしくてびょーし。
じゅうぐんきしゃわたいいそーとーのたいぐうをうける。
ちゃわんにかかるほど、しゃつのそでのふくらかなので、かきいだくてーにちゃわんをもった。
いろいろかくしてきょーまではんにんとともにいる。
うらこわねながらいきをひいた。
あにがじゃけんにされた。
かれわふふくそーにつぶやいた。
もぞーひんばかりでなく、ほんもののどいつしょーこーやへーそつのへるめっとをうっているのもある。
かのじょといっしょにいるとおちつきます。
こまってるひとにむけてきふをしました。
おちこんでいるのか、うつむいてじっとしている。
かいりょーがすすむとぱんわどんどんおいしくなる。
こんなことをいいながら、きのみじかいおじいさんわげたをつっかけて、そそくさとでていってしまった。
かのじょとはつでーとのきょーわゆめうつつ。
おなじとーきょーのなをよぶにも、こんごわおそらくきゅうとーきょーとしんとーきょーとにくべつされるであろー。
こしふりをにふんかんつづける。
おくびょーものがにげだした。
えはがきといっしょにぎんいろのるあーをまっしゅにおくった。
おむらいすにわけちゃっぷがいちばん。
すとれすわてきどにはっさんしましょー。
このひとよりぞはじまりける。
ちゅうがくせーのとき、ひしょりょこーちゅうにたいちょーをくずした。
ぐんいわびょーいんのもんにはいるのである。
いちにちじゅうあかるいびゃくやわ、いっさいたいよーがしずまないことでおこります。
もー、あなたにばかりもせーいっぱい、だれにもみせられますからだでわないんです。
みんなそろってうみにとびこんだ。
なんだそりゃ、とーてーむりなおねがいだ。
ちょーちふすわこわいびょーき。
はいきゅうわばれーぼーるのことです。
まっちをかいにはいったのかな。
ぼんさいわふぜーがある。
やがてばいしんいんわごーぎをするためにほーてーをでていった。
げーじゅつのもとめるえーえんせーにぎもんをかんじる。
ききつけて、くだんのおーな、ぶるぶるとかぶりをふった。
きぇるつぇをとおってどびぇに、ざりぴぇからみぇいにいく。
まりあーんすけーらーずにぇをおとずれる。
にゅうぎゅうをみながら、れてゅのはいったぴっつぁをたべる。
うどぅのそーしゃをせりょーした。
いんすてぃてゅーとで、りでゅーすのはなしとあわせて、るでゅっくのはなしもきいた。
ぎぇなーをみていぇーいとさけぶ。
すぃーでぃーをききながら、でゃーこんをたべる。
てょさんわずぃーぶらをみた。
れゔぉりゅーしょん。れぎゅれーしょん。えでゅけーしょん。
ぶれんどでょーと。らーてゃん。
あっあの。いっいえ。えっえぇ。おっおい。んーとね。
いぶかしげにみあげたあまぐもから、こはくいろのどらごんがぎゅんとあらわれた。
ぬのでぎゅぎゅっとぬんちゃくをしばる。
ふくをぬぐが、いつもじょーげがさかさまだ。
ほーかごのおんがくしつで、こーおんをがんばってだした。
もごもごしながらゆうぎゃぐわおもしろくない。
かいすいぎょのぎょぎょーのいっかんとして、ちぎょがそだてられている。
むんむんとしたねっきに、あのたんすいぎょもへとへとになっている。
へすてぃあしょちょーわ、じゅうにおんおんがくのさほーをしっている。
しゅさいしゃわ、このふぇすのきゃぱがちいさいことを、せしるからきいた。
ははわ、さふらんらいすと、さつまいものはいったしちゅーと、ぽとふをはふはふしながらたべた。
そして、さきょーとちさわへふぁえきについた。
ねもととそはらわしゅくんをひはんした。
けけっとわらいながら、つはらわぱとかーにのった。
きゅきゅっとならしたくつでぱすをだした。
てぃふぁにーわぱぴーにむぎゅっとだきつきながら、ちゅちゅっときすをし、せんきゅといった。
そのぎぐんわ、いちへくたーるほふくぜんしんをした。
へへっと、きゃつわこびへつらった。
ほとんどのひちょーさしゃわ、しゃしんをしゃさいした。
ほさが、いっさんかたんそちゅうどくになるとゆうはぷにんぐわおきなかった。
すちゅーでんとがひさいするふぁくたーを、かのーなかぎりとりはらう。
かふぇとわ、ぶれっくふぁすととして、ふぉかっちゃをたべれるばしょでもある。
ふつう、しょきゅうしゃでわ、こーおんをのばすことわできない。
かれからしたら、さちゅうかんからみるけしきわきちょーだった。
しぇぱーどとどーきょちゅうに、ふぉすたーわそのかくづけひょーをみた。
きょきょねん、きょすうとへちょーちょーについてまなんだ。
きゃたつのうえでひゅひゅーとかぜがふくと、かれわせすじをのばした。
ひるにわぺすかとーれを、よるにわすしをぱくぱくたべた。
けふぃあにかんするこのほんわ、しょはんでわさんびゃくぶくらいだったが、つぎからはきゅうてきにぞーかした。
ひふがわたしのふぇちである。
しゃはんできょふをきずくとゆう、かれのもくろみわとちゅうでへしおられた。
さひょーのとおりのししゅつになることが、あるいみわがしゃのしゃふうである。
このこふうなしゅひょーわこきょーのものだ。
そのほつれがふはいしているとゆうのわ、こちょーしたひょーげんだとおもう。
そのえーがのしゅつえんしゃであるかれが、しゅはんであるかのーせーわふぃふてぃーふぃふてぃーだ。
ちゅくんのはちょーわ、ぱつんときょーつうしている。

View File

@ -0,0 +1 @@
輩は猫である。名前はまだ無い。どこで生れたかとんと見当がつかぬ。何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。吾輩はここで始めて人間というものを見た。しかもあとで聞くとそれは書生という人間中で一番獰悪な種族であったそうだ。

View File

@ -0,0 +1 @@
わがはいわねこである。なまえわまだない。どこでうまれたかとんとけんとうがつかぬ。なんでもうすぐらいじめじめしたところでにゃあにゃあないていたことだけはきおくしている。わがはいわここではじめてにんげんというものをみた。しかもあとできくとそれわしょせいというにんげんちゅうでいちばんどうあくなしゅぞくであったそうだ。

BIN
recorder/public/coffee.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

BIN
recorder/public/favicon.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="ja" style="width: 100%; height: 100%; overflow: hidden">
<head>
<meta charset="utf-8" />
<title>voice recorder</title>
</head>
<body style="width: 100%; height: 100%; margin: 0px">
<div id="app" style="width: 100%; height: 100%"></div>
<noscript>
<strong>javascriptを有効にしてください</strong>
</noscript>
</body>
</html>

View File

@ -0,0 +1,36 @@
const StorageTypes = {
local: "local",
server: "server",
} as const
export type StorageTypes = typeof StorageTypes[keyof typeof StorageTypes]
export type ApplicationSetting =
{
"app_title": string
"use_mel_spectrogram": boolean
"storage_type": StorageTypes
"current_text": string,
"current_text_index": number,
"sample_rate": number,
"text": CorpusTextSetting[]
}
export type CorpusTextSetting = {
"title": string,
"wavPrefix": string,
"file": string,
"file_hira": string
}
export const InitialApplicationSetting = require("../../public/assets/setting.json")
export const fetchApplicationSetting = async (settingPath: string | null): Promise<ApplicationSetting> => {
const url = settingPath || `./assets/setting.json`
console.log("PATH", settingPath)
const res = await fetch(url, {
method: "GET"
});
const setting = await res.json() as ApplicationSetting
return setting;
}

View File

@ -0,0 +1,43 @@
export type DeviceInfo = {
label: string,
deviceId: string,
}
export type UpdateListener = {
update: () => void
}
//////////////////////////////
// Class
//////////////////////////////
export class DeviceManager {
realAudioInputDevices: DeviceInfo[] = []
realVideoInputDevices: DeviceInfo[] = []
realAudioOutputDevices: DeviceInfo[] = []
updateListener: UpdateListener = {
update: () => { console.log("update devices") }
}
setUpdateListener = (updateListener: UpdateListener) => {
this.updateListener = updateListener
}
// (A) Device List生成
reloadDevices = async () => {
try {
await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
} catch (e) {
console.warn("Enumerate device error::", e)
}
const mediaDeviceInfos = await navigator.mediaDevices.enumerateDevices();
this.realAudioInputDevices = mediaDeviceInfos.filter(x => { return x.kind === "audioinput" }).map(x => { return { label: x.label, deviceId: x.deviceId } })
this.realVideoInputDevices = mediaDeviceInfos.filter(x => { return x.kind === "videoinput" }).map(x => { return { label: x.label, deviceId: x.deviceId } })
this.realAudioOutputDevices = mediaDeviceInfos.filter(x => { return x.kind === "audiooutput" }).map(x => { return { label: x.label, deviceId: x.deviceId } })
this.realAudioInputDevices.push({ label: "none", deviceId: "none" })
this.realVideoInputDevices.push({ label: "none", deviceId: "none" })
this.updateListener.update()
}
}

View File

@ -0,0 +1,113 @@
import { FixedUserData } from "../002_hooks/013_useAudioControllerState";
export const fetchTextResource = async (url: string): Promise<string> => {
const res = await fetch(url, {
method: "GET"
});
const text = res.text()
return text;
}
export const postVoice = async (title: string, prefix: string, index: number, blob: Blob) => {
// const url = `./api/voice/${title}/${prefix}/${index}`
// const url = `./api/voice`
// !!!!!!!!!!! COLABのプロキシがRoot直下のパスしか通さない??? !!!!!!
// !!!!!!!!!!! Bodyで参照、設定コマンドを代替する。 !!!!!!
const url = `/api`
const blobBuffer = await blob.arrayBuffer()
const obj = {
command: "POST_VOICE",
data: Buffer.from(blobBuffer).toString("base64"),
title: title,
prefix: prefix,
index: index
};
const body = JSON.stringify(obj);
const res = await fetch(`${url}`, {
method: "POST",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: body
})
const receivedJson = await res.json()
const message = receivedJson["message"]
console.debug("POST VOICE RES:", message)
return
}
export const getVoice = async (title: string, prefix: string, index: number) => {
if (!title || !prefix) {
return null
}
const url = `/api`
const obj = {
command: "GET_VOICE",
title: title,
prefix: prefix,
index: index
};
const body = JSON.stringify(obj);
const res = await fetch(`${url}`, {
method: "POST",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: body
})
const receivedJson = await res.json()
// const message = receivedJson["message"]
const dataBase64 = receivedJson["data"]
// console.log("GET VOICE RES:", message, dataBase64)
if (!dataBase64) {
return null;
}
const buf = Buffer.from(dataBase64, "base64")
const blob = new Blob([buf.buffer])
return blob
}
export const postVoice__ = async (title: string, index: number, userData: FixedUserData) => {
const url = `/api/voice/${title}/${index}`
const micWavBlob = await userData.micWavBlob!.arrayBuffer()
const vfWavBlob = await userData.vfWavBlob!.arrayBuffer()
const micF32 = await userData.micWavSamples!
const vfF32 = await userData.vfWavSamples!
const obj = {
micWavBlob: Buffer.from(micWavBlob).toString("base64"),
vfWavBlob: Buffer.from(vfWavBlob).toString("base64"),
micF32: Buffer.from(micF32).toString("base64"),
vfF32: Buffer.from(vfF32).toString("base64")
};
const body = JSON.stringify(obj);
const res = await fetch(`${url}`, {
method: "POST",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: body
})
const receivedJson = await res.json()
const changedVoiceBase64 = receivedJson["changedVoiceBase64"]
const buf = Buffer.from(changedVoiceBase64, "base64")
const ab = new ArrayBuffer(buf.length);
const view = new Uint8Array(ab);
for (let i = 0; i < buf.length; ++i) {
view[i] = buf[i];
}
return ab
}

View File

@ -0,0 +1,71 @@
import { useEffect, useRef, useState } from "react"
import { ApplicationSetting, fetchApplicationSetting, InitialApplicationSetting } from "../001_clients_and_managers/000_ApplicationSettingLoader"
export type ApplicationSettingManagerStateAndMethod = {
applicationSetting: ApplicationSetting
setUseMelSpectrogram: (val: boolean) => void
setCurrentText: (val: string) => void
setCurrentTextIndex: (val: number) => void
clearSetting: () => void
}
export const useApplicationSettingManager = (): ApplicationSettingManagerStateAndMethod => {
const applicationSettingRef = useRef<ApplicationSetting>(InitialApplicationSetting)
const [applicationSetting, setApplicationSetting] = useState<ApplicationSetting>(applicationSettingRef.current)
useEffect(() => {
const url = new URL(window.location.href);
const params = url.searchParams;
const settingPath = params.get('setting_path') || null
const loadApplicationSetting = async () => {
if (localStorage.applicationSetting) {
applicationSettingRef.current = JSON.parse(localStorage.applicationSetting) as ApplicationSetting
console.log("Application setting is loaded from local", applicationSettingRef.current)
setApplicationSetting({ ...applicationSettingRef.current })
} else {
applicationSettingRef.current = await fetchApplicationSetting(settingPath)
console.log("Application setting is loaded from server", applicationSettingRef.current)
setApplicationSetting({ ...applicationSettingRef.current })
}
setApplicationSetting({ ...applicationSettingRef.current })
}
loadApplicationSetting()
}, [])
/** (3) Setter */
/** (3-1) Common */
const updateApplicationSetting = () => {
const tmpApplicationSetting = JSON.parse(JSON.stringify(applicationSettingRef.current)) as ApplicationSetting // 大きなデータをリプレースするためのテンポラリ(レコーダでは今のところ不要だが、他のアプリとの処理共通化のため残している。)
localStorage.applicationSetting = JSON.stringify(tmpApplicationSetting)
setApplicationSetting({ ...applicationSettingRef.current })
}
/** (3-2) Setting */
const setUseMelSpectrogram = (val: boolean) => {
applicationSettingRef.current.use_mel_spectrogram = val
updateApplicationSetting()
}
const setCurrentText = (val: string) => {
applicationSettingRef.current.current_text = val
updateApplicationSetting()
}
const setCurrentTextIndex = (val: number) => {
applicationSettingRef.current.current_text_index = val
updateApplicationSetting()
}
const clearSetting = () => {
localStorage.removeItem("applicationSetting")
}
return {
applicationSetting,
setUseMelSpectrogram,
setCurrentText,
setCurrentTextIndex,
clearSetting,
}
}

View File

@ -0,0 +1,33 @@
import localForage from "localforage";
export type IndexedDBState = {
dummy: string
}
export type IndexedDBStateAndMethod = IndexedDBState & {
setItem: (key: string, value: unknown) => Promise<void>,
getItem: (key: string) => Promise<unknown>
}
export const useIndexedDB = (): IndexedDBStateAndMethod => {
localForage.config({
driver: localForage.INDEXEDDB,
name: 'app',
version: 1.0,
storeName: 'appStorage',
description: 'appStorage'
})
const setItem = async (key: string, value: unknown) => {
await localForage.setItem(key, value)
}
const getItem = async (key: string) => {
return await localForage.getItem(key)
}
return {
dummy: "",
setItem,
getItem
}
}

View File

@ -0,0 +1,90 @@
import { useEffect, useMemo, useState } from "react"
import { DeviceInfo, DeviceManager } from "../001_clients_and_managers/001_DeviceManager"
type DeviceManagerState = {
lastUpdateTime: number
audioInputDevices: DeviceInfo[]
videoInputDevices: DeviceInfo[]
audioOutputDevices: DeviceInfo[]
audioInputDeviceId: string | null
videoInputDeviceId: string | null
audioOutputDeviceId: string | null
}
export type DeviceManagerStateAndMethod = DeviceManagerState & {
reloadDevices: () => Promise<void>
setAudioInputDeviceId: (val: string | null) => void
setVideoInputDeviceId: (val: string | null) => void
setAudioOutputDeviceId: (val: string | null) => void
}
export const useDeviceManager = (): DeviceManagerStateAndMethod => {
const [lastUpdateTime, setLastUpdateTime] = useState(0)
const [audioInputDeviceId, _setAudioInputDeviceId] = useState<string | null>(null)
const [videoInputDeviceId, _setVideoInputDeviceId] = useState<string | null>(null)
const [audioOutputDeviceId, _setAudioOutputDeviceId] = useState<string | null>(null)
const deviceManager = useMemo(() => {
const manager = new DeviceManager()
manager.setUpdateListener({
update: () => {
setLastUpdateTime(new Date().getTime())
}
})
manager.reloadDevices()
return manager
}, [])
// () Enumerate
const reloadDevices = useMemo(() => {
return async () => {
deviceManager.reloadDevices()
}
}, [])
const setAudioInputDeviceId = async (val: string | null) => {
localStorage.audioInputDevice = val;
_setAudioInputDeviceId(val)
}
useEffect(() => {
const audioInputDeviceId = localStorage.audioInputDevice || null
_setAudioInputDeviceId(audioInputDeviceId)
}, [])
const setVideoInputDeviceId = async (val: string | null) => {
localStorage.videoInputDevice = val;
_setVideoInputDeviceId(val)
}
useEffect(() => {
const videoInputDeviceId = localStorage.videoInputDevice || null
_setVideoInputDeviceId(videoInputDeviceId)
}, [])
const setAudioOutputDeviceId = async (val: string | null) => {
localStorage.audioOutputDevice = val;
_setAudioOutputDeviceId(val)
}
useEffect(() => {
const audioOutputDeviceId = localStorage.audioOutputDevice || null
_setAudioOutputDeviceId(audioOutputDeviceId)
}, [])
return {
lastUpdateTime,
audioInputDevices: deviceManager.realAudioInputDevices,
videoInputDevices: deviceManager.realVideoInputDevices,
audioOutputDevices: deviceManager.realAudioOutputDevices,
audioInputDeviceId,
videoInputDeviceId,
audioOutputDeviceId,
reloadDevices,
setAudioInputDeviceId,
setVideoInputDeviceId,
setAudioOutputDeviceId,
}
}

View File

@ -0,0 +1,60 @@
import { useEffect, useState } from "react"
import { fetchTextResource } from "../001_clients_and_managers/002_ResourceLoader"
import { useAppSetting } from "../003_provider/AppSettingProvider"
export type CorpusTextData = {
"title": string,
"wavPrefix": string,
"file": string,
"file_hira": string,
"text": string[]
"text_hira": string[]
}
export type CorpusDataState = {
corpusTextData: { [title: string]: CorpusTextData }
corpusLoaded: boolean
}
export type CorpusDataStateAndMethod = CorpusDataState & {}
export const useCorpusData = (): CorpusDataStateAndMethod => {
const { applicationSetting } = useAppSetting()
const textSettings = applicationSetting.applicationSetting.text
const [corpusTextData, setCorpusTextData] = useState<{ [title: string]: CorpusTextData }>({})
const [corpusLoaded, setCorpusLoaded] = useState<boolean>(false)
useEffect(() => {
if (!textSettings) {
return
}
const loadCorpusText = async () => {
const newCorpusTextData: { [title: string]: CorpusTextData } = {}
for (const x of textSettings) {
const text = await fetchTextResource(x.file)
const textHira = await fetchTextResource(x.file_hira)
const splitText = text.split("\n").filter(x => { return x.length > 0 })
const splitTextHira = textHira.split("\n").filter(x => { return x.length > 0 })
const data: CorpusTextData = {
title: x.title,
wavPrefix: x.wavPrefix,
file: x.file,
file_hira: x.file_hira,
text: splitText,
text_hira: splitTextHira,
}
newCorpusTextData[data.title] = data
}
setCorpusTextData(newCorpusTextData)
setCorpusLoaded(true)
}
loadCorpusText()
}, [textSettings])
return {
corpusTextData,
corpusLoaded,
}
}

View File

@ -0,0 +1,34 @@
import { ApplicationSetting } from "../001_clients_and_managers/000_ApplicationSettingLoader"
import { generateWavNameForLocalStorage } from "../const"
import { IndexedDBStateAndMethod } from "./001_useIndexedDB"
import { FixedUserData } from "./013_useAudioControllerState"
export type UseAppStateStorageProps = {
applicationSetting: ApplicationSetting | null
indexedDBState: IndexedDBStateAndMethod
}
export type AppStateStorageState = {
}
export type AppStateStorageStateAndMethod = AppStateStorageState & {
saveUserData: (title: string, prefix: string, index: number, userData: FixedUserData) => void
loadUserData: (title: string, prefix: string, index: number) => Promise<FixedUserData | null>
}
export const useAppStateStorage = (props: UseAppStateStorageProps): AppStateStorageStateAndMethod => {
const saveUserData = async (_title: string, prefix: string, index: number, userData: FixedUserData) => {
const { micString } = generateWavNameForLocalStorage(prefix, index)
props.indexedDBState.setItem(micString, userData)
}
const loadUserData = async (_title: string, prefix: string, index: number): Promise<FixedUserData | null> => {
const { micString } = generateWavNameForLocalStorage(prefix, index)
const obj = await props.indexedDBState.getItem(micString) as FixedUserData
return obj
}
return {
saveUserData,
loadUserData,
}
}

View File

@ -0,0 +1,272 @@
import { VoiceFocusDeviceTransformer, VoiceFocusTransformDevice } from "amazon-chime-sdk-js";
import { useEffect, useMemo, useState } from "react";
import { Duplex, DuplexOptions } from "readable-stream";
import MicrophoneStream from "microphone-stream";
import { useAppSetting } from "../003_provider/AppSettingProvider";
export type MediaRecorderState = {
micMediaStream: MediaStream | undefined,
vfMediaStream: MediaStream | undefined
}
export type MediaRecorderStateAndMethod = MediaRecorderState & {
setNewAudioInputDevice: (deviceId: string) => Promise<void>
startRecord: () => void
pauseRecord: () => void
clearRecordedData: () => void
getRecordedDataBlobs: () => {
micWavBlob: Blob;
vfWavBlob: Blob;
micDuration: number;
vfDuration: number;
micSamples: Float32Array;
vfSamples: Float32Array;
}
}
class AudioStreamer extends Duplex {
chunks: Float32Array[] = []
SampleRate: number
constructor(options: DuplexOptions & {
SampleRate: number
}) {
super(options);
this.SampleRate = options.SampleRate
}
private initializeData = () => {
this.chunks = []
}
clearRecordedData = () => {
this.initializeData()
}
getRecordedData = () => {
const sampleSize = this.chunks.reduce((prev, cur) => {
return prev + cur.length
}, 0)
const samples = new Float32Array(sampleSize);
let sampleIndex = 0
// this.chunks.forEach(floatArray => {
// floatArray.forEach(val => {
// samples[sampleIndex] = val
// sampleIndex++;
// })
// })
for (let i = 0; i < this.chunks.length; i++) {
for (let j = 0; j < this.chunks[i].length; j++) {
samples[sampleIndex] = this.chunks[i][j];
sampleIndex++;
}
}
// console.log("samples:c2", this.chunks[0][0])
// console.log("samples:c2", this.chunks[0][1])
// console.log("samples:c2", this.chunks[0])
// console.log("samples:s", samples[0])
// console.log("samples:s", samples[1])
// console.log("samples:s", samples[2])
// console.log("samples:s", samples)
const writeString = (view: DataView, offset: number, string: string) => {
for (var i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
};
const floatTo16BitPCM = (output: DataView, offset: number, input: Float32Array) => {
for (var i = 0; i < input.length; i++, offset += 2) {
var s = Math.max(-1, Math.min(1, input[i]));
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
}
};
const buffer = new ArrayBuffer(44 + samples.length * 2);
const view = new DataView(buffer);
// https://www.youfit.co.jp/archives/1418
writeString(view, 0, 'RIFF'); // RIFFヘッダ
view.setUint32(4, 32 + samples.length * 2, true); // これ以降のファイルサイズ
writeString(view, 8, 'WAVE'); // WAVEヘッダ
writeString(view, 12, 'fmt '); // fmtチャンク
view.setUint32(16, 16, true); // fmtチャンクのバイト数
view.setUint16(20, 1, true); // フォーマットID
view.setUint16(22, 1, true); // チャンネル数
view.setUint32(24, this.SampleRate, true); // サンプリングレート
view.setUint32(28, this.SampleRate * 2, true); // データ速度
view.setUint16(32, 2, true); // ブロックサイズ
view.setUint16(34, 16, true); // サンプルあたりのビット数
writeString(view, 36, 'data'); // dataチャンク
view.setUint32(40, samples.length * 2, true); // 波形データのバイト数
floatTo16BitPCM(view, 44, samples); // 波形データ
console.log(view)
const audioBlob = new Blob([view], { type: 'audio/wav' });
const duration = samples.length / this.SampleRate
// audioBlob.arrayBuffer().then((buffer) => {
// console.log("DATALENGTH1", samples.length * 2)
// const oldView = new DataView(buffer);
// console.log("DATALENGTH2", view.getUint32(40, true))
// console.log("DATALENGTH3", oldView.getUint32(40, true))
// })
return { audioBlob, duration, samples }
// var url = URL.createObjectURL(audioBlob);
// // var a = document.createElement('a');
// // a.href = url;
// // a.download = 'test.wav';
// // a.click();
// // return this.chunks
// return url
}
public _write(chunk: AudioBuffer, _encoding: any, callback: any) {
const buffer = chunk.getChannelData(0);
console.log("SAMPLERATE:", chunk.sampleRate, chunk.numberOfChannels, chunk.length)
var bufferData = new Float32Array(chunk.length);
for (var i = 0; i < chunk.length; i++) {
bufferData[i] = buffer[i];
}
this.chunks.push(bufferData)
callback();
}
}
export const useMediaRecorder = (): MediaRecorderStateAndMethod => {
const { applicationSetting, deviceManagerState } = useAppSetting()
const audioContext = useMemo(() => {
return new AudioContext({ sampleRate: applicationSetting.applicationSetting.sample_rate });
}, [])
const [voiceFocusDeviceTransformer, setVoiceFocusDeviceTransformer] = useState<VoiceFocusDeviceTransformer>();
const [voiceFocusTransformDevice, setVoiceFocusTransformDevice] = useState<VoiceFocusTransformDevice | null>(null)
const outputNode = useMemo(() => {
return audioContext.createMediaStreamDestination();
}, [])
const [micMediaStream, setMicMediaStream] = useState<MediaStream>()
const [vfMediaStream, setVfMediaStream] = useState<MediaStream>()
const micAudioStreamer = useMemo(() => {
return new AudioStreamer({ objectMode: true, SampleRate: applicationSetting.applicationSetting.sample_rate })
}, [])
const micStream = useMemo(() => {
const s = new MicrophoneStream({
objectMode: true,
bufferSize: 1024,
context: audioContext
});
s.pipe(micAudioStreamer)
return s
}, [])
const vfAudioStreamer = useMemo(() => {
return new AudioStreamer({ objectMode: true, SampleRate: applicationSetting.applicationSetting.sample_rate })
}, [])
const vfStream = useMemo(() => {
const s = new MicrophoneStream({
objectMode: true,
bufferSize: 1024,
context: audioContext
})
s.pipe(vfAudioStreamer)
return s
}, [])
const setNewAudioInputDevice = async (deviceId: string) => {
console.log("setNewAudioInputDevice", deviceId)
let vf = voiceFocusDeviceTransformer
if (!vf) {
vf = await VoiceFocusDeviceTransformer.create({ variant: 'c20' })
setVoiceFocusDeviceTransformer(vf)
}
if (micMediaStream) {
micMediaStream.getTracks().forEach(x => {
x.stop()
})
}
const constraints: MediaStreamConstraints = {
audio: {
deviceId: deviceId,
sampleRate: applicationSetting.applicationSetting.sample_rate,
// sampleSize: 16,
channelCount: 1,
echoCancellation: true,
noiseSuppression: true,
},
video: false
}
const newMicMediaStream = await navigator.mediaDevices.getUserMedia(constraints)
// newMicMediaStream.getTracks().forEach(t => {
// console.log("Capability:", t.getCapabilities())
// console.log("Constraint:", t.getConstraints())
// })
let currentDevice = voiceFocusTransformDevice
if (!currentDevice) {
currentDevice = (await vf.createTransformDevice(newMicMediaStream)) || null;
setVoiceFocusTransformDevice(currentDevice)
} else {
currentDevice.chooseNewInnerDevice(newMicMediaStream)
}
const nodeToVF = audioContext.createMediaStreamSource(newMicMediaStream);
const voiceFocusNode = await currentDevice!.createAudioNode(audioContext);
nodeToVF.connect(voiceFocusNode.start);
voiceFocusNode.end.connect(outputNode);
setMicMediaStream(newMicMediaStream)
setVfMediaStream(outputNode.stream)
micStream.setStream(newMicMediaStream)
micStream.pauseRecording()
vfStream.setStream(outputNode.stream)
vfStream.pauseRecording()
}
useEffect(() => {
setNewAudioInputDevice(deviceManagerState.audioInputDeviceId || "")
}, [deviceManagerState.audioInputDeviceId])
const startRecord = () => {
console.log("start record")
micAudioStreamer.clearRecordedData()
micStream!.playRecording()
vfAudioStreamer.clearRecordedData()
vfStream!.playRecording()
}
const pauseRecord = () => {
micStream!.pauseRecording()
vfStream!.pauseRecording()
}
const clearRecordedData = () => {
micAudioStreamer.clearRecordedData()
vfAudioStreamer.clearRecordedData()
}
const getRecordedDataBlobs = () => {
const { audioBlob: micWavBlob, duration: micDuration, samples: micSamples } = micAudioStreamer.getRecordedData()
const { audioBlob: vfWavBlob, duration: vfDuration, samples: vfSamples } = vfAudioStreamer.getRecordedData()
return { micWavBlob, vfWavBlob, micDuration, vfDuration, micSamples, vfSamples }
}
const retVal: MediaRecorderStateAndMethod = {
micMediaStream,
vfMediaStream,
setNewAudioInputDevice,
startRecord,
pauseRecord,
clearRecordedData,
getRecordedDataBlobs
}
return retVal
}

View File

@ -0,0 +1,168 @@
import { useEffect, useMemo, useRef, useState } from "react"
import { useAppSetting } from "../003_provider/AppSettingProvider"
export const AudioControllerStateType = {
stop: "stop",
record: "record",
play: "play"
} as const
export type AudioControllerStateType = typeof AudioControllerStateType[keyof typeof AudioControllerStateType]
export type FixedUserData = {
micWavBlob: Blob | undefined
vfWavBlob: Blob | undefined
micWavSamples: Float32Array | undefined
vfWavSamples: Float32Array | undefined
region: [number, number] | undefined
micSpec: string
vfSpec: string
}
type TempUserData = {
micWavBlob: Blob | undefined
vfWavBlob: Blob | undefined
micWavSamples: Float32Array | undefined
vfWavSamples: Float32Array | undefined
region: [number, number] | undefined
micSpec: string
vfSpec: string
}
const initialFixedUserData: FixedUserData = {
micWavBlob: undefined,
vfWavBlob: undefined,
micWavSamples: undefined,
vfWavSamples: undefined,
region: [0, 0],
micSpec: "",
vfSpec: ""
}
const initialTempUserData: TempUserData = {
micWavBlob: undefined,
vfWavBlob: undefined,
micWavSamples: undefined,
vfWavSamples: undefined,
region: [0, 0],
micSpec: "",
vfSpec: ""
}
export type AudioControllerState = {
audioControllerState: AudioControllerStateType
unsavedRecord: boolean
tempUserData: TempUserData
}
export type AudioControllerStateAndMethod = AudioControllerState & {
setAudioControllerState: (val: AudioControllerStateType) => void
setUnsavedRecord: (val: boolean) => void
setTempWavBlob: (micBlob: Blob | undefined, vfBlob: Blob | undefined, micWavSamples: Float32Array | undefined, vfWavSamples: Float32Array | undefined, micSpec: string, vfSpec: string, region: [number, number]) => void
saveWavBlob: () => void
restoreFixedUserData: () => void
}
export const useAudioControllerState = (): AudioControllerStateAndMethod => {
const { applicationSetting, appStateStorageState } = useAppSetting()
const wavFilePrefix = useMemo(() => {
const currentTextInfo = applicationSetting.applicationSetting.text.find(x => {
return x.title == applicationSetting.applicationSetting.current_text
})
return currentTextInfo?.wavPrefix || "unknown"
}, [applicationSetting.applicationSetting.current_text])
const [audioControllerState, setAudioControllerState] = useState<AudioControllerStateType>("stop")
const [unsavedRecord, setUnsavedRecord] = useState<boolean>(false);
const fixedUserDataRef = useRef<FixedUserData>({ ...initialFixedUserData })
const tempUserDataRef = useRef<TempUserData>({ ...initialTempUserData })
const [_fixedUserData, setFixedUserData] = useState<FixedUserData>(fixedUserDataRef.current)
const [tempUserData, setTempUserData] = useState<TempUserData>(tempUserDataRef.current)
// 録音したデータをテンポラリバッファに格納
const setTempWavBlob = (micBlob: Blob | undefined, vfBlob: Blob | undefined, micWavSamples: Float32Array | undefined, vfWavSamples: Float32Array | undefined, micSpec: string, vfSpec: string, region: [number, number]) => {
tempUserDataRef.current.micWavBlob = micBlob
tempUserDataRef.current.vfWavBlob = vfBlob
tempUserDataRef.current.micWavSamples = micWavSamples
tempUserDataRef.current.vfWavSamples = vfWavSamples
tempUserDataRef.current.region = region
tempUserDataRef.current.micSpec = micSpec
tempUserDataRef.current.vfSpec = vfSpec
setTempUserData({ ...tempUserDataRef.current })
}
// テンポラリをストレージにセーブ
const saveWavBlob = () => {
fixedUserDataRef.current.micWavBlob = tempUserDataRef.current.micWavBlob
fixedUserDataRef.current.vfWavBlob = tempUserDataRef.current.vfWavBlob
fixedUserDataRef.current.micWavSamples = tempUserDataRef.current.micWavSamples
fixedUserDataRef.current.vfWavSamples = tempUserDataRef.current.vfWavSamples
fixedUserDataRef.current.region = tempUserDataRef.current.region
fixedUserDataRef.current.micSpec = tempUserDataRef.current.micSpec
fixedUserDataRef.current.vfSpec = tempUserDataRef.current.vfSpec
setFixedUserData({ ...fixedUserDataRef.current })
appStateStorageState.saveUserData(applicationSetting.applicationSetting.current_text, wavFilePrefix, applicationSetting.applicationSetting.current_text_index, fixedUserDataRef.current)
}
// ストレージからロードして、確定データを復帰
const loadWavBlob = async () => {
const userData = await appStateStorageState.loadUserData(applicationSetting.applicationSetting.current_text, wavFilePrefix, applicationSetting.applicationSetting.current_text_index)
if (!userData) {
fixedUserDataRef.current.micWavBlob = undefined
fixedUserDataRef.current.vfWavBlob = undefined
fixedUserDataRef.current.micWavSamples = undefined
fixedUserDataRef.current.vfWavSamples = undefined
fixedUserDataRef.current.region = undefined
fixedUserDataRef.current.micSpec = ""
fixedUserDataRef.current.vfSpec = ""
} else {
fixedUserDataRef.current.micWavBlob = userData.micWavBlob
fixedUserDataRef.current.vfWavBlob = userData.vfWavBlob
fixedUserDataRef.current.micWavSamples = userData.micWavSamples
fixedUserDataRef.current.vfWavSamples = userData.vfWavSamples
fixedUserDataRef.current.region = userData.region
fixedUserDataRef.current.micSpec = userData.micSpec
fixedUserDataRef.current.vfSpec = userData.vfSpec
}
setFixedUserData({ ...fixedUserDataRef.current })
restoreFixedUserData()
}
// テンポラリを捨てて確定データを復帰
const restoreFixedUserData = () => {
tempUserDataRef.current.micWavBlob = fixedUserDataRef.current.micWavBlob
tempUserDataRef.current.vfWavBlob = fixedUserDataRef.current.vfWavBlob
tempUserDataRef.current.micWavSamples = fixedUserDataRef.current.micWavSamples
tempUserDataRef.current.vfWavSamples = fixedUserDataRef.current.vfWavSamples
tempUserDataRef.current.region = fixedUserDataRef.current.region
tempUserDataRef.current.micSpec = fixedUserDataRef.current.micSpec
tempUserDataRef.current.vfSpec = fixedUserDataRef.current.vfSpec
setTempUserData({ ...tempUserDataRef.current })
}
// ページ切り替え
useEffect(() => {
loadWavBlob()
}, [applicationSetting.applicationSetting.current_text_index, wavFilePrefix])
return {
audioControllerState,
unsavedRecord,
tempUserData,
setAudioControllerState,
setUnsavedRecord,
setTempWavBlob,
saveWavBlob,
restoreFixedUserData,
}
}

View File

@ -0,0 +1,162 @@
import { useEffect, useRef, useState } from "react";
import WaveSurfer from "wavesurfer.js"
import RegionsPlugin from "wavesurfer.js/src/plugin/regions";
import TimelinePlugin from "wavesurfer.js/src/plugin/timeline";
import { ListenerDescriptor } from "wavesurfer.js/types/util";
import { useAppSetting } from "../003_provider/AppSettingProvider";
export type WaveSurferState = {
dummy: string
}
export type WaveSurferStateAndMethod = WaveSurferState & {
loadMusic: (blob: Blob) => Promise<void>
emptyMusic: () => void
play: () => void
playRegion: () => void
stop: () => void
init: () => void
setListener: (l: WaveSurferListener) => void
getTimeInfos: () => {
totalTime: number;
currentTime: number;
remainingTime: number;
}
setRegion: (start: number, end: number) => void
}
export type WaveSurferListener = {
audioprocess: () => void
finish: () => void
ready: () => void
regionUpdate: (start: number, end: number) => void
}
export const useWaveSurfer = (): WaveSurferStateAndMethod => {
const { deviceManagerState } = useAppSetting()
const waveSurferRef = useRef<WaveSurfer>()
const [waveSurfer, setWaveSurfer] = useState<WaveSurfer>()
useEffect(() => {
waveSurferRef.current = WaveSurfer.create({
container: '#waveform',
plugins: [
TimelinePlugin.create({
container: "#wave-timeline",
primaryLabelInterval: (_pxPerSec: number) => { return 2 },
secondaryLabelInterval: (_pxPerSec: number) => { return 1 },
// primaryFontColor: "#fff",
fontSize: 20
}),
RegionsPlugin.create({
regionsMinLength: 1,
regions: [
{
start: 0,
end: 1,
loop: false,
color: 'hsla(400, 100%, 30%, 0.3)'
},
]
})
]
})
setWaveSurfer(waveSurferRef.current)
}, [])
const loadMusic = async (blob: Blob) => {
if (!waveSurferRef.current) {
return
}
waveSurferRef.current.loadBlob(blob);
}
const emptyMusic = () => {
waveSurfer!.empty()
}
const play = () => {
waveSurfer!.play()
}
const playRegion = () => {
Object.values(waveSurfer!.regions.list)[0].play()
}
const stop = () => {
waveSurfer!.stop()
}
const init = () => {
waveSurfer?.init()
// waveSurfer?.loadBlob()
}
const listenersRef = useRef<ListenerDescriptor[]>([])
const setListener = (l: WaveSurferListener) => {
if (!waveSurfer) {
return
}
listenersRef.current.forEach(x => {
waveSurfer.un(x.name, x.callback)
})
const l1 = waveSurfer.on("region-update-end", () => {
const region = Object.values(waveSurfer.regions.list)[0]
if (!region) {
console.warn("no region")
return
}
l.regionUpdate(region.start, region.end)
})
const l2 = waveSurfer.on("audioprocess", l.audioprocess)
const l3 = waveSurfer.on("finish", l.finish)
const l3_2 = waveSurfer.on("region-out", l.finish)
// That event doesnt trigger as Im using webaudio. I read in the documentation that:waveform-ready Fires after the waveform is drawn when using the MediaElement backend. If youre using the WebAudio backend, you can use ready. (https://lightrun.com/answers/katspaugh-wavesurfer-js-save-wavesurfer-state-and-preload-on-reload)
// waveSurfer.on('waveform-ready', () => {
// console.log("ready!!!!")
// })
const l4 = waveSurfer.on("ready", l.ready)
listenersRef.current = [l1, l2, l3, l3_2, l4]
}
const getTimeInfos = () => {
let totalTime = 0
let currentTime = 0
let remainingTime = 0
if (waveSurfer) {
totalTime = waveSurfer.getDuration()
currentTime = waveSurfer.getCurrentTime()
remainingTime = totalTime - currentTime
}
return { totalTime, currentTime, remainingTime }
}
const setRegion = (start: number, end: number) => {
if (!waveSurfer) {
return
}
const region = Object.values(waveSurfer.regions.list)[0]
if (!region) {
console.warn("no region")
return
}
console.log()
region.update({ start: start, end: end })
}
useEffect(() => {
if (!waveSurfer || !deviceManagerState.audioOutputDeviceId) {
return
}
waveSurfer.setSinkId(deviceManagerState.audioOutputDeviceId)
}, [waveSurfer, deviceManagerState.audioOutputDeviceId])
return {
dummy: "dummy",
loadMusic,
emptyMusic,
play,
playRegion,
stop,
init,
setListener,
getTimeInfos,
setRegion
}
}

View File

@ -0,0 +1,24 @@
import { StateControlCheckbox, useStateControlCheckbox } from "../100_components/003_hooks/useStateControlCheckbox";
export type StateControls = {
openRightSidebarCheckbox: StateControlCheckbox
}
type FrontendManagerState = {
stateControls: StateControls
};
export type FrontendManagerStateAndMethod = FrontendManagerState & {
}
export const useFrontendManager = (): FrontendManagerStateAndMethod => {
// (1) Controller Switch
const openRightSidebarCheckbox = useStateControlCheckbox("open-right-sidebar-checkbox");
const returnValue: FrontendManagerStateAndMethod = {
stateControls: {
// (1) Controller Switch
openRightSidebarCheckbox,
},
};
return returnValue;
};

View File

@ -0,0 +1,42 @@
import React, { useContext } from "react";
import { ReactNode } from "react";
import { ApplicationSettingManagerStateAndMethod, useApplicationSettingManager } from "../002_hooks/000_useApplicationSettingManager";
import { IndexedDBStateAndMethod, useIndexedDB } from "../002_hooks/001_useIndexedDB";
import { DeviceManagerStateAndMethod, useDeviceManager } from "../002_hooks/002_useDeviceManager";
import { AppStateStorageStateAndMethod, useAppStateStorage } from "../002_hooks/004_useAppStateStorage";
type Props = {
children: ReactNode;
};
type AppSettingValue = {
applicationSetting: ApplicationSettingManagerStateAndMethod
indexedDBState: IndexedDBStateAndMethod;
deviceManagerState: DeviceManagerStateAndMethod;
appStateStorageState: AppStateStorageStateAndMethod
};
const AppSettingContext = React.createContext<AppSettingValue | null>(null);
export const useAppSetting = (): AppSettingValue => {
const state = useContext(AppSettingContext);
if (!state) {
throw new Error("useAppSetting must be used within AppSettingProvider");
}
return state;
};
export const AppSettingProvider = ({ children }: Props) => {
const applicationSetting = useApplicationSettingManager();
const indexedDBState = useIndexedDB();
const deviceManagerState = useDeviceManager();
const appStateStorageState = useAppStateStorage({ applicationSetting: applicationSetting.applicationSetting, indexedDBState })
const providerValue = {
applicationSetting,
indexedDBState,
deviceManagerState,
appStateStorageState,
};
return <AppSettingContext.Provider value={providerValue}>{children}</AppSettingContext.Provider>;
};

View File

@ -0,0 +1,46 @@
import React, { useContext } from "react";
import { ReactNode } from "react";
import { CorpusDataStateAndMethod, useCorpusData } from "../002_hooks/003_useCorpusData";
import { MediaRecorderStateAndMethod, useMediaRecorder } from "../002_hooks/012_useMediaRecorder";
import { AudioControllerStateAndMethod, useAudioControllerState } from "../002_hooks/013_useAudioControllerState";
import { useWaveSurfer, WaveSurferStateAndMethod } from "../002_hooks/014_useWaveSurfer";
import { FrontendManagerStateAndMethod, useFrontendManager } from "../002_hooks/100_useFrontendManager";
type Props = {
children: ReactNode;
};
interface AppStateValue {
mediaRecorderState: MediaRecorderStateAndMethod
audioControllerState: AudioControllerStateAndMethod
waveSurferState: WaveSurferStateAndMethod
corpusDataState: CorpusDataStateAndMethod
frontendManagerState: FrontendManagerStateAndMethod;
}
const AppStateContext = React.createContext<AppStateValue | null>(null);
export const useAppState = (): AppStateValue => {
const state = useContext(AppStateContext);
if (!state) {
throw new Error("useAppState must be used within AppStateProvider");
}
return state;
};
export const AppStateProvider = ({ children }: Props) => {
const corpusDataState = useCorpusData()
const mediaRecorderState = useMediaRecorder()
const audioControllerState = useAudioControllerState()
const waveSurferState = useWaveSurfer()
const frontendManagerState = useFrontendManager();
const providerValue = {
mediaRecorderState,
audioControllerState,
waveSurferState,
corpusDataState,
frontendManagerState
};
return <AppStateContext.Provider value={providerValue}>{children}</AppStateContext.Provider>;
};

View File

@ -0,0 +1,63 @@
@import url("https://fonts.googleapis.com/css2?family=Chicle&family=Poppins:ital,wght@0,200;0,400;0,600;1,200;1,400;1,600&display=swap");
@import "./002_Front.css";
@import "./010_Frame.css";
@import "./020_Header.css";
@import "./030_Body.css";
@import "./101_RotatedButton.css";
/* ;
@import "./010_Dialog.css";
@import "./101_processing.css";
@import "./102_RotatedButton.css";
@import "./103_tooltips.css";
@import "./104_signage.css"; */
:root {
--text-color: #333;
--company-color1: rgba(64, 119, 187, 1);
--company-color2: rgba(29, 47, 78, 1);
--company-color3: rgba(255, 255, 255, 1);
--company-color1-alpha: rgba(64, 119, 187, 0.3);
--company-color2-alpha: rgba(29, 47, 78, 0.3);
--company-color3-alpha: rgba(255, 255, 255, 0.3);
--global-shadow-color: rgba(0, 0, 0, 0.4);
--sidebar-transition-time: 0.3s;
--sidebar-transition-animation: ease-in-out;
--header-height: 1.5rem;
--right-sidebar-width: 320px;
--dialog-border-color: rgba(100, 100, 100, 1);
--dialog-shadow-color: rgba(0, 0, 0, 0.3);
--dialog-background-color: rgba(255, 255, 255, 1);
--dialog-primary-color: rgba(19, 70, 209, 1);
--dialog-active-color: rgba(40, 70, 209, 1);
--dialog-input-border-color: rgba(200, 200, 200, 1);
--dialog-submit-button-color: rgba(180, 190, 230, 1);
--dialog-cancel-button-color: rgba(235, 80, 80, 1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Poppins", sans-serif;
}
html {
font-size: 16px;
}
body {
height: 100%;
width: 100%;
color: var(--text-color);
background: linear-gradient(45deg, var(--company-color1) 0, 5%, var(--company-color2) 5% 10%, var(--company-color3) 10% 80%, var(--company-color1) 80% 85%, var(--company-color2) 85% 100%);
}
.application-container {
position: relative;
height: 100vh;
width: 100%;
overflow: hidden;
list-style-type: none;
}

View File

@ -0,0 +1,371 @@
@import url("https://fonts.googleapis.com/css2?family=Chicle&family=Poppins:ital,wght@0,200;0,400;0,600;1,200;1,400;1,600&display=swap");
/* @import "./002_Frame.css";
@import "./010_Dialog.css";
@import "./101_processing.css";
@import "./102_RotatedButton.css";
@import "./103_tooltips.css";
@import "./104_signage.css"; */
:root {
--text-color: #333;
--company-color1: rgba(64, 119, 187, 1);
--company-color2: rgba(29, 47, 78, 1);
--company-color3: rgba(255, 255, 255, 1);
--company-color1-alpha: rgba(64, 119, 187, 0.3);
--company-color2-alpha: rgba(29, 47, 78, 0.3);
--company-color3-alpha: rgba(255, 255, 255, 0.3);
--global-shadow-color: rgba(0, 0, 0, 0.4);
--sidebar-transition-time: 0.3s;
--sidebar-transition-animation: ease-in-out;
--header-height: 1.5rem;
--right-sidebar-width: 320px;
--dialog-border-color: rgba(100, 100, 100, 1);
--dialog-shadow-color: rgba(0, 0, 0, 0.3);
--dialog-background-color: rgba(255, 255, 255, 1);
--dialog-primary-color: rgba(19, 70, 209, 1);
--dialog-active-color: rgba(40, 70, 209, 1);
--dialog-input-border-color: rgba(200, 200, 200, 1);
--dialog-submit-button-color: rgba(180, 190, 230, 1);
--dialog-cancel-button-color: rgba(235, 80, 80, 1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Poppins", sans-serif;
}
html {
font-size: 16px;
}
body {
height: 100%;
width: 100%;
color: var(--text-color);
background: linear-gradient(45deg, var(--company-color1) 0, 5%, var(--company-color2) 5% 10%, var(--company-color3) 10% 80%, var(--company-color1) 80% 85%, var(--company-color2) 85% 100%);
}
.application-container {
position: relative;
height: 100vh;
width: 100%;
overflow: hidden;
list-style-type: none;
}
.state-control-checkbox {
display: none;
}
.video-for-recorder-container {
position: absolute;
left: -1000px;
width: 30px;
height: 30px;
}
/* */
/* */
/* */
/* start button */
.front-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: 30px;
}
.front-title {
font-size: 4rem;
font-weight: 100;
}
.front-description {
font-size: 0.9rem;
text-align: center;
}
.front-description-img {
height: 2rem;
width: 50%;
height: 50%;
border: 3px solid;
}
.front-description-strong {
color: #f66;
font-size: 0.9rem;
font-weight: 600;
}
.front-start-button {
font-size: 4rem;
border: 3px solid #333;
background: #eef;
width: 500px;
padding: 15px;
cursor: pointer;
text-align: center;
margin: 100px 0 0 0 auto;
user-select: none;
}
.front-note {
font-size: 1rem;
}
.front-attention {
font-size: 0.8rem;
color: #f55;
font-weight: 600;
}
.front-disclaimer {
font-size: 0.8rem;
}
/* Header */
.header {
position: fixed;
top: 0px;
left: 0px;
height: var(--header-height);
background: #ffe;
width: 100vw;
display: flex;
justify-content: space-between;
}
.header-application-title-container {
display: flex;
}
.header-application-title-logo {
width: var(--header-height);
height: var(--header-height);
padding: 2px 2px 2px 2px;
margin: 0px 2px 0px 5px;
}
.header-application-title-text {
font-weight: 600;
margin: 0px 2px 0px 2px;
}
.header-device-selector-container {
margin: 0 10px 0 0;
display: flex;
}
.header-device-selector-text {
margin: 0px 2px 0px 10px;
}
.device-selector-option {
font-size: 1rem;
}
.device-selector-select {
max-width: 10rem;
font-size: 0.7rem;
}
/* Body */
.body {
position: fixed;
top: var(--header-height);
width: 100%;
height: calc(100% - var(--header-height));
}
/* Body -> Controller */
.controller {
display: flex;
margin: 10px 0 10px 40px;
}
/* Body -> Controller -> Corpus */
.corpus-selector-label {
font-size: 1.3rem;
font-weight: 600;
padding: 0 4px 0 4px;
}
.corpus-selector-option {
font-size: 1rem;
padding: 0 4px 0 4px;
}
.corpus-selector-select {
max-width: 10rem;
font-size: 1rem;
padding: 0 4px 0 4px;
}
/* Body -> Controller -> Text Index */
.text-index-selector-container {
display: flex;
user-select: none;
padding: 0 40px 0 40px;
text-align: center;
}
.text-index-selector-button {
color: #333;
cursor: pointer;
font-size: 1.3rem;
font-weight: 600;
border: 1px solid #000;
width: 30px;
border-radius: 3px;
}
.text-index-selector-button-disable {
font-size: 1.3rem;
font-weight: 600;
color: #ddd;
border: 1px solid #000;
width: 30px;
border-radius: 3px;
}
.text-index-selector-current-index {
font-size: 1.3rem;
font-weight: 600;
padding: 0 2px 0 2px;
width: 100px;
}
/* Body -> Controller -> Audio Controller */
.audio-controller-button-container {
display: flex;
user-select: none;
text-align: center;
}
.audio-controller-button {
color: #000;
font-weight: 600;
cursor: pointer;
border: 1px solid #444;
border-radius: 3px;
width: 100px;
margin: 0 4px 0 4px;
}
.audio-controller-button-disabled {
color: #aaa;
border: 1px solid #444;
border-radius: 3px;
width: 100px;
margin: 0 4px 0 4px;
}
.audio-controller-button-active {
color: #444;
font-weight: 600;
background: #dfd;
border: 1px solid #444;
border-radius: 3px;
width: 100px;
margin: 0 4px 0 4px;
}
.audio-controller-button-attention {
color: #44f;
font-weight: 600;
cursor: pointer;
background: #fcc;
border: 1px solid #444;
border-radius: 3px;
width: 100px;
margin: 0 4px 0 4px;
}
/* Body -> Controller -> Export Controller */
.export-controller-button-container {
display: flex;
flex-direction: row;
user-select: none;
text-align: center;
padding: 0 60px 0 60px;
}
.export-controller-button-export {
color: #000;
font-weight: 600;
cursor: pointer;
border: 1px solid #444;
border-radius: 3px;
width: 100px;
margin: 0 4px 0 4px;
}
.export-controller-button-export-disabled {
color: #aaa;
font-weight: 600;
border: 1px solid #444;
border-radius: 3px;
width: 100px;
margin: 0 4px 0 4px;
}
.export-controller-button-export-memo {
color: rgb(190, 129, 75);
font-weight: 600;
}
.export-controller-export-progress {
}
/* */
/* */
/* */
/* */
.recorder-card-container {
height: 300px;
width: 90%;
margin: 0 20px 0 20px;
overflow-y: hidden;
overflow-x: hidden;
}
.recorder-card-list {
position: relative;
display: flex;
flex-direction: column;
width: 100%;
transition: all var(--sidebar-transition-time) var(--sidebar-transition-animation);
}
.recorder-card {
border: 1px solid rgb(103, 103, 103);
background: rgba(255, 255, 238, 0.8);
height: 300px;
width: 100%;
border-radius: 15px;
}
.recorder-card-title {
font-size: 1.5rem;
padding: 10px;
color: #644;
}
.recorder-card-text {
font-size: 1.7rem;
padding: 4px 10px 4px 10px;
color: #c44;
}
.recorder-card-text_hira {
font-size: 1.7rem;
padding: 4px 10px 4px 10px;
color: #477;
}
/* */
/* */
/* */
.waveform-container,
.mel-spectrogram-container
{
width: 500px;
box-sizing: content-box;
/* border: 1px solid #aaa; */
margin: 20px auto;
/* padding: 30px; */
background: #fff;
}
.mel-spectrogram-div{
width:100%;
height:150px;
display:flex;
object-fit:contain;
margin-top:10px;
}
.mel-spectrogram-canvas{
width: 100%;
}
.waveform-header {
display: flex;
}
.waveform-header-item {
padding: 0 5px 0 5px;
width: 300px;
}
.waveform-header-item-warn {
padding: 0 5px 0 5px;
width: 200px;
color: #f00;
}

View File

@ -0,0 +1,59 @@
.front-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: 30px;
}
.front-title {
font-size: 4rem;
font-weight: 100;
}
.front-description {
font-size: 0.9rem;
text-align: center;
}
.front-description-img {
height: 2rem;
width: 50%;
height: 50%;
border: 3px solid;
}
.front-description-strong {
color: #f66;
font-size: 0.9rem;
font-weight: 600;
}
.front-start-button {
font-size: 4rem;
border: 3px solid #333;
width: 500px;
padding: 15px;
cursor: pointer;
text-align: center;
margin: 100px 0 0 0 auto;
user-select: none;
}
.front-start-button-color {
background: #fee;
}
.front-note {
font-size: 1rem;
}
.front-attention {
font-size: 0.8rem;
color: #f55;
font-weight: 600;
}
.front-disclaimer {
font-size: 0.8rem;
}
.front-clear-setting{
color:#999;
border:solid 2px #999;
padding:2px;
border-radius:4px;
user-select:none;
cursor:pointer;
}

View File

@ -0,0 +1,77 @@
/* Header */
.header-container {
position: fixed;
top: 0px;
left: 0px;
height: var(--header-height);
width: 100vw;
}
/* Body*/
.body-container {
position: fixed;
top: var(--header-height);
left: 0px;
height: calc(100vh - var(--header-height));
background: linear-gradient(45deg, var(--company-color1) 0, 5%, var(--company-color2) 5% 10%, var(--company-color3) 10% 80%, var(--company-color1) 80% 85%, var(--company-color2) 85% 100%);
}
.open-right-sidebar-checkbox:checked + .body-container {
width: calc(100vw - var(--right-sidebar-width));
transition: all var(--sidebar-transition-time) var(--sidebar-transition-animation);
}
.open-right-sidebar-checkbox + .body-container {
width: calc(100vw);
transition: all var(--sidebar-transition-time) var(--sidebar-transition-animation);
}
/* RightSidebar*/
.right-sidebar-container {
position: fixed;
top: var(--header-height);
height: calc(100vh - var(--header-height));
display: flex;
flex-direction: column;
width: var(--right-sidebar-width);
background: var(--company-color3);
z-index: 100;
}
.right-sidebar-container:before {
content: "";
position: absolute;
height: 100vh;
width: var(--right-sidebar-width);
background: var(--company-color2-alpha);
z-index: -1;
}
.right-sidebar-container:after {
content: "";
position: absolute;
height: 100vh;
width: var(--right-sidebar-width);
background: var(--company-color1-alpha);
clip-path: ellipse(158% 41% at 60% 30%);
z-index: -1;
}
.open-right-sidebar-checkbox:checked + .right-sidebar-container {
right: 0;
transition: all var(--sidebar-transition-time) var(--sidebar-transition-animation);
}
.open-right-sidebar-checkbox + .right-sidebar-container {
right: calc(-1 * var(--right-sidebar-width));
transition: all var(--sidebar-transition-time) var(--sidebar-transition-animation);
}
.split-2-5-3 {
& > div:nth-child(1) {
left: 0px;
width: 20%;
}
& > div:nth-child(2) {
left: 20%;
width: 50%;
}
& > div:nth-child(3) {
left: 70%;
width: 30%;
}
}

View File

@ -0,0 +1,52 @@
/* Header */
.header {
height: 100%;
width: 100%;
background: #ffe;
display: flex;
justify-content: space-between;
}
.header-application-title-container {
display: flex;
.header-application-title-logo {
width: var(--header-height);
height: var(--header-height);
padding: 2px 2px 2px 2px;
margin: 0px 2px 0px 5px;
}
.header-application-title-text {
font-weight: 600;
margin: 0px 2px 0px 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.header-device-selector-container{
display: flex;
& > div{
/* text-overflow: ellipsis; */
white-space: nowrap;
overflow: hidden;
font-size: 1rem;
margin-left:0.5rem;
margin-right:0.5rem;
}
.device-selector-select {
max-width: 10rem;
font-size: 0.7rem;
margin-left:0.5rem;
margin-right:0.5rem;
}
}
.header-button-container {
display: flex;
}
.header-button-spacer {
width: 1rem;
}

View File

@ -0,0 +1,288 @@
.body {
/* background: var(--company-color1); */
background:rgba(250,240,240,0.98);
width: 100%;
height: 100%;
flex-grow: 1;
display:flex;
flex-direction:column;
* {
overflow-x: hidden;
overflow-y: hidden;
}
}
.body-panel{
width: 100%;
}
.height-10{
min-height:10%;
}
.height-30{
min-height:30%;
height:30%;
}
.height-40{
min-height:40%;
}
.height-60{
min-height:60%;
}
.body-panel-row{
margin:1rem;
display:flex;
flex-direction:row;
user-select:none;
.label{
color:#597;
font-size:1.3rem;
font-weight:600;
}
.text{
color:#555;
font-size:1rem;
font-weight:600;
vertical-align: middle;
}
.buttons{
display:flex;
flex-direction:row;
.button{
min-width:10%;
text-align:center;
margin-left:0.5rem;
margin-right:0.5rem;
padding:4px;
border-radius:6px;
font-weight:600;
border:solid 1px #888;
color:#000;
background:#fff;
cursor: pointer;
}
.active{
color:#3b3;
background:#fff;
cursor: default;
}
.disable{
background:rgba(0,5,0,0.3);
color:rgba(0,5,0,0.3);
cursor: default;
}
.attention{
background:rgba(200,5,0,0.6);
cursor: default;
}
}
.selector-container{
display:flex;
* {
margin-left:2px;
margin-right:2px;
}
.selector{
.select{
color:#2a4;
}
}
}
.pager-container{
display:flex;
.index-selector-button{
text-align:center;
margin-left:0.5rem;
margin-right:0.5rem;
padding:4px;
border-radius:6px;
color:#000;
cursor: pointer;
width:1.5rem;
}
.disable{
color:rgba(0,5,0,0.3);
}
.label{
text-align:center;
width:6rem;
}
}
}
.body-panel-area{
width:100%;
height:100%;
padding:2rem;
display:flex;
flex-direction:column;
user-select:none;
.card{
border:solid 1px #000;
border-radius:10px;
width:100%;
min-height: 10rem;
background: rgba(200,200,110,0.9);
padding:20px;
overflow-y: scroll;
.title{
font-size:1.5rem;
}
.text{
display:flex;
font-size:1.5rem;
color:#333;
font-weight:200;
.tag{
color:#633;
width:8rem;
font-weight:600;
}
}
&::-webkit-scrollbar {
width: 10px;
height: 10px;
}
&::-webkit-scrollbar-track {
background-color: rgba(0, 0, 0, 0.1);
border-radius: 10px;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.4);
border-radius: 10px;
}
}
}
.split-2-8{
& > div:nth-child(1) {
left: 0px;
width: 20%;
}
& > div:nth-child(2) {
left: 20%;
width: 80%;
}
}
.split-3-4-3{
& > div:nth-child(1) {
left: 0px;
width: 30%;
}
& > div:nth-child(2) {
left: 30%;
width: 40%;
}
& > div:nth-child(3) {
left: 70%;
width: 30%;
}
}
.split-2-6-2{
& > div:nth-child(1) {
left: 0px;
width: 20%;
}
& > div:nth-child(2) {
left: 20%;
width: 60%;
}
& > div:nth-child(3) {
left: 80%;
width: 20%;
}
}
.split-2-2-5-1{
& > div:nth-child(1) {
left: 0px;
width: 20%;
}
& > div:nth-child(2) {
left: 20%;
width: 20%;
}
& > div:nth-child(3) {
left: 40%;
width: 50%;
}
& > div:nth-child(4) {
left: 90%;
width: 10%;
}
}
.split-1-1-2-1-2{
& > div:nth-child(1) {
/* left: 0px; */
width: 10%;
}
& > div:nth-child(2) {
/* left: 10%; */
width: 10%;
}
& > div:nth-child(3) {
/* left: 20%; */
width: 30%;
}
& > div:nth-child(4) {
/* left: 50%; */
width: 10%;
}
& > div:nth-child(5) {
/* left: 60%; */
width: 10%;
}
}
.waveform-container,
.mel-spectrogram-container
{
width: 500px;
margin: 0 auto;
}
.mel-spectrogram-div{
width:100%;
height:150px;
display:flex;
object-fit:contain;
margin-top:10px;
}
.mel-spectrogram-canvas{
width: 100%;
}
.waveform-header {
display: flex;
}
.waveform-header-item {
padding: 0 5px 0 5px;
width: 250px;
}
.waveform-header-item-warn {
padding: 0 5px 0 5px;
width: 200px;
color: #f00;
}
.button{
min-width:10%;
text-align:center;
margin-left:0.5rem;
margin-right:0.5rem;
padding:4px;
border-radius:6px;
font-weight:600;
border:solid 1px #888;
color:#000;
background:#fff;
cursor: pointer;
}

View File

@ -0,0 +1,296 @@
@import "./041_VoiceLoaderAudioPlayer.css";
.right-sidebar {
}
/* Partition */
.sidebar-partition {
position: static;
display: flex;
flex-direction: column;
width: 100%;
color: rgba(255, 255, 255, 1);
background: rgba(0, 0, 0, 0);
z-index: 10;
overflow: hidden;
}
.state-control-checkbox:checked + .sidebar-partition .sidebar-content {
max-height: 800px;
transition: all var(--sidebar-transition-time) var(--sidebar-transition-animation);
}
.state-control-checkbox + .sidebar-partition .sidebar-content {
max-height: 0px;
transition: all var(--sidebar-transition-time) var(--sidebar-transition-animation);
}
/* Header */
.sidebar-header {
position: static;
width: 100%;
height: var(--header-height);
font-size: 1.1rem;
background: rgba(10, 10, 10, 0.5);
display: flex;
justify-content: space-between;
.sidebar-header-title {
padding-left: 1rem;
user-select: none;
}
.sidebar-header-caret {
align-items: right;
}
}
/* Content */
.sidebar-content {
padding: 0px 5px 0px 5px;
position: static;
width: 100%;
height: auto;
/* height: calc(100% - var(--header-height)); */
background: rgba(200, 0, 0, 1);
user-select: none;
}
/***** 行分割 *****/
.sidebar-content-row-1 {
display: flex;
width: 100%;
justify-content: center;
margin: 1px 0px 1px 0px;
}
/***** 行分割(x2) *****/
.sidebar-content-row-3-7 {
display: flex;
width: 100%;
justify-content: center;
margin: 1px 0px 1px 0px;
& > div:nth-child(1) {
left: 0px;
width: 30%;
}
& > div:nth-child(2) {
left: 30%;
width: 70%;
}
}
.sidebar-content-row-5-5 {
display: flex;
width: 100%;
justify-content: center;
margin: 1px 0px 1px 0px;
& > div:nth-child(1) {
left: 0px;
width: 50%;
}
& > div:nth-child(2) {
left: 50%;
width: 50%;
}
}
.sidebar-content-row-7-3 {
display: flex;
width: 100%;
justify-content: center;
margin: 1px 0px 1px 0px;
& > div:nth-child(1) {
left: 0px;
width: 70%;
}
& > div:nth-child(2) {
left: 70%;
width: 30%;
}
}
/***** 行分割(x3) *****/
.sidebar-content-row-3-5-2 {
display: flex;
width: 100%;
justify-content: center;
margin: 1px 0px 1px 0px;
& > div:nth-child(1) {
left: 0px;
width: 30%;
}
& > div:nth-child(2) {
left: 30%;
width: 50%;
}
& > div:nth-child(3) {
left: 80%;
width: 20%;
}
}
.sidebar-content-row-4-3-3 {
display: flex;
width: 100%;
justify-content: center;
margin: 1px 0px 1px 0px;
& > div:nth-child(1) {
left: 0px;
width: 40%;
}
& > div:nth-child(2) {
left: 40%;
width: 30%;
}
& > div:nth-child(3) {
left: 70%;
width: 30%;
}
}
/***** Divider *****/
.sidebar-content-row-dividing {
height: 5px;
width: 80%;
background: #ffffff88;
margin: 10px 0px 10px 0px;
border-radius: 5px;
}
/***** Padding *****/
.pad-left-03 {
padding-left: 0.3rem;
}
.pad-left-1 {
padding-left: 1rem;
}
.pad-left-3 {
padding-left: 3rem;
}
.stick-to-right {
text-align: right;
}
.stick-to-left {
text-align: left;
}
.row-space-holder-2 {
height: 2rem;
}
.no-wrap {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/**** 行中身 ****/
/* Label */
.sidebar-content-row-label {
width: 100%;
}
.sidebar-content-row-label-small {
font-size: 0.75rem;
}
/* Button */
.sidebar-content-row-button,
.sidebar-content-row-button-activated,
.sidebar-content-row-button-stanby {
padding: 0px 5px 0px 5px;
margin: 0px 5px 0px 5px;
border-radius: 2px;
border: 1px solid #446;
cursor: pointer;
/* width: 30%; */
text-align: center;
font-weight: 100;
}
.sidebar-content-row-button-activated {
/* width: 50%; */
background: #bbd;
color: #000;
}
.sidebar-content-row-button-activated:hover {
/* background: #4f5; */
font-weight: 600;
}
.sidebar-content-row-button,
.sidebar-content-row-button-stanby {
background: #555;
}
.sidebar-content-row-button:hover,
.sidebar-content-row-button-stanby:hover {
/* background: #666; */
font-weight: 400;
}
/* Select */
.sidebar-content-row-select {
}
.sidebar-content-row-select-option {
font-size: 1rem;
}
.sidebar-content-row-select-select {
max-width: 90%;
min-width: 30%;
font-size: 0.7rem;
}
/* Device Select */
.device-selector-option {
font-size: 1rem;
}
.device-selector-select {
max-width: 90%;
min-width: 50%;
font-size: 0.7rem;
}
/* input */
.sidebar-content-row-input {
}
.sidebar-content-row-input-input {
max-width: 90%;
min-width: 30%;
font-size: 0.7rem;
}
.sidebar-content-row-input-input-full {
width: 90%;
font-size: 0.7rem;
}
.sidebar-content-row-input-checkbox {
max-width: 90%;
min-width: 30%;
font-size: 0.7rem;
}
/* Visualizer */
.output-audio-visializer {
margin: 5%;
width: 40%;
height: 50px;
}
.output-audio-visializer-canvas {
width: 100%;
height: 100%;
}
/* Misc */
.sidebar-content-row-container {
width: 100%;
height: 100px;
}
.avatar-controller-video {
width: 100%;
height: 100%;
object-fit: contain;
transform: scaleX(-1);
padding: 10px;
}
.avatar-controller-avatar-area {
width: 100%;
height: 100%;
display: flex;
padding: 10px;
}
.avatar-controller-background-video {
width: 1px;
height: 1px;
position: absolute;
left: -100px;
}

View File

@ -0,0 +1,70 @@
/* 前提条件 */
.rotate-button-container {
height: var(--header-height);
width: var(--header-height);
position: relative;
}
.rotate-button {
display: none;
}
.rotate-button ~ .rotate-lable {
padding: 2px;
position: absolute;
transition: all 0.3s;
cursor: pointer;
height: var(--header-height);
width: var(--header-height);
}
.rotate-button ~ .rotate-lable > * {
width: 100%;
height: 100%;
float: left;
transition: all 0.3s;
.spin-on {
width: 100%;
height: 100%;
display: none;
}
.spin-off {
width: 100%;
height: 100%;
display: blcok;
}
}
.rotate-button ~ .rotate-lable > .colored {
color: rgba(200, 200, 200, 0.8);
background: rgba(0, 0, 0, 1);
transition: all 0.3s;
.spin-on {
display: none;
}
.spin-off {
display: block;
}
}
.rotate-button:checked ~ .rotate-lable > .colored {
color: rgba(50, 240, 50, 0.8);
background: rgba(60, 60, 60, 1);
transition: all 0.3s;
.spin-on {
display: block;
}
.spin-off {
display: none;
}
}
.rotate-button:checked ~ .rotate-lable > .spinner {
width: 100%;
height: 100%;
transform: rotate(-180deg);
transition: all 0.3s;
box-sizing: border-box;
.spin-on {
display: block;
}
.spin-off {
display: none;
}
}

View File

@ -0,0 +1,41 @@
import React, { useMemo } from "react";
import { useAppSetting } from "../../003_provider/AppSettingProvider";
import { RightSidebarButton } from "./102_RightSidebarButton";
import { DeviceSelector } from "./103_DeviceSelector";
export const Header = () => {
const { applicationSetting } = useAppSetting()
const header = useMemo(() => {
return (
<div className="header">
<div className="header-application-title-container">
<img src="./assets/icons/zun.png" className="header-application-title-logo"></img>
<div className="header-application-title-text">Corpus Voice Recorder</div>
</div>
<div className="header-device-selector-container">
<div className="header-device-selector-text">Mic:</div>
<DeviceSelector deviceType={"audioinput"}></DeviceSelector>
<div className="header-device-selector-text">Sample Rate:{applicationSetting.applicationSetting.sample_rate}Hz</div>
<div className="header-device-selector-text">Sample Depth:16bit</div>
<div className="header-device-selector-text">Speaker:</div>
<DeviceSelector deviceType={"audiooutput"}></DeviceSelector>
</div>
<div className="header-button-container">
<a className="header-button-link" href="https://github.com/w-okada/voice-changer/wiki" target="_blank" rel="noopener noreferrer">
<img src="./assets/icons/help-circle.svg" />
</a>
<div className="header-button-spacer"></div>
<a className="header-button-link" href="https://github.com/w-okada/voice-changer" target="_blank" rel="noopener noreferrer">
<img src="./assets/icons/github.svg" />
</a>
<div className="header-button-spacer"></div>
<RightSidebarButton></RightSidebarButton>
</div>
</div>
)
}, [])
return header;
};

View File

@ -0,0 +1,37 @@
import { IconName, IconPrefix } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { useMemo } from "react";
import { StateControlCheckbox } from "../003_hooks/useStateControlCheckbox";
export const AnimationTypes = {
colored: "colored",
spinner: "spinner",
} as const;
export type AnimationTypes = typeof AnimationTypes[keyof typeof AnimationTypes];
export type HeaderButtonProps = {
stateControlCheckbox: StateControlCheckbox;
tooltip: string;
onIcon: [IconPrefix, IconName];
offIcon: [IconPrefix, IconName];
animation: AnimationTypes;
tooltipClass?: string;
};
export const HeaderButton = (props: HeaderButtonProps) => {
const headerButton = useMemo(() => {
const tooltipClass = props.tooltipClass || "tooltip-bottom";
return (
<div className={`rotate-button-container ${tooltipClass}`} data-tooltip={props.tooltip}>
{props.stateControlCheckbox.trigger}
<label htmlFor={props.stateControlCheckbox.className} className="rotate-lable">
<div className={props.animation}>
<FontAwesomeIcon icon={props.onIcon} className="spin-on" />
<FontAwesomeIcon icon={props.offIcon} className="spin-off" />
</div>
</label>
</div>
);
}, []);
return headerButton;
};

View File

@ -0,0 +1,17 @@
import React, { useMemo } from "react";
import { AnimationTypes, HeaderButton, HeaderButtonProps } from "./101_HeaderButton";
import { useAppState } from "../../003_provider/AppStateProvider"
export const RightSidebarButton = () => {
const { frontendManagerState } = useAppState();
const rightSidebarButtonProps: HeaderButtonProps = {
stateControlCheckbox: frontendManagerState.stateControls.openRightSidebarCheckbox,
tooltip: "open/close",
onIcon: ["fas", "angles-right"],
offIcon: ["fas", "angles-right"],
animation: AnimationTypes.spinner,
};
const rightSidebarButton = useMemo(() => {
return <HeaderButton {...rightSidebarButtonProps}></HeaderButton>;
}, []);
return rightSidebarButton;
};

View File

@ -0,0 +1,83 @@
import React, { Suspense, useMemo } from "react";
import { useAppSetting } from "../../003_provider/AppSettingProvider";
export const DeviceType = {
audioinput: "audioinput",
videoinput: "videoinput",
audiooutput: "audiooutput",
} as const;
export type DeviceType = typeof DeviceType[keyof typeof DeviceType];
export type DeviceManagerProps = {
deviceType: DeviceType;
};
export const DeviceSelector = (props: DeviceManagerProps) => {
const { deviceManagerState } = useAppSetting()
const targetDevices = useMemo(() => {
if (props.deviceType === "audioinput") {
return deviceManagerState.audioInputDevices;
} else if (props.deviceType === "videoinput") {
return deviceManagerState.videoInputDevices;
} else {
return deviceManagerState.audioOutputDevices;
}
}, [deviceManagerState.audioInputDevices, deviceManagerState.videoInputDevices, deviceManagerState.audioOutputDevices]);
const currentValue = useMemo(() => {
if (props.deviceType === "audioinput") {
return deviceManagerState.audioInputDeviceId || "none";
} else if (props.deviceType === "videoinput") {
return deviceManagerState.videoInputDeviceId || "none";
} else {
return deviceManagerState.audioOutputDeviceId || "none";
}
}, [deviceManagerState.audioInputDeviceId, deviceManagerState.videoInputDeviceId, deviceManagerState.audioOutputDeviceId]);
const setDeviceId = (deviceId: string) => {
if (props.deviceType === "audioinput") {
deviceManagerState.setAudioInputDeviceId(deviceId);
} else if (props.deviceType === "videoinput") {
deviceManagerState.setVideoInputDeviceId(deviceId);
} else {
deviceManagerState.setAudioOutputDeviceId(deviceId);
}
};
const options = useMemo(() => {
return targetDevices.map((x) => {
return (
<option className="device-selector-option" key={x.deviceId} value={x.deviceId}>
{x.label}
</option>
);
});
}, [targetDevices]);
const selector = useMemo(() => {
return (
<select
defaultValue={currentValue}
onChange={(e) => {
setDeviceId(e.target.value);
}}
className="device-selector-select"
>
{options}
</select>
);
}, [targetDevices, options, currentValue]);
const Wrapper = () => {
if (targetDevices.length === 0) {
throw new Promise((resolve) => setTimeout(resolve, 1000 * 0.5));
}
return selector;
};
return (
<Suspense fallback={<>device loading...</>}>
<Wrapper></Wrapper>
</Suspense>
);
};

View File

@ -0,0 +1,42 @@
import React from "react"
import { CorpusSelector } from "./201_CorpusSelector"
import { TextIndexSelector } from "./202_TextIndexSelector"
import { AudioController } from "./203_AudioController"
import { ExportController } from "./204_ExportController"
import { CorpusTextArea } from "./205_CorpusTextArea"
import { WaveSurferView } from "./206_WaveSurferView"
export const Body = () => {
return (
<div className="body">
<div className="body-panel height-10">
<div className="body-panel-row split-2-2-5-1">
<div className="selector-container">
<CorpusSelector></CorpusSelector>
</div>
<div className="pager-container split-3-4-3">
<TextIndexSelector></TextIndexSelector>
</div>
<div className="buttons">
<AudioController></AudioController>
</div>
<div className="buttons">
<ExportController></ExportController>
</div>
</div>
</div>
<div className="body-panel height-30">
<div className="body-panel-area">
<CorpusTextArea></CorpusTextArea>
</div>
</div>
<div className="body-panel height-60">
<div className="body-panel-area">
<WaveSurferView></WaveSurferView>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,64 @@
import React, { Suspense, useMemo } from "react";
import { useAppSetting } from "../../003_provider/AppSettingProvider";
import { useAppState } from "../../003_provider/AppStateProvider";
export const CorpusSelector = () => {
const { applicationSetting } = useAppSetting()
const { corpusDataState, audioControllerState } = useAppState();
const audioActive = useMemo(() => {
return audioControllerState.audioControllerState === "play" || audioControllerState.audioControllerState === "record";
}, [audioControllerState.audioControllerState]);
const unsavedRecord = useMemo(() => {
return audioControllerState.unsavedRecord;
}, [audioControllerState.unsavedRecord]);
const options = useMemo(() => {
const options = Object.keys(corpusDataState.corpusTextData).map((title) => {
return (
<option key={title} value={title}>
{title}
</option>
);
});
if (!applicationSetting.applicationSetting.current_text) {
options.unshift(<option key={"none"} value={"none"}></option>);
}
return options;
}, [corpusDataState.corpusTextData]);
const selector = useMemo(() => {
const disabled = audioActive || unsavedRecord
return (
<>
<div className="label">Corpus:</div>
<div className="selector">
<select
disabled={disabled ? true : false}
defaultValue={applicationSetting.applicationSetting.current_text || ""}
onChange={(e) => {
applicationSetting.setCurrentText(e.target.value);
applicationSetting.setCurrentTextIndex(0);
}}
className="select"
>
{options}
</select>
</div>
</>
);
}, [applicationSetting.applicationSetting.current_text, options, audioActive, unsavedRecord]);
const Wrapper = () => {
if (Object.keys(corpusDataState.corpusTextData).length === 0) {
throw new Promise((resolve) => setTimeout(resolve, 1000 * 2));
}
return selector;
};
return (
<Suspense fallback={<>loading...</>}>
<Wrapper></Wrapper>
</Suspense>
);
};

View File

@ -0,0 +1,77 @@
import React, { useMemo } from "react";
import { useAppState } from "../../003_provider/AppStateProvider";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useAppSetting } from "../../003_provider/AppSettingProvider";
export const TextIndexSelector = () => {
const { applicationSetting } = useAppSetting()
const { corpusDataState, audioControllerState } = useAppState();
const audioActive = useMemo(() => {
return audioControllerState.audioControllerState === "play" || audioControllerState.audioControllerState === "record";
}, [audioControllerState.audioControllerState]);
const unsavedRecord = useMemo(() => {
return audioControllerState.unsavedRecord;
}, [audioControllerState.unsavedRecord]);
const prevButton = useMemo(() => {
let className = "";
let prevIndex = () => { };
if (applicationSetting.applicationSetting.current_text_index === 0 || audioActive || unsavedRecord) {
className = "index-selector-button disable";
} else {
className = "index-selector-button";
prevIndex = () => {
applicationSetting.setCurrentTextIndex(applicationSetting.applicationSetting.current_text_index - 1);
};
}
return (
<div className={className} onClick={prevIndex}>
<FontAwesomeIcon icon={["fas", "angle-left"]} size="1x" />
</div>
);
}, [applicationSetting.applicationSetting.current_text, applicationSetting.applicationSetting.current_text_index, audioActive, unsavedRecord]);
const nextButton = useMemo(() => {
const corpus = corpusDataState.corpusTextData[applicationSetting.applicationSetting.current_text];
if (!corpus) {
return <></>
}
let className = "";
let nextIndex = () => { };
const length = corpus.text.length;
if (applicationSetting.applicationSetting.current_text_index === length - 1 || audioActive || unsavedRecord) {
className = "index-selector-button disable";
} else {
className = "index-selector-button";
nextIndex = () => {
applicationSetting.setCurrentTextIndex(applicationSetting.applicationSetting.current_text_index + 1);
};
}
return (
<div className={className} onClick={nextIndex}>
<FontAwesomeIcon icon={["fas", "angle-right"]} size="1x" />
</div>
);
}, [corpusDataState.corpusTextData, applicationSetting.applicationSetting.current_text, applicationSetting.applicationSetting.current_text_index, audioActive, unsavedRecord]);
const indexText = useMemo(() => {
const corpus = corpusDataState.corpusTextData[applicationSetting.applicationSetting.current_text];
if (!corpus) {
return <></>
}
const length = corpus.text.length;
const text = `${applicationSetting.applicationSetting.current_text_index + 1}/${length}`;
return <div className="label">{text}</div>;
}, [corpusDataState.corpusTextData, applicationSetting.applicationSetting.current_text, applicationSetting.applicationSetting.current_text_index]);
return (
<>
{prevButton}
{indexText}
{nextButton}
</>
);
};

View File

@ -0,0 +1,154 @@
import React, { useMemo } from "react";
import { useAppSetting } from "../../003_provider/AppSettingProvider";
import { useAppState } from "../../003_provider/AppStateProvider";
const enabledButtonClass = "button";
const disabledButtonClass = "button disable";
const activeButtonClass = "button active";
const attentionButtonClass = "button attention";
type ButtonStates = {
recordButtonClass: string;
stopButtonClass: string;
playButtonClass: string;
keepButtonClass: string;
dismissButtonClass: string;
recordAction: () => void;
stopAction: () => void;
playAction: () => void;
keepAction: () => void;
dismissAction: () => void;
};
export const AudioController = () => {
const { applicationSetting } = useAppSetting()
const { audioControllerState, mediaRecorderState, waveSurferState } = useAppState();
const { recordButton, stopButton, playButton, keepButton, dismissButton } = useMemo(() => {
const buttonStates: ButtonStates = {
recordButtonClass: "",
stopButtonClass: "",
playButtonClass: "",
keepButtonClass: "",
dismissButtonClass: "",
recordAction: () => { },
stopAction: () => { },
playAction: () => { },
keepAction: () => { },
dismissAction: () => { },
};
switch (audioControllerState.audioControllerState) {
case "stop":
// ボタンの状態
buttonStates.recordButtonClass = enabledButtonClass; // [action needed]
buttonStates.stopButtonClass = activeButtonClass;
if (audioControllerState.tempUserData.vfWavBlob) {
buttonStates.playButtonClass = enabledButtonClass; // [action needed]
} else {
buttonStates.playButtonClass = disabledButtonClass;
}
if (audioControllerState.unsavedRecord) {
{
// セーブされていない新録がある場合。
buttonStates.keepButtonClass = attentionButtonClass; // [action needed]
buttonStates.dismissButtonClass = attentionButtonClass; // [action needed]
}
} else {
buttonStates.keepButtonClass = disabledButtonClass;
buttonStates.dismissButtonClass = disabledButtonClass;
}
// ボタンのアクション
buttonStates.recordAction = () => {
audioControllerState.setAudioControllerState("record");
mediaRecorderState.startRecord();
};
if (audioControllerState.tempUserData.vfWavBlob) {
// バッファ上に音声がある場合。(ローカルストレージ、新録両方。)
buttonStates.playAction = () => {
audioControllerState.setAudioControllerState("play");
waveSurferState.playRegion();
};
}
if (audioControllerState.unsavedRecord) {
// セーブされていない新録がある場合。
buttonStates.keepAction = () => {
audioControllerState.saveWavBlob()
audioControllerState.setUnsavedRecord(false);
};
buttonStates.dismissAction = () => {
audioControllerState.restoreFixedUserData()
audioControllerState.setUnsavedRecord(false);
};
}
break;
case "record":
buttonStates.recordButtonClass = activeButtonClass;
buttonStates.stopButtonClass = enabledButtonClass; // [action needed]
buttonStates.playButtonClass = disabledButtonClass;
buttonStates.keepButtonClass = disabledButtonClass;
buttonStates.dismissButtonClass = disabledButtonClass;
buttonStates.stopAction = () => {
audioControllerState.setAudioControllerState("stop");
mediaRecorderState.pauseRecord();
const { micWavBlob, vfWavBlob, vfDuration, vfSamples, micSamples } = mediaRecorderState.getRecordedDataBlobs();
// const micSpec = drawMel(micSamples, applicationSetting.applicationSetting.sample_rate)
// const vfSpec = drawMel(vfSamples, applicationSetting.applicationSetting.sample_rate)
audioControllerState.setTempWavBlob(micWavBlob, vfWavBlob, micSamples, vfSamples, "", "", [0, vfDuration])
audioControllerState.setUnsavedRecord(true);
waveSurferState.loadMusic(vfWavBlob);
};
break;
case "play":
buttonStates.recordButtonClass = disabledButtonClass;
buttonStates.stopButtonClass = enabledButtonClass; // [action needed]
buttonStates.playButtonClass = activeButtonClass;
buttonStates.keepButtonClass = disabledButtonClass;
buttonStates.dismissButtonClass = disabledButtonClass;
buttonStates.stopAction = () => {
waveSurferState.stop();
audioControllerState.setAudioControllerState("stop");
};
break;
}
const recordButton = (
<div className={buttonStates.recordButtonClass} onClick={buttonStates.recordAction}>
record
</div>
);
const stopButton = (
<div className={buttonStates.stopButtonClass} onClick={buttonStates.stopAction}>
stop
</div>
);
const playButton = (
<div className={buttonStates.playButtonClass} onClick={buttonStates.playAction}>
play
</div>
);
const keepButton = (
<div className={buttonStates.keepButtonClass} onClick={buttonStates.keepAction}>
keep
</div>
);
const dismissButton = (
<div className={buttonStates.dismissButtonClass} onClick={buttonStates.dismissAction}>
dismiss
</div>
);
return { recordButton, stopButton, playButton, keepButton, dismissButton };
}, [audioControllerState.audioControllerState, audioControllerState.tempUserData, applicationSetting.applicationSetting.current_text, applicationSetting.applicationSetting.current_text_index, mediaRecorderState.startRecord, mediaRecorderState.pauseRecord]);
return (
<>
{recordButton} {stopButton} {playButton} {keepButton} {dismissButton}
</>
);
};

View File

@ -0,0 +1,164 @@
import React, { useMemo, useState } from "react";
import { useAppState } from "../../003_provider/AppStateProvider";
import { convert48KhzTo24Khz, generateTextFileName, generateWavFileName } from "../../const";
import JSZip from "jszip";
import { useAppSetting } from "../../003_provider/AppSettingProvider";
export const ExportController = () => {
const { appStateStorageState } = useAppSetting()
const { corpusDataState } = useAppState();
const [_exporting, setExporting] = useState<boolean>(false);
const updateProgress = (totalNum: number, processedNum: number) => {
const progress = document.getElementById("export-controller-export-progress") as HTMLDivElement;
progress.innerText = `${processedNum}/${totalNum}`;
};
const exportWav = async () => {
const zip = new JSZip();
const totalNum = Object.values(corpusDataState.corpusTextData).reduce((prev, cur) => {
return prev + cur.text.length
}, 0)
let processedNum = 0;
setExporting(true);
updateProgress(totalNum, 0);
for (let i = 0; i < Object.values(corpusDataState.corpusTextData).length; i++) {
const targetCorpus = Object.values(corpusDataState.corpusTextData)[i]
const prefix = targetCorpus.wavPrefix
for (let j = 0; j < targetCorpus.text.length; j++) {
processedNum++;
updateProgress(totalNum, processedNum);
const userData = await appStateStorageState.loadUserData("", prefix, j)
if (!userData || !userData.micWavBlob || !userData.vfWavBlob) {
continue;
}
const fileName = generateWavFileName(prefix, j);
const textFileName = generateTextFileName(prefix, j)
const textData = targetCorpus.text[j]
const textHiraData = targetCorpus.text_hira[j]
// 生データ
// zip.file(`raw/${fileName}`, userData.micWavBlob);
// updateProgress(corpus.text.length * 6, processedNum++);
// // 24Khzデータ
// const wav24Khz = convert48KhzTo24Khz(userData.micWavBlob);
// zip.file(`raw24k/${fileName}`, wav24Khz);
// updateProgress(corpus.text.length * 6, processedNum++);
// Cropデータ
const region = userData.region
const start = region ? region[0] : 0;
const end = region ? region[1] : 0;
// console.log("REGION", region)
// const wav24KhzTrim = convert48KhzTo24Khz(userData.micWavBlob, start, end);
// zip.file(`rawTrim24k/${fileName}`, wav24KhzTrim);
// updateProgress(corpus.text.length * 6, processedNum++);
// VF生データ
zip.file(`00_myvoice/vf/${fileName}`, userData.vfWavBlob);
// updateProgress(corpus.text.length * 6, processedNum++);
// // VF 24Khzデータ
// const vfWav24Khz = convert48KhzTo24Khz(userData.vfWavBlob);
// zip.file(`vf24k/${fileName}`, vfWav24Khz);
// updateProgress(corpus.text.length * 6, processedNum++);
// VF Cropデータ
const vfWav24KhzTrim = convert48KhzTo24Khz(userData.vfWavBlob, start, end);
zip.file(`00_myvoice/wav/${fileName}`, vfWav24KhzTrim);
// updateProgress(corpus.text.length * 6, processedNum++);
// TXT
zip.file(`00_myvoice/text/${textFileName}`, textHiraData);
zip.file(`00_myvoice/readable_text/${textFileName}`, textData);
}
}
// for (let i = 0; i < corpus.text.length; i++) {
// const userData = await appStateStorageState.loadUserData(title, prefix, i)
// if (!userData || !userData.micWavBlob || !userData.vfWavBlob) {
// updateProgress(corpus.text.length * 6, (processedNum += 6));
// continue;
// }
// const fileName = generateWavFileName(prefix, i);
// // 生データ
// zip.file(`raw/${fileName}`, userData.micWavBlob);
// updateProgress(corpus.text.length * 6, processedNum++);
// // 24Khzデータ
// const wav24Khz = convert48KhzTo24Khz(userData.micWavBlob);
// zip.file(`raw24k/${fileName}`, wav24Khz);
// updateProgress(corpus.text.length * 6, processedNum++);
// // Cropデータ
// const region = userData.region
// const start = region ? region[0] : 0;
// const end = region ? region[1] : 0;
// // console.log("REGION", region)
// const wav24KhzTrim = convert48KhzTo24Khz(userData.micWavBlob, start, end);
// zip.file(`rawTrim24k/${fileName}`, wav24KhzTrim);
// updateProgress(corpus.text.length * 6, processedNum++);
// // VF生データ
// zip.file(`vf/${fileName}`, userData.vfWavBlob);
// updateProgress(corpus.text.length * 6, processedNum++);
// // VF 24Khzデータ
// const vfWav24Khz = convert48KhzTo24Khz(userData.vfWavBlob);
// zip.file(`vf24k/${fileName}`, vfWav24Khz);
// updateProgress(corpus.text.length * 6, processedNum++);
// // VF Cropデータ
// const vfWav24KhzTrim = convert48KhzTo24Khz(userData.vfWavBlob, start, end);
// zip.file(`vfTrim24k/${fileName}`, vfWav24KhzTrim);
// updateProgress(corpus.text.length * 6, processedNum++);
// }
setExporting(false);
const blob = await zip.generateAsync({ type: "blob" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `myvoice.zip`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const exportButton = useMemo(() => {
const className = "button";
return (
<>
<div className={className} onClick={exportWav}>
Export
</div>
</>
);
}, [corpusDataState]);
return (
<>
{exportButton}
<span id="export-controller-export-progress" className="text" ></span >
</>
);
};

View File

@ -0,0 +1,28 @@
import React from "react";
import { useMemo } from "react";
import { useAppSetting } from "../../003_provider/AppSettingProvider";
import { useAppState } from "../../003_provider/AppStateProvider";
export const CorpusTextArea = () => {
const { applicationSetting } = useAppSetting()
const { corpusDataState } = useAppState();
const { text, text_hira } = useMemo(() => {
const corpus = corpusDataState.corpusTextData[applicationSetting.applicationSetting.current_text];
const text = corpus?.text[applicationSetting.applicationSetting.current_text_index] || "";
const text_hira = corpus?.text_hira[applicationSetting.applicationSetting.current_text_index] || "";
return { text, text_hira };
}, [corpusDataState.corpusTextData, applicationSetting.applicationSetting.current_text, applicationSetting.applicationSetting.current_text_index]);
return (
<div className="card">
<div className="title">{applicationSetting.applicationSetting.current_text_index + 1}</div>
<div className="text">
<div className="tag"></div>
<div>{text}</div></div>
<div className="text">
<div className="tag"></div>
<div>{text_hira}</div></div>
</div >
);
};

View File

@ -0,0 +1,102 @@
import React, { useEffect, useMemo } from "react"
import { useAppSetting } from "../../003_provider/AppSettingProvider";
import { useAppState } from "../../003_provider/AppStateProvider";
import { generateEmptyWav } from "../../const";
import { MelSpectrogram } from "./207_MelSpectrogram";
export const WaveSurferView = () => {
const { applicationSetting } = useAppSetting()
const { audioControllerState, waveSurferState, } = useAppState();
useEffect(() => {
const loadMusic = async () => {
if (!audioControllerState.tempUserData.vfWavBlob) {
const dummy = generateEmptyWav()
await waveSurferState.loadMusic(dummy);
return
}
await waveSurferState.loadMusic(audioControllerState.tempUserData.vfWavBlob);
}
loadMusic()
}, [audioControllerState.tempUserData]);
useEffect(() => {
const regionTimeDiv = document.getElementById("waveform-region-time") as HTMLDivElement;
const timeDiv = document.getElementById("waveform-time") as HTMLDivElement;
if (!audioControllerState.tempUserData.vfWavBlob) {
waveSurferState.setRegion(0, 0);
regionTimeDiv.innerText = `please record.`;
timeDiv.innerText = `no record.`
return
}
const start = audioControllerState.tempUserData.region ? audioControllerState.tempUserData.region[0] : 0;
const end = audioControllerState.tempUserData.region ? audioControllerState.tempUserData.region[1] : 1;
const dur = end - start;
regionTimeDiv.innerText = `Region:${start.toFixed(2)} - ${end.toFixed(2)} [${dur.toFixed(2)}]`;
waveSurferState.setRegion(start, end);
}, [audioControllerState.tempUserData])
// Wavesurfer
useEffect(() => {
const timeDiv = document.getElementById("waveform-time") as HTMLDivElement;
waveSurferState.setListener({
audioprocess: () => {
const timeInfos = waveSurferState.getTimeInfos();
if (timeInfos.totalTime < 1) {
timeDiv.className = "waveform-header-item-warn";
timeDiv.innerText = `WARNING!!! Under 1sec. Time:${timeInfos.currentTime.toFixed(2)} / ${timeInfos.totalTime.toFixed(2)}`;
} else {
timeDiv.className = "waveform-header-item";
timeDiv.innerText = `Time:${timeInfos.currentTime.toFixed(2)} / ${timeInfos.totalTime.toFixed(2)}`;
}
},
finish: () => {
audioControllerState.setAudioControllerState("stop");
},
ready: () => {
const timeInfos = waveSurferState.getTimeInfos();
if (timeInfos.totalTime < 1) {
timeDiv.className = "waveform-header-item-warn";
timeDiv.innerText = `WARNING!!! Under 1sec. Time:${timeInfos.currentTime.toFixed(2)} / ${timeInfos.totalTime.toFixed(2)}`;
} else {
timeDiv.className = "waveform-header-item";
timeDiv.innerText = `Time:${timeInfos.currentTime.toFixed(2)} / ${timeInfos.totalTime.toFixed(2)}`;
}
},
regionUpdate: (start: number, end: number) => {
if (!applicationSetting.applicationSetting.current_text) {
return;
}
audioControllerState.setTempWavBlob(audioControllerState.tempUserData.micWavBlob, audioControllerState.tempUserData.vfWavBlob, audioControllerState.tempUserData.micWavSamples, audioControllerState.tempUserData.vfWavSamples, audioControllerState.tempUserData.micSpec, audioControllerState.tempUserData.vfSpec, [start, end])
audioControllerState.setUnsavedRecord(true);
},
});
}, [waveSurferState.setListener, waveSurferState.getTimeInfos, applicationSetting.applicationSetting.current_text, applicationSetting.applicationSetting.current_text_index, audioControllerState.unsavedRecord, audioControllerState.tempUserData]);
const view = useMemo(() => {
return (
<>
<div className="height-40 waveform-container">
<div className="waveform-header">
<div id="waveform-time" className="waveform-header-item"></div>
<div id="waveform-region-time" className="waveform-header-item"></div>
</div>
<div id="waveform"></div>
<div id="wave-timeline"></div>
</div>
<div className="height-60 mel-spectrogram-container">
<MelSpectrogram></MelSpectrogram>
</div>
</>
)
}, [])
return (
<>
{view}
</>
)
}

View File

@ -0,0 +1,320 @@
// Original is from https://github.com/magenta/magenta-js (d8a7668 on 2 Nov 2021 Git stats)
// I extracted functions from original repos.to use.
// @ts-ignore
import * as FFT from 'fft.js';
type SpecParams = {
sampleRate: number;
hopLength?: number;
winLength?: number;
nFft?: number;
nMels?: number;
power?: number;
fMin?: number;
fMax?: number;
}
const hannWindow = (length: number) => {
const win = new Float32Array(length);
for (let i = 0; i < length; i++) {
win[i] = 0.5 * (1 - Math.cos((2 * Math.PI * i) / (length - 1)));
}
return win;
}
const padConstant = (data: Float32Array, padding: number | number[]) => {
let padLeft, padRight;
if (typeof padding === 'object') {
[padLeft, padRight] = padding;
} else {
padLeft = padRight = padding;
}
const out = new Float32Array(data.length + padLeft + padRight);
out.set(data, padLeft);
return out;
}
const padCenterToLength = (data: Float32Array, length: number) => {
// If data is longer than length, error!
if (data.length > length) {
throw new Error('Data is longer than length.');
}
const paddingLeft = Math.floor((length - data.length) / 2);
const paddingRight = length - data.length - paddingLeft;
return padConstant(data, [paddingLeft, paddingRight]);
}
const padReflect = (data: Float32Array, padding: number) => {
const out = padConstant(data, padding);
for (let i = 0; i < padding; i++) {
// Pad the beginning with reflected values.
out[i] = out[2 * padding - i];
// Pad the end with reflected values.
out[out.length - i - 1] = out[out.length - 2 * padding + i - 1];
}
return out;
}
const frame = (
data: Float32Array, frameLength: number,
hopLength: number): Float32Array[] => {
const bufferCount = Math.floor((data.length - frameLength) / hopLength) + 1;
const buffers = Array.from(
{ length: bufferCount }, (_x, _i) => new Float32Array(frameLength));
for (let i = 0; i < bufferCount; i++) {
const ind = i * hopLength;
const buffer = data.slice(ind, ind + frameLength);
buffers[i].set(buffer);
// In the end, we will likely have an incomplete buffer, which we should
// just ignore.
if (buffer.length !== frameLength) {
continue;
}
}
return buffers;
}
const applyWindow = (buffer: Float32Array, win: Float32Array) => {
if (buffer.length !== win.length) {
console.error(
`Buffer length ${buffer.length} != window length ${win.length}.`);
return null;
}
const out = new Float32Array(buffer.length);
for (let i = 0; i < buffer.length; i++) {
out[i] = win[i] * buffer[i];
}
return out;
}
const fft = (y: Float32Array) => {
const fft = new FFT(y.length);
const out = fft.createComplexArray();
const data = fft.toComplexArray(y, null);
fft.transform(out, data);
return out;
}
const stft = (y: Float32Array, params: SpecParams): Float32Array[] => {
const nFft = params.nFft || 2048;
const winLength = params.winLength || nFft;
const hopLength = params.hopLength || Math.floor(winLength / 4);
let fftWindow = hannWindow(winLength);
// Pad the window to be the size of nFft.
fftWindow = padCenterToLength(fftWindow, nFft);
// Pad the time series so that the frames are centered.
y = padReflect(y, Math.floor(nFft / 2));
// Window the time series.
const yFrames = frame(y, nFft, hopLength);
// Pre-allocate the STFT matrix.
const stftMatrix: Float32Array[] = [];
const width = yFrames.length;
const height = nFft + 2;
for (let i = 0; i < width; i++) {
// Each column is a Float32Array of size height.
const col = new Float32Array(height);
stftMatrix[i] = col;
}
for (let i = 0; i < width; i++) {
// Populate the STFT matrix.
const winBuffer = applyWindow(yFrames[i], fftWindow);
const col = fft(winBuffer!);
stftMatrix[i].set(col.slice(0, height));
}
return stftMatrix;
}
const pow = (arr: Float32Array, power: number) => {
return arr.map((v) => Math.pow(v, power));
}
function magSpectrogram(
stft: Float32Array[], power: number): [Float32Array[], number] {
const spec = stft.map((fft) => pow(mag(fft), power));
const nFft = stft[0].length - 1;
return [spec, nFft];
}
const mag = (y: Float32Array) => {
const out = new Float32Array(y.length / 2);
for (let i = 0; i < y.length / 2; i++) {
out[i] = Math.sqrt(y[i * 2] * y[i * 2] + y[i * 2 + 1] * y[i * 2 + 1]);
}
return out;
}
interface MelParams {
sampleRate: number;
nFft?: number;
nMels?: number;
fMin?: number;
fMax?: number;
}
const linearSpace = (start: number, end: number, count: number) => {
// Include start and endpoints.
const delta = (end - start) / (count - 1);
const out = new Float32Array(count);
for (let i = 0; i < count; i++) {
out[i] = start + delta * i;
}
return out;
}
const calculateFftFreqs = (sampleRate: number, nFft: number) => {
return linearSpace(0, sampleRate / 2, Math.floor(1 + nFft / 2));
}
const hzToMel = (hz: number): number => {
return 1125.0 * Math.log(1 + hz / 700.0);
}
function melToHz(mel: number): number {
return 700.0 * (Math.exp(mel / 1125.0) - 1);
}
const calculateMelFreqs = (
nMels: number, fMin: number, fMax: number): Float32Array => {
const melMin = hzToMel(fMin);
const melMax = hzToMel(fMax);
// Construct linearly spaced array of nMel intervals, between melMin and
// melMax.
const mels = linearSpace(melMin, melMax, nMels);
const hzs = mels.map((mel) => melToHz(mel));
return hzs;
}
const internalDiff = (arr: Float32Array): Float32Array => {
const out = new Float32Array(arr.length - 1);
for (let i = 0; i < arr.length; i++) {
out[i] = arr[i + 1] - arr[i];
}
return out;
}
const outerSubtract = (arr: Float32Array, arr2: Float32Array): Float32Array[] => {
const out: Float32Array[] = [];
for (let i = 0; i < arr.length; i++) {
out[i] = new Float32Array(arr2.length);
}
for (let i = 0; i < arr.length; i++) {
for (let j = 0; j < arr2.length; j++) {
out[i][j] = arr[i] - arr2[j];
}
}
return out;
}
const createMelFilterbank = (params: MelParams): Float32Array[] => {
const fMin = params.fMin || 0;
const fMax = params.fMax || params.sampleRate / 2;
const nMels = params.nMels || 128;
const nFft = params.nFft || 2048;
// Center freqs of each FFT band.
const fftFreqs = calculateFftFreqs(params.sampleRate, nFft);
// (Pseudo) center freqs of each Mel band.
const melFreqs = calculateMelFreqs(nMels + 2, fMin, fMax);
const melDiff = internalDiff(melFreqs);
const ramps = outerSubtract(melFreqs, fftFreqs);
const filterSize = ramps[0].length;
const weights: Float32Array[] = [];
for (let i = 0; i < nMels; i++) {
weights[i] = new Float32Array(filterSize);
for (let j = 0; j < ramps[i].length; j++) {
const lower = -ramps[i][j] / melDiff[i];
const upper = ramps[i + 2][j] / melDiff[i + 1];
const weight = Math.max(0, Math.min(lower, upper));
weights[i][j] = weight;
}
}
// Slaney-style mel is scaled to be approx constant energy per channel.
for (let i = 0; i < weights.length; i++) {
// How much energy per channel.
const enorm = 2.0 / (melFreqs[2 + i] - melFreqs[i]);
// Normalize by that amount.
weights[i] = weights[i].map((val) => val * enorm);
}
return weights;
}
const applyFilterbank = (
mags: Float32Array, filterbank: Float32Array[]): Float32Array => {
if (mags.length !== filterbank[0].length) {
throw new Error(
`Each entry in filterbank should have dimensions ` +
`matching FFT. |mags| = ${mags.length}, ` +
`|filterbank[0]| = ${filterbank[0].length}.`);
}
// Apply each filter to the whole FFT signal to get one value.
const out = new Float32Array(filterbank.length);
for (let i = 0; i < filterbank.length; i++) {
// To calculate filterbank energies we multiply each filterbank with the
// power spectrum.
const win = applyWindow(mags, filterbank[i]);
// Then add up the coefficents.
out[i] = win!.reduce((a, b) => a + b);
}
return out;
}
const applyWholeFilterbank = (
spec: Float32Array[], filterbank: Float32Array[]): Float32Array[] => {
// Apply a point-wise dot product between the array of arrays.
const out: Float32Array[] = [];
for (let i = 0; i < spec.length; i++) {
out[i] = applyFilterbank(spec[i], filterbank);
}
return out;
}
export const melSpectrogram = (y: Float32Array, params: SpecParams): Float32Array[] => {
if (!params.power) {
params.power = 2.0;
}
const stftMatrix = stft(y, params);
const [spec, nFft] = magSpectrogram(stftMatrix, params.power);
params.nFft = nFft;
const melBasis = createMelFilterbank(params);
return applyWholeFilterbank(spec, melBasis);
}
const max = (arr: Float32Array) => {
return arr.reduce((a, b) => Math.max(a, b));
}
export const powerToDb = (spec: Float32Array[], amin = 1e-10, topDb = 80.0) => {
const width = spec.length;
const height = spec[0].length;
const logSpec: Float32Array[] = [];
for (let i = 0; i < width; i++) {
logSpec[i] = new Float32Array(height);
}
for (let i = 0; i < width; i++) {
for (let j = 0; j < height; j++) {
const val = spec[i][j];
logSpec[i][j] = 10.0 * Math.log10(Math.max(amin, val));
}
}
if (topDb) {
if (topDb < 0) {
throw new Error(`topDb must be non-negative.`);
}
for (let i = 0; i < width; i++) {
const maxVal = max(logSpec[i]);
for (let j = 0; j < height; j++) {
logSpec[i][j] = Math.max(logSpec[i][j], maxVal - topDb);
}
}
}
return logSpec;
}

View File

@ -0,0 +1,105 @@
import React, { useEffect, useMemo } from "react"
import { useAppSetting } from "../../003_provider/AppSettingProvider";
import { useAppState } from "../../003_provider/AppStateProvider";
import { drawMel } from "../../const";
const MelTypes = {
mic: "mic",
vf: "vf"
} as const
type MelTypes = typeof MelTypes[keyof typeof MelTypes]
export const MelSpectrogram = () => {
const { applicationSetting } = useAppSetting()
const { audioControllerState } = useAppState();
useEffect(() => {
const drawMel = (type: MelTypes, data: string) => {
let canvas = document.getElementById("mel") as HTMLCanvasElement
if (type === "mic") {
canvas = document.getElementById("mel_mic") as HTMLCanvasElement
} else {
canvas = document.getElementById("mel_vf") as HTMLCanvasElement
}
const ctx2d = canvas.getContext("2d")!
if (!data) {
ctx2d.clearRect(0, 0, canvas.width, canvas.height)
return
}
const img = new Image()
img.src = data
img.onload = () => {
console.log("ONLOAD")
ctx2d.drawImage(img, 0, 0, canvas.width, canvas.height)
}
}
if (audioControllerState.tempUserData.micSpec.length == 0) {
return
}
if (applicationSetting.applicationSetting.use_mel_spectrogram) {
drawMel("mic", audioControllerState.tempUserData.micSpec)
drawMel("vf", audioControllerState.tempUserData.vfSpec)
}
}, [audioControllerState.tempUserData.micSpec, audioControllerState.tempUserData.vfSpec, applicationSetting.applicationSetting?.use_mel_spectrogram])
const mel = useMemo(() => {
if (!applicationSetting.applicationSetting.use_mel_spectrogram) {
return <></>
}
if (!audioControllerState.tempUserData.vfWavBlob ||
!audioControllerState.tempUserData.micWavBlob
) {
return <></>
}
if (audioControllerState.tempUserData.micSpec.length == 0) {
const generateMelSpec = () => {
if (
!audioControllerState.tempUserData.vfWavSamples ||
!audioControllerState.tempUserData.micWavSamples
) {
return
}
const micSpec = drawMel(audioControllerState.tempUserData.micWavSamples, applicationSetting.applicationSetting.sample_rate)
const vfSpec = drawMel(audioControllerState.tempUserData.vfWavSamples, applicationSetting.applicationSetting.sample_rate)
audioControllerState.setTempWavBlob(
audioControllerState.tempUserData.micWavBlob,
audioControllerState.tempUserData.vfWavBlob,
audioControllerState.tempUserData.micWavSamples,
audioControllerState.tempUserData.vfWavSamples,
micSpec,
vfSpec,
audioControllerState.tempUserData.region!)
audioControllerState.setUnsavedRecord(true);
}
return (
<div onClick={generateMelSpec} className="button">Generate MelSpec</div>
)
}
return (
<>
<div className="mel-spectrogram-div">
<canvas id="mel_mic" className="mel-spectrogram-canvas"></canvas>
</div>
<div className="mel-spectrogram-div">
<canvas id="mel_vf" className="mel-spectrogram-canvas"></canvas>
</div>
</>
)
}, [audioControllerState.tempUserData.micSpec, audioControllerState.tempUserData.vfSpec, audioControllerState.tempUserData.vfWavBlob, applicationSetting.applicationSetting.use_mel_spectrogram])
return (
<>
{mel}
</>
)
}

View File

@ -0,0 +1,11 @@
import React from "react";
export const RightSidebar = () => {
return (
<>
<div className="right-sidebar">
No info.
</div>
</>
);
};

View File

@ -0,0 +1,88 @@
import React, { useMemo, useRef } from "react";
import { useEffect } from "react";
export type StateControlCheckbox = {
trigger: JSX.Element;
updateState: (newVal: boolean) => void;
className: string;
};
export const useStateControlCheckbox = (className: string, changeCallback?: (newVal: boolean) => void): StateControlCheckbox => {
const currentValForTriggerCallbackRef = useRef<boolean>(false);
// (4) トリガチェックボックス
const callback = useMemo(() => {
console.log("generate callback function", className);
return (newVal: boolean) => {
if (!changeCallback) {
return;
}
// 値が同じときはスルー (== 初期値(undefined)か、値が違ったのみ発火)
if (currentValForTriggerCallbackRef.current === newVal) {
return;
}
// 初期値(undefined)か、値が違ったのみ発火
currentValForTriggerCallbackRef.current = newVal;
changeCallback(currentValForTriggerCallbackRef.current);
};
}, []);
const trigger = useMemo(() => {
if (changeCallback) {
return (
<input
type="checkbox"
className={`${className} state-control-checkbox rotate-button`}
id={`${className}`}
onChange={(e) => {
callback(e.target.checked);
}}
/>
);
} else {
return <input type="checkbox" className={`${className} state-control-checkbox rotate-button`} id={`${className}`} />;
}
}, []);
useEffect(() => {
const checkboxes = document.querySelectorAll(`.${className}`);
// (1) On/Off同期
checkboxes.forEach((x) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
x.onchange = (ev) => {
updateState(ev.target.checked);
};
});
// (2) 全エレメントoff
const removers = document.querySelectorAll(`.${className}-remover`);
removers.forEach((x) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
x.onclick = (ev) => {
if (ev.target.className.indexOf(`${className}-remover`) > 0) {
updateState(false);
}
};
});
}, []);
// (3) ステート変更
const updateState = useMemo(() => {
return (newVal: boolean) => {
const currentCheckboxes = document.querySelectorAll(`.${className}`);
currentCheckboxes.forEach((y) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
y.checked = newVal;
});
if (changeCallback) {
callback(newVal);
}
};
}, []);
return {
trigger,
updateState,
className,
};
};

View File

@ -0,0 +1,74 @@
import React, { useEffect } from "react";
import { AudioOutputElementId } from "../const";
import { useAppState } from "../003_provider/AppStateProvider";
import { useAppSetting } from "../003_provider/AppSettingProvider";
import { Header } from "./002_parts/100_Header";
import { Body } from "./002_parts/200_Body";
import { RightSidebar } from "./002_parts/300_RightSidebar";
export const Frame = () => {
const { applicationSetting } = useAppSetting()
const { frontendManagerState, corpusDataState, audioControllerState } = useAppState();
// ハンドルキーボードイベント
useEffect(() => {
const keyDownHandler = (ev: KeyboardEvent) => {
console.log("EVENT:", ev);
const audioActive = audioControllerState.audioControllerState === "play" || audioControllerState.audioControllerState === "record";
const unsavedRecord = audioControllerState.unsavedRecord;
const key = ev.code;
switch (key) {
case "ArrowUp":
case "ArrowLeft":
if (applicationSetting.applicationSetting.current_text_index > 0 && !audioActive && !unsavedRecord) {
applicationSetting.setCurrentTextIndex(applicationSetting.applicationSetting.current_text_index - 1);
}
return;
case "ArrowDown":
case "ArrowRight":
if (!applicationSetting.applicationSetting.current_text) {
return;
}
if (applicationSetting.applicationSetting.current_text_index < corpusDataState.corpusTextData[applicationSetting.applicationSetting.current_text].text.length - 1 && !audioActive && !unsavedRecord) {
applicationSetting.setCurrentTextIndex(applicationSetting.applicationSetting.current_text_index + 1);
}
return;
}
if (key === "Space") {
// let color = Math.floor(Math.random() * 0xFFFFFF).toString(16);
// for(let count = color.length; count < 6; count++) {
// color = '0' + color;
// }
// setBackgroundColor('#' + color);
}
};
document.addEventListener("keydown", keyDownHandler, false);
return () => {
document.removeEventListener("keydown", keyDownHandler);
};
}, [applicationSetting.applicationSetting.current_text_index, audioControllerState.unsavedRecord, audioControllerState.audioControllerState]);
return (
<div>
<audio src="" id={AudioOutputElementId}></audio>
<div className="header-container">
<Header></Header>
</div>
{frontendManagerState.stateControls.openRightSidebarCheckbox.trigger}
<div className="body-container">
<Body></Body>
</div>
{frontendManagerState.stateControls.openRightSidebarCheckbox.trigger}
<div className="right-sidebar-container">
<RightSidebar></RightSidebar>
</div>
</div>
);
};

3
recorder/src/@types/window.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
interface Window {
}

42
recorder/src/App.tsx Normal file
View File

@ -0,0 +1,42 @@
import React from "react";
import { Frame } from "./100_components/100_Frame";
import { ErrorBoundary } from 'react-error-boundary'
import { library } from "@fortawesome/fontawesome-svg-core";
import { fas } from "@fortawesome/free-solid-svg-icons";
import { far } from "@fortawesome/free-regular-svg-icons";
import { fab } from "@fortawesome/free-brands-svg-icons";
import { useAppSetting } from "./003_provider/AppSettingProvider";
library.add(fas, far, fab);
const MyFallbackComponent = () => {
console.log("FALLBACK")
return (
<div role="alert">
<p>Something went wrong: clear setting and reloading...</p>
</div>
)
}
const App = () => {
const { applicationSetting } = useAppSetting()
return (
<ErrorBoundary
FallbackComponent={MyFallbackComponent}
onError={(error, errorInfo) => {
console.log(error, errorInfo)
applicationSetting.clearSetting()
// location.reload()
}}
onReset={() => {
console.log("RESET!")
applicationSetting.clearSetting()
}}
>
<div className="application-container"><Frame /></div>
</ErrorBoundary>
)
};
export default App;

134
recorder/src/const.ts Normal file
View File

@ -0,0 +1,134 @@
import { melSpectrogram, powerToDb } from "./100_components/002_parts/207-1MelSpectrogramUtil"
export const AudioOutputElementId = "audio-output-element"
export const generateWavFileName = (prefix: string, index: number) => {
const indexString = String(index + 1).padStart(3, '0')
return `${prefix}${indexString}.wav`
}
export const generateTextFileName = (prefix: string, index: number) => {
const indexString = String(index + 1).padStart(3, '0')
return `${prefix}${indexString}.txt`
}
export const generateWavNameForLocalStorage = (prefix: string, index: number) => {
const indexString = String(index + 1).padStart(3, '0')
const vfString = `${prefix}${indexString}_vf`
const micString = `${prefix}${indexString}_mic`
return { micString, vfString }
}
export const generateRegionNameForLocalStorage = (prefix: string, index: number) => {
const indexString = String(index + 1).padStart(3, '0')
const regionString = `${prefix}${indexString}_region`
return regionString
}
const writeString = (view: DataView, offset: number, string: string) => {
for (var i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
};
export const generateEmptyWav = () => {
const buffer = new ArrayBuffer(44 + 0 * 2);
const view = new DataView(buffer);
// https://www.youfit.co.jp/archives/1418
writeString(view, 0, 'RIFF'); // RIFFヘッダ
view.setUint32(4, 32 + 0 * 2, true); // これ以降のファイルサイズ
writeString(view, 8, 'WAVE'); // WAVEヘッダ
writeString(view, 12, 'fmt '); // fmtチャンク
view.setUint32(16, 16, true); // fmtチャンクのバイト数
view.setUint16(20, 1, true); // フォーマットID
view.setUint16(22, 1, true); // チャンネル数
view.setUint32(24, 48000, true); // サンプリングレート
view.setUint32(28, 48000 * 2, true); // データ速度
view.setUint16(32, 2, true); // ブロックサイズ
view.setUint16(34, 16, true); // サンプルあたりのビット数
writeString(view, 36, 'data'); // dataチャンク
view.setUint32(40, 0 * 2, true); // 波形データのバイト数
// floatTo16BitPCM(view, 44, samples); // 波形データ
// console.log(view)
const audioBlob = new Blob([view], { type: 'audio/wav' });
// const duration = samples.length / SampleRate
return audioBlob
}
export const convert48KhzTo24Khz = async (blob: Blob, startSec?: number, endSec?: number) => {
blob.arrayBuffer;
const oldView = new DataView(await blob.arrayBuffer());
const sampleBytes = oldView.getUint32(40, true); // サンプルデータサイズ = 長さ * 2byte(16bit)
const sampleLength24Khz = Math.floor(sampleBytes / 2 / 2) + 1;
// サンプルデータサイズ  / 2bytes(16bit) => サンプル数(48Khz),
// サンプル数(48Khz) / 2 = サンプル数(24Khz) ※ 小数点切り捨て + 1
const startIndex = startSec ? Math.floor(startSec * 24000) : 0;
const endIndex = endSec ? Math.floor(endSec * 24000) : sampleLength24Khz - 1;
// console.log("index:::", startIndex, endIndex, startSec, endSec)
let sampleNum = endIndex - startIndex;
if (sampleNum > sampleLength24Khz) {
sampleNum = sampleLength24Khz;
}
// console.log("cut...", startIndex, endIndex, sampleNum);
const buffer = new ArrayBuffer(44 + sampleNum * 2);
const newView = new DataView(buffer);
// https://www.youfit.co.jp/archives/1418
writeString(newView, 0, "RIFF"); // RIFFヘッダ
newView.setUint32(4, 32 + sampleNum * 2, true); // これ以降のファイルサイズ
writeString(newView, 8, "WAVE"); // WAVEヘッダ
writeString(newView, 12, "fmt "); // fmtチャンク
newView.setUint32(16, 16, true); // fmtチャンクのバイト数
newView.setUint16(20, 1, true); // フォーマットID
newView.setUint16(22, 1, true); // チャンネル数
newView.setUint32(24, 24000, true); // サンプリングレート
newView.setUint32(28, 24000 * 2, true); // データ速度
newView.setUint16(32, 2, true); // ブロックサイズ
newView.setUint16(34, 16, true); // サンプルあたりのビット数
writeString(newView, 36, "data"); // dataチャンク
newView.setUint32(40, sampleNum * 2, true); // 波形データのバイト数
const offset = 44;
// console.log("converting...", sampleBytes);
for (let i = 0; i < sampleNum; i++) {
try {
const org = oldView.getInt16(offset + 4 * (i + startIndex), true);
newView.setInt16(offset + 2 * i, org, true);
} catch (e) {
console.log(e, "reading...", offset + 4 * i, 4 * i);
break;
}
}
const audioBlob = new Blob([newView], { type: "audio/wav" });
return audioBlob;
};
export const drawMel = (data: Float32Array, sampleRate: number) => {
const canvas = document.createElement("canvas")
const sp_t = melSpectrogram(data, { sampleRate: sampleRate })
const sp = powerToDb(sp_t)
const width = sp.length
const height = sp[0].length
canvas.width = width
canvas.height = height
const img = new ImageData(width, height)
for (let i = 0; i < height; i++) {
for (let j = 0; j < width; j++) {
const offset = ((i * width) + j) * 4
const data = sp[j][height - i]
// console.log(offset)
img.data[offset + 0] = 0
img.data[offset + 1] = ((data + 100) / 100) * 255
img.data[offset + 2] = 0
img.data[offset + 3] = 255
}
}
const ctx = canvas.getContext("2d")!
ctx.putImageData(img, 0, 0)
const png = canvas.toDataURL('image/png')
return png
}

97
recorder/src/index.tsx Normal file
View File

@ -0,0 +1,97 @@
import * as React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import { AppStateProvider } from "./003_provider/AppStateProvider";
import { AppSettingProvider, useAppSetting } from "./003_provider/AppSettingProvider";
import "./100_components/001_css/001_App.css";
const AppStateProviderWrapper = () => {
const { applicationSetting, deviceManagerState } = useAppSetting();
const [firstTach, setFirstTouch] = React.useState<boolean>(false);
if (!applicationSetting || !firstTach) {
const clearSetting = () => {
const result = window.confirm('設定を初期化します。');
if (result) {
applicationSetting.clearSetting()
location.reload()
}
}
return (
<div className="front-container">
<div className="front-title">Corpus Voice Recorder</div>
<div className="front-description">
<p></p>
<p></p>
<p>
使
<a href="https://github.com/w-okada/voice-changer" target="_blank" rel="noopener noreferrer"></a>
</p>
<p className="front-description-strong">使 </p>
<p>
<a href="https://www.buymeacoffee.com/wokad">
<img className="front-description-img" src="./coffee.png"></img>
</a>
</p>
<a></a>
</div>
<div
className="front-start-button front-start-button-color"
onClick={() => {
setFirstTouch(true);
}}
>
Click to start
</div>
<div className="front-note">確認動作環境:Windows 11 + Chrome</div>
<div className="front-description">
<p>ITAコーパスのemotionとrecitationの台本が登録されています</p>
<p>
<a href="https://github.com/isletennos/MMVC_Trainer" target="_blank">
MMVC
</a>
使48000Hz, 16bitの録音設定になっています
</p>
<p>
(24000Hzに変換します)
</p>
</div>
{/* <div className="front-attention">
<p>Exportをお願いします</p>
<p></p>
</div> */}
<div className="front-disclaimer">使使 </div>
<div className="front-clear-setting" onClick={clearSetting}>
Clear Setting
</div>
</div>
);
} else if (deviceManagerState.audioInputDevices.length === 0) {
return (
<>
<div className="start-button">Loading Devices...</div>
</>
);
} else {
return (
<AppStateProvider>
<App />
</AppStateProvider>
);
}
};
const container = document.getElementById("app")!;
const root = createRoot(container);
root.render(
<AppSettingProvider>
<AppStateProviderWrapper></AppStateProviderWrapper>
</AppSettingProvider>
);

33
recorder/tsconfig.json Normal file
View File

@ -0,0 +1,33 @@
{
"compilerOptions": {
"target": "ES2020",
"jsx": "react",
"lib": ["dom"],
/* */
"forceConsistentCasingInFileNames": true,
/* */
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
/* Module */
"moduleResolution": "node",
"esModuleInterop": true,
"isolatedModules": true,
"allowSyntheticDefaultImports": true,
/* */
// "noEmit": true,
/* For avoid WebGL2 error */
/* https://stackoverflow.com/questions/52846622/error-ts2430-interface-webglrenderingcontext-incorrectly-extends-interface-w */
"skipLibCheck": true
},
/* tsc */
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"]
}

View File

@ -0,0 +1,65 @@
/* eslint @typescript-eslint/no-var-requires: "off" */
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const webpack = require("webpack");
const CopyPlugin = require("copy-webpack-plugin");
module.exports = {
entry: path.resolve(__dirname, "src/index.tsx"),
output: {
path: path.resolve(__dirname, "..", "docs"),
filename: "index.js",
assetModuleFilename: "assets/[name][ext][hash]",
},
resolve: {
modules: [path.resolve(__dirname, "node_modules")],
extensions: [".ts", ".tsx", ".js"],
fallback: {
buffer: require.resolve("buffer/"),
},
},
module: {
rules: [
{
test: [/\.ts$/, /\.tsx$/],
use: [
{
loader: "ts-loader",
options: {
// transpileOnly: true,
configFile: "tsconfig.json",
},
},
],
},
{
test: /\.css$/,
use: ["style-loader", { loader: "css-loader", options: { importLoaders: 1 } }, "postcss-loader"],
},
{
test: /\.html$/,
loader: "html-loader",
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "public/index.html"),
filename: "./index.html",
}),
new webpack.ProvidePlugin({
Buffer: ["buffer", "Buffer"],
process: "process/browser",
}),
new CopyPlugin({
patterns: [
{
from: "public/",
globOptions: {
ignore: ["**/index.html*"],
},
},
],
}),
],
};

28
recorder/webpack.dev.js Normal file
View File

@ -0,0 +1,28 @@
const path = require("path");
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js')
module.exports = merge(common, {
mode: 'development',
devServer: {
// proxy: {
// "/api": {
// target: "http://192.168.0.3:8000",
// },
// },
static: {
directory: path.join(__dirname, "../docs"),
},
headers: {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
},
https: true,
client: {
overlay: {
errors: false,
warnings: false,
},
},
},
})

6
recorder/webpack.prod.js Normal file
View File

@ -0,0 +1,6 @@
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js')
module.exports = merge(common, {
mode: 'production',
})