JS初心者が名取さなが喋るatomパッケージを作ってみた話

# 注意
この記事はjavascriptの**ガチ初心者**が書いてます。間違ってること多々あると思うので良かったら教えてください:pray:
怒られたりしたら消す場合があります。

# つくったもの
おはようございナース:eggplant:！
atomで<font color="HotPink">名取さな</font>ちゃんが喋ってくれるプラグインを作りました！
さなちゃんコーディングするところ見てて...
機能とかは↓
https://atom.io/packages/natorisana-voice

よかったら使ってみてね！

### ~~使用者の感想~~
- ~~18歳高専生~~  
~~プログラミング中に<font color="HotPink">名取さな</font>ちゃんの声が聞けて開発効率が**劇的に**向上しました！！~~  
~~保存時に「あい～」と喋るので、聞くために保存をこまめに行うようになりました！！！~~

# きっかけ
ひまのあさん(Twitter: [@h1manoa](https://twitter.com/h1manoa))が作成した**素晴らしすぎる**vimプラグインを見つけて感動し~~たんですがvimが使えないことに気づい~~て、atomプラグインを作ることにしました。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">改行する度に(InsertModeでEnterする度)に名取さなの「ってね」が再生されるプラグインです。<a href="https://t.co/JdmmOXw8Qv">https://t.co/JdmmOXw8Qv</a></p>&mdash; ひまのあ🍆🍆🍆@1日目 西め07a (@h1manoa) <a href="https://twitter.com/h1manoa/status/1008785980986462208?ref_src=twsrc%5Etfw">2018年6月18日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# 技術的(?)な話
- [atomパッケージの開発方法](#atomのパッケージ開発方法)
- [atomパッケージの公開方法](#atomパッケージの公開方法)
- [atomAPIで使ったやつの説明](#atomapiで使ったやつの説明)
- [WebAudioAPIでローカルファイル再生する](#webaudioapiでローカルファイル再生する)


## 環境
- 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`）
開発中（編集中）のパッケージを読み込む

ということが可能になる。


### 生成されるファイルの軽い説明
![image.png](https://qiita-image-store.s3.amazonaws.com/0/137033/b5cb066e-bba5-3aad-6baa-83db69bb7e5a.png)

- 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](https://flight-manual.atom.io/hacking-atom/sections/package-word-count/)**を見ような


### プログラム
今回はUIを使わないから`test-package.js`で`test-package-view.js`のモジュール(?)を使っている部分を消した。

```js:test-package.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`に記述する。その他は適当に関数つくったり。

設定画面に項目を追加したい場合は

```js
  config:{
    '音量':{
      'type':'integer',
      'default':100,
      'minimum':0,
      'maximum':1000,
      'description':'[%] (1~1000)',
      'order':1
    },
    'てねっ':{
        ....
    },
    ....
  }
```
みたいにすると追加される。

### 実行方法
`ctrl + shift + P`でコマンドパレットを開き、'パッケージ名:toggle'で実行する。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/137033/69f55dc5-39c4-2797-d5eb-2768e632a837.png)
今後、ファイルを編集したときは実行する前に、
**`ctrl + shift + F5` (または `ctrl + shift + P`でコマンドパレットを開き`Window:Reload`を実行) で更新を反映してから**実行する。
（**これめっちゃimportant**）

### デバッグ方法
`ctrl + shift + I`でChromeの開発者ツールと同じやつ出てくるからそこでコンソールとか見ような:muscle:
(ちなみに同じelectron使ってるdiscordとかでも`ctrl + shift + I`すると出てくる)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/137033/c3c8ef5e-038b-3abe-1c77-84fbc3419da0.png)




## atomパッケージの公開方法
package.jsonの`description`,`repository`が設定されていることを確認する。
**`version`は自分で変更しないこと。（最初は0.0.0のまま）**
ここでREADME.mdも作ろうね :heart:

githubでリポジトリを作り、ファイルをプッシュする。

[atom.io](https://atom.io/)にgithubでログインできるようにしておく。(?)

※パッケージ名で検索してみたり、`https://atom.io/packages/パッケージ名`にアクセスしてみたりしてパッケージ名が被ってないことを確認しておく

----
ここから先はWindowsでやるとちゃんと指定してるに、`could not read Username for 'https://github.com': No error`っていうエラーが出るから**Linuxでやった**。:sweat:

----

`package.json`と同じ階層で`apm login`を実行するとatom.ioのマイページに飛ぶので、
トークンをコピーしてコマンドラインに貼る

`apm publish {major|minor|patch}`を実行し、githubにインストールすると:star:公開される:star:

- `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](https://atom.io/docs/api/v1.28.1)に全部載ってるから詳しくはそっち見てね

### 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でわかる
![image.png](https://qiita-image-store.s3.amazonaws.com/0/137033/8081c760-0b2e-67cd-04b8-758bc4396ac8.png)


```js

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
テキストエディタからバッファを取得する

```js
atom.workspace.observeTextEditors(editor => {
      let buffer = editor.getBuffer();
                   ...
}
```

#### onDidChange
変更されたときに呼び出される

```js
//カウントダウン
      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
変更やめたときに呼び出される

```js
//うんうん
      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`のときだけしか実行されないから、他の保存方法でも実行されるようにこっちで実装した。

```js
      buffer.onDidSave((event)=>{
        if(atom.config.get('natorisana-voice.あい～')){
          this.play(this.vAi);
        }
      });
```

### atom.workspace.getActiveTextEditor →　editor.insertText
アクティブなエディタを取得し、テキストを挿入する
インデントされている場所に挿入すると、2行目以降がちゃんとインデントされないからオプションは要修正

```js
  insert(){
    let editor = atom.workspace.getActiveTextEditor()
    editor.insertText(this.sa_na,{select:true,autoIndentNewline:true});
  }
```

## WebAudioAPIでローカルファイル再生する
（なんとなくでしか理解できてないので間違ってるかも）

### AudioContextを作成する

```js

  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ヨクワカンナイ）

```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`で再生

```js
  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);
  },
```




# 最後に
僕の誕生日<font color="HotPink">名取さな</font>ちゃんと一緒なんですよ（めっちゃうれしい）