-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathCli.cs
More file actions
236 lines (212 loc) · 13.1 KB
/
Copy pathCli.cs
File metadata and controls
236 lines (212 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
using System.Reflection;
using PrettyConsole;
using SharpModConsolePlayer.Renderer;
using static PrettyConsole.Color;
using static SharpModConsolePlayer.Renderer.ConsoleRenderer;
namespace SharpModConsolePlayer {
internal class Cli {
internal string ModFile { get; set; } = string.Empty;
internal List<string> ModFiles { get; init; } = [];
internal int SampleRate { get; init; } = 44100;
internal int BitDepth { get; init; } = 16;
internal int Channels { get; init; } = 2;
internal bool Loop { get; init; } = false;
internal bool Randomize { get; init; } = false;
internal string ExportPath { get; init; } = string.Empty;
internal int SampleHeight { get; init; } = 0;
internal bool ShowMetadata { get; init; } = true;
private static readonly int[] ValidSampleRates = [8000, 11025, 16000, 22050, 32000, 44100, 48000, 88200, 96000];
private static readonly int[] ValidBitDepths = [8, 16];
private static readonly int[] ValidSampleHeights = [0, 1, 2, 3];
private const int KeyColumnWidth = 25;
private const int DescColumnWidth = 45;
// KeyVisibleWidth must be kept in sync by hand with the visible character count of each row's WriteKey lambda.
private static readonly (ViewMode Mode, int KeyVisibleWidth, string Description, Action WriteKey)[] keyBindings = [
(ViewMode.Any, 3, "Toggle between patterns and samples view.", () => Console.WriteInterpolated($"{Green}Tab{Default}")),
(ViewMode.Any, 2, "Show this help", () => Console.WriteInterpolated($"{Green}F1{Default}")),
(ViewMode.Any, 5, "Toggle pause", () => Console.WriteInterpolated($"{Green}Space{Default}")),
(ViewMode.Any, 12, "Scroll channels horizontally", () => Console.WriteInterpolated($"{Green}Left{Default} / {Green}Right{Default}")),
(ViewMode.Any, 9, "Scroll samples vertically", () => Console.WriteInterpolated($"{Green}Up{Default} / {Green}Down{Default}")),
(ViewMode.Any, 17, "Seek track backward/forward", () => Console.WriteInterpolated($"{Green}PageUp{Default} / {Green}PageDown{Default}")),
(ViewMode.Any, 10, "Jump to previous/next file in the playlist", () => Console.WriteInterpolated($"{Green}Home{Default} / {Green}End{Default}")),
(ViewMode.Any, 5, "Toggle mute on channels 1-9", () => Console.WriteInterpolated($"{Green}1{Default} - {Green}9{Default}")),
(ViewMode.Any, 13, "Toggle mute on channels 10-18", () => Console.WriteInterpolated($"{Green}Shift{Default} + {Green}1{Default} - {Green}9{Default}")),
(ViewMode.Any, 12, "Toggle mute on channels 19-27", () => Console.WriteInterpolated($"{Green}Ctrl{Default} + {Green}1{Default} - {Green}9{Default}")),
(ViewMode.Any, 7, "Stop playback and exit", () => Console.WriteInterpolated($"{Green}Esc{Default} | {Green}Q{Default}")),
(ViewMode.Samples, 0, "───────────────────────────────────────────", () => { }),
(ViewMode.Samples, 7, "Cycle waveform display modes", () => {
string state = Samples.RowsPerSample switch {
0 => " ○ ",
1 => " ─ ",
2 => " ═ ",
3 => " ≡ ",
_ => throw new InvalidOperationException()
};
Console.WriteInterpolated($"{Green}H {Yellow}[{state}]{Default}");
}),
(ViewMode.Samples, 7, "Toggle sample metadata", () => {
string state = Samples.ShowMetadata ? " ● " : " ○ ";
Console.WriteInterpolated($"{Green}M {Yellow}[{state}]{Default}");
}),
];
internal static Cli? Parse(string[] args) {
if(args.Length == 0) {
PrintUsage();
return null;
}
List<string> modFiles = [];
int sampleRate = 44100;
int bitDepth = 16;
bool loop = false;
string exportPath = string.Empty;
bool randomize = false;
int sampleHeight = 0;
bool showMetadata = true;
for(int i = 0; i < args.Length; i++) {
string a = args[i];
switch(a) {
case "-h":
case "--help":
PrintUsage();
return null;
case "-r":
case "--sample-rate":
if(!TryReadIntOption(args, ref i, a, ValidSampleRates, out sampleRate)) return null;
break;
case "-b":
case "--bit-depth":
if(!TryReadIntOption(args, ref i, a, ValidBitDepths, out bitDepth)) return null;
break;
case "-l":
case "--loop":
loop = true;
break;
case "-x":
case "--export":
if(i + 1 >= args.Length) { PrintError($"Option {a} requires a path."); return null; }
exportPath = args[++i];
break;
case "-z":
case "--randomize":
randomize = true;
break;
case "-H":
case "--sample-height":
if(!TryReadIntOption(args, ref i, a, ValidSampleHeights, out sampleHeight)) return null;
break;
case "-m":
case "--no-metadata":
showMetadata = false;
break;
default:
if(a.StartsWith('-')) {
PrintError($"Unknown option: {a}");
return null;
}
modFiles.Add(a);
break;
}
}
if(modFiles.Count == 0) {
PrintError("Missing required <modfile> argument.");
return null;
}
if(exportPath.Length > 0) loop = false;
return new Cli {
ModFile = modFiles[0],
ModFiles = modFiles,
SampleRate = sampleRate,
BitDepth = bitDepth,
Loop = loop,
ExportPath = exportPath,
Randomize = randomize,
SampleHeight = sampleHeight,
ShowMetadata = showMetadata
};
}
private static bool TryReadIntOption(string[] args, ref int i, string name, int[] allowed, out int value) {
value = 0;
if(i + 1 >= args.Length) {
PrintError($"Option {name} requires a value.");
return false;
}
string raw = args[++i];
if(!int.TryParse(raw, out value)) {
PrintError($"Option {name} expects an integer, got '{raw}'.");
return false;
}
if(Array.IndexOf(allowed, value) < 0) {
PrintError($"Option {name} value '{raw}' is not allowed. Valid: {string.Join(", ", allowed)}.");
return false;
}
return true;
}
private static void PrintError(string message) {
Console.WriteLineInterpolated($"{Red}error:{Default} {message}");
Console.NewLine();
PrintUsage();
}
private static void PrintUsage() {
string name = Assembly.GetExecutingAssembly().GetName().Name ?? "SharpModConsolePlayer";
string version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "0.0.0";
Console.WriteLineInterpolated($"{Magenta}{name}{Default} {DarkGray} {version}{Default}");
Console.WriteLineInterpolated($"{DarkGray}A console player for MOD/S3M/XM tracker files.{Default}");
Console.NewLine();
Console.WriteLineInterpolated($"{Yellow}USAGE{Default}");
Console.WriteLineInterpolated($" {White}{name}{Default} {Cyan}<modfile>{Default} [{Green}options{Default}]");
Console.NewLine();
Console.WriteLineInterpolated($"{Yellow}ARGUMENTS{Default}");
Console.WriteLineInterpolated($" {Cyan}<modfile>{Default} Can be a single file, a directory (recursively searches for supported files)");
Console.WriteLineInterpolated($" or a glob pattern (e.g. {DarkGray}music/*.mod{Default})");
Console.NewLine();
Console.WriteLineInterpolated($"{Yellow}OPTIONS{Default}");
Console.WriteLineInterpolated($" {Green}-r{Default}, {Green}--sample-rate{Default} {DarkGray}<hz>{Default} Output sample rate in Hz. Default: {White}44100{Default}");
Console.WriteLineInterpolated($" Valid: {DarkGray}{string.Join(", ", ValidSampleRates)}{Default}");
Console.WriteLineInterpolated($" {Green}-b{Default}, {Green}--bit-depth{Default} {DarkGray}<n>{Default} Output bit depth. Default: {White}16{Default}");
Console.WriteLineInterpolated($" Valid: {DarkGray}8, 16{Default}");
Console.WriteLineInterpolated($" {Green}-l{Default}, {Green}--loop{Default} Loop the track when it ends");
Console.WriteLineInterpolated($" {Green}-x{Default}, {Green}--export{Default} {DarkGray}<path>{Default} Render the track to a WAV file at {DarkGray}<path>{Default} (no live playback)");
Console.WriteLineInterpolated($" {Green}-z{Default}, {Green}--randomize{Default} Randomize the order of files in the playlist");
Console.WriteLineInterpolated($" {Green}-H{Default}, {Green}--sample-height{Default} {DarkGray}<n>{Default} Console rows per sample waveform. Default: {White}0{Default} ({DarkGray}0 hides the waveform{Default})");
Console.WriteLineInterpolated($" Valid: {DarkGray}0, 1, 2, 3{Default}");
Console.WriteLineInterpolated($" {Green}-m{Default}, {Green}--no-metadata{Default} Hide sample metadata columns (Length, Vol, Fmt, LoopStart, LoopEnd)");
Console.WriteLineInterpolated($" {Green}-h{Default}, {Green}--help{Default} Show this help and exit");
Console.NewLine();
Console.WriteLineInterpolated($"{Yellow}EXAMPLES{Default}");
Console.WriteLineInterpolated($" {DarkGray}# Play a single file{Default}");
Console.WriteLineInterpolated($" {White}{name}{Default} {Cyan}\"mods{Path.DirectorySeparatorChar}Future Crew - Second Reality.S3M\"{Default}");
Console.WriteLineInterpolated($" {DarkGray}# Play every supported file in a directory (recursively){Default}");
Console.WriteLineInterpolated($" {White}{name}{Default} {Cyan}mods{Path.DirectorySeparatorChar}{Default}");
Console.WriteLineInterpolated($" {DarkGray}# Play every .XM file matched by a glob pattern{Default}");
Console.WriteLineInterpolated($" {White}{name}{Default} {Cyan}mods{Path.DirectorySeparatorChar}*.XM{Default}");
Console.NewLine();
Console.WriteLineInterpolated($"{Yellow}KEYS{Default}");
PrintKeyBindings(prefix: " ", mode: ViewMode.Patterns);
}
internal static void PrintKeyBindings(string prefix = "", string suffix = "", ViewMode mode = ViewMode.Any) {
int rowsNeeded = 0;
for(int i = 0; i < keyBindings.Length; i++) {
var viewMode = keyBindings[i].Mode;
if(viewMode == ViewMode.Any || viewMode == mode) rowsNeeded++;
}
int col = Console.CursorLeft;
int row = Console.CursorTop;
// On Windows, SetCursorPosition throws when y >= BufferHeight, which happens when
// output has reached the bottom of the buffer (typical when BufferHeight == WindowHeight).
// Emit blank lines to force the buffer to scroll, then pull `row` up by the same amount.
int overflow = (row + rowsNeeded) - Console.BufferHeight;
if(overflow > 0) {
for(int i = 0; i < overflow; i++) Console.NewLine();
row -= overflow;
}
for(int i = 0; i < keyBindings.Length; i++) {
var (viewMode, vkeyWidth, description, writeKey) = keyBindings[i];
if(viewMode != ViewMode.Any && viewMode != mode) continue;
Console.SetCursorPosition(col, row + i);
Console.WriteInterpolated($"{Cyan}{prefix}{Default}");
writeKey();
Console.WriteLineInterpolated($"{new WhiteSpace(KeyColumnWidth - vkeyWidth)}{description}{new WhiteSpace(DescColumnWidth - description.Length)}{Cyan}{suffix}{Default}");
}
}
}
}