commit
9d9bb98495
6 changed files with 579 additions and 0 deletions
@ -0,0 +1,119 @@ |
||||
C.A.V.A |
||||
========= |
||||
Console-based Audio Visualizer for Alsa |
||||
|
||||
 |
||||
|
||||
by [Karl Stavestrand](mailto:karl@stavestrand.no ) |
||||
|
||||
What it is |
||||
---------- |
||||
C.A.V.A is a bar spectrum analyzer for audio using Alsa for input. The frequency range is limited to 80-8000Hz. I know that the human ear can hear from 20 up to 20,000 Hz (and probably "sence" even higher frequncies). But the frequencies between 80 - 8000Hz seemed to me to be the sounds that are most distinguishable. (I do believe telephones used to be limited to 8kHz as well.) |
||||
|
||||
This is my first actuall "published" work and I am not a proffesional programmer so the source code is probably, by all conventions, a complete mess. |
||||
|
||||
This program is not intendent to use for scientific purposes. |
||||
|
||||
Please excuse all the typos as I am both dyslectic and foreign. |
||||
|
||||
Any tips wil be received with much apreciation. |
||||
|
||||
|
||||
Build Requirements |
||||
------------------ |
||||
* alsa dev files: (http://alsa-project.org/) |
||||
* FFTW: (http://www.fftw.org/) |
||||
|
||||
Debian/Raspbian users can get this with: |
||||
apt-get install libfftw3-dev libasound2-dev |
||||
|
||||
|
||||
|
||||
How to get started |
||||
------------- |
||||
|
||||
``` |
||||
make |
||||
``` |
||||
|
||||
|
||||
Capturing audio straight from output |
||||
------------- |
||||
|
||||
If you want to capture audio straight fom the output(not just mic or line-in), you must first create an alsa loopback interface and then output the audio simultaneously to both the loopback and your normal interface. |
||||
|
||||
Here is how to create a loopback interface: |
||||
|
||||
- Copy the file "alsa-aloop.conf" to your "/etc/modprobe.d/" directory. You might have to change the index=1 to match your sound setup. Look at "aplay -l" to se what index is available. |
||||
- Add the line "snd-aloop" to /etc/modules |
||||
- Run "sudo modprobe snd_aloop" |
||||
|
||||
Hopefully your "aplay -l" should now contain a couple of "Loopback" interfaces. |
||||
|
||||
Now playing the audio throug your Loopback interface will make it possible to capture by cava, but there will be no sound in your speakers :( |
||||
|
||||
Not to worry! There are (at least) two ways of sending the audio output to the loopback and your actual audio interface at the same time: |
||||
|
||||
- pulseaudio (easy): in the file "/etc/pulse/default.pa" add the line "load-module module-combine-sink" (in pulseaudio versions < 2.0 the module is only called "module-combine"). Then restart pulseaudio. |
||||
|
||||
Pulseaudio setup can also be done in paprefs (debain: sudo apt-get install paprefs && paprefs&), the far right tab "Simultaneous Output" and check the box. |
||||
|
||||
After that there should be an extra Output in your sound options called Simultaneous output to... Note that when using this method if you turn down the volume on the Simultaneous output, this will effect the cavalizer. So to avoid this you have to select the actual output then turn down the volume then select the Simultaneous output again. |
||||
|
||||
- alsa (hard): look at the inculded example file: /etc/asound.cnf. I was able to make this work on my laptop an Asus ux31 running elemetary os. But had no luck on my Rasberry PI with an USB DAC ruunig rasbian. |
||||
|
||||
Read more about the alsa method here: http://stackoverflow.com/questions/12984089/capture-playback-on-play-only-sound-card-with-alsa |
||||
|
||||
|
||||
Cava defaults to "hw:1,1" if your loopback interface is not on that index, or you want to capture audio from somewhere else, simply cahnge this with the -d option. |
||||
|
||||
Running via ssh: |
||||
|
||||
to run via ssh to external monitor: |
||||
|
||||
~# ./cava [options] > /dev/console |
||||
|
||||
(must be root to redirect to console, simple sudo is not enouh, run "sudo su" first) |
||||
|
||||
|
||||
|
||||
A note on fonts: |
||||
-------------------- |
||||
Since the graphics are simply based on chrachters the preformance is dependent on the font in the terminal. |
||||
|
||||
In ttys: |
||||
|
||||
If you run this in the conosle (tty1-7) the program will change the font to the included "cava.psf" (actually "unifont") this is because that font looks good with this program and beause i had to change the font slightly. |
||||
|
||||
In console fonts it seems that only 256 unicode charachters are suported, probably since theese are bitmap fonts. I could not find a font that had unicode charachters 2581-2587, these are the 1/8 - 7/8 blocks used on the top of each bar to increase resolution. |
||||
|
||||
So in cava.psf the charachters 1-7 is actually replaced by unicode charachters 2581-2587. Not to worry, when cava is excited the font is suposed to change. However if you notice that 1-7 is replaced by partial blocks just change the font with "setfont". |
||||
|
||||
Another note on fonts, "setfont" is suposed to return the defualt font, but this usally isn't set and I haven't found another way to get the current font. So what cava does is setting the font to "Lat2-Fixed16" on exit (ctrl+c), this should be included in all mayor distros. I think it reverts to your default at reboot. |
||||
|
||||
In terminal emulators: |
||||
|
||||
In terminal emulator like xTerm, the font is chosen in the software and cannot be changed by an apliaction. So find your terminal settings and try out diferent fonts, some look pretty crap with the block charachters :( . "WenQuanYi Micro Hei Mono" is what I have found to look pretty good, But it still looks better in the tty. |
||||
|
||||
If you experince an issue with latency, try to increase the font size. This will reduce the number of charachters that has to be printed out. |
||||
|
||||
!!warning!! cava also turns of the cursor, it's suposed to turn it back on on exit (ctrl+c), but in case it terminates unexpectetly, run: "setterm -cursor on" to get the cursor back. |
||||
|
||||
|
||||
|
||||
A note on latency: |
||||
-------------------- |
||||
If there is a huge buffer in your audio device, you might experience that cava is actually faster then the audio you heare. This will reduce the experience of the visualization. To fix this you can try to increase the buffer settings in the audio playing software. |
||||
|
||||
Usage |
||||
-------------------- |
||||
./cava [options] |
||||
|
||||
Options: |
||||
-b 1..(console columns/2-1) or 200, number of bars in the spectrum (default 20 + fills up the console), program wil auto adjust to maxsize if input is to high) |
||||
|
||||
-d 'alsa device', name of alsa capture device (default 'hw:1,1') |
||||
|
||||
-c color suported colors: red, green, yellow, magenta, cyan, white, bliue (default: cyan) |
||||
|
||||
exit with ctrl+c |
||||
@ -0,0 +1,437 @@ |
||||
#include<stdio.h> |
||||
#include<stdbool.h> |
||||
#include <time.h> |
||||
#include <math.h> |
||||
#include <alsa/asoundlib.h> |
||||
#include <sys/ioctl.h> |
||||
#include <unistd.h> |
||||
#include <fftw3.h> |
||||
#define PI 3.14159265358979323846 |
||||
#include<unistd.h> |
||||
#include<signal.h> |
||||
#include<string.h> |
||||
#include <getopt.h> |
||||
|
||||
struct sigaction old_action; |
||||
|
||||
|
||||
void sigint_handler(int sig_no) |
||||
{ |
||||
printf("\033[0m\n"); |
||||
system("setfont /usr/share/consolefonts/Lat2-Fixed16.psf.gz "); |
||||
system("setterm -cursor on"); |
||||
system("clear");
|
||||
printf("CTRL-C pressed -- goodbye\n"); |
||||
sigaction(SIGINT, &old_action, NULL); |
||||
kill(0, SIGINT); |
||||
} |
||||
|
||||
|
||||
int main(int argc, char **argv) |
||||
{ |
||||
int M=4096; |
||||
signed char *buffer; |
||||
snd_pcm_t *handle; |
||||
snd_pcm_hw_params_t *params; |
||||
unsigned int val; |
||||
snd_pcm_uframes_t frames; |
||||
char *device = "hw:1,1"; |
||||
float fc[200];//={150.223,297.972,689.062,1470,3150,5512.5,11025,18000};
|
||||
float fr[200];//={0.00340905,0.0067567,0.015625,0.0333,0.07142857,0.125,0.25,0.4};
|
||||
int lcf[200], hcf[200]; |
||||
double f[200]; |
||||
double x[M]; |
||||
double peak[201]; |
||||
float y[M/2+1]; |
||||
long int lpeak,hpeak; |
||||
int bands=20; |
||||
int sleep=0; |
||||
float h; |
||||
int i, n, o, size, dir, err,xb,yb,bw,format,rate,width,height,c,rest,virt; |
||||
int autoband=1; |
||||
//long int peakhist[bands][400];
|
||||
double temp; |
||||
double sum=0; |
||||
int16_t hi;
|
||||
int q=0; |
||||
val=44100; |
||||
int debug=0; |
||||
struct winsize w; |
||||
double in[2*(M/2+1)]; |
||||
fftw_complex out[M/2+1][2]; |
||||
fftw_plan p; |
||||
struct timespec start, stop; |
||||
double accum; |
||||
char *color; |
||||
int col = 37; |
||||
//---------- THIS IS THE END OF INIT, MUST STOP PUTTING INIT BELOW
|
||||
|
||||
|
||||
//**arg handler**//
|
||||
while ((c = getopt (argc, argv, "b:d:c:t:B")) != -1) |
||||
switch (c) |
||||
{ |
||||
case 'b': |
||||
bands = atoi(optarg); |
||||
autoband=0; //dont automaticly add bands to fill frame
|
||||
if (bands>200)bands=200; |
||||
break; |
||||
case 'd': |
||||
device = optarg; |
||||
break; |
||||
case 'c': |
||||
col=0; |
||||
color = optarg; |
||||
if(strcmp(color,"red")==0) col=31; |
||||
if(strcmp(color,"green")==0) col=32; |
||||
if(strcmp(color,"yellow")==0) col=33; |
||||
if(strcmp(color,"blue")==0) col=34; |
||||
if(strcmp(color,"magenta")==0) col=35; |
||||
if(strcmp(color,"cyan")==0) col=36; |
||||
if(strcmp(color,"white")==0) col=37; |
||||
if(col==0) |
||||
{ |
||||
printf("color %s not suprted\n",color); |
||||
exit(1); |
||||
} |
||||
break; |
||||
case '?': |
||||
printf ("\nUsage : ./cava [options]\n\nOptions:\n\t-b 1..(console columns/2-1) or 200, number of bars in the spectrum (default 20 + fills up the console), program wil auto adjust to maxsize if input is to high)\n\n\t-d 'alsa device', name of alsa capture device (default 'hw:1,1')\n\n\t-c color\tsuported colors: red, green, yellow, magenta, cyan, white, blue (default: cyan)\n\n\""); |
||||
return 1; |
||||
default: |
||||
abort (); |
||||
} |
||||
|
||||
|
||||
//**ctrl c handler**//
|
||||
|
||||
struct sigaction action; |
||||
memset(&action, 0, sizeof(action)); |
||||
action.sa_handler = &sigint_handler; |
||||
sigaction(SIGINT, &action, &old_action); |
||||
|
||||
|
||||
//**drawing frame**//
|
||||
if(debug==0){ |
||||
virt = system("setfont cava.psf"); |
||||
system("setterm -cursor off"); |
||||
} |
||||
|
||||
|
||||
//getting h*w of term
|
||||
ioctl(STDOUT_FILENO, TIOCGWINSZ, &w); |
||||
if(bands>(int)w.ws_col/2-1)bands=(int)w.ws_col/2-1; //handle for user setting to many bars
|
||||
height=(int)w.ws_row-1; |
||||
width=(int)w.ws_col-bands-1; |
||||
bool matrix[width][height]; |
||||
bw=width/bands; |
||||
|
||||
//if no bands are selected it tries to padd the default 20 if there is extra room
|
||||
if(autoband==1) bands=bands+(((w.ws_col)-(bw*bands+bands-1))/(bw+1)); |
||||
|
||||
//checks if there is stil extra room, will use this to center
|
||||
rest=(((w.ws_col)-(bw*bands+bands-1))); |
||||
if(rest<0)rest=0; |
||||
|
||||
//resetting console
|
||||
printf("\033[0m\n"); |
||||
system("clear"); |
||||
|
||||
printf("\033[%dm",col);//setting volor
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
if(debug==0){ |
||||
for(yb=0;yb<w.ws_row;yb++) |
||||
{ |
||||
for(xb=0;xb<w.ws_col;xb++) |
||||
{ |
||||
if(yb==0) |
||||
{ |
||||
if(xb==0)printf("\u250c");//top left
|
||||
else if(xb==w.ws_col-1)printf("\u2510");//top right
|
||||
else printf("\u2500");//top
|
||||
} |
||||
else if(yb==w.ws_row-1) |
||||
{
|
||||
if(xb==0)printf("\u2514");//bottom left
|
||||
else if(xb==w.ws_col-1)printf("\u2518");//bottom right
|
||||
else printf("\u2500");//bottom
|
||||
} |
||||
else
|
||||
{ |
||||
if (xb==0||xb==w.ws_col-1)printf("\u2502");//left and right
|
||||
else printf("%1c",32);
|
||||
} |
||||
} |
||||
if(yb!=w.ws_row-1)printf("\n"); |
||||
} |
||||
printf("\r\033[%dC",1); |
||||
printf("%c[%dA",27,w.ws_row-2);//backup
|
||||
fflush(stdout); |
||||
} |
||||
*/ |
||||
|
||||
|
||||
//**init sound device***//
|
||||
|
||||
if ((err = snd_pcm_open(&handle, device, SND_PCM_STREAM_CAPTURE , 0) < 0)) |
||||
printf("error opening stream: %s\n",snd_strerror(err) ); |
||||
else |
||||
if(debug==1){ printf("open stream succes\n"); }
|
||||
snd_pcm_hw_params_alloca(¶ms); |
||||
snd_pcm_hw_params_any (handle, params); |
||||
snd_pcm_hw_params_set_access(handle, params, SND_PCM_ACCESS_RW_INTERLEAVED); |
||||
snd_pcm_hw_params_set_format(handle, params, SND_PCM_FORMAT_S16_LE);
|
||||
snd_pcm_hw_params_set_channels(handle, params, 2); |
||||
val = 44100; |
||||
snd_pcm_hw_params_set_rate_near(handle, params, &val, &dir); |
||||
frames = 32; |
||||
snd_pcm_hw_params_set_period_size_near(handle, params, &frames, &dir); |
||||
|
||||
err = snd_pcm_hw_params(handle, params); |
||||
if (err < 0) { |
||||
fprintf(stderr, |
||||
"unable to set hw parameters: %s\n", |
||||
snd_strerror(err)); |
||||
exit(1); |
||||
} |
||||
|
||||
|
||||
snd_pcm_hw_params_get_period_size(params,&frames, &dir);
|
||||
snd_pcm_hw_params_get_period_time(params, &val, &dir); |
||||
|
||||
snd_pcm_hw_params_get_format(params, (snd_pcm_format_t * )&val); //getting format
|
||||
|
||||
if(val<6)format=16; |
||||
else if(val>5&&val<10)format=24; |
||||
else if(val>9)format=32; |
||||
|
||||
size = frames * (format/8)*2; /* bytes/sample * 2 channels */ |
||||
buffer = (char *) malloc(size);
|
||||
|
||||
if(debug==1)printf("detected format: %d\n",format); |
||||
|
||||
snd_pcm_hw_params_get_rate( params, &rate, &dir); //getting rate
|
||||
if(debug==1)printf("detected rate: %d\n",rate); |
||||
|
||||
|
||||
|
||||
//**calculating cutof frequencies**/
|
||||
for(n=0;n<bands+1;n++) |
||||
{
|
||||
fc[n]=8000*pow(10,-2+(((float)n/(float)bands)*2));//decided to cut it at 10k, little interesting to hear above
|
||||
fr[n]=fc[n]/(rate); //remember nyquist
|
||||
lcf[n]=fr[n]*(M/2+1); |
||||
|
||||
if(n!=0)hcf[n-1]=lcf[n]; |
||||
|
||||
if(debug==1&&n!=0){printf("%d: %f -> %f (%d -> %d) \n",n,fc[n-1],fc[n],lcf[n-1],hcf[n-1]);} |
||||
} |
||||
|
||||
|
||||
|
||||
p = fftw_plan_dft_r2c_1d(M, in, *out, FFTW_MEASURE); //planning to rock
|
||||
|
||||
//**start main loop**//
|
||||
while (1) |
||||
{ |
||||
/*
|
||||
if( clock_gettime( CLOCK_REALTIME, &start) == -1 ) { |
||||
perror( "clock gettime" ); |
||||
exit( EXIT_FAILURE ); |
||||
} |
||||
*/ |
||||
|
||||
//**filling of the buffer**//
|
||||
for(o=0;o<M/32;o++) |
||||
{ |
||||
err = snd_pcm_readi(handle, buffer, frames); |
||||
if (err == -EPIPE) { |
||||
/* EPIPE means overrun */ |
||||
if(debug==1){ fprintf(stderr, "overrun occurred\n");} |
||||
snd_pcm_prepare(handle); |
||||
} else if (err < 0) { |
||||
if(debug==1){ fprintf(stderr, "error from read: %s\n", |
||||
snd_strerror(err));} |
||||
} else if (err != (int)frames) { |
||||
if(debug==1){ fprintf(stderr, "short read, read %d %d frames\n", err,(int)frames);} |
||||
} |
||||
|
||||
//sorting out one channel and only biggest octet
|
||||
n=0;//frame counter
|
||||
for (i=0; i<size ; i=i+(format/8)*2) |
||||
{ |
||||
// left right
|
||||
//structuere [litte],[litte],[big],[big],[litte],[litte],[big],[big]...
|
||||
|
||||
x[n+(int)frames*o] = (buffer[i+(format/4)-1]+buffer[i+(format/8)-1])/2;//avg of left and right
|
||||
|
||||
if(x[n+(int)frames*o]>hpeak) hpeak=x[o]; |
||||
if(x[n+(int)frames*o]<lpeak) lpeak=x[o]; |
||||
n++;
|
||||
} |
||||
} |
||||
|
||||
|
||||
if(debug==1){ system("clear");} |
||||
|
||||
|
||||
//**populating input buffer & checking if there is sound**//
|
||||
lpeak=0; |
||||
hpeak=0; |
||||
for (i=0;i<(2*(M/2+1));i++) |
||||
{ |
||||
if(i<M) |
||||
{ |
||||
in[i]=x[i]; |
||||
if(x[i]>hpeak) hpeak=x[i]; |
||||
if(x[i]<lpeak) lpeak=x[i]; |
||||
}
|
||||
else in[i]=0; |
||||
// if(debug==1) printf("%d %f\n",i,in[i]);
|
||||
} |
||||
peak[bands]=(hpeak+abs(lpeak)); |
||||
|
||||
//**if sound go ahead with fft**//
|
||||
if (peak[bands]!=0)
|
||||
{ |
||||
sleep=0; //wake if was sleepy
|
||||
|
||||
fftw_execute(p); //applying FFT to signal
|
||||
|
||||
//saving freq domian of input signal
|
||||
for (i=0;i<M/2+1;i++) |
||||
{ |
||||
y[i]=pow(pow(*out[i][0],2)+pow(*out[i][1],2),0.5); |
||||
} |
||||
|
||||
//seperating freq bands
|
||||
for(o=0;o<bands;o++) |
||||
{
|
||||
peak[o]=0; |
||||
|
||||
//getting peaks
|
||||
for (i=lcf[o];i<=hcf[o];i++) |
||||
{ |
||||
if(y[i]>peak[o]) peak[o]=y[i];
|
||||
} |
||||
|
||||
//if (peak[o]>sum)sum=peak[o]; peakest values are ussaly around 50k but i have seen 113000
|
||||
//divides on 200 because of complex mplification
|
||||
//mulitplise by log of frequency probably because of eq master cd standard, riaa...?
|
||||
|
||||
|
||||
f[o]=((peak[o]*(float)height*log(hcf[o]+10)*log(hcf[o]+10))) /(254*(M/8)*(log(hcf[bands-1]))) ; //weighing signal to height and frequency
|
||||
|
||||
if(f[o]>height)f[o]=height;//just in case
|
||||
|
||||
if(debug==1){ printf("%d: f:%f->%f peak:%f adjpeak: %f \n",o,fc[o],fc[o+1],peak[o],f[o]);} |
||||
} |
||||
if(debug==1){ printf("topp overall unfiltered:%f \n",peak[bands]); |
||||
|
||||
} |
||||
//if(debug==1){ printf("topp overall alltime:%f \n",sum);}
|
||||
} |
||||
else//**if no signal don't bother**//
|
||||
{ |
||||
if (sleep>(rate*5)/M)//if no signal for 5 sec, go to sleep mode
|
||||
{ |
||||
if(debug==1)printf("no sound detected for 5 sec, going to sleep mode\n"); |
||||
usleep(1*1000000);//wait one sec, then check sound again. Maybe break, close and reopen?
|
||||
continue; |
||||
} |
||||
if(debug==1)printf("no sound detected, trying again\n");
|
||||
sleep++; |
||||
continue; |
||||
} |
||||
|
||||
//**DRAWING**// -- put in function file maybe?
|
||||
if (debug==0) |
||||
{ |
||||
for (n=(height-1);n>=0;n--) |
||||
{ |
||||
o=0;
|
||||
for (i=0;i<width;i++) |
||||
{ |
||||
|
||||
//next bar? make a space
|
||||
if(i!=0&&i%bw==0){ |
||||
o++; |
||||
if(o<bands)printf(" ");
|
||||
}
|
||||
|
||||
//draw color or blank
|
||||
if(o<bands){ //watch so it doesnt draw to far
|
||||
if(f[o]-n<0.125) printf(" "); //blank
|
||||
else if (f[o]-n>1) printf("\u2588");//color
|
||||
else//top color, finding fraction
|
||||
{ |
||||
c=((((f[o]-(float)n)-0.125)/0.875*7)+1); |
||||
switch (c) |
||||
{ |
||||
case 1: |
||||
if(virt==0)printf("1"); |
||||
else printf("\u2581"); |
||||
break; |
||||
case 2: |
||||
if(virt==0)printf("2");
|
||||
else printf("\u2582");
|
||||
break; |
||||
case 3: |
||||
if(virt==0)printf("3"); |
||||
else printf("\u2583");
|
||||
break; |
||||
case 4: |
||||
if(virt==0)printf("4");
|
||||
else printf("\u2584"); |
||||
break; |
||||
case 5: |
||||
if(virt==0)printf("5"); |
||||
else printf("\u2585");
|
||||
break; |
||||
case 6: |
||||
if(virt==0)printf("6");
|
||||
else printf("\u2586"); |
||||
break; |
||||
case 7: |
||||
if(virt==0)printf("7");
|
||||
else printf("\u2587"); |
||||
break; |
||||
default: |
||||
printf(" ");
|
||||
} |
||||
}
|
||||
}
|
||||
|
||||
|
||||
} |
||||
printf("\n\033[%dC",(rest/2));//next line and one to the right
|
||||
} |
||||
|
||||
printf("\033[%dA",height);//backup
|
||||
|
||||
|
||||
} |
||||
//
|
||||
|
||||
//snd_pcm_drain(handle);
|
||||
//snd_pcm_drop(handle);
|
||||
//snd_pcm_close(handle);
|
||||
/* //tried to make this frameramet limit to save cpu....
|
||||
if( clock_gettime( CLOCK_REALTIME, &stop) == -1 ) { |
||||
perror( "clock gettime" ); |
||||
exit( EXIT_FAILURE ); |
||||
} |
||||
*/ |
||||
//accum = (( stop.tv_sec - start.tv_sec )+ ( stop.tv_nsec - start.tv_nsec ))/1000000;
|
||||
//if(accum<50000&&accum>0)usleep(50000-accum); //16666.666666667 usec = 60 fps
|
||||
//printf("time used: %f\n",accum);
|
||||
} |
||||
|
||||
fftw_destroy_plan(p); |
||||
return 0; |
||||
} |
||||
|
||||
@ -0,0 +1,20 @@ |
||||
pcm.!default { |
||||
type plug # <-- no { here |
||||
slave.pcm { |
||||
type multi |
||||
slaves { |
||||
a { channels 2 pcm "hw:0,0" } # the real device |
||||
b { channels 2 pcm "hw:1,0" } # the loopback driver |
||||
} |
||||
bindings { |
||||
0 { slave a channel 0 } |
||||
1 { slave a channel 1 } |
||||
2 { slave b channel 0 } |
||||
3 { slave b channel 1 } |
||||
} |
||||
} |
||||
ttable [ |
||||
[ 1 0 1 0 ] # left -> a.left, b.left |
||||
[ 0 1 0 1 ] # right -> a.right, b.right |
||||
] |
||||
} |
||||
@ -0,0 +1 @@ |
||||
options snd-aloop index=1 enable=1 pcm_substreams=4 id=Loopback0 |
||||
Loading…
Reference in new issue