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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										73
									
								
								dca0/pcm.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								dca0/pcm.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
			
		||||
// Helper functions for extracting pcm audio from web and local sources.
 | 
			
		||||
package dca0
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"strconv"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type PcmOptions struct {
 | 
			
		||||
	FfmpegPath string
 | 
			
		||||
	Channels   int
 | 
			
		||||
	SampleRate int
 | 
			
		||||
	// Seek means where to start in seconds.
 | 
			
		||||
	Seek float32
 | 
			
		||||
	// Duration means where to stop (in seconds after the time specified by Seek).
 | 
			
		||||
	// If Duration is set to 0, the stream will ignore it and encode all the way
 | 
			
		||||
	// to the end of the input.
 | 
			
		||||
	Duration float32
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getDefaultPcmOptions(ffmpegPath string) PcmOptions {
 | 
			
		||||
	return PcmOptions{
 | 
			
		||||
		FfmpegPath: ffmpegPath,
 | 
			
		||||
		Channels:   2,
 | 
			
		||||
		// 48000 is the only one used by discord currently.
 | 
			
		||||
		SampleRate: 48000,
 | 
			
		||||
		Seek:       0,
 | 
			
		||||
		Duration:   0,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Input can be either a local file or an http(s) address. It can be of any
 | 
			
		||||
// audio format supported by ffmpeg.
 | 
			
		||||
// Wait must be called on the returned command to free its resources after
 | 
			
		||||
// everything has been read.
 | 
			
		||||
func getPcm(input string, opts PcmOptions) (io.ReadCloser, *exec.Cmd, error) {
 | 
			
		||||
	if input == "" {
 | 
			
		||||
		return nil, nil, errors.New("dca0.getPcm() called with empty input")
 | 
			
		||||
	}
 | 
			
		||||
	var cmdOpts []string
 | 
			
		||||
	cmdOpts = append(cmdOpts, []string{
 | 
			
		||||
		"-vn", // No video.
 | 
			
		||||
		"-sn", // No subtitle.
 | 
			
		||||
		"-dn", // No data encoding.
 | 
			
		||||
	}...)
 | 
			
		||||
	if opts.Seek != 0.0 {
 | 
			
		||||
		cmdOpts = append(cmdOpts,
 | 
			
		||||
			"-accurate_seek",
 | 
			
		||||
			"-ss", strconv.FormatFloat(float64(opts.Seek), 'f', 5, 32))
 | 
			
		||||
	}
 | 
			
		||||
	if opts.Duration != 0.0 {
 | 
			
		||||
		cmdOpts = append(cmdOpts,
 | 
			
		||||
			"-t", strconv.FormatFloat(float64(opts.Duration), 'f', 5, 32))
 | 
			
		||||
	}
 | 
			
		||||
	cmdOpts = append(cmdOpts, []string{
 | 
			
		||||
		"-i", input,
 | 
			
		||||
		"-f", "s16le", // Signed int16 samples.
 | 
			
		||||
		"-ar", strconv.Itoa(opts.SampleRate),
 | 
			
		||||
		"-ac", strconv.Itoa(opts.Channels), // Number of audio channels.
 | 
			
		||||
		"pipe:1", // Output to stdout.
 | 
			
		||||
	}...)
 | 
			
		||||
	cmd := exec.Command(opts.FfmpegPath, cmdOpts...)
 | 
			
		||||
	stdout, err := cmd.StdoutPipe()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if err := cmd.Start(); err != nil {
 | 
			
		||||
		return nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return stdout, cmd, nil
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user