🔊

スマートスピーカーで自由に音楽を再生するやつを作った

2024/05/07に公開

音楽配信系のサブスクを契約すれば基本はいらないものですが, 配信されていない教材系の音声とかを再生するときに便利なものを作りました. (目新しいものやテクいことはしていません)
(YouTube Premiumがあれば同じことができるかもしれませんが, 検証はしていません. 今回は自作を目標にしたためです)

何を作ったか

クラウドのストレージに音声をアップロードし, スマートスピーカー(Google Nest Mini)で再生するというものを作りました.
スマートスピーカーと同じWi-Fiで動作するPC上で動かすことを想定しています(理由は後述します)

技術検討

最初は, スマートスピーカーを使うことは固定としてどうやって再生させるかを検討しました.
GoogleのスマートスピーカーにはActions on Goolgeでカスタマイズする機能があります. 以前はこの中にある会話型アクションを使ってVoice Botのように制御できたのですが, サポートが終了したようです.
また, 同機器内でJSのランタイムがあるようでLocal Fulfillmentという機能でJSを機器にアップロードして動作させることができるようですが, ドキュメントがまだ充実していなさそうなのと, 審査が必要とのことで今回は見送りました.
そのため, 同一Wi-FiのPCから直接接続して制御する方法を採用しました.
他は好みに寄せて, 言語はGo, GCP(Cloud Storage, Cloud Datastore), お金がかからないようにという方針で作っています.

業務フロー

  1. 手動でGCSに音声ファイルをアップロード
  2. PC上でプログラム実行
  3. Datastoreにメタデータ書き込み
  4. GCSのバケットの一番上から1曲ずつ再生する

リポジトリ

https://github.com/dstnk0208/home-iot-gramophone/tree/main
gramophoneは蓄音機という意味です. (普通にデジタルなシステムですが)

コード

GWの自由工作のノリで作ったので, 改善の余地がたくさんあるコードですが, 簡単に載せていきます.

リポジトリ構成

config/
infrastructure/
model/
service/
go.mod
go.sum
main.go

config

ymlを読み込んだ時の構造体を定義しています

config.go
package config

type Config struct {
	GCP       GCP       `yaml:"gcp"`
	Datastore Datastore `yaml:"datastore"`
	Storage   Storage   `yaml:"storage"`
	Speaker   Speaker   `yaml:"speaker"`
}

type GCP struct {
	ProjectID string `yaml:"projectID"`
}

type Datastore struct {
	DBName string `yaml:"dbName"`
	Kind   string `yaml:"kind"`
}

type Storage struct {
	BucketName string `yaml:"bucketName"`
	TimeoutSec int    `yaml:"timeoutsec"`
}

type Speaker struct {
	IPAddress string `yaml:"ipAddress"`
	Port      string `yaml:"port"`
	Msg       string `yaml:"msg"`
	Language  string `yaml:"language"`
}

infrastructure

DatastoreのCRUDを実装したものとCloud Storageのバケット読み込みやファイルダウンロードなどを実装しています.

model

Datastoreのキーやプロパティを定義しています

service

連続再生するにあたって曲の再生時間を知らないといけないため, 音声ファイルを解析して曲の再生時間を取得しています.
serviceのDIがないので, 中途半端になっています. (Controllerもなかったりするのですが)

sound.go
package service

import (
	"io"
	"log"

	"github.com/mjibson/go-dsp/wav"
)

func CalcPlaySec(rc io.Reader) (int, error) {
	w, err := wav.New(rc)
	if err != nil {
		return 0, err
	}
	log.Printf("Duration: %v", w.Duration.Seconds())
	return int(w.Duration.Seconds()), nil
}

スピーカーを制御するコードです.
今回は以下を使用しました. IP/PortでGoogle Nest Miniと接続するものです.
https://github.com/kunihiko-t/google-home-notifier-go

speaker.go
package service

import (
	"log"
	"time"

	notifier "github.com/kunihiko-t/google-home-notifier-go"
)

func Greeting(n *notifier.Notifier, msg, lang string) {
	n.Notify(msg, lang)
	time.Sleep(20 * time.Second)
}

func Play(n *notifier.Notifier, url string, playSec int) {
	err := n.Play(url)
	if err != nil {
		log.Println(err)
	}
	time.Sleep(time.Duration(playSec) * time.Second)
}

func PlayAll(n *notifier.Notifier, bucketName string, objects []string, playSec int) {
	for _, o := range objects {
		// HACK ME
		url := "https://storage.googleapis.com" + "/" + bucketName + "/" + o
		log.Printf("url: %v", url)
		err := n.Play(url)
		if err != nil {
			log.Println(err)
		}
		time.Sleep(time.Duration(playSec) * time.Second)
	}
}

func Stop(n *notifier.Notifier) {
	err := n.Stop()
	if err != nil {
		log.Println(err)
	}
	log.Println("stopped")
}

func Quit(n *notifier.Notifier) {
	err := n.Quit()
	if err != nil {
		log.Println(err)
	}
	log.Println("quited")
}

main.go

cockroachdb/errorsへの移行をし始めています. (main.go以外は移行できていない) 
また, プログラム起動時にGCSとDatastoreを全件見に行く実装になっているため修正が必要です. GCSのほうはバケットのサブディレクトリ単位で取得するような実装に修正したほうがいいと思っています.
Datastoreの方は, GCSのイベントを監視して更新があったらDatastoreを更新するようなCloud Runを1個立てようと思っています.

最後に

まだ実装のアラはたくさんありますが, 直しつつこれを拡張して自由に曲順を指定したり, バッチ化して指定時間に再生するなどの機能追加を考えています.
また, 最終的にはラズパイ上で動作させることを考えているので, CDを整えていきたいという展望があります.

Discussion