From 3955a64d9f0dd51a8fc720958e4c4fee3399b6ec Mon Sep 17 00:00:00 2001 From: Rob Watson Date: Sun, 5 Sep 2021 11:49:59 +0200 Subject: [PATCH] poc: introduce AudioDownloader --- .gitignore | 2 +- backend/main.go | 124 ++++++++++++++++++++++++++++++++++++------------ 2 files changed, 94 insertions(+), 32 deletions(-) diff --git a/.gitignore b/.gitignore index e9ff18a..40732b0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -*.m4a +/backend/cache/ diff --git a/backend/main.go b/backend/main.go index d0d392e..e43c77b 100644 --- a/backend/main.go +++ b/backend/main.go @@ -3,8 +3,12 @@ package main import ( "bytes" "encoding/binary" + "fmt" + "io" "log" + "os" "os/exec" + "strconv" "github.com/kkdai/youtube/v2" ) @@ -12,49 +16,107 @@ import ( const ( ItagM4AAudio = 140 SizeOfInt16 = 2 + + DefaultAudioCodec = "pcm_s16le" + DefaultFormat = "s16le" + DefaultSampleRate = 44100 ) -// TODO: process as stream to avoid loading full buffer in memory +type byteToIntConverter struct { + callback func(p []uint16) + buf []uint16 +} -func main() { - var client youtube.Client - - videoID := "s_oJYdRlrv0" - - video, err := client.GetVideo(videoID) - if err != nil { - log.Fatal(err) +func (w byteToIntConverter) Write(p []byte) (int, error) { + if len(p)%2 != 0 { + panic("need to handle odd number of bytes") } - format := video.Formats.FindByItag(ItagM4AAudio) - stream, _, err := client.GetStream(video, format) - if err != nil { - log.Fatal(err) + // 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]) } - // TODO: force 44.1khz - cmd := exec.Command("ffmpeg", "-i", "-", "-f", "s16le", "-acodec", "pcm_s16le", "-") - cmd.Stdin = stream + w.callback(w.buf) - var out bytes.Buffer - cmd.Stdout = &out + 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.Fatalf("err = %v, stdErr = %s", err, errOut.String()) + if err := cmd.Run(); err != nil { + log.Println(errOut.String()) + return fmt.Errorf("error calling ffmpeg: %v", err) } - log.Printf("byteLen = %d", out.Len()) - - // TODO: reflect.Slice to avoid copying? - data := make([]uint16, 0, out.Len()/SizeOfInt16) - for i := 0; i < out.Len(); i += SizeOfInt16 { - v := binary.LittleEndian.Uint16(out.Bytes()[i : i+SizeOfInt16]) - data = append(data, v) - } - - log.Printf("intLen = %d", len(data)) - log.Printf("ints = %+v", data[0:8192]) + 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") }