I am a student that has become quite interested in the C++ language and I am currently working on a little project involving printing large number of coloured ASCII characters on the Windows Terminal which is an optimised version of the old cmd.exe
. My project consists of creating small "ASCII videos" by converting frames from a video into ASCII images and printing them sequentially (see or video link). I am currently using ANSI escape sequences like \033[38;2;⟨r⟩;⟨g⟩;⟨b⟩m
to manually set the colours I want for each ASCII character like so :
string char_ramp("$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\" ^ `'. ");
int brightness(some_number);
(...)
current_frame += string{} + "\033[38;2;" + to_string(r) + ";" + to_string(g) + ";" + to_string(b) + "m" + char_ramp[(int)(greyscale_index / 255 * (char_ramp.size() - 1)) / brightness] + "\033[0m";
Basically, I am building the frames line by line by adding the ASCII character corresponding to the current pixel's level of brightness on a grey scale character ramp and then I colourise it with an ANSI escape sequence.
But doing it this way seems to be very demanding for the terminal because I get frame drops when I print the colored ASCII frames. Say for example that if the original video was at 30 fps, the colored ASCII video would drop to roughly 15 fps.
I tried removing the colours and it seemed to be working fine, no frame drops.
Some friends suggested I should be setting the buffer data directly instead of doing all the extra logic of printing lines. However, I don't know how I could do it without changing my way of constructing the ASCII frames as I really need them to be stored in a specific way, or at least stored in a way that allows me to control the output rate. I heard that the WriteConsoleOutputCharacter
function allows me to do some things but I didn't understand how I could implement it in my code.
I don't really care about memory efficiency here so wouldn't there be a way to set the frame data into the buffer and then just iterate through the frames I want to display?
If that is just a wrong call, then what would be an efficient way of displaying these ascii frames on the terminal?
Some time ago I wrote some code to do stream-style output directly to a Windows console, with color support.
// WinBuf.hpp:
#pragma once
#include <ios>
#include <ostream>
#include <windows.h>
//! A C++ streambuf that writes directly to a Windows console
class WinBuf : public std::streambuf
{
HANDLE h;
public:
//! Create a WinBuf from an Windows handle
//! @param h handle to a Windows console
WinBuf(HANDLE h) : h(h) {}
WinBuf(WinBuf const &) = delete;
WinBuf &operator=(WinBuf const &) = delete;
//! Return the handle to which this buffer is attached
HANDLE handle() const { return h; }
protected:
virtual int_type overflow(int_type c) override
{
if (c != EOF)
{
DWORD written;
WriteConsole(h, &c, 1, &written, nullptr);
}
return c;
}
virtual std::streamsize xsputn(char_type const *s, std::streamsize count) override
{
DWORD written;
WriteConsole(h, s, DWORD(count), &written, nullptr);
return written;
}
};
//! A C++ ostream that writes via the preceding WinBuf
class WinStream : public virtual std::ostream
{
WinBuf buf;
public:
//! Create stream for a Windows console, defaulting to standard output
WinStream(HANDLE dest = GetStdHandle(STD_OUTPUT_HANDLE))
: buf(dest), std::ostream(&buf)
{
}
//! return a pointer to the underlying WinBuf
WinBuf *rdbuf() { return &buf; }
};
//! Provide the ability to set attributes (text colors)
class SetAttr
{
WORD attr;
public:
//! Save user's attribute for when this SetAttr object is written out
SetAttr(WORD attr) : attr(attr) {}
//! Support writing the SetAttr object to a WinStream
//! @param w a WinStream object to write to
//! @param c An attribute to set
friend WinStream &operator<<(WinStream &w, SetAttr const &c)
{
WinBuf *buf = w.rdbuf();
auto h = buf->handle();
SetConsoleTextAttribute(h, c.attr);
return w;
}
//! support combining attributes
//! @param r the attribute to combine with this one
SetAttr operator|(SetAttr const &r)
{
return SetAttr(attr | r.attr);
}
};
//! Support setting the position for succeeding output
class gotoxy
{
COORD coord;
public:
//! Save position for when object is written to stream
gotoxy(SHORT x, SHORT y) : coord{ .X = x, .Y = y} {}
//! support writing gotoxy object to stream
friend WinStream &operator<<(WinStream &w, gotoxy const &pos)
{
WinBuf *buf = w.rdbuf();
auto h = buf->handle();
SetConsoleCursorPosition(h, pos.coord);
return w;
}
};
//! Clear the "screen"
class cls
{
char ch;
public:
//! Create screen clearing object
//! @param ch character to use to fill screen
cls(char ch = ' ') : ch(ch) {}
//! Support writing to a stream
//! @param os the WinStream to write to
//! @param c the cls object to write
friend WinStream &operator<<(WinStream &os, cls const &c)
{
COORD tl = {0, 0};
CONSOLE_SCREEN_BUFFER_INFO s;
WinBuf *w = os.rdbuf();
HANDLE console = w->handle();
GetConsoleScreenBufferInfo(console, &s);
DWORD written, cells = s.dwSize.X * s.dwSize.Y;
FillConsoleOutputCharacter(console, c.ch, cells, tl, &written);
FillConsoleOutputAttribute(console, s.wAttributes, cells, tl, &written);
SetConsoleCursorPosition(console, tl);
return os;
}
};
//! Provide some convenience instances of the SetAttr object
//! to (marginally) ease setting colors.
extern SetAttr red;
extern SetAttr green;
extern SetAttr blue;
extern SetAttr intense;
extern SetAttr red_back;
extern SetAttr blue_back;
extern SetAttr green_back;
extern SetAttr intense_back;
The attributes are defined in a matching .cpp file:
// Winbuf.cpp:
#include "WinBuf.hpp"
SetAttr red{FOREGROUND_RED};
SetAttr green{FOREGROUND_GREEN};
SetAttr blue{FOREGROUND_BLUE};
SetAttr intense{FOREGROUND_INTENSITY};
SetAttr red_back{BACKGROUND_RED};
SetAttr green_back{BACKGROUND_GREEN};
SetAttr blue_back{BACKGROUND_BLUE};
SetAttr intense_back{BACKGROUND_INTENSITY};
Here's a quick demo/test program to show how it's used:
// test_Winbuf.cpp
#include "winbuf.hpp"
int main()
{
WinStream w;
// the colors OR together, so red | green | blue gives white:
auto color = red | green | blue | blue_back;
w << color << cls() << gotoxy(10, 4) << "This is a string\n";
for (int i = 0; i < 10; i++)
w << "Line: " << i << "\n";
w << (green | blue_back);
for (int i = 0; i < 10; i++)
w << "Line: " << i + 10 << "\n";
w << gotoxy(20, 10) << "Stuck in the middle with you!\n";
w << gotoxy(0, 30) << color << "The end\n";
}
At in my experience, this is quite a bit faster than using ANSI escape sequences (and, for what little it's worth, also works on older versions of Windows).