注意
この記事はjavascriptのガチ初心者が書いてます。間違ってること多々あると思うので良かったら教えてください
怒られたりしたら消す場合があります。
つくったもの
おはようございナース!
atomで名取さなちゃんが喋ってくれるプラグインを作りました!
さなちゃんコーディングするところ見てて...
機能とかは↓
https://atom.io/packages/natorisana-voice
よかったら使ってみてね!
使用者の感想
-
18歳高専生
プログラミング中に名取さなちゃんの声が聞けて開発効率が劇的に向上しました!!
保存時に「あい~」と喋るので、聞くために保存をこまめに行うようになりました!!!
きっかけ
ひまのあさん(Twitter: @h1manoa)が作成した素晴らしすぎるvimプラグインを見つけて感動したんですがvimが使えないことに気づいて、atomプラグインを作ることにしました。
改行する度に(InsertModeでEnterする度)に名取さなの「ってね」が再生されるプラグインです。https://t.co/JdmmOXw8Qv
— ひまのあ🍆🍆🍆@1日目 西め07a (@h1manoa) 2018年6月18日
技術的(?)な話
環境
- Windows10
- Git v2.15
- atom v1.28.1
atomのパッケージ開発方法
他に詳しく説明している記事やサイトがいっぱいあったから軽い説明だけ
パッケージを生成する
[Package] → [Package Generator] でパッケージを生成する。
TestPackage
と入力するとtest-package
という名前のフォルダがC:\Users\ユーザ名\github\
に生成され、
C:\Users\ユーザ名\.atom\packages
にジャンクションが作られる。
C:\Users\ユーザ名\github\
でapm develop フォルダ名
でC:\Users\ユーザ名\.atom\dev\packages
にジャンクションが作られ、開発パッケージとして追加される。
ここで、C:\Users\ユーザ名\.atom\packages
のジャンクションを削除しておくと、パッケージを公開した後に公開済みのものをインストールすると、
-
通常起動
公開したパッケージを読み込む -
開発モード(
atom -d
)
開発中(編集中)のパッケージを読み込む
ということが可能になる。
生成されるファイルの軽い説明
- keymaps / test-package.json
ショートカットキーとか設定するファイル -
lib / test-package.js
プログラムのメインファイル - lib / test-package-view.js
プログラムのUI部分を記述するファイル - menus / test-package.json
上の[package]クリックしたら出るやつのところの項目を設定するファイル(語彙力) - spec/
テスト用のファイルらしいけどやったことないからよくわからん - styles/test-package.less
スタイル設定用ファイル - package.json
パッケージの名前、メインファイル、バージョン、依存関係などを記述するファイル - .gitignore
gitの管理対象外にするファイルを記述するファイル
使わないやつは消しても問題ないのでspecフォルダは削除した()
※詳しくは**atomのFlight Manual**を見ような
プログラム
今回はUIを使わないからtest-package.js
でtest-package-view.js
のモジュール(?)を使っている部分を消した。
'use babel';
import { CompositeDisposable } from 'atom';
export default {
subscriptions: null,
activate(state) {
this.subscriptions = new CompositeDisposable();
// toggleコマンドを登録する
this.subscriptions.add(atom.commands.add('atom-workspace', {
'test-package:toggle': () => this.toggle()
}));
},
deactivate() {
this.subscriptions.dispose();
},
serialize() {
},
toggle() {
}
};
初期処理はactivate
に記述する。その他は適当に関数つくったり。
設定画面に項目を追加したい場合は
config:{
'音量':{
'type':'integer',
'default':100,
'minimum':0,
'maximum':1000,
'description':'[%] (1~1000)',
'order':1
},
'てねっ':{
....
},
....
}
みたいにすると追加される。
実行方法
ctrl + shift + P
でコマンドパレットを開き、'パッケージ名:toggle'で実行する。
今後、ファイルを編集したときは実行する前に、
ctrl + shift + F5
(または ctrl + shift + P
でコマンドパレットを開きWindow:Reload
を実行) で更新を反映してから実行する。
(これめっちゃimportant)
デバッグ方法
ctrl + shift + I
でChromeの開発者ツールと同じやつ出てくるからそこでコンソールとか見ような
(ちなみに同じelectron使ってるdiscordとかでもctrl + shift + I
すると出てくる)
atomパッケージの公開方法
package.jsonのdescription
,repository
が設定されていることを確認する。
version
は自分で変更しないこと。(最初は0.0.0のまま)
ここでREADME.mdも作ろうね
githubでリポジトリを作り、ファイルをプッシュする。
atom.ioにgithubでログインできるようにしておく。(?)
※パッケージ名で検索してみたり、https://atom.io/packages/パッケージ名
にアクセスしてみたりしてパッケージ名が被ってないことを確認しておく
ここから先はWindowsでやるとちゃんと指定してるに、could not read Username for 'https://github.com': No error
っていうエラーが出るからLinuxでやった。
package.json
と同じ階層でapm login
を実行するとatom.ioのマイページに飛ぶので、
トークンをコピーしてコマンドラインに貼る
apm publish {major|minor|patch}
を実行し、githubにインストールすると公開される
-
major
バージョンが 1.0.0 増える -
minor
バージョンが 0.1.0 増える -
patch
バージョンが 0.0.1 増える
公開に失敗した時の対処(エラーが出たりgithubログイン出来なかったときとか)
1.問題のコミットを消す
Prepare 1.0.0 release
みたいなコミットが出来てるからgit reset --hard 1つ前のコミットのハッシュ値
で戻す
2.タグを消す
git tag
で問題のタグ名を見つけてgit tag -d タグ名
atomAPIで使ったやつの説明
Atom API Referenceに全部載ってるから詳しくはそっち見てね
atom.packages.getPackageDirPaths
パッケージのあるディレクトリのパスが配列で取得できる。
開発者モードだと最初の要素が.atom\dev
で、通常時だと最初の要素が.atom\packages
なので
atom.packages.getPackageDirPaths()[0]
とすると、両方の場合に対応できる
atom.config.get
atom.config.get('パッケージ名.変数名')
設定で指定されている値を取得する
例:atom.config.get('natorisana-voice.てねっ')
atom.keymaps.onDidMatchBinding
event.binding.command
で実行されたショートカットキー名を取得出来る
ショートカットキー名はctrl + .
で有効になるKey Binding Resolverでわかる
atom.keymaps.onDidMatchBinding((event)=>{
switch (event.binding.command) {
case 'editor:newline':
//改行
if(atom.config.get('natorisana-voice.てねっ')){
this.play(this.vTene);
}
break;
case 'core:confirm':
//確定
if(atom.config.get('natorisana-voice.はいはい')){
this.play(this.vHihi);
}
break;
default:
}
});
atom.workspace.observeTextEditors → editor.getBuffer
テキストエディタからバッファを取得する
atom.workspace.observeTextEditors(editor => {
let buffer = editor.getBuffer();
...
}
onDidChange
変更されたときに呼び出される
//カウントダウン
buffer.onDidChange((event)=>{
if(atom.config.get('natorisana-voice.カウントダウン')){
let text = buffer.getTextInRange(event.newRange);
if(text.length == 1){
for(let i=0;i<10;++i){
if(text == i.toString()){
this.play(this.vCountDown[i]);
break;
}
}
}
}
});
onDidStopChanging
変更やめたときに呼び出される
//うんうん
buffer.onDidStopChanging((event)=>{
if(atom.config.get('natorisana-voice.うんうん')){
if(event.changes.length){
let text = event.changes[0].newText;
if(text.indexOf('\n')==-1 && text.indexOf('\r')==-1){
this.play(this.vUnun);
}
}
}
});
onDidSave
セーブのときに呼び出される。
onDidMatchBinding
だとctrl + s
のときだけしか実行されないから、他の保存方法でも実行されるようにこっちで実装した。
buffer.onDidSave((event)=>{
if(atom.config.get('natorisana-voice.あい~')){
this.play(this.vAi);
}
});
atom.workspace.getActiveTextEditor → editor.insertText
アクティブなエディタを取得し、テキストを挿入する
インデントされている場所に挿入すると、2行目以降がちゃんとインデントされないからオプションは要修正
insert(){
let editor = atom.workspace.getActiveTextEditor()
editor.insertText(this.sa_na,{select:true,autoIndentNewline:true});
}
WebAudioAPIでローカルファイル再生する
(なんとなくでしか理解できてないので間違ってるかも)
AudioContextを作成する
context:null,
....
//ボイス入れるための変数
vStart:null,
vAi:null,
vHihi:null,
vCheer:null,
vTene:null,
vUnun:null,
vCountDown:[],
activate(state) {
....
this.context = new window.AudioContext();
this.loadVoice();
this.eventSet();
},
deactivate() {
this.context.dispose();
},
ファイルを取得する
atom.packages.getPackageDirPaths
を使ってパス指定したらローカルファイルを取得できた。
context.decodeAudioData
でarraybuffer
をaudiobuffer
に変換できる。
C言語脳なのでgetFileに変数の参照渡せばいいのではって思ったけど、型が違うってエラーで怒られたから代入の関数を渡すようにした。(ワタシJSヨクワカンナイ)
getFile(filename,callback){
let path = atom.packages.getPackageDirPaths()[0]+'/natorisana-voice/voice/';
let request = new XMLHttpRequest();
request.open('GET',path+filename+'.mp3',true);
request.responseType = 'arraybuffer';
request.send();
request.onload = ()=>{
let res = request.response;
this.context.decodeAudioData(res,(buf)=>{
callback(buf);
});
};
},
loadVoice(){
//おはようございナース
this.getFile('oha',(buf)=>{this.vStart = buf;
if(atom.config.get('natorisana-voice.おはようございナース!')){
this.play(this.vStart);
}
});
//あい~
this.getFile('ai~',(buf)=>{this.vAi = buf;});
//はいはい
this.getFile('hihi',(buf)=>{this.vHihi = buf;});
//応援
this.getFile('ganbare',(buf)=>{this.vCheer = buf;});
//てねっ
this.getFile('tene', (buf)=>{this.vTene = buf;});
//うんうん
this.getFile('unun',(buf)=>{this.vUnun = buf;});
//カウントダウン
for(let i=0;i<10;i++){
this.getFile('countdown/'+i,(buf)=>{this.vCountDown[i] = buf;});
}
},
再生する
context
は一度だけ作るだけだが、source
は再生のたびに作る。
source
を音量のgain
に接続して、再生先のdestination
に接続する。
フィルターみたいにいろいろ接続していくらしい。
start
で再生
play(buffer){
let source = this.context.createBufferSource();
source.buffer = buffer;
let gain = this.context.createGain();
gain.gain.value = atom.config.get('natorisana-voice.音量')*0.01;
gain.connect(this.context.destination);
source.connect(gain);
source.start(0);
},
最後に
僕の誕生日名取さなちゃんと一緒なんですよ(めっちゃうれしい)