Last active
January 2, 2023 03:26
-
-
Save SocketByte/e8befc069010fb600030f15e00ed684a to your computer and use it in GitHub Desktop.
Bad Apple Visualization in a Console Terminal
This file contains 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 ( | |
"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