仕事中でもDJがしたい

本稿は、クソアプリ Advent Calendar 2019 12日目の記事です。

# 概要
毎日毎日CUIに向かってvimやemacsやnanoを駆使してYAMLを書いたりコーディングしたり。
そんな仕事に日々追われるエンジニアの皆さんに足りていないもの。

そう、**クラブ通い** ですね。

仕事で忙しい上に年末の忘年会ラッシュで皆さんもなかなか週次でクラブに足を運ぶことができていないのではないでしょうか。
私もその一人です。
音楽を聞くだけであればYoutubeのMIX動画などである程度事足りますが、実際にDJをするとなると難しい。
家に帰るともう夜遅くて気力がないし、土日は体力回復のためにひたすら寝るか、死んだような目でYoutubeで魚を捌く動画か焚き火の動画を見て過ごすのが関の山。

となるともう**仕事中にDJをする**しかありませんね。

仕事中にDJをするのは少々困難が伴います。
業務中、CUIしか開いていないであろう皆さんがrekordboxやTRAKTORなどを開いていようものであれば上司はカンカンに怒って謎のNoSQLの検証タスクをアサインしてくることでしょう。


**そんな状況を打破するため、CUIでDJするためのアプリケーションを作りました。**

https://github.com/amaretto/punos
![punos](https://user-images.githubusercontent.com/1157121/67624665-87350280-f86e-11e9-8c25-ec07e1518a83.gif)

# 画面

![Screen Shot 2019-12-10 at 0.12.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93536/d531e7ed-a416-c45c-43a6-6a789e37a6b1.png)


黒背景に緑文字。**「業務中です」**と言っても全く違和感のない、いつものCUI画面ですね。

上から順に下記情報が表示されています。

- ロゴ
- 今再生中の曲名
- 楽曲の再生位置
- 各種再生情報(モード、Cueのポイント、ボリューム、再生速度、フィルタ情報)
- 曲の波形(waveform)


##### 2画面
![ss.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93536/1ef53e5a-daed-1122-a2d9-ce401383bd8e.png)

このツールは画面分割/複数タブ/複数ウィンドウで開くことでより本格的なDJプレイが可能です。
~~**ロゴがでかくて邪魔**~~ 中心にガイドラインがあるので楽曲のテンポを合わせやすそうですね。

##### 楽曲選択
![Screen Shot 2019-12-11 at 23.52.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93536/cca7607a-0209-ca52-1275-2cae3f08aff0.png)

simple is best
カーソルではなくjkで上下移動


# 仕様
Go製。
楽曲の再生や波形の作成周りには[faiface/beep](https://github.com/gdamore/tcell) 、[hajimehoshi/oto](https://github.com/hajimehoshi/oto)を利用。
UIは[gdamore/tcell](https://github.com/gdamore/tcell)を使って[gdamore/govisor](https://github.com/gdamore/govisor)を参考に実装。
いずれも素敵なライブラリ&tool。

# 機能
今のところ下記の機能があります。

- 再生/一時停止
- 早送り/巻き戻し
- ボリューム調整
- 楽曲スピード調整
- 楽曲位置表示
- Cue 設定/移動
- 波形情報表示

# インストール&実行
```
$ go get github.com/amaretto/punos/cmd/punos
(mp3ファイルが入った"mp3"というディレクトリが存在するパスで)
$ punos
```
※初回実行時には、下記操作方法に従って楽曲ロード画面に遷移した後、aキーを押して楽曲の全件分析(波形情報の作成)が必要になります。


#  操作方法
### PunosPanel(メイン画面)
| key         | description          |
|-------------|----------------------|
| Space       | 再生/一時停止           |
| ESC         | 終了                 |
| w           | 早送り         |
| q           | 巻き戻し               |
| s           | ボリュームアップ            |
| a           | ボリュームダウン          |
| x           | スピードアップ             |
| z           | スピードダウン           |
| f           | 楽曲ロード画面に遷移 |

### LoadPanel(楽曲ロード画面)
| key         | description          |
|-------------|----------------------|
| ESC         | 終了                 |
| j           | カーソルを下に移動          |
| k           | カーソルを上に移動            |
| l           | カーソルの位置にある楽曲をメイン画面にロード           |
| a           | 楽曲分析(waveform作成)    |



# 実装あれこれ
面倒だったのは主に波形情報周り。

### 波形情報(waveform)の生成
go製でwavファイルなどから波形情報をpngで出力する[mdlayher/waveform](https://github.com/mdlayher/waveform)を参考にmp3版を実装。
mp3のデータ部から得られた音の大きさを算出し、これをスムージング・正規化した上で#の羅列により
波形として表現している。

```go
// GenRawWave generate raw waveform values([]float64) from volume values
func GenRawWave(streamer beep.StreamSeeker, sampleInterval int) []float64 {
	var tmp [2][2]float64
	var count, ncount int
	var rwave []float64
	rwave = make([]float64, 100000)

	for {
		// check EOF
		if sn, sok := streamer.Stream(tmp[:1]); sn == 0 && !sok {
			break
		}
		samplel := tmp[0][0]
		sampler := tmp[0][1]

		sumSquare := math.Pow(samplel, 2)
		sumSquare += math.Pow(sampler, 2)
		value := math.Sqrt(sumSquare)

		if count%sampleInterval == 0 {
			rwave[ncount] = value
			ncount++
		}

		count++
	}

	rwave = rwave[:ncount]
	return rwave
}


// SmoothRawWave make raw wave values smoothly for visualizaiton
func SmoothRawWave(rwave []float64) {
	var sample float64
	sample = 3
	var sum float64
	for i := 0; i < len(rwave); i++ {
		if i < len(rwave)-int(sample) {
			sum = 0
			for j := 0; j < int(sample); j++ {
				sum += rwave[i+j]
			}
			rwave[i] = sum / sample
		} else {
			rwave[i] = rwave[i-1]
		}
	}
}

// NormalizeRawWave arrange wave values utilizing heightMax as height
func NormalizeRawWave(rwave []float64, heightMax, valMax float64) []int {
	var max float64
	var limit float64
	max = 1.0
	limit = heightMax

	var r []int
	r = make([]int, len(rwave))
	for i, num := range rwave {
		r[i] = int(math.Ceil(limit * num / max))
	}

	return r
}
```

### 波形情報(表示用)の作成
先に示したコードで作成した波形情報から、tcell上で表示するためのstring配列を生成したりしています。

```go
// Wave2str generate strings express waveform
func Wave2str(wave []int, limit int) []string {
	var waveStr []string
	var fill []bool
	waveStr = make([]string, limit)
	fill = make([]bool, len(wave))
	for i := limit; i > 0; i-- {
		str := ""
		for j, num := range wave {
			if j == len(wave)/2-1 {
				str = str + "|"
			} else if num >= i || fill[num] {
				str = str + "#"
				fill[num] = true
			} else {
				str = str + " "
			}
		}
		waveStr[limit-i] = str
	}
	return waveStr
}
```


# Future Work&終わりに

"DJ用アプリケーション"としてはまだまだ、というかほぼただの音楽プレイヤーなので機能をぼちぼち実装していきたいなと。

- High-Pass/Low-Pass Filter実装
    - GitHubレポジトリ内には残滓があるが、BiQuadFilterを実装してみたもののどうも上手くいかず。
- 操作性の向上(キー配列見直し、楽曲位置微調整機能)
- UI/UX改善
- ピッチを変えないスピード調整
- BPM Detection
- ~~そもそもデジタル信号処理の勉強~~

Go初心者且つ、デジタル信号処理に関してはド素人なので、実装改善やアドバイスなどあれば色々コメント頂けますと幸いです！