mouse wheel now works in seeking bar
[amper.git] / aplayer.d
blobcc35044e75426d635ecf05a4e9ed9a72899dc609
1 /* coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
2 * Understanding is not required. Only obedience.
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 module aplayer;
18 private:
20 import core.atomic;
21 import core.time;
23 import std.concurrency;
24 import std.datetime;
26 import arsd.simpledisplay;
28 import iv.cmdcon;
29 import iv.cmdcongl;
30 import iv.mbandeq;
31 import iv.simplealsa;
32 import iv.strex;
33 import iv.vfs;
35 import iv.drflac;
36 import iv.minimp3;
37 import iv.mp3scan;
38 import iv.tremor;
39 import iv.dopus;
42 // ////////////////////////////////////////////////////////////////////////// //
43 public struct AudioStream {
44 private:
45 VFile fl;
46 string type;
48 public:
49 //long timetotal; // in milliseconds
50 uint rate = 1; // just in case
51 ubyte channels = 1; // just in case
52 ulong samplestotal; // multiplied by channels
53 ulong samplesread; // samples read so far, multiplied by channels
54 string album;
55 string artist;
56 string title;
58 @property ulong framesread () const pure nothrow @safe @nogc { pragma(inline, true); return samplesread/channels; }
59 @property ulong framestotal () const pure nothrow @safe @nogc { pragma(inline, true); return samplestotal/channels; }
61 @property uint timeread () const pure nothrow @safe @nogc { pragma(inline, true); return cast(uint)(samplesread*1000/rate/channels); }
62 @property uint timetotal () const pure nothrow @safe @nogc { pragma(inline, true); return cast(uint)(samplestotal*1000/rate/channels); }
64 public:
65 bool valid () {
66 if (type.length == 0) return false;
67 switch (type[0]) {
68 case 'f': return (ff !is null);
69 case 'v': return (vi !is null);
70 case 'm': return mp3.valid;
71 case 'o': return (of !is null);
72 default:
74 return false;
77 @property string typestr () const pure nothrow @safe @nogc { return type; }
79 void close () {
80 if (type.length == 0) return;
81 switch (type[0]) {
82 case 'f': if (ff !is null) { drflac_close(ff); ff = null; } break;
83 case 'v': if (vi !is null) { vi = null; ov_clear(&vf); } break;
84 case 'm': if (mp3.valid) mp3.close(); break;
85 default:
87 type = null;
90 int readFrames (void* buf, int count) {
91 if (count < 1) return 0;
92 if (count > int.max/4) count = int.max/4;
93 if (!valid) return 0;
94 switch (type[0]) {
95 case 'f':
96 if (ff is null) return 0;
97 int[512] flcbuf;
98 int res = 0;
99 count *= channels;
100 short* bp = cast(short*)buf;
101 while (count > 0) {
102 int xrd = (count <= flcbuf.length ? count : cast(int)flcbuf.length);
103 auto rd = drflac_read_s32(ff, xrd, flcbuf.ptr); // samples
104 if (rd <= 0) break;
105 samplesread += rd; // number of samples read
106 foreach (int v; flcbuf[0..cast(int)rd]) *bp++ = cast(short)(v>>16);
107 res += rd;
108 count -= rd;
110 return cast(int)(res/channels); // number of frames read
111 case 'v':
112 if (vi is null) return 0;
113 int currstream = 0;
114 auto ret = ov_read(&vf, cast(ubyte*)buf, count*2*channels, &currstream);
115 if (ret <= 0) return 0; // error or eof
116 samplesread += ret/2; // number of samples read
117 return ret/2/channels; // number of frames read
118 case 'o':
119 auto dptr = cast(short*)buf;
120 if (of is null) return 0;
121 int total = 0;
122 while (count > 0) {
123 while (count > 0 && smpbufpos < smpbufused) {
124 *dptr++ = smpbuf.ptr[smpbufpos++];
125 if (channels == 2) *dptr++ = smpbuf.ptr[smpbufpos++];
126 --count;
127 ++total;
128 samplesread += channels;
130 if (count == 0) break;
131 auto rd = of.readFrame();
132 if (rd.length == 0) break;
133 if (rd.length > smpbuf.length) smpbuf.length = rd.length;
134 smpbuf[0..rd.length] = rd[];
135 smpbufpos = 0;
136 smpbufused = cast(uint)rd.length;
138 return total;
139 case 'm':
140 // yes, i know that frames are not independend, and i should actually
141 // seek to a frame with a correct sync word. meh.
142 if (!mp3.valid) return 0;
143 auto mfm = mp3.frameSamples;
144 if (mp3smpused+channels > mfm.length) {
145 mp3smpused = 0;
146 if (!mp3.decodeNextFrame(&reader)) return 0;
147 mfm = mp3.frameSamples;
148 if (mp3.sampleRate != rate || mp3.channels != channels) return 0;
150 int res = 0;
151 ushort* b = cast(ushort*)buf;
152 auto oldmpu = mp3smpused;
153 while (count > 0 && mp3smpused+channels <= mfm.length) {
154 *b++ = mfm[mp3smpused++];
155 if (channels == 2) *b++ = mfm[mp3smpused++];
156 --count;
157 ++res;
159 samplesread += mp3smpused-oldmpu; // number of samples read
160 return res;
161 default: break;
163 return 0;
166 // return new frame index
167 ulong seekToTime (uint msecs) {
168 if (!valid) return 0;
169 ulong snum = cast(ulong)msecs*rate/1000*channels; // sample number
170 switch (type[0]) {
171 case 'f':
172 if (ff is null) return 0;
173 if (ff.totalSampleCount < 1) return 0;
174 if (snum >= ff.totalSampleCount) {
175 drflac_seek_to_sample(ff, 0);
176 return 0;
178 if (!drflac_seek_to_sample(ff, snum)) {
179 drflac_seek_to_sample(ff, 0);
180 return 0;
182 samplesread = snum;
183 return snum/channels;
184 case 'v':
185 if (vi is null) return 0;
186 if (ov_pcm_seek(&vf, snum/channels) == 0) {
187 samplesread = ov_pcm_tell(&vf)*channels;
188 return samplesread/channels;
190 ov_pcm_seek(&vf, 0);
191 return 0;
192 case 'o':
193 if (of is null) return 0;
194 of.seek(msecs);
195 samplesread = of.smpcurtime*channels;
196 return samplesread/channels;
197 case 'm':
198 if (!mp3.valid) return 0;
199 mp3smpused = 0;
200 if (mp3info.index.length == 0 || snum == 0) {
201 // alas, we cannot seek here
202 samplesread = 0;
203 fl.seek(0);
204 mp3.restart(&reader);
205 return 0;
207 // find frame containing our sample
208 // stupid binary search; ignore overflow bug
209 ulong start = 0;
210 ulong end = mp3info.index.length-1;
211 while (start <= end) {
212 ulong mid = (start+end)/2;
213 auto smps = mp3info.index[cast(size_t)mid].samples;
214 auto smpe = (mp3info.index.length-mid > 0 ? mp3info.index[cast(size_t)(mid+1)].samples : samplestotal);
215 if (snum >= smps && snum < smpe) {
216 // i found her!
217 samplesread = snum;
218 fl.seek(mp3info.index[cast(size_t)mid].fpos);
219 mp3smpused = cast(uint)(snum-smps);
220 mp3.sync(&reader);
221 return snum;
223 if (snum < smps) end = mid-1; else start = mid+1;
225 // alas, we cannot seek
226 samplesread = 0;
227 fl.seek(0);
228 mp3.restart(&reader);
229 return 0;
230 default: break;
232 return 0;
235 private:
236 drflac* ff;
237 MP3Decoder mp3;
238 Mp3Info mp3info; // scanned info, frame index
239 uint mp3smpused;
240 OggVorbis_File vf;
241 vorbis_info* vi;
242 OpusFile of;
243 short[] smpbuf;
244 uint smpbufpos, smpbufused;
246 int reader (void[] buf) {
247 try {
248 auto rd = fl.rawRead(buf);
249 return cast(int)rd.length;
250 } catch (Exception e) {}
251 return -1;
254 public:
255 static AudioStream open (VFile fl) {
256 //import std.string : fromStringz;
257 AudioStream sio;
258 fl.seek(0);
259 // determine format
260 try {
261 auto fpos = fl.tell;
262 // flac
263 try {
264 import core.stdc.stdio;
265 import core.stdc.stdlib : malloc, free;
266 uint commentCount;
267 char* fcmts;
268 scope(exit) if (fcmts !is null) free(fcmts);
269 sio.ff = drflac_open_file(fl, (void* pUserData, drflac_metadata* pMetadata) {
270 if (pMetadata.type == DRFLAC_METADATA_BLOCK_TYPE_VORBIS_COMMENT) {
271 if (fcmts !is null) free(fcmts);
272 auto csz = drflac_vorbis_comment_size(pMetadata.data.vorbis_comment.commentCount, pMetadata.data.vorbis_comment.comments);
273 if (csz > 0 && csz < 0x100_0000) {
274 fcmts = cast(char*)malloc(cast(uint)csz);
275 } else {
276 fcmts = null;
278 if (fcmts is null) {
279 commentCount = 0;
280 } else {
281 import core.stdc.string : memcpy;
282 commentCount = pMetadata.data.vorbis_comment.commentCount;
283 memcpy(fcmts, pMetadata.data.vorbis_comment.comments, cast(uint)csz);
287 if (sio.ff !is null) {
288 scope(failure) drflac_close(sio.ff);
289 if (sio.ff.sampleRate < 1024 || sio.ff.sampleRate > 96000) throw new Exception("fucked flac");
290 if (sio.ff.channels < 1 || sio.ff.channels > 2) throw new Exception("fucked flac");
291 sio.rate = cast(uint)sio.ff.sampleRate;
292 sio.channels = cast(ubyte)sio.ff.channels;
293 sio.type = "flac";
294 sio.fl = fl;
295 sio.samplestotal = sio.ff.totalSampleCount;
296 //conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (flac)");
298 drflac_vorbis_comment_iterator i;
299 drflac_init_vorbis_comment_iterator(&i, commentCount, fcmts);
300 uint commentLength;
301 const(char)* pComment;
302 while ((pComment = drflac_next_vorbis_comment(&i, &commentLength)) !is null) {
303 if (commentLength > 1024*1024*2) break; // just in case
304 //conwriteln(" ", pComment[0..commentLength]);
305 auto cmts = pComment[0..commentLength];
306 //conwriteln(" <", cmts, ">");
307 if (cmts.startsWithCI("ALBUM=")) sio.album = cmts[6..$].xstrip.idup;
308 else if (cmts.startsWithCI("ARTIST=")) sio.artist = cmts[7..$].xstrip.idup;
309 else if (cmts.startsWithCI("TITLE=")) sio.title = cmts[6..$].xstrip.idup;
312 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
313 return sio;
315 } catch (Exception) {}
316 fl.seek(fpos);
317 // vorbis
318 try {
319 if (ov_fopen(fl, &sio.vf) == 0) {
320 scope(failure) ov_clear(&sio.vf);
321 sio.type = "vorbis";
322 sio.fl = fl;
323 sio.vi = ov_info(&sio.vf, -1);
324 if (sio.vi.rate < 1024 || sio.vi.rate > 96000) throw new Exception("fucked vorbis");
325 if (sio.vi.channels < 1 || sio.vi.channels > 2) throw new Exception("fucked vorbis");
326 sio.rate = sio.vi.rate;
327 sio.channels = cast(ubyte)sio.vi.channels;
328 //conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (vorbis)");
329 //conwriteln("streams: ", ov_streams(&sio.vf));
330 //conwriteln("bitrate: ", ov_bitrate(&sio.vf));
331 sio.samplestotal = ov_pcm_total(&sio.vf)*sio.channels;
332 if (auto vc = ov_comment(&sio.vf, -1)) {
333 //conwriteln("Encoded by: ", vc.vendor.fromStringz.recodeToKOI8);
334 foreach (immutable idx; 0..vc.comments) {
335 //conwriteln(" ", vc.user_comments[idx][0..vc.comment_lengths[idx]].recodeToKOI8);
336 auto cmts = vc.user_comments[idx][0..vc.comment_lengths[idx]];
337 if (cmts.startsWithCI("ALBUM=")) sio.album = cmts[6..$].xstrip.idup;
338 else if (cmts.startsWithCI("ARTIST=")) sio.artist = cmts[7..$].xstrip.idup;
339 else if (cmts.startsWithCI("TITLE=")) sio.title = cmts[6..$].xstrip.idup;
342 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
343 return sio;
345 } catch (Exception) {}
346 fl.seek(fpos);
347 // opus
348 try {
349 OpusFile of = opusOpen(fl);
350 scope(failure) opusClose(of);
351 if (of.rate < 1024 || of.rate > 96000) throw new Exception("fucked opus");
352 if (of.channels < 1 || of.channels > 2) throw new Exception("fucked opus");
353 sio.of = of;
354 sio.type = "opus";
355 sio.fl = fl;
356 sio.rate = of.rate;
357 sio.channels = of.channels;
358 //conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (opus)");
359 sio.samplestotal = of.smpduration*sio.channels;
360 //if (of.vendor.length) conwriteln("Encoded by: ", of.vendor.recodeToKOI8);
361 foreach (immutable cidx; 0..of.commentCount) {
362 //conwriteln(" ", of.comment(cidx).recodeToKOI8);
363 auto cmts = of.comment(cidx);
364 if (cmts.startsWithCI("ALBUM=")) sio.album = cmts[6..$].xstrip.idup;
365 else if (cmts.startsWithCI("ARTIST=")) sio.artist = cmts[7..$].xstrip.idup;
366 else if (cmts.startsWithCI("TITLE=")) sio.title = cmts[6..$].xstrip.idup;
368 //TODO: comments
369 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
370 return sio;
371 } catch (Exception) {}
372 fl.seek(fpos);
373 // mp3
374 try {
375 sio.fl = fl;
376 sio.mp3 = new MP3Decoder(&sio.reader);
377 sio.type = "mp3";
378 if (sio.mp3.valid) {
379 // scan file to determine number of frames
380 auto xfp = fl.tell;
381 fl.seek(fpos);
382 sio.mp3info = mp3Scan!true((void[] buf) => cast(int)fl.rawRead(buf).length); // build index too
383 fl.seek(xfp);
384 if (sio.mp3info.valid) {
385 if (sio.mp3.sampleRate < 1024 || sio.mp3.sampleRate > 96000) throw new Exception("fucked mp3");
386 if (sio.mp3.channels < 1 || sio.mp3.channels > 2) throw new Exception("fucked mp3");
387 sio.rate = sio.mp3.sampleRate;
388 sio.channels = sio.mp3.channels;
389 sio.samplestotal = sio.mp3info.samples;
390 //conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (mp3)");
391 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
392 return sio;
395 sio.mp3 = null;
396 } catch (Exception) {}
397 sio.mp3 = null;
398 fl.seek(fpos);
399 } catch (Exception) {}
400 return AudioStream.init;
405 // ////////////////////////////////////////////////////////////////////////// //
406 __gshared Tid playertid;
409 // ////////////////////////////////////////////////////////////////////////// //
410 public class EventFileLoaded { string filename; string album; string artist; string title; int duration; bool success; }
411 public class EventFileComplete {}
413 // ////////////////////////////////////////////////////////////////////////// //
414 __gshared bool started = false;
415 struct TMsgQuitReq {}
417 public void aplayStart () { if (!started) { started = true; playertid = spawn(&playerThread, thisTid); } }
418 public void aplayShutdown () { if (started) { started = false; playertid.send(TMsgQuitReq()); } }
421 // ////////////////////////////////////////////////////////////////////////// //
422 // reply with EventFileLoaded
423 struct TMsgPlayFileReq { string filename; bool forcestart; }
424 struct TMsgStopFileReq {}
425 struct TMsgTogglePauseReq {}
426 struct TMsgPauseReq { bool pause; }
427 struct TMsgSeekReq { int timems; }
429 //TODO: queue this
430 public void aplayPlayFile (string fname, bool forcestart) { if (started) playertid.send(TMsgPlayFileReq(fname.length ? fname : "", forcestart)); }
431 public void aplayStopFile () { if (started) playertid.send(TMsgStopFileReq()); }
432 public void aplayTogglePause () { if (started) playertid.send(TMsgTogglePauseReq()); }
433 public void aplayPause (bool pause) { if (started) playertid.send(TMsgPauseReq(pause)); }
434 public void aplaySeekMS (int timems) { if (started) playertid.send(TMsgSeekReq(timems)); }
437 // ////////////////////////////////////////////////////////////////////////// //
438 // reply with EventFileLoaded
439 struct TMsgPlayGainReq {
440 int prc; // percents
443 public void aplayPlayGain (int prc) { if (started) playertid.send(TMsgPlayGainReq(prc)); else alsaGain = prc; }
446 // ////////////////////////////////////////////////////////////////////////// //
447 public bool aplayIsPlaying () { return atomicLoad(aplPlaying); }
448 public bool aplayIsPaused () { return atomicLoad(aplPaused); }
449 public int aplayCurTime () { return atomicLoad(aplCurTime)/1000; }
450 public int aplayTotalTime () { return atomicLoad(aplTotalTime)/1000; }
451 public int aplayCurTimeMS () { return atomicLoad(aplCurTime); }
452 public int aplayTotalTimeMS () { return atomicLoad(aplTotalTime); }
454 public int aplaySampleRate () { return atomicLoad(aplSampleRate); }
455 public bool aplayIsStereo () { return (atomicLoad(aplChannels) == 2); }
458 // ////////////////////////////////////////////////////////////////////////// //
459 enum BUF_SIZE = 4096;
460 __gshared short[BUF_SIZE] buffer;
462 __gshared bool paused = false;
463 shared bool aplPlaying = false;
464 shared bool aplPaused = false;
465 shared int aplCurTime = 0;
466 shared int aplTotalTime = 0;
467 shared int aplSampleRate = 48000;
468 shared int aplChannels = 2;
471 // ////////////////////////////////////////////////////////////////////////// //
472 void playerThread (Tid ownerTid) {
473 AudioStream sio;
474 scope(exit) sio.close();
475 bool doQuit = false;
476 string newfilereq = null;
477 bool forcestart = false;
478 int newtime = -666;
480 uint realRate = alsaGetBestSampleRate(48000);
481 conwriteln("real sampling rate: ", realRate);
482 if (realRate != 44100 && realRate != 48000) {
483 realRate = 48000;
484 conwriteln("WARNING! something is wrong with ALSA! trying to fix it...");
487 while (!doQuit) {
488 receiveTimeout((sio.valid && !paused ? Duration.min : 42.seconds),
489 (TMsgQuitReq req) {
490 doQuit = true;
492 (TMsgPlayFileReq req) {
493 newfilereq = (req.filename.length ? req.filename : "");
494 forcestart = req.forcestart;
496 (TMsgPlayGainReq req) {
497 alsaGain = req.prc;
499 (TMsgStopFileReq req) {
500 paused = false;
501 sio.close();
502 if (alsaIsOpen) alsaShutdown();
504 (TMsgTogglePauseReq req) {
505 if (sio.valid) paused = !paused; else paused = false;
507 (TMsgPauseReq req) {
508 if (sio.valid) paused = req.pause;
510 (TMsgSeekReq req) {
511 newtime = req.timems;
512 if (newtime < 0) newtime = 0;
516 if (doQuit) break;
518 if (newfilereq !is null) {
519 newtime = -666;
520 auto reply = new EventFileLoaded();
521 reply.filename = newfilereq;
522 reply.success = false;
523 newfilereq = null;
524 bool wasplaying = sio.valid;
525 sio.close();
526 try {
527 sio = AudioStream.open(VFile(reply.filename));
528 } catch (Exception e) {
529 sio = AudioStream.init;
531 if (sio.valid) {
532 reply.duration = cast(int)(sio.timetotal/1000);
533 reply.album = sio.album;
534 reply.artist = sio.artist;
535 reply.title = sio.title;
536 reply.success = true;
537 if (forcestart) paused = false; else paused = !wasplaying;
538 } else {
539 if (alsaIsOpen) alsaShutdown();
541 if (glconCtlWindow !is null) glconCtlWindow.postEvent(reply);
544 if (!sio.valid) {
545 newtime = -666;
546 paused = false;
547 atomicStore(aplPaused, false);
548 atomicStore(aplPlaying, false);
549 atomicStore(aplCurTime, 0);
550 atomicStore(aplTotalTime, 0);
551 //atomicStore(aplSampleRate, 48000);
552 //atomicStore(aplChannels, 2);
553 continue;
556 if (newtime != -666) {
557 int tm = newtime;
558 newtime = -666;
559 if (tm >= sio.timetotal) tm = (sio.timetotal ? sio.timetotal-1 : 0);
560 sio.seekToTime(cast(uint)tm);
563 if (!paused) {
564 if (!alsaIsOpen || alsaRate != sio.rate || alsaChannels != sio.channels) {
565 if (alsaIsOpen) alsaShutdown();
566 if (!alsaInit(sio.rate, sio.channels)) assert(0, "cannot init ALSA playback");
568 auto frmread = sio.readFrames(buffer.ptr, BUF_SIZE/sio.channels);
569 if (frmread <= 0) {
570 atomicStore(aplPaused, false);
571 atomicStore(aplPlaying, false);
572 atomicStore(aplCurTime, 0);
573 atomicStore(aplTotalTime, 0);
574 if (glconCtlWindow !is null) glconCtlWindow.postEvent(new EventFileComplete());
575 sio.close();
576 } else {
577 atomicStore(aplPaused, false);
578 atomicStore(aplPlaying, true);
579 atomicStore(aplCurTime, cast(int)sio.timeread);
580 atomicStore(aplTotalTime, cast(int)sio.timetotal);
581 atomicStore(aplSampleRate, cast(int)sio.rate);
582 atomicStore(aplChannels, cast(int)sio.channels);
583 alsaWriteShort(buffer[0..frmread*sio.channels]);
585 } else {
586 atomicStore(aplPlaying, true);
587 atomicStore(aplPaused, true);
588 if (alsaIsOpen) alsaShutdown();
594 // ////////////////////////////////////////////////////////////////////////// //
595 shared static this () {
596 alsaEqBands[] = 0;
598 conRegVar!alsaRQuality(0, 10, "rsquality", "resampling quality; 0=worst, 10=best, default is 8");
599 conRegVar!alsaDevice("device", "audio output device");
600 //conRegVar!alsaGain(-100, 1000, "gain", "playback gain (0: normal; -100: silent; 100: 2x)");
601 conRegVar!alsaLatencyms(5, 5000, "latency", "playback latency, in milliseconds");
602 conRegVar!alsaEnableResampling("use_resampling", "allow audio resampling?");
603 conRegVar!alsaEnableEqualizer("use_equalizer", "allow audio equalizer?");
605 //conRegVar!paused("paused", "is playback paused?");
607 // lol, `std.trait : ParameterDefaults()` blocks using argument with name `value`
608 conRegFunc!((int idx, byte value) {
609 if (value < -70) value = -70;
610 if (value > 30) value = 30;
611 if (idx >= 0 && idx < alsaEqBands.length) {
612 if (alsaEqBands[idx] != value) {
613 alsaEqBands[idx] = value;
615 } else {
616 conwriteln("invalid equalizer band index: ", idx);
618 })("eq_band", "set equalizer band #n to v (band 0 is preamp)");
620 conRegFunc!(() {
621 alsaEqBands[] = 0;
622 })("eq_reset", "reset equalizer");
624 //conRegFunc!(() { conaction = Action.Next; })("next", "next song");
625 //conRegFunc!(() { conaction = Action.Prev; })("prev", "previous song");