🐻

Discordのbotをffmpegを使って音声を流す方法

2022/06/18に公開

事前準備

今回はGoで実装します。
ffmpegをインストールしておきます。
DiscordのBotを作成してトークンを取得します。
そしてBotを構築しておきます。

func main() {
	dg, err := discordgo.New("Bot " + "Token")
	if err != nil {
		fmt.Println("error creating Discord session,", err)
		return
	}

	dg.AddHandler(MusicPlay)

	err = dg.Open()
	if err != nil {
		fmt.Println("error opening connection,", err)
		return
	}

	fmt.Println("Bot is now running.  Press CTRL-C to exit.")
	sc := make(chan os.Signal, 1)
	signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill)
	<-sc

	dg.Close()
}

チャットから音声ファイルの取得

ffmpegでURLを指定すればなんでも再生できますが今回はチャットから音声ファイルを取得して再生したいと思います。

func MusicPlay(s*discordgo.Session, m *discordgo.Message) {
  if m.Author.ID == s.State.User.ID {
		return
	}

	if len(m.Message.Attachments) != 1 {
		return
	}

  url := m.Message.Attachments[0].URL
	sp := strings.Split(url, ".")
	l := len(sp) - 1
	if len(sp) != 2 && !(sp[l] == "mp3" || sp[l] == "wav") {
		return
	}

  v, err := c.ChannelVoiceJoin(s, m.Message)
	if err != nil {
		s.ChannelMessageSend(m.ChannelID, "ボイスチャンネルに入れません")
		return
	}

  ctx, cancel := context.WithCancel(context.Background())
  Play(s, m, v, ctx, url)
}

とりあえずm.Message.Attachmentsが1以上あればファイルが存在するとして取得します。
拡張子がmp3またはwavならc.ChannelVoiceJoin(s, m.Message)で音声チャンネルに参加します。

ffmpegの構造体を作成

type ffmpeg struct {
  *exec.Cmd
}

func NewFfmpeg() (*ffmpeg, error) {
	cmdPath, err := exec.LookPath("ffmpeg")
	if err != nil {
		return nil, err
	}

	return &ffmpeg{
		exec.CommandContext(
			context.Background(),
			cmdPath,
		),
		}, nil
}

func (f *ffmpeg) SetArgs(args ...string) {
	f.Args = append(f.Args, args...)
}

func (f *ffmpeg) Start(output string) error {
	f.SetArgs(output)
	return f.Cmd.Start()
}

func (f *ffmpeg) Kill() error {
	return f.Cmd.Process.Kill()
}

func (f *ffmpeg) Play(buf *bufio.Reader, send chan[]int16 , ctx context.Context) error {
	for {
		audiobuf := make([]int16, 960*2)
		if err := binary.Read(buf, binary.LittleEndian, &audiobuf); err != nil {
			return err
		}
		select {
		case send <- audiobuf:
			continue
		case <-ctx.Done():
			return nil
		}
	}
}

単純にffmpegをGoから実行する構造体です。
Play関数でffmpegから受け取ったデータを読み込んでsendにチャネルで送ります。

再生部分の実装

func Play(s *discordgo.Session, m *discordgo.Message, v *discordgo.VoiceConnection, url string, ctx context.Context) error {
	ffmpegCmd, err := NewFfmpeg()
	if err != nil {
		return err
	}

	ffmpegArgs := []string{
		"-i", url,
		"-f", "s16le",
		"-ar", "48000",
		"-ac", "2",
	}

	ffmpegCmd.SetArgs(ffmpegArgs...)
	ffmpegout, err := ffmpegCmd.StdoutPipe()
	if err != nil {
		return err
	}

	ffmpegbuf := bufio.NewReaderSize(ffmpegout, 16384)
	err = ffmpegCmd.Start("pipe:1")
	if err != nil {
		log.Println("ffmpeg error:" + err.Error())
		return err
	}

	go func(ctx context.Context) {
		<-ctx.Done()
		log.Println("ffmpeg done")
		err = ffmpegCmd.Kill()
		if err != nil {
			log.Println("ffmpeg kill error:" + err.Error())
			return
		}
	}(ctx)

	go func(ctx context.Context) {
		v.Speaking(true)
		send := make(chan []int16, 2)
		defer close(send)
		defer v.Speaking(false)

		go func() {
			dgvoice.SendPCM(v, send)
		}()

		err := ffmpegCmd.Play(ffmpegbuf, send, ctx)
		if err != nil {
			return
		}
	}(ctx)

	return nil
}

部分部分に切り取って見ていきます。

ffmpegの実行

	ffmpegCmd, err := NewFfmpeg()
	if err != nil {
		return err
	}

	ffmpegArgs := []string{
		"-i", url,
		"-f", "s16le",
		"-ar", "48000",
		"-ac", "2",
	}

  ffmpegCmd.SetArgs(ffmpegArgs...)
	ffmpegout, err := ffmpegCmd.StdoutPipe()
	if err != nil {
		return err
	}

	ffmpegbuf := bufio.NewReaderSize(ffmpegout, 16384)
	err = ffmpegCmd.Start("pipe:1")
	if err != nil {
		log.Println("ffmpeg error:" + err.Error())
		return err
	}

ffmpegの引数にDiscordのファイルから取得したURLをしている指定してます。ffmpegCmd.Start("pipe:1")で標準出力に出力することを指定しています。
ffmpegout, err := ffmpegCmd.StdoutPipe()で標準出力から受け取っています。

終了を検知

	go func(ctx context.Context) {
		<-ctx.Done()
		log.Println("ffmpeg done")
		err = ffmpegCmd.Kill()
		if err != nil {
			log.Println("ffmpeg kill error:" + err.Error())
			return
		}
	}(ctx)

この部分で音楽の終了を検知してffmpegをkillしています。

ボイスチャンネルに送信

	go func(ctx context.Context) {
		v.Speaking(true)
		send := make(chan []int16, 2)
		defer close(send)
		defer v.Speaking(false)

		go func() {
			dgvoice.SendPCM(v, send)
		}()

		err := ffmpegCmd.Play(ffmpegbuf, send, ctx)
		if err != nil {
			return
		}
	}(ctx)

dgvoiceのSendPCM(v, send)ffmpegCmd.Play()から送られたデータをDiscordのチャンネルに送信しています。

終わり

終わり。
適当にノリで作ったので間違いがあれば教えて下さい。

GitHubで編集を提案

Discussion