seeking bar now works
[amper.git] / aplayer.d
bloba8d199f0753f9c805792b24fabe303fad63f2a8e
1 /*
2 Copyright (c) 2016, Ketmar // Invisible Vector
4 Redistribution and use in source and binary forms, with or without
5 modification, are permitted provided that the following conditions
6 are met:
8 - Redistributions of source code must retain the above copyright
9 notice, this list of conditions and the following disclaimer.
11 - Redistributions in binary form must reproduce the above copyright
12 notice, this list of conditions and the following disclaimer in the
13 documentation and/or other materials provided with the distribution.
15 - Neither the name of the Xiph.org Foundation nor the names of its
16 contributors may be used to endorse or promote products derived from
17 this software without specific prior written permission.
19 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22 A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION
23 OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 module aplayer is aliced;
32 private:
34 import core.atomic;
35 import core.time;
37 import std.concurrency;
38 import std.datetime;
40 import arsd.simpledisplay;
42 import iv.cmdcon;
43 import iv.cmdcongl;
44 import iv.mbandeq;
45 import iv.simplealsa;
46 import iv.strex;
47 import iv.vfs;
49 import iv.drflac;
50 import iv.minimp3;
51 import iv.mp3scan;
52 import iv.tremor;
53 import iv.dopus;
56 // ////////////////////////////////////////////////////////////////////////// //
57 public struct AudioStream {
58 private:
59 VFile fl;
60 string type;
62 public:
63 //long timetotal; // in milliseconds
64 uint rate = 1; // just in case
65 ubyte channels = 1; // just in case
66 ulong samplestotal; // multiplied by channels
67 ulong samplesread; // samples read so far, multiplied by channels
68 string album;
69 string artist;
70 string title;
72 @property ulong framesread () const pure nothrow @safe @nogc { pragma(inline, true); return samplesread/channels; }
73 @property ulong framestotal () const pure nothrow @safe @nogc { pragma(inline, true); return samplestotal/channels; }
75 @property uint timeread () const pure nothrow @safe @nogc { pragma(inline, true); return cast(uint)(samplesread*1000/rate/channels); }
76 @property uint timetotal () const pure nothrow @safe @nogc { pragma(inline, true); return cast(uint)(samplestotal*1000/rate/channels); }
78 public:
79 bool valid () {
80 if (type.length == 0) return false;
81 switch (type[0]) {
82 case 'f': return (ff !is null);
83 case 'v': return (vi !is null);
84 case 'm': return mp3.valid;
85 case 'o': return (of !is null);
86 default:
88 return false;
91 @property string typestr () const pure nothrow @safe @nogc { return type; }
93 void close () {
94 if (type.length == 0) return;
95 switch (type[0]) {
96 case 'f': if (ff !is null) { drflac_close(ff); ff = null; } break;
97 case 'v': if (vi !is null) { vi = null; ov_clear(&vf); } break;
98 case 'm': if (mp3.valid) mp3.close(); break;
99 default:
101 type = null;
104 int readFrames (void* buf, int count) {
105 if (count < 1) return 0;
106 if (count > int.max/4) count = int.max/4;
107 if (!valid) return 0;
108 switch (type[0]) {
109 case 'f':
110 if (ff is null) return 0;
111 int[512] flcbuf;
112 int res = 0;
113 count *= channels;
114 short* bp = cast(short*)buf;
115 while (count > 0) {
116 int xrd = (count <= flcbuf.length ? count : cast(int)flcbuf.length);
117 auto rd = drflac_read_s32(ff, xrd, flcbuf.ptr); // samples
118 if (rd <= 0) break;
119 samplesread += rd; // number of samples read
120 foreach (int v; flcbuf[0..cast(int)rd]) *bp++ = cast(short)(v>>16);
121 res += rd;
122 count -= rd;
124 return cast(int)(res/channels); // number of frames read
125 case 'v':
126 if (vi is null) return 0;
127 int currstream = 0;
128 auto ret = ov_read(&vf, cast(ubyte*)buf, count*2*channels, &currstream);
129 if (ret <= 0) return 0; // error or eof
130 samplesread += ret/2; // number of samples read
131 return ret/2/channels; // number of frames read
132 case 'o':
133 auto dptr = cast(short*)buf;
134 if (of is null) return 0;
135 int total = 0;
136 while (count > 0) {
137 while (count > 0 && smpbufpos < smpbufused) {
138 *dptr++ = smpbuf.ptr[smpbufpos++];
139 if (channels == 2) *dptr++ = smpbuf.ptr[smpbufpos++];
140 --count;
141 ++total;
142 samplesread += channels;
144 if (count == 0) break;
145 auto rd = of.readFrame();
146 if (rd.length == 0) break;
147 if (rd.length > smpbuf.length) smpbuf.length = rd.length;
148 smpbuf[0..rd.length] = rd[];
149 smpbufpos = 0;
150 smpbufused = cast(uint)rd.length;
152 return total;
153 case 'm':
154 // yes, i know that frames are not independend, and i should actually
155 // seek to a frame with a correct sync word. meh.
156 if (!mp3.valid) return 0;
157 auto mfm = mp3.frameSamples;
158 if (mp3smpused+channels > mfm.length) {
159 mp3smpused = 0;
160 if (!mp3.decodeNextFrame(&reader)) return 0;
161 mfm = mp3.frameSamples;
162 if (mp3.sampleRate != rate || mp3.channels != channels) return 0;
164 int res = 0;
165 ushort* b = cast(ushort*)buf;
166 auto oldmpu = mp3smpused;
167 while (count > 0 && mp3smpused+channels <= mfm.length) {
168 *b++ = mfm[mp3smpused++];
169 if (channels == 2) *b++ = mfm[mp3smpused++];
170 --count;
171 ++res;
173 samplesread += mp3smpused-oldmpu; // number of samples read
174 return res;
175 default: break;
177 return 0;
180 // return new frame index
181 ulong seekToTime (uint msecs) {
182 if (!valid) return 0;
183 ulong snum = cast(ulong)msecs*rate/1000*channels; // sample number
184 switch (type[0]) {
185 case 'f':
186 if (ff is null) return 0;
187 if (ff.totalSampleCount < 1) return 0;
188 if (snum >= ff.totalSampleCount) {
189 drflac_seek_to_sample(ff, 0);
190 return 0;
192 if (!drflac_seek_to_sample(ff, snum)) {
193 drflac_seek_to_sample(ff, 0);
194 return 0;
196 samplesread = snum;
197 return snum/channels;
198 case 'v':
199 if (vi is null) return 0;
200 if (ov_pcm_seek(&vf, snum/channels) == 0) {
201 samplesread = ov_pcm_tell(&vf)*channels;
202 return samplesread/channels;
204 ov_pcm_seek(&vf, 0);
205 return 0;
206 case 'o':
207 if (of is null) return 0;
208 of.seek(msecs);
209 samplesread = of.smpcurtime*channels;
210 return samplesread/channels;
211 case 'm':
212 if (!mp3.valid) return 0;
213 mp3smpused = 0;
214 if (mp3info.index.length == 0 || snum == 0) {
215 // alas, we cannot seek here
216 samplesread = 0;
217 fl.seek(0);
218 mp3.restart(&reader);
219 return 0;
221 // find frame containing our sample
222 // stupid binary search; ignore overflow bug
223 ulong start = 0;
224 ulong end = mp3info.index.length-1;
225 while (start <= end) {
226 ulong mid = (start+end)/2;
227 auto smps = mp3info.index[cast(size_t)mid].samples;
228 auto smpe = (mp3info.index.length-mid > 0 ? mp3info.index[cast(size_t)(mid+1)].samples : samplestotal);
229 if (snum >= smps && snum < smpe) {
230 // i found her!
231 samplesread = snum;
232 fl.seek(mp3info.index[cast(size_t)mid].fpos);
233 mp3smpused = cast(uint)(snum-smps);
234 mp3.sync(&reader);
235 return snum;
237 if (snum < smps) end = mid-1; else start = mid+1;
239 // alas, we cannot seek
240 samplesread = 0;
241 fl.seek(0);
242 mp3.restart(&reader);
243 return 0;
244 default: break;
246 return 0;
249 private:
250 drflac* ff;
251 MP3Decoder mp3;
252 Mp3Info mp3info; // scanned info, frame index
253 uint mp3smpused;
254 OggVorbis_File vf;
255 vorbis_info* vi;
256 OpusFile of;
257 short[] smpbuf;
258 uint smpbufpos, smpbufused;
260 int reader (void[] buf) {
261 try {
262 auto rd = fl.rawRead(buf);
263 return cast(int)rd.length;
264 } catch (Exception e) {}
265 return -1;
268 public:
269 static AudioStream open (VFile fl) {
270 //import std.string : fromStringz;
271 AudioStream sio;
272 fl.seek(0);
273 // determine format
274 try {
275 auto fpos = fl.tell;
276 // flac
277 try {
278 import core.stdc.stdio;
279 import core.stdc.stdlib : malloc, free;
280 uint commentCount;
281 char* fcmts;
282 scope(exit) if (fcmts !is null) free(fcmts);
283 sio.ff = drflac_open_file(fl, (void* pUserData, drflac_metadata* pMetadata) {
284 if (pMetadata.type == DRFLAC_METADATA_BLOCK_TYPE_VORBIS_COMMENT) {
285 if (fcmts !is null) free(fcmts);
286 auto csz = drflac_vorbis_comment_size(pMetadata.data.vorbis_comment.commentCount, pMetadata.data.vorbis_comment.comments);
287 if (csz > 0 && csz < 0x100_0000) {
288 fcmts = cast(char*)malloc(cast(uint)csz);
289 } else {
290 fcmts = null;
292 if (fcmts is null) {
293 commentCount = 0;
294 } else {
295 import core.stdc.string : memcpy;
296 commentCount = pMetadata.data.vorbis_comment.commentCount;
297 memcpy(fcmts, pMetadata.data.vorbis_comment.comments, cast(uint)csz);
301 if (sio.ff !is null) {
302 scope(failure) drflac_close(sio.ff);
303 if (sio.ff.sampleRate < 1024 || sio.ff.sampleRate > 96000) throw new Exception("fucked flac");
304 if (sio.ff.channels < 1 || sio.ff.channels > 2) throw new Exception("fucked flac");
305 sio.rate = cast(uint)sio.ff.sampleRate;
306 sio.channels = cast(ubyte)sio.ff.channels;
307 sio.type = "flac";
308 sio.fl = fl;
309 sio.samplestotal = sio.ff.totalSampleCount;
310 conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (flac)");
312 drflac_vorbis_comment_iterator i;
313 drflac_init_vorbis_comment_iterator(&i, commentCount, fcmts);
314 uint commentLength;
315 const(char)* pComment;
316 while ((pComment = drflac_next_vorbis_comment(&i, &commentLength)) !is null) {
317 if (commentLength > 1024*1024*2) break; // just in case
318 //conwriteln(" ", pComment[0..commentLength]);
319 auto cmts = pComment[0..commentLength];
320 //conwriteln(" <", cmts, ">");
321 if (cmts.startsWithCI("ALBUM=")) sio.album = cmts[6..$].xstrip.idup;
322 else if (cmts.startsWithCI("ARTIST=")) sio.artist = cmts[7..$].xstrip.idup;
323 else if (cmts.startsWithCI("TITLE=")) sio.title = cmts[6..$].xstrip.idup;
326 conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
327 return sio;
329 } catch (Exception) {}
330 fl.seek(fpos);
331 // vorbis
332 try {
333 if (ov_fopen(fl, &sio.vf) == 0) {
334 scope(failure) ov_clear(&sio.vf);
335 sio.type = "vorbis";
336 sio.fl = fl;
337 sio.vi = ov_info(&sio.vf, -1);
338 if (sio.vi.rate < 1024 || sio.vi.rate > 96000) throw new Exception("fucked vorbis");
339 if (sio.vi.channels < 1 || sio.vi.channels > 2) throw new Exception("fucked vorbis");
340 sio.rate = sio.vi.rate;
341 sio.channels = cast(ubyte)sio.vi.channels;
342 conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (vorbis)");
343 conwriteln("streams: ", ov_streams(&sio.vf));
344 conwriteln("bitrate: ", ov_bitrate(&sio.vf));
345 sio.samplestotal = ov_pcm_total(&sio.vf)*sio.channels;
346 if (auto vc = ov_comment(&sio.vf, -1)) {
347 //conwriteln("Encoded by: ", vc.vendor.fromStringz.recodeToKOI8);
348 foreach (immutable idx; 0..vc.comments) {
349 //conwriteln(" ", vc.user_comments[idx][0..vc.comment_lengths[idx]].recodeToKOI8);
350 auto cmts = vc.user_comments[idx][0..vc.comment_lengths[idx]];
351 if (cmts.startsWithCI("ALBUM=")) sio.album = cmts[6..$].xstrip.idup;
352 else if (cmts.startsWithCI("ARTIST=")) sio.artist = cmts[7..$].xstrip.idup;
353 else if (cmts.startsWithCI("TITLE=")) sio.title = cmts[6..$].xstrip.idup;
356 conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
357 return sio;
359 } catch (Exception) {}
360 fl.seek(fpos);
361 // opus
362 try {
363 OpusFile of = opusOpen(fl);
364 scope(failure) opusClose(of);
365 if (of.rate < 1024 || of.rate > 96000) throw new Exception("fucked opus");
366 if (of.channels < 1 || of.channels > 2) throw new Exception("fucked opus");
367 sio.of = of;
368 sio.type = "opus";
369 sio.fl = fl;
370 sio.rate = of.rate;
371 sio.channels = of.channels;
372 conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (opus)");
373 sio.samplestotal = of.smpduration*sio.channels;
374 //if (of.vendor.length) conwriteln("Encoded by: ", of.vendor.recodeToKOI8);
375 foreach (immutable cidx; 0..of.commentCount) {
376 //conwriteln(" ", of.comment(cidx).recodeToKOI8);
377 auto cmts = of.comment(cidx);
378 if (cmts.startsWithCI("ALBUM=")) sio.album = cmts[6..$].xstrip.idup;
379 else if (cmts.startsWithCI("ARTIST=")) sio.artist = cmts[7..$].xstrip.idup;
380 else if (cmts.startsWithCI("TITLE=")) sio.title = cmts[6..$].xstrip.idup;
382 //TODO: comments
383 conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
384 return sio;
385 } catch (Exception) {}
386 fl.seek(fpos);
387 // mp3
388 try {
389 sio.fl = fl;
390 sio.mp3 = new MP3Decoder(&sio.reader);
391 sio.type = "mp3";
392 if (sio.mp3.valid) {
393 // scan file to determine number of frames
394 auto xfp = fl.tell;
395 fl.seek(fpos);
396 sio.mp3info = mp3Scan!true((void[] buf) => cast(int)fl.rawRead(buf).length); // build index too
397 fl.seek(xfp);
398 if (sio.mp3info.valid) {
399 if (sio.mp3.sampleRate < 1024 || sio.mp3.sampleRate > 96000) throw new Exception("fucked mp3");
400 if (sio.mp3.channels < 1 || sio.mp3.channels > 2) throw new Exception("fucked mp3");
401 sio.rate = sio.mp3.sampleRate;
402 sio.channels = sio.mp3.channels;
403 sio.samplestotal = sio.mp3info.samples;
404 conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (mp3)");
405 conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
406 return sio;
409 sio.mp3 = null;
410 } catch (Exception) {}
411 sio.mp3 = null;
412 fl.seek(fpos);
413 } catch (Exception) {}
414 return AudioStream.init;
419 // ////////////////////////////////////////////////////////////////////////// //
420 __gshared Tid playertid;
423 // ////////////////////////////////////////////////////////////////////////// //
424 public class EventFileLoaded { string filename; string album; string artist; string title; int duration; bool success; }
425 public class EventFileComplete {}
428 // ////////////////////////////////////////////////////////////////////////// //
429 __gshared bool started = false;
430 struct TMsgQuitReq {}
432 public void aplayStart () { if (!started) { started = true; playertid = spawn(&playerThread, thisTid); } }
433 public void aplayShutdown () { if (started) { started = false; playertid.send(TMsgQuitReq()); } }
436 // ////////////////////////////////////////////////////////////////////////// //
437 // reply with EventFileLoaded
438 struct TMsgPlayFileReq { string filename; bool forcestart; }
439 struct TMsgStopFileReq {}
440 struct TMsgTogglePauseReq {}
441 struct TMsgSeekReq { int timems; }
443 //TODO: queue this
444 public void aplayPlayFile (string fname, bool forcestart) { if (started) playertid.send(TMsgPlayFileReq(fname.length ? fname : "", forcestart)); }
445 public void aplayStopFile () { if (started) playertid.send(TMsgStopFileReq()); }
446 public void aplayTogglePause () { if (started) playertid.send(TMsgTogglePauseReq()); }
447 public void aplaySeekMS (int timems) { if (started) playertid.send(TMsgSeekReq(timems)); }
450 // ////////////////////////////////////////////////////////////////////////// //
451 // reply with EventFileLoaded
452 struct TMsgPlayGainReq {
453 int prc; // percents
456 public void aplayPlayGain (int prc) { if (started) playertid.send(TMsgPlayGainReq(prc)); else alsaGain = prc; }
459 // ////////////////////////////////////////////////////////////////////////// //
460 public bool aplayIsPlaying () { return atomicLoad(aplPlaying); }
461 public int aplayCurTime () { return atomicLoad(aplCurTime)/1000; }
462 public int aplayTotalTime () { return atomicLoad(aplTotalTime)/1000; }
463 public int aplayCurTimeMS () { return atomicLoad(aplCurTime); }
464 public int aplayTotalTimeMS () { return atomicLoad(aplTotalTime); }
467 // ////////////////////////////////////////////////////////////////////////// //
468 enum BUF_SIZE = 4096;
469 __gshared short[BUF_SIZE] buffer;
471 __gshared bool paused = false;
472 shared bool aplPlaying = false;
473 shared int aplCurTime = 0;
474 shared int aplTotalTime = 0;
477 // ////////////////////////////////////////////////////////////////////////// //
478 void playerThread (Tid ownerTid) {
479 AudioStream sio;
480 scope(exit) sio.close();
481 bool doQuit = false;
482 string newfilereq = null;
483 bool forcestart = false;
484 int newtime = -666;
486 uint realRate = alsaGetBestSampleRate(sio.rate);
487 conwriteln("real sampling rate: ", realRate);
489 //bool oldpaused = !paused;
490 //int oldgain = alsaGain;
492 while (!doQuit) {
493 receiveTimeout((sio.valid && !paused ? Duration.min : 42.seconds),
494 (TMsgQuitReq req) {
495 doQuit = true;
497 (TMsgPlayFileReq req) {
498 newfilereq = (req.filename.length ? req.filename : "");
499 forcestart = req.forcestart;
501 (TMsgPlayGainReq req) {
502 alsaGain = req.prc;
504 (TMsgStopFileReq req) {
505 paused = false;
506 sio.close();
507 if (alsaIsOpen) alsaShutdown();
509 (TMsgTogglePauseReq req) {
510 paused = !paused;
512 (TMsgSeekReq req) {
513 newtime = req.timems;
514 if (newtime < 0) newtime = 0;
518 if (doQuit) break;
520 if (newfilereq !is null) {
521 newtime = -666;
522 auto reply = new EventFileLoaded();
523 reply.filename = newfilereq;
524 reply.success = false;
525 newfilereq = null;
526 bool wasplaying = sio.valid;
527 sio.close();
528 try {
529 sio = AudioStream.open(VFile(reply.filename));
530 } catch (Exception e) {
531 sio = AudioStream.init;
533 if (sio.valid) {
534 reply.duration = cast(int)(sio.timetotal/1000);
535 reply.album = sio.album;
536 reply.artist = sio.artist;
537 reply.title = sio.title;
538 reply.success = true;
539 if (forcestart) paused = false; else paused = !wasplaying;
540 } else {
541 if (alsaIsOpen) alsaShutdown();
543 if (glconCtlWindow !is null) glconCtlWindow.postEvent(reply);
546 if (!sio.valid) {
547 newtime = -666;
548 paused = false;
549 atomicStore(aplPlaying, false);
550 atomicStore(aplCurTime, 0);
551 atomicStore(aplTotalTime, 0);
552 continue;
555 if (newtime != -666) {
556 int tm = newtime;
557 newtime = -666;
558 if (tm >= sio.timetotal) tm = (sio.timetotal ? sio.timetotal-1 : 0);
559 sio.seekToTime(cast(uint)tm);
562 if (!paused) {
563 if (!alsaIsOpen /*|| alsaRate != sio.rate*/ || alsaChannels != sio.channels) {
564 if (alsaIsOpen) alsaShutdown();
565 if (!alsaInit(/*sio.rate*/realRate, sio.channels)) assert(0, "cannot init ALSA playback");
567 auto frmread = sio.readFrames(buffer.ptr, BUF_SIZE/sio.channels);
568 if (frmread <= 0) {
569 atomicStore(aplPlaying, false);
570 atomicStore(aplCurTime, 0);
571 atomicStore(aplTotalTime, 0);
572 if (glconCtlWindow !is null) glconCtlWindow.postEvent(new EventFileComplete());
573 sio.close();
574 } else {
575 atomicStore(aplPlaying, true);
576 atomicStore(aplCurTime, cast(int)sio.timeread);
577 atomicStore(aplTotalTime, cast(int)sio.timetotal);
578 alsaWriteShort(buffer[0..frmread*sio.channels]);
580 } else {
581 if (alsaIsOpen) alsaShutdown();
587 // ////////////////////////////////////////////////////////////////////////// //
588 shared static this () {
589 alsaEqBands[] = 0;
591 conRegVar!alsaRQuality(0, 10, "rsquality", "resampling quality; 0=worst, 10=best, default is 8");
592 conRegVar!alsaDevice("device", "audio output device");
593 //conRegVar!alsaGain(-100, 1000, "gain", "playback gain (0: normal; -100: silent; 100: 2x)");
594 conRegVar!alsaLatencyms(5, 5000, "latency", "playback latency, in milliseconds");
595 conRegVar!alsaEnableResampling("use_resampling", "allow audio resampling?");
596 conRegVar!alsaEnableEqualizer("use_equalizer", "allow audio equalizer?");
598 //conRegVar!paused("paused", "is playback paused?");
600 // lol, `std.trait : ParameterDefaults()` blocks using argument with name `value`
601 conRegFunc!((int idx, byte value) {
602 if (value < -70) value = -70;
603 if (value > 30) value = 30;
604 if (idx >= 0 && idx < alsaEqBands.length) {
605 if (alsaEqBands[idx] != value) {
606 alsaEqBands[idx] = value;
608 } else {
609 conwriteln("invalid equalizer band index: ", idx);
611 })("eq_band", "set equalizer band #n to v (band 0 is preamp)");
613 conRegFunc!(() {
614 alsaEqBands[] = 0;
615 })("eq_reset", "reset equalizer");
617 //conRegFunc!(() { conaction = Action.Next; })("next", "next song");
618 //conRegFunc!(() { conaction = Action.Prev; })("prev", "previous song");