Skip to content

Instantly share code, notes, and snippets.

@SocketByte
Last active January 2, 2023 03:26
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save SocketByte/e8befc069010fb600030f15e00ed684a to your computer and use it in GitHub Desktop.
Save SocketByte/e8befc069010fb600030f15e00ed684a to your computer and use it in GitHub Desktop.
Bad Apple Visualization in a Console Terminal
package main
import (
"fmt"
"github.com/faiface/beep/mp3"
"github.com/faiface/beep/speaker"
"image"
"image/color"
"image/jpeg"
"io/ioutil"
"log"
"os"
"strconv"
"strings"
"time"
)
// WARNING: Due to lack of time, this code does not contain any SetCursorPosition tricks or any clearing of the console.
// You have to modify your console window size to properly see the animation, especially if you're on 1440p or 4K,
// on 1080p you probably only have to go fullscreen.
// FRAMES:
// ./resources/frames/frame-%d.jpg
// AUDIO:
// ./resources/bad-apple.mp3
// Frame mapping requires extracted frames structured as "video/frames/frame-%d.jpg"
// You can extract frames using this ffmpeg command:
// ffmpeg -i "video/badapple.mp4" -vf "scale=80:60" "video/frames/frame-%d.jpg"
// Set to false if you already have frames.dat file.
const BaMapFrames = true
const BaFps = 30
const AsciiMap = "@@#%xo;:,."
func RGBAToGrayscale(rgba color.Color) uint8 {
r, g, b, _ := rgba.RGBA()
lum := 0.299 * float64(r) + 0.587 * float64(g) + 0.114 * float64(b)
return uint8(lum / 256)
}
func DecodeImage(file *os.File) ([][]uint8, int, int, error) {
img, _, err := image.Decode(file)
if err != nil {
return nil, 0, 0, err
}
bounds := img.Bounds()
width, height := bounds.Max.X, bounds.Max.Y
var pixels [][]uint8
for y := 0; y < height; y++ {
var row []uint8
for x := 0; x < width; x++ {
row = append(row, RGBAToGrayscale(img.At(x, y)))
}
pixels = append(pixels, row)
}
return pixels, width, height, nil
}
func MapFrame(frame int) string {
path := "./resources/frames/frame-" + strconv.Itoa(frame) + ".jpg"
file, err := os.Open(path)
if err != nil {
log.Fatal(err)
}
defer file.Close()
pixels, width, height, err := DecodeImage(file)
if err != nil {
log.Fatal(err)
}
var frameAscii string
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
luminosity := pixels[y][x]
mapIndex := (255 - int32(luminosity)) * 10 / 256
mapValue := AsciiMap[mapIndex]
frameAscii += string(mapValue) + string(mapValue)
}
frameAscii += "\n" // new line
}
//fmt.Print(frameAscii)
return frameAscii
}
func MapFrames() {
files,_ := ioutil.ReadDir("resources\\frames")
var allFrames string
for frame := 1; frame < len(files); frame++ {
ascii := MapFrame(frame)
allFrames += ascii + "\r"
fmt.Println("[frame: ", frame, "] DONE. ", len(files) - frame, " left.")
}
err := ioutil.WriteFile("frames.dat", []byte(allFrames), 0644)
if err != nil {
fmt.Println("could not map frames\n", err)
}
}
func ReadData() []string {
bytes, err := ioutil.ReadFile("frames.dat")
if err != nil {
log.Fatal(err)
}
return strings.Split(string(bytes), "\r")
}
func main() {
image.RegisterFormat("jpg", "jpg", jpeg.Decode, jpeg.DecodeConfig)
// Yes, I should make CLI options for this, but I'm pretty lazy.
if BaMapFrames {
MapFrames()
}
f, err := os.Open("resources/bad-apple.mp3")
if err != nil {
log.Fatal(err)
}
streamer, format, err := mp3.Decode(f)
if err != nil {
log.Fatal(err)
}
defer streamer.Close()
_ = speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10))
speaker.Play(streamer)
// Had to do this to synchronize the audio with the video properly.
time.Sleep(540 * time.Millisecond)
frames := ReadData()
frame := 1
for range time.Tick(1000 / BaFps * time.Millisecond) {
fmt.Println(frames[frame])
frame++
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment