Skip to content

Instantly share code, notes, and snippets.

@leduyquang753
Last active March 18, 2023 13:19
Show Gist options
  • Save leduyquang753/a660613e2e32696d6fb8c2bf8fb8ad61 to your computer and use it in GitHub Desktop.
Save leduyquang753/a660613e2e32696d6fb8c2bf8fb8ad61 to your computer and use it in GitHub Desktop.
/*
Terminal audio player.
Targetted at Windows, uses LibWinMedia (https://github.com/harmonoid/libwinmedia/)
for audio playback.
Run on the command line. Arguments:
1. The name of the audio file to play.
2. The volume to play from 0 to 100 (optional, default 100).
Playback can be seeked by clicking on the progress bar and paused by pressing Enter.
Volume can be changed by pressing up and down arrow keys.
Quit playback immediately by pressing Esc.
*/
#include <algorithm>
#include <cctype>
#include <chrono>
#include <cmath>
#include <codecvt>
#include <filesystem>
#include <iostream>
#include <locale>
#include <string>
#include <vector>
#include <windows.h>
#include <libwinmedia.hpp>
std::string formatTime(const int seconds) {
std::string res;
if (seconds > 3599) {
res += std::to_string(seconds/3600);
res += 'h';
if (seconds/60%60 < 10) res += '0';
}
if (seconds > 59) {
res += std::to_string(seconds/60%60);
res += ':';
if (seconds%60 < 10) res += '0';
}
res += std::to_string(seconds%60);
if (seconds < 60) res += '"';
return res;
}
void changeVolume(
lwm::Player &player, const int volume, std::string &output, const int displayPos,
bool &volumeDisplayed, std::chrono::time_point<std::chrono::steady_clock> &lastVolumeChange
) {
player.SetVolume(std::pow(volume/100., 4));
std::string display = "Volume: ", number = std::to_string(volume);
display.append(3 - number.size(), ' ');
display += number;
output += "\x1B[38;2;100;100;100m\x1B[";
output += std::to_string(displayPos);
output += 'G';
output += display;
output += "\x1B[0m";
volumeDisplayed = true;
lastVolumeChange = std::chrono::steady_clock::now();
}
int main(int argc, char **argv) {
if (argc < 2) {
std::cerr << "Specify an audio file to play.\n";
return 1;
}
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> converter;
auto path = std::filesystem::absolute(std::filesystem::path(converter.from_bytes(argv[1])));
if (!std::filesystem::exists(path)) {
std::cerr << "The specified file does not exist.\n";
return 2;
}
int volume = 100;
if (argc > 2) {
std::string volumeString = argv[2];
if (std::all_of(
volumeString.begin(), volumeString.end(),
[](const unsigned char c){ return std::isdigit(c); }
)) {
volume = std::stoi(volumeString);
}
}
using namespace std::chrono_literals;
HANDLE outputConsole = GetStdHandle(STD_OUTPUT_HANDLE);
HANDLE inputConsole = GetStdHandle(STD_INPUT_HANDLE);
DWORD consoleMode;
GetConsoleMode(outputConsole, &consoleMode);
SetConsoleMode(outputConsole, consoleMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
GetConsoleMode(inputConsole, &consoleMode);
SetConsoleMode(inputConsole, consoleMode & ~ENABLE_QUICK_EDIT_MODE | ENABLE_MOUSE_INPUT);
lwm::Player player(0);
lwm::Media media(path.string(), 0, true);
long duration = media.duration();
player.Open(std::vector<lwm::Media>{media});
CONSOLE_SCREEN_BUFFER_INFO screenInfo;
GetConsoleScreenBufferInfo(outputConsole, &screenInfo);
int screenWidth = screenInfo.dwSize.X, barPosition;
const std::string lengthString = formatTime(duration/1000);
std::string initOutput = "\x1B[?25l\n\x1B[";
initOutput += std::to_string(screenWidth-lengthString.size()+1);
initOutput += 'G';
initOutput += lengthString;
std::cout << initOutput;
GetConsoleScreenBufferInfo(outputConsole, &screenInfo);
barPosition = screenInfo.dwCursorPosition.Y-1;
const bool longerThanOneMinute = duration > 59999;
int oldSeconds = -1, oldProgress = 0;
bool playing = false, complete = false, pointingToBar = false, needsRedraw = false, volumeDisplayed = false;
std::chrono::time_point<std::chrono::steady_clock> lastVolumeChange;
player.events()->IsPlaying([&](const bool currentlyPlaying){
playing = currentlyPlaying;
});
player.events()->IsCompleted([&](const bool completed){
if (completed) complete = true;
});
player.SetVolume(std::pow(volume/100., 4));
player.Play();
std::string output;
while (true) {
output.clear();
DWORD eventCount;
GetNumberOfConsoleInputEvents(inputConsole, &eventCount);
while (eventCount != 0) {
INPUT_RECORD input;
DWORD eventsRead;
ReadConsoleInput(inputConsole, &input, 1, &eventsRead);
switch (input.EventType) {
case KEY_EVENT: {
const KEY_EVENT_RECORD &keyEvent = input.Event.KeyEvent;
if (keyEvent.bKeyDown) switch (keyEvent.wVirtualKeyCode) {
case VK_RETURN:
if (playing) player.Pause();
else player.Play();
break;
case VK_UP:
changeVolume(
player, volume = std::min(100, volume+1), output,
screenWidth-lengthString.size()-14,
volumeDisplayed, lastVolumeChange
);
break;
case VK_DOWN:
changeVolume(
player, volume = std::max(0, volume-1), output,
screenWidth-lengthString.size()-14,
volumeDisplayed, lastVolumeChange
);
break;
case VK_ESCAPE:
complete = true;
break;
}
break;
}
case MOUSE_EVENT: {
const MOUSE_EVENT_RECORD &mouseEvent = input.Event.MouseEvent;
const bool onBar = mouseEvent.dwMousePosition.Y == barPosition;
if (mouseEvent.dwButtonState & FROM_LEFT_1ST_BUTTON_PRESSED && onBar) {
const long
currentPosition = player.GetPosition(),
seekPosition
= static_cast<double>(duration) / screenWidth
* mouseEvent.dwMousePosition.X;
player.Seek(seekPosition);
if (seekPosition < currentPosition) {
output += "\x1B[1A\x1B[1G\x1B[0K\x1B[1B";
oldSeconds = -1;
oldProgress = 0;
}
}
if (onBar) {
if (!pointingToBar) pointingToBar = true;
const int seconds = static_cast<long>(
static_cast<double>(duration) / screenWidth
* mouseEvent.dwMousePosition.X
) / 1000;
const std::string seekTime = formatTime(seconds);
output += "\x1B[";
output += std::to_string(lengthString.size() + 4);
output += "G\x1B[38;2;100;100;100m";
output.append(
lengthString.size()-seekTime.size()
+ (longerThanOneMinute && seconds<60 ? 1 : 0),
' '
);
output += seekTime;
output += "\x1B[0m";
if (seconds > 59) output += ' ';
} else if (pointingToBar) {
pointingToBar = false;
output += "\x1B[";
output += std::to_string(lengthString.size() + 4);
output += 'G';
output.append(lengthString.size()+1, ' ');
}
break;
}
case WINDOW_BUFFER_SIZE_EVENT: {
GetConsoleScreenBufferInfo(outputConsole, &screenInfo);
screenWidth = screenInfo.dwSize.X;
barPosition = screenInfo.dwCursorPosition.Y-1;
needsRedraw = true;
volumeDisplayed = false;
break;
}
}
GetNumberOfConsoleInputEvents(inputConsole, &eventCount);
}
if (needsRedraw) {
output += "\x1B[1G\x1B[0K\x1B[1A\x1B[0K\x1B[1B\x1B[";
output += std::to_string(screenWidth-lengthString.size()+1);
output += 'G';
output += lengthString;
oldSeconds = -1;
oldProgress = 0;
needsRedraw = false;
}
if (
volumeDisplayed
&& std::chrono::duration<double>(std::chrono::steady_clock::now()-lastVolumeChange) >= 2s
) {
output += "\x1B[";
output += std::to_string(screenWidth-lengthString.size()-14);
output += "G ";
volumeDisplayed = false;
}
const long position = player.GetPosition();
const int progress = static_cast<double>(std::clamp(position, 0l, duration))/duration*8*screenWidth;
if (progress != oldProgress) {
output += "\x1B[1A\x1B[";
output += std::to_string(oldProgress/8 + 1);
output += 'G';
for (int i = progress/8 - oldProgress/8; i != 0; i--)
output += "\u2588";
if (progress%8 != 0) {
std::string tip = "\u2590";
tip[2] -= progress%8;
output += tip;
}
output += "\x1B[1B";
oldProgress = progress;
}
const int seconds = position/1000;
if (seconds != oldSeconds) {
output += "\x1B[1G";
const std::string timeString = formatTime(seconds);
output.append(
lengthString.size()-timeString.size()
+ (longerThanOneMinute && seconds<60 ? 1 : 0),
' '
);
output += timeString;
if (oldSeconds < 60 && seconds > 59) output += ' ';
oldSeconds = seconds;
}
std::cout << output << std::flush;
if (complete) break;
std::this_thread::sleep_for(15ms);
}
player.Pause();
player.Dispose();
media.Dispose();
output.clear();
output += "\x1B[";
output += std::to_string(lengthString.size() + 4);
output += 'G';
output.append(lengthString.size()+1, ' ');
output += "\x1B[";
output += std::to_string(screenWidth-lengthString.size()-14);
output += "G \x1B[?25h\n";
std::cout << output;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment