Skip to content

Instantly share code, notes, and snippets.

@XiaoMengXinX
Last active August 24, 2022 13:09
Show Gist options
  • Select an option

  • Save XiaoMengXinX/5f1569ca7ceaed50132d6da23febb996 to your computer and use it in GitHub Desktop.

Select an option

Save XiaoMengXinX/5f1569ca7ceaed50132d6da23febb996 to your computer and use it in GitHub Desktop.
Play badapple with golang in console
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