Last active
August 24, 2022 13:09
-
-
Save XiaoMengXinX/5f1569ca7ceaed50132d6da23febb996 to your computer and use it in GitHub Desktop.
Play badapple with golang in console
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package main | |
| import ( | |
| "bytes" | |
| "errors" | |
| "flag" | |
| "fmt" | |
| "image" | |
| "image/color" | |
| "image/jpeg" | |
| "log" | |
| "os" | |
| "os/exec" | |
| "strconv" | |
| "strings" | |
| "time" | |
| "github.com/faiface/beep" | |
| "github.com/faiface/beep/mp3" | |
| "github.com/faiface/beep/speaker" | |
| ) | |
| const ( | |
| str = "@#8G0CLft1in;:,. " | |
| wordRatio = float32(36) / float32(15) | |
| ) | |
| var arg = os.Args | |
| var videoFile string | |
| var newWidth, newFps int | |
| var isSkipProcess bool | |
| func init() { | |
| widthArg := flag.Int("w", 0, "The width of the output") | |
| fpsArg := flag.Int("f", 0, "The fps of the output") | |
| isSkipProcessArg := flag.Bool("k", false, "If the video processing temporary files already exist, use this option to skip the processing.") | |
| flag.Usage = func() { | |
| fmt.Printf(`Usage: | |
| %s (video file) [-w width] [-f fps] [-k] | |
| Options: | |
| -h, --help Show this screen. | |
| -w The width of the output | |
| -f The fps of the output | |
| -k If the video processing temporary files already exist, use this option to skip the processing.`, arg[0]) | |
| } | |
| if len(arg) < 2 { | |
| flag.Usage() | |
| os.Exit(0) | |
| } else { | |
| videoFile = arg[1] | |
| os.Args = arg[1:] | |
| } | |
| flag.Parse() | |
| newWidth = *widthArg | |
| newFps = *fpsArg | |
| isSkipProcess = *isSkipProcessArg | |
| } | |
| func main() { | |
| // check if ffmpeg is installed | |
| _, err := exec.LookPath("ffmpeg") | |
| if err != nil { | |
| log.Fatalln("ffmpeg is not installed") | |
| } | |
| // get information of the video | |
| fps := getVIdeoFPS(videoFile) | |
| width, _ := getVideoSize(videoFile) | |
| if newWidth == 0 { | |
| newWidth = width | |
| } | |
| if newFps == 0 { | |
| newFps = fps | |
| } | |
| if !isSkipProcess { | |
| // extract video frames and convert to image | |
| if err := extractFrames(videoFile, newWidth, newFps); err != nil { | |
| log.Println(errors.New("extract frames error")) | |
| log.Fatalln(err) | |
| } | |
| } | |
| var streamer beep.StreamSeekCloser | |
| // extract audio from video | |
| if !isSkipProcess { | |
| err = extractAudio(videoFile) | |
| } else { | |
| _, err = os.Stat("tmp/output.mp3") | |
| } | |
| if err != nil { | |
| log.Println(err) | |
| } else { | |
| // init audio streamer | |
| streamer, err = initAudio() | |
| defer streamer.Close() | |
| if err != nil { | |
| log.Println(err) | |
| } | |
| // play audio | |
| speaker.Play(streamer) | |
| } | |
| // calculate the delay between each frame | |
| delay := time.Millisecond * 1000 / time.Duration(newFps) | |
| startTime := time.Now() | |
| for i := 1; ; { | |
| // calculate which frame to show | |
| i = int(time.Since(startTime)/delay) + 1 | |
| // read image file | |
| imgFile, err := os.ReadFile("./tmp/" + strconv.Itoa(i) + ".jpg") | |
| if err != nil { | |
| break | |
| } | |
| img, err := jpeg.Decode(bytes.NewReader(imgFile)) | |
| if err != nil { | |
| log.Println(errors.New("decode image file error")) | |
| log.Fatalln(err) | |
| } | |
| // convert to ascii | |
| ascii := gray2ascii(compressImg(color2Gray(img), newWidth, wordRatio)) | |
| // back to first char | |
| os.Stderr.WriteString("\033[0;0H") | |
| // print ascii | |
| os.Stderr.WriteString(ascii) | |
| time.Sleep(delay) | |
| } | |
| } | |
| func extractAudio(videoPath string) error { | |
| if _, err := os.Stat("./tmp"); err != nil { | |
| _ = os.MkdirAll("./tmp", os.ModePerm) | |
| } | |
| cmd := exec.Command("ffmpeg", "-i", videoPath, "-vn", "-y", "tmp/output.mp3") | |
| err := cmd.Run() | |
| if err != nil { | |
| return err | |
| } | |
| return nil | |
| } | |
| func extractFrames(videoPath string, width int, fps int) error { | |
| if _, err := os.Stat("./tmp"); err != nil { | |
| _ = os.MkdirAll("./tmp", os.ModePerm) | |
| } | |
| cmd := exec.Command("ffmpeg", "-i", videoPath, "-vf", "scale="+strconv.Itoa(width)+":-1,fps="+strconv.Itoa(fps), "-y", "tmp/%d.jpg") | |
| err := cmd.Run() | |
| if err != nil { | |
| return err | |
| } | |
| return nil | |
| } | |
| func initAudio() (streamer beep.StreamSeekCloser, err error) { | |
| f, err := os.Open("./tmp/output.mp3") | |
| if err != nil { | |
| return | |
| } | |
| streamer, format, err := mp3.Decode(f) | |
| if err != nil { | |
| return | |
| } | |
| err = speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10)) | |
| if err != nil { | |
| return | |
| } | |
| return | |
| } | |
| func getVideoSize(videoPath string) (width, height int) { | |
| cmd := exec.Command("ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height", "-of", "csv=s=x:p=0", videoPath) | |
| out, err := cmd.CombinedOutput() | |
| if err != nil { | |
| log.Println(errors.New("get video size error")) | |
| log.Fatalln(err) | |
| } | |
| args := strings.Split(string(out), "x") | |
| width, _ = strconv.Atoi(args[0]) | |
| height, _ = strconv.Atoi(strings.Trim(args[1], "\n")) | |
| return width, height | |
| } | |
| func getVIdeoFPS(videoPath string) int { | |
| cmd := exec.Command("ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=r_frame_rate", "-of", "csv=s=x:p=0", videoPath) | |
| out, err := cmd.CombinedOutput() | |
| if err != nil { | |
| log.Println(errors.New("get video fps error")) | |
| log.Fatalln(err) | |
| } | |
| args := strings.Split(string(out), "/") | |
| fps, _ := strconv.Atoi(args[0]) | |
| return fps | |
| } | |
| // compress the image to a fixed width to adapt to terminal output | |
| func compressImg(img *image.RGBA, size int, wordRatio float32) *image.RGBA { | |
| dx := img.Bounds().Dx() | |
| dy := img.Bounds().Dy() | |
| scale := float32(dx) / float32(size) | |
| wh := float32(dx) / float32(dy) | |
| rgbSmall := image.NewRGBA(image.Rect(0, 0, size, int(float32(size)/(wh*wordRatio)))) | |
| bounds := rgbSmall.Bounds() | |
| for i := 0; i < bounds.Dx(); i++ { | |
| for j := 0; j < bounds.Dy(); j++ { | |
| var gray2 int | |
| c2 := img.RGBAAt(int(float32(i)*scale), int(float32(j)*scale*wordRatio)) | |
| c3 := img.RGBAAt(int(float32(i)*scale)+1, int(float32(j)*scale*wordRatio)) | |
| c4 := img.RGBAAt(int(float32(i)*scale)-1, int(float32(j)*scale*wordRatio)) | |
| c5 := img.RGBAAt(int(float32(i)*scale), int(float32(j)*scale*wordRatio-1)) | |
| c6 := img.RGBAAt(int(float32(i)*scale), int(float32(j)*scale*wordRatio+1)) | |
| gray2 = (int(c2.R) + int(c3.R) + int(c4.R) + int(c5.R) + int(c6.R)) / 5 | |
| c := color.RGBA{R: uint8(gray2), G: uint8(gray2), B: uint8(gray2)} | |
| rgbSmall.SetRGBA(i, j, c) | |
| } | |
| } | |
| return rgbSmall | |
| } | |
| // convert the image to grayscale | |
| func color2Gray(img image.Image) *image.RGBA { | |
| bounds := img.Bounds() | |
| w := bounds.Dx() | |
| h := bounds.Dy() | |
| newRgba := image.NewRGBA(bounds) | |
| for i := 0; i < w; i++ { | |
| for j := 0; j < h; j++ { | |
| cl := img.At(i, j) | |
| r, g, b, _ := cl.RGBA() | |
| gray := (r*30 + g*59 + b*11 + 50) / 100 | |
| newC := color.RGBA{R: uint8(gray), G: uint8(gray), B: uint8(gray)} | |
| newRgba.SetRGBA(i, j, newC) | |
| } | |
| } | |
| return newRgba | |
| } | |
| // convert the grayscale to ascii art | |
| func gray2ascii(img *image.RGBA) (output string) { | |
| bounds := img.Bounds() | |
| w := bounds.Dx() | |
| h := bounds.Dy() | |
| for i := 0; i < h; i++ { | |
| line := "" | |
| for j := 0; j < w; j++ { | |
| gray := img.RGBAAt(j, i).R | |
| num := int(gray) * len(str) / 256 | |
| line += string(str[num]) | |
| } | |
| line += "\n" | |
| output += line | |
| } | |
| return | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment