add code
This commit is contained in:
261
dca0/dca0.go
Normal file
261
dca0/dca0.go
Normal file
@@ -0,0 +1,261 @@
|
||||
package dca0
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"layeh.com/gopus"
|
||||
)
|
||||
|
||||
type CacheOverflowError struct {
|
||||
MaxCacheBytes int
|
||||
}
|
||||
|
||||
func (e *CacheOverflowError) Error() string {
|
||||
return "audio too large: the maximum cache limit of " + strconv.Itoa(e.MaxCacheBytes) + " bytes has been exceeded"
|
||||
}
|
||||
|
||||
type Command interface{}
|
||||
|
||||
type CommandStop struct{}
|
||||
type CommandPause struct{}
|
||||
type CommandResume struct{}
|
||||
type CommandStartLooping struct{}
|
||||
type CommandStopLooping struct{}
|
||||
type CommandSeek float32 // In seconds.
|
||||
type CommandGetPlaybackTime struct{} // Gets the playback time.
|
||||
type CommandGetDuration struct{} // Attempts to get the duration. Only succeeds if the encoder is already done.
|
||||
|
||||
type Response interface{}
|
||||
|
||||
type ResponsePlaybackTime float32 // Playback time in seconds.
|
||||
type ResponseDuration float32 // Duration in seconds.
|
||||
type ResponseDurationUnknown struct{} // Returned if the duration is unknown.
|
||||
|
||||
type Dca0Options struct {
|
||||
PcmOptions
|
||||
// 960 (20ms at 48kHz) is currently the only one supported by discordgo.
|
||||
// If it is changed nonetheless, it changes the playback speed (without
|
||||
// changing the pitch because of how discord and opus work).
|
||||
FrameSize int
|
||||
Bitrate int
|
||||
// Maximum number of bytes that may be cached. A 3-minute song usually uses
|
||||
// about 2.7MB. If the capacity is full, an error will be sent and the
|
||||
// function will exit.
|
||||
MaxCacheBytes int
|
||||
}
|
||||
|
||||
func GetDefaultOptions(ffmpegPath string) Dca0Options {
|
||||
return Dca0Options{
|
||||
PcmOptions: getDefaultPcmOptions(ffmpegPath),
|
||||
FrameSize: 960,
|
||||
// 64000 is Discord's default.
|
||||
Bitrate: 64000,
|
||||
// Max cache size of 20MB.
|
||||
MaxCacheBytes: 20000000,
|
||||
}
|
||||
}
|
||||
|
||||
type Dca0Encoder struct {
|
||||
opusEnc *gopus.Encoder
|
||||
}
|
||||
|
||||
func NewEncoder(opts Dca0Options) (*Dca0Encoder, error) {
|
||||
opusEnc, err := gopus.NewEncoder(opts.SampleRate, opts.Channels, gopus.Audio)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Dca0Encoder{
|
||||
opusEnc: opusEnc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Sends the individual opus frames as byte arrays through the specified
|
||||
// channel.
|
||||
// Input can be either a local file or an http(s) address. It can be of any
|
||||
// format supported by ffmpeg.
|
||||
// Caches the entire opus data due to some problems when reading from ffmpeg
|
||||
// too slowly.
|
||||
func (e *Dca0Encoder) GetOpusFrames(input string, opts Dca0Options, ch chan<- []byte, errCh chan<- error, cmdCh <-chan Command, respCh chan<- Response) {
|
||||
pcm, cmd, err := getPcm(input, opts.PcmOptions)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
|
||||
// How many opus frames are played per second.
|
||||
framesPerSecond := float32(opts.SampleRate) / float32(opts.FrameSize)
|
||||
// Potential maximum samples an audio frame can have.
|
||||
maxSamples := opts.FrameSize * opts.Channels
|
||||
// One pcm sample equals two bytes.
|
||||
maxBytes := maxSamples * 2
|
||||
|
||||
// Size of the opus frame cache.
|
||||
cacheSize := 0
|
||||
// We're storing all opus frames in this array as a cache.
|
||||
opusFrames := make([][]byte, 0, 512)
|
||||
// Opus frame read position.
|
||||
rp := 0
|
||||
// We're sending frames through this before appending them to opusFrames.
|
||||
frameCh := make(chan []byte, 8)
|
||||
|
||||
sampleBytes := make([]byte, maxBytes)
|
||||
samples := make([]int16, maxSamples)
|
||||
|
||||
// Used by the encoder to tell the main process when it's done encoding.
|
||||
encoderDone := make(chan struct{})
|
||||
// Used by the main process to tell the encoder to stop.
|
||||
encoderStop := make(chan struct{})
|
||||
// Launch the encoder.
|
||||
// Encode opus data and send it through frameCh.
|
||||
go func() {
|
||||
var killedFfmpeg bool
|
||||
encoderLoop:
|
||||
for {
|
||||
// Stop encoding if the main process tells us to.
|
||||
select {
|
||||
case <-encoderStop:
|
||||
// Kill ffmpeg using SIGINT.
|
||||
cmd.Process.Signal(os.Interrupt)
|
||||
pcm.Close()
|
||||
killedFfmpeg = true
|
||||
// Exit the loop.
|
||||
break encoderLoop
|
||||
default:
|
||||
}
|
||||
|
||||
// Efficiently read the sample bytes outputted by ffmpeg into the
|
||||
// int16 sample slice.
|
||||
_, err := io.ReadFull(pcm, sampleBytes)
|
||||
if err != nil {
|
||||
// We also want to stop on ErrUnexpectedEOF because a frame can
|
||||
// currently ONLY be 960 * 2 samples large. If a frame were
|
||||
// smaller, Discord would just slow the audio down. Since a
|
||||
// frame is just 20ms long, we can discard the last one without
|
||||
// any issues.
|
||||
if err == io.EOF || err == io.ErrUnexpectedEOF {
|
||||
break
|
||||
} else {
|
||||
errCh <- err
|
||||
}
|
||||
}
|
||||
|
||||
// Read samples from binary, similarly to how the go package binary does
|
||||
// it but more efficiently since we're not allocating anything.
|
||||
for i := range samples {
|
||||
samples[i] = int16(binary.LittleEndian.Uint16(sampleBytes[2*i:]))
|
||||
}
|
||||
|
||||
// Encode samples as opus.
|
||||
frame, err := e.opusEnc.Encode(samples, opts.FrameSize, maxBytes)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
}
|
||||
|
||||
if cacheSize > opts.MaxCacheBytes {
|
||||
errCh <- &CacheOverflowError{
|
||||
MaxCacheBytes: opts.MaxCacheBytes,
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
cacheSize += len(frame)
|
||||
frameCh <- frame
|
||||
}
|
||||
// Wait for ffmpeg to close.
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
// Ffmpeg returns 255 if it was killed using SIGINT. Therefore that
|
||||
// wouldn't be an error.
|
||||
switch e := err.(type) {
|
||||
case *exec.ExitError:
|
||||
if e.ExitCode() == 255 {
|
||||
if !killedFfmpeg {
|
||||
errCh <- err
|
||||
}
|
||||
}
|
||||
default:
|
||||
errCh <- err
|
||||
}
|
||||
}
|
||||
// Tell the main process that the encoder is done.
|
||||
encoderDone <- struct{}{}
|
||||
}()
|
||||
|
||||
encoderRunning := true
|
||||
paused := false
|
||||
loop := false
|
||||
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case v := <-frameCh:
|
||||
opusFrames = append(opusFrames, v)
|
||||
case <-encoderDone:
|
||||
encoderRunning = false
|
||||
case receivedCmd := <-cmdCh:
|
||||
switch v := receivedCmd.(type) {
|
||||
case CommandStop:
|
||||
if encoderRunning {
|
||||
encoderStop <- struct{}{}
|
||||
}
|
||||
break loop
|
||||
case CommandPause:
|
||||
paused = true
|
||||
case CommandResume:
|
||||
paused = false
|
||||
case CommandStartLooping:
|
||||
loop = true
|
||||
case CommandStopLooping:
|
||||
loop = false
|
||||
case CommandSeek:
|
||||
rp = int(float32(v) * framesPerSecond)
|
||||
case CommandGetPlaybackTime:
|
||||
respCh <- ResponsePlaybackTime(float32(rp) / framesPerSecond)
|
||||
case CommandGetDuration:
|
||||
if encoderRunning {
|
||||
respCh <- ResponseDurationUnknown{}
|
||||
} else {
|
||||
respCh <- ResponseDuration(float32(len(opusFrames)) / framesPerSecond)
|
||||
}
|
||||
}
|
||||
default:
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
}
|
||||
|
||||
if !paused && rp < len(opusFrames) {
|
||||
if encoderRunning {
|
||||
select {
|
||||
case ch <- opusFrames[rp]:
|
||||
rp++
|
||||
default:
|
||||
}
|
||||
} else {
|
||||
ch <- opusFrames[rp]
|
||||
rp++
|
||||
}
|
||||
}
|
||||
|
||||
if !encoderRunning && rp >= len(opusFrames) {
|
||||
if loop {
|
||||
rp = 0
|
||||
} else {
|
||||
// We're done sending opus data.
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the encoder to finish if it's still running.
|
||||
if encoderRunning {
|
||||
<-encoderDone
|
||||
encoderRunning = false
|
||||
// TODO: I want to make this unnecessary. I have just noticed that
|
||||
// this channel often get stuck so a panic is more helpful than that.
|
||||
close(respCh)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user