package main import ( "bytes" "encoding/binary" "fmt" "io" "log" "os" "os/exec" "strconv" "github.com/kkdai/youtube/v2" ) const ( ItagM4AAudio = 140 SizeOfInt16 = 2 DefaultAudioCodec = "pcm_s16le" DefaultFormat = "s16le" DefaultSampleRate = 44100 ) type byteToIntConverter struct { callback func(p []uint16) buf []uint16 } func (w byteToIntConverter) Write(p []byte) (int, error) { if len(p)%2 != 0 { panic("need to handle odd number of bytes") } // grow the internal buffer if necessary: exp := len(p) / SizeOfInt16 if len(w.buf) < exp { w.buf = append(w.buf, make([]uint16, exp)...) } for i := 0; i < len(p); i += SizeOfInt16 { w.buf[i/2] = binary.LittleEndian.Uint16(p[i : i+SizeOfInt16]) } w.callback(w.buf) return len(p), nil } type AudioDownloader struct { videoID string ytClient youtube.Client } func NewAudioDownloader(videoID string) *AudioDownloader { return &AudioDownloader{videoID: videoID} } func (d *AudioDownloader) ReadInts(fn func(p []uint16)) error { cachePath := fmt.Sprintf("cache/%s.raw", d.videoID) var reader io.Reader fetch := true if _, err := os.Stat(cachePath); err == nil { if fptr, err := os.Open(cachePath); err == nil { defer fptr.Close() reader = fptr fetch = false } else { log.Printf("warning: error opening cache file: %v", err) } } if fetch { log.Println("fetching audio stream from youtube...") video, err := d.ytClient.GetVideo(d.videoID) if err != nil { return fmt.Errorf("error fetching video: %v", err) } format := video.Formats.FindByItag(ItagM4AAudio) stream, _, err := d.ytClient.GetStream(video, format) if err != nil { return fmt.Errorf("error fetching stream: %v", err) } cacheFile, err := os.Create(cachePath) if err != nil { return fmt.Errorf("error creating cache file: %v", err) } reader = io.TeeReader(stream, cacheFile) } var errOut bytes.Buffer cmd := exec.Command("ffmpeg", "-i", "-", "-f", DefaultFormat, "-ar", strconv.Itoa(DefaultSampleRate), "-acodec", DefaultAudioCodec, "-") cmd.Stdin = reader cmd.Stdout = byteToIntConverter{callback: fn} cmd.Stderr = &errOut if err := cmd.Run(); err != nil { log.Println(errOut.String()) return fmt.Errorf("error calling ffmpeg: %v", err) } return nil } func main() { videoID := "s_oJYdRlrv0" downloader := NewAudioDownloader(videoID) err := downloader.ReadInts(func(p []uint16) { // log.Printf("ints = %+v", p[0:16]) }) if err != nil { log.Fatalf("error calling downloader: %v", err) } log.Println("done") }