Termux-packages: Integrating ALSA and Pulseaudio into Termux

Created on 6 Mar 2017  路  166Comments  路  Source: termux/termux-packages

Goals

This issue is for tracking progress on integrating ALSA and Pulseaudio into Termux.
Of course, ALSA in Termux will not have direct access to sound hardware on an Android device unless rooted and the vendor uses an ALSA driver in the kernel. The goal is to have a means to communicate with the OpenSL ES subsystem in Android as efficiently as possible. Currently, this is done by streaming a Pulseaudio sink onto the localhost network. In future, it may be possible to write or adapt a Pulseaudio backend driver or an ALSA plugin to talk directly to OpenSL ES, using the so-called "fast mixer", with minimum latency when possible.

Overview of tasks and status

Building and using the required packages

  • [x] libpulseaudio (#825)

    • [x] pulseaudio

    • [x] libsndfile

    • [ ] fftw (useful?)

    • [ ] libspeex (useful?)

    • [ ] libsoxr (useful?)

    • [ ] liborc (useful?)

    • [ ] libwebrtc-audio-processing (useful?)

  • [ ] alsa-lib (#825, disabled: not working yet)
  • [ ] alsa-plugins (#825, disabled: not working yet)
  • [ ] alsa-utils (#825, disabled: not working yet)

Writing OpenSL ES backend drivers and plugins

  • [ ] for ALSA
  • [ ] for PulseAudio

Enabling audio related packages to make use of ALSA and/or Pulseaudio

  • [x] cmus (#209)

    • [x] libmad (in repository)

    • [x] libvorbis (in repository)

    • [x] libflac (in repository)

    • [ ] mp4v2 (needed?)

    • [ ] ...

  • [ ] mpv
  • [ ] mpd
  • [ ] espeak
  • [ ] ...

Detailed description

Building and using the required packages

All packages build without problems on my system. They're mostly in a minimal, PoC state, I'm open for discussion on what extra dependencies to include, any tweaks to make.

libpulseaudio is usable with Pulseaudio enabled packages, even without pulseaudio installed:

  • Install the XServer XSDL app from Google Play
  • Make sure to add the following to ~/.bash_profile:
export PULSE_SERVER=tcp::4712
  • Open the XServer XSDL app
  • In Termux, run your Pulseaudio enabled application as usual

Known issue: Sound gets muted when device screen turns off or XServer XSDL is not running in the foreground.

pulseaudio is a really bare-bones, minimal affair, no speex (I doubt if it is worth the CPU resources on Android), no ORC (AFAICT it currently only supports an architecture with MMX/SSE instructions anyway). I've also disabled Neon optimization: the build generates assembly which the assembler seems not to be able to handle (it spews errors about unknown registers, q0 among them). I symlinked the shared libraries into $TERMUX_PREFIX/lib to avoid having to write an ugly wrapper. If there are any conflicting filenames, I'd like to hear about them.
To get sound output from applications running in Termux, you can adapt and run the following script:

setup-pulseaudio.sh:

#!/data/data/com.termux/files/usr/bin/sh

TERMUX_PREFIX=~/../usr

# Do not shutdown server when idle.
sed -i $TERMUX_PREFIX/etc/pulse/daemon.conf \
    -e '/^; exit-idle-time =/{s/^; //;s/ 20/ -1/}'

# When no sinks are available, Pulseaudio automatically adds a null sink named 'auto_null'.

# Stream the 'auto_null' sink over TCP.
# This can be played by Simple Protocol Player app on Google Play.
sed -i $TERMUX_PREFIX/etc/pulse/default.pa \
    -e '$aload-module module-simple-protocol-tcp source=auto_null.monitor port=12345 record=true'

# In addition, or alternatively,
# add a null sink named 'rtp' and stream it over RTP.
# This can be played by VLC, etc.
# sed -i $TERMUX_PREFIX/etc/pulse/default.pa \
#     -e '/^#load-module module-null-sink sink_name=rtp /s/#//' \
#     -e '/^#load-module module-rtp-send source=rtp.monitor$/s/#//'

To play sound from a TCP stream:

  • Install Simple Protocol Player from Google Play
  • in Termux, run pulseaudio --daemon
  • Open the Simple Protocol Player app
  • Make sure that IP Address is set to 127.0.0.1 and Audio Port is set to 12345 (or whatever value was set in the setup-pulseaudio.sh script
  • Tap the Play button
  • In Termux, run your Pulseaudio enabled application as usual

To play sound from an RTP stream:

  • Install VLC for Android (or a similar app which can play back RTP streams) from Google Play
  • In Termux, run pulseaudio --daemon
  • Open VLC or similar media player app
  • Choose to play back an RTP stream (how to do this depends on the media player app) from host 127.0.0.1, port ??? (TODO: Add simple way to discover RTP port number)
  • In Termux, run your Pulseaudio enabled application as usual

alsa-lib is pretty complete, I've worked around a problem where the path /etc/asound.conf is hardcoded, which may conflict with system ALSA and isn't useful on non-rooted devices anyway.

alsa-utils: Also pretty complete, not sure if we need all of the utilities. Fun tidbit: alsamixer runs really well in Termux terminal emulator, it's even possible to drag with a finger to change the volume, yay!

alsa-plugins: Only built the Pulseaudio plugin. I'm not sure if we need the samplerate plugin because Pulseaudio and OpenSL ES can both resample audio.

Writing OpenSL ES backend drivers and plugins

pulseaudio is one of the targets for a dedicated OpenSL ES driver, I haven't looked into that yet. There appears to be a (reportedly buggy) backend available, maybe we can adapt that.

alsa-plugins is another target for more direct interaction with OpenSL ES, this time as a (pair of?) plugin(s), a virtual PCM and CTL device.

Enabling audio related packages to make use of ALSA and/or Pulseaudio

cmus runs on Termux. Some of its optional dependencies (support for various file types) still need to be packaged.

Resources

ALSA

Pulseaudio

Android

Third-party libraries

enhancement

Most helpful comment

For anyone using cmus + simpleprotocolplayer: it is possible to start simpleprotolplayer for command line in termux using the intents described in this simpleprotocolplayer issue.

I am now using a small script to allow me to fully control my music playing device when only connected to it over ssh:

bash-4.4$ more bin/am_protocolplayer.sh
#!/data/data/com.termux/files/usr/bin/bash
if [ $1 == "play" ]; then
    am startservice --user 0 -a com.kaytat.simpleprotocolplayer.action.PLAY -e ip_addr 127.0.0.1 --ei audio_port 12345 --ei sample_rate 44100 --ez stereo true --ei buffer_ms 500 com.kaytat.simpleprotocolplayer/.MusicService
elif [ $1 == "stop" ]; then
    am startservice --user 0 -a com.kaytat.simpleprotocolplayer.action.STOP com.kaytat.simpleprotocolplayer/.MusicService
fi

am_protocolplayer.sh play starts the streaming and am_protocolplayer.sh stop stops the streaming.

All 166 comments

Nice!

I've also disabled Neon optimization: the build generates assembly which the assembler seems not to be able to handle (it spews errors about unknown registers, q0 among them).

This could be due to clang trying to use its integrated assembler. Does adding -no-integrated-as to CFLAGS fix this and allow neon optimizations?

Using gcc fixes that neon issue

Thanks for your very quick reply! I will look into it.

Woops, sorry about that! Reopened.

Very interesting the fact that this project is beyond Termux scope. It would be great if eventually it became a official port.

I hope to push the package build scripts to my fork for testing soon, but I'm hitting a problem with a bogus buffer size when using the ALSA Pulseaudio plugin which causes speaker-test to crash with a Floating point exception. Stay tuned 馃槃

@DoktorStein Yes, I agree that it goes beyond Termux as such, but as Termux is such a wonderful project and has a very convenient build system, I think it is only fitting to let Termux be the first to benefit 馃槈

Added some resources. slesTestFeedback.cpp in particular looks very interesting, it appears to be some kind of reference test which firmware developers can use to check audio latency on their device before they ship a new ROM. This also explains why it uses some private APIs which are not exposed by the NDK (but can be ported I think).

I have got cmus running on Termux and playing sound. Still need to package some extra optional dependencies.

I have just found out a patch adding opensles sink to pulseaudio here: https://github.com/glance-/pulseaudio-android-ndk/blob/master/pulseaudio-patches/0003-WIP-opensl-sink.patch
I haven't yet tried to build this I have just looked at it briefly and I have found out it's a different implementation to the one you have mentioned in the first comment here.

Apparently there is issue with some missing symbols. I'm on LineAgeOS Android 7 and I'm getting it too. I have tried to install glib in case it may help but it appears the missing symbol comes from the android runtime...
Here is where I have seen the issue mentioned for the first time... https://github.com/cmus/cmus/issues/688

I'm also experiencing the missing symbol issue on arm android 7.0 with a AOSP-rom.

hmm. Looking through the list of packages, I assume libandroid-glob is somehow supposed to address this. However I haven't yet understood how to most easily build a package. Originally I was about to build it on the device with termux running however when looking at termux-package repo, I can only see packages and scripts to use in its specific build environment. So I need to learn all this before I can try doing something usefull here.
Edit: Linking with libandroid-glob the same way we are linking with liblog should be enough I think. At least this is how it looks like when comparing this with other packages relying on libandroid-glob. I don't have the dev envo configured yet, but I imagine this will be a simple thing to do.

@pvagner I tried building with the option as you suggested and it solved the error! (I'm still not getting audio though so there are more problems. gdb cmus warns about a couple of missing symbols).

Would you like to open a pull request to solve the issue (and add libandroid-glob as a dependency)?

@Grimler91 have you configured your pulseaudio in termux as proposed by @ackalker via a simple script setup-pulseaudio.sh and are you using either Simple Protocol Player to play back the audio through local network or VLC to play an rtp stream? Apparently no one else besides @ackalker got thus far as we are only discovering all this after a few months.
If you have got this far, perhaps you can make the pull request earlier. I am still getting all the stuf setup and I would like to get it building for me before submitting anything. It might take me a few hours or a few days, I am not sure yet how I can manage...

@pvagner Errrm, no I haven't. That could be my problem, I should start reading the instructions before testing stuff.

I tried it again and managed to play local music files using cmus and "simple protocol player" :)
Cool that it works!

I'll open a pull request shortly. (And ask if you have any questions about how to setup the build environment)

@Grimler91 thank you! I've got it building and the sound is playing over here as well.

For anyone using cmus + simpleprotocolplayer: it is possible to start simpleprotolplayer for command line in termux using the intents described in this simpleprotocolplayer issue.

I am now using a small script to allow me to fully control my music playing device when only connected to it over ssh:

bash-4.4$ more bin/am_protocolplayer.sh
#!/data/data/com.termux/files/usr/bin/bash
if [ $1 == "play" ]; then
    am startservice --user 0 -a com.kaytat.simpleprotocolplayer.action.PLAY -e ip_addr 127.0.0.1 --ei audio_port 12345 --ei sample_rate 44100 --ez stereo true --ei buffer_ms 500 com.kaytat.simpleprotocolplayer/.MusicService
elif [ $1 == "stop" ]; then
    am startservice --user 0 -a com.kaytat.simpleprotocolplayer.action.STOP com.kaytat.simpleprotocolplayer/.MusicService
fi

am_protocolplayer.sh play starts the streaming and am_protocolplayer.sh stop stops the streaming.

Hi! Have working OpenSLES output if pulseaudio writes data to pipe. It can be used instead of simpleprotocolplayer. For now I'm trying to integrate it into module-pipe-sink (using anonymous pipe as buffer). https://gist.github.com/twaik/2c766116950e668989f3497a03b29862
Can anyone help me with that?

Have working OpenSL ES sink. Who I can talk to to test, improve and integrate it to pulseaudio source?

@twaik I assume you want to integrate it into termux's pulseaudio and not the upstream pulseaudio?

Any one can suggest changes to the build scripts. In this case I guess you can suggest a patch to be added to libpulseaudio.

Edit: is it the file you linked to? How should it be integrated into pulseaudio and how can it be tested?

@Grimler91 I have built it on top of that source, but I think it can be integrated into termux-packages repos. I didn't send message to upstream developers because it is not complete yet and because upstream pa have no Android native build support (except this ). It works on my device well but I don't know about running that on other devices. I published the code here. Don't move it to master branch, test first. I need to know restult of a test.

Also these lines need to be added to Makefile.am

...
modlibexec_LTLIBRARIES += \
...
        module-sles-sink.la
...
SYMDEF_FILES = \
...
        module-sles-sink-symdef.h \
...
module_sles_sink_la_SOURCES = modules/sles/module-sles-sink.c
module_sles_sink_la_LDFLAGS = $(MODULE_LDFLAGS) -lOpenSLES
module_sles_sink_la_LIBADD = $(MODULE_LIBADD)
...

Did anyone test that?

tried just now won't compile
error: undefined reference to 'initSL'
am i using the wrong source as base do i need to use pelya's ?
that won't compile with latest ndk ....

@its-pointless sorry, just fixed it

am i using the wrong source as base do i need to use pelya's ?

No, his source have no output to OpenSL, it is a stub one.
But my source has.

ok its working on the arm device i have
i will see if i can get working on aarch64

can confirm this works on both arm and aarch64 devices. Nice

@its-pointless Does it work well?

In terms of sound im not enough of an audiophile to tell. But i have cmus working. Have to do some scripting so all the sound apps work properly in the same way mpv does. In terms of behaviour i will have to do some more testing later.

i will make a pull request with your code added twaik.

@its-pointless OK. Waiting for remarks. Please, tell me if you found something.

About apps using ALSA: try to use this config for alsa

pcm.pulse {
type pulse
}
ctl.pulse {
type pulse
}
pcm.!default {
type pulse
}
ctl.!default {
type pulse
}

On my current phone whenever sound is active the "boom sound" becomss activated . This stays on when using pulse audio daemon. Would this have an impact on power consumption?
Also alsa compaitibilty is something i should really test for you right?

What is the "boom sound"? I didn't hear about that before.
Alsa compatibility is just another function can be used in Termux. Look at the topic name :)

its just a mostly software thing from htc. Not significant in of itself but rather indictitive of the software using audio. Its an equalizer that "improves" sound.

I don't know if it can help, but you should try to insert these lines

//change stream type to avoid activating HTC's "boom sound"
SLint32 androidStreamType = SL_ANDROID_STREAM_MEDIA;
//androidStreamType = streamTypeEnum = SL_ANDROID_STREAM_NOTIFICATION;
//androidStreamType = streamTypeEnum = SL_ANDROID_STREAM_RING;
//androidStreamType = streamTypeEnum = SL_ANDROID_STREAM_SYSTEM;
//androidStreamType = streamTypeEnum = SL_ANDROID_STREAM_VOICE;
SLAndroidConfigurationItf androidConfig;
result = (*uriPlayerObject)->GetInterface(s->bqPlayerObject, SL_IID_ANDROIDCONFIGURATION, &androidConfig);checkResult(result);
result = (*androidConfig)->SetConfiguration(androidConfig, SL_ANDROID_KEY_STREAM_TYPE, &androidStreamType, sizeof(SLint32));checkResult(result);

here

I don't know which of SL_ANDROID_STREAM_* activates "boom sound" feature. Need testing.

Maybe "SL_ANDROID_STREAM_SYSTEM" will be the best

i doubt it will make much difference. so not really worth getting into it.

Iv got it to output from alsa sound out but only aplay and it comes out garbled fartung noise. And it takes a while to get that farting noise ..

For me it works cool. What did you do?
Maybe that was an ALSA bug or something... I have installed alsa-utils and libasound2-plugins into chrooted debian environment (Linux Deploy app). I didn't build alsa for android yet.

Yes its android alsa...

"[ao/alsa] Invalid period size set."
When using mpv with alsa audio out

Debian mpv works cool too. I can not reproduce the bug on my device

Yeah its likely caused by bionic libc and the restrictions of app,
As in its meant to be our area ... damn it.

I have created repo for my friend. The script inside builds android native Pulseaudio. Can you try to integrate your version of alsa tools and mpv inside it? I can try to fix that

It contains only pulseaudio with its prerequisites build scripts

I have built android native alsa with pulseaudio module included. It works well. I think you were right about the problem in your area.

I can share source with you if you want

I think it's time to update the first message in this topic.
OpenSL ES is working (but testing)
slesTestFeedback.cpp was useless (for me)
My first try (SLES pipe player), if it is interesting, is here and it is based on intbufq.c
My second try is being tested in @its-pointless 's fork

Its likely due to restrictions of being non root.
From termux's build-package.sh
Remove from the NDK in favour of that from the libandroid-shmem.
Remove as it doesn't work for non-root.

i should probably update sox build as well. It can now playback.
Only issue is pulseaudio daemon and how we manage it.
Should it be on 100%? no
Should we add to the neccesary scripts to avoid library clashes a command to launch pulseaudio daemon?

I tried to build pulseaudio statically but it looks like pulseaudio can't use statically linked modules. My pulseaudio commandline looks like
adb shell HOME=/pulse TMPDIR=/pulse LD_LIBRARY_PATH=/usr/lib:/usr/lib/pulseaudio/:/usr/lib/pulse-11.1/modules/ /usr/bin/pulseaudio --disable-shm -n -F android-pulseaudio.conf --daemonize=false --use-pid-file=false --log-target=stderr --log-level=debug --system=false --dl-search-path=/usr/lib/pulse-11.1/modules/
May be we can try to modify src/pulsecore/module.c or libltdl/ltdl.c to use statically linked modules.

No its fine as is. The library clash is due to things like ffmpeg libs being in /system/lib and our $PREFIX/lib
Those ffmpeg libs are rom dependent and break stuff. So we preload our specific libs thst clash first then /system/lib then load $PREFIX/lib if you try loading our libs first opensles libs get missing symbols and incompatibility.

Only thing missing mic support ... :)

I think we need to finish work on output driver first.

i got unit tests working in termux
added libcheck etc

Anyway @fornwall is currently adding this to master branch thanks @twaik . Packages will be available shortly...

Thanks :)

Hi, regards, I like very much cmus, is my favorite music player and I love termux, and I really apreciate the work that you all are doing with cmus and termux, maybe I dont't know so much about c, but I want to help you all to make a good integration of cmus in android and that it runs almost natively on android, so I will start to compile and test yours forks of pulse audio, and I will try to debug , let you know about how works and bugs and all related about this, only I hope will be helpfull, so if you need help with some tasks do not hesitate to contact me, I will try to help you all in everything that I can do, so thank you very much for work in this, and let's go to make Open source, regards again, and I will be in contact.

cmus says:
cmus: error: neither USER or USERNAME environment variable set

@twaik @its-pointless
https://github.com/termux/termux-packages/blob/674a9bc39e1ea90fab7b04b2e8f2c37b95df8b5d/packages/libpulseaudio/module-sles-sink.c#L324
Are you sure this is appropriate and necessary? The default conf has 44.1kHz and 48kHz as default and alternate sample rate, which should really work fine on every phone (or rather, their SLES library). 32kHz doesn't even provide full band quality and the hard-coding makes it impossible for users to adjust.

I had a look and tried out different rates and they work on my devices. But it really should be getting that info at runtime but that is not implemented as yet.

Cmus asks for username on my aarch64 device and not arm which means im going to fix that at some point its on my todo list twaik.

I think the best solution is get termux-app to pass the device configuration as in get PROPERTY_OUTPUT_SAMPLE_RATE from audiomanager. This is the devices specific default sample rate normally in common terms 44100 or 48000. I think this would be enough to allow low latency.

https://gist.github.com/twaik/0eaee765c712ddd03fc9dc4eaac09cfc . Fixed setting sampling rate from config.

pass the device configuration as in get PROPERTY_OUTPUT_SAMPLE_RATE
Should I do that?

afaik there is no way to get that info in ndk only. I could be wrong though.

@meefik wrote minimal config for pulse, it can be useful.

load-module module-native-protocol-tcp auth-anonymous=true port=4712
load-module module-sles-sink

It is used in LinuxDeploy project.
commandline for pulse is
pulseaudio -D -n --exit-idle-time=-1 --disallow-exit=true --disable-shm=true --use-pid-file=false --log-target=stderr --dl-search-path=/data/user/0/ru.meefik.linuxdeploy/libs -F /data/user/0/ru.meefik.linuxdeploy/etc/pulseaudio.conf

@its-pointless what about export PROPERTY_OUTPUT_SAMPLE_RATE + getenv("PROPERTY_OUTPUT_SAMPLE_RATE");?

or just env PROPERTY_OUTPUT_SAMPLE_RATE=44100 pulseaudio?

Yeah that is what i was thinking and have termux-app export it on start.

@its-pointless done

check it out for errors, please

@twaik @its-pointless I still don't get why you hard-code (even dynamically) the sample rate (and other specs) in this sink module, instead of having it work in the expected way (that is, set the spec as the pulse conf).

Is it because you want to have pulse do the resampling instead of AudioTrack? I am not exactly sure what this PROPERTY_OUTPUT_SAMPLE_RATE is but I assume it is the rate that AudioTrack is using to open the device? If that's the case, note that the rate can often change on the run (e.g. from wired to bluetooth or usb dac, or vice versa), so after all it would be better if the user can set a preferred rate according to the daily use case.

@tomty89 The reason was in that.

you want to have pulse do the resampling instead of AudioTrack
Audiotrack resampling is disabled for PCM buffers.

the rate can often change on the run (e.g. from wired to bluetooth or usb dac, or vice versa)
Pulseaudio does not handle all that stuff, but Audioflinger does.

@twaik I am not sure what you mean by "disabled for PCM buffers". You mean the stage from AudioTrack to the device or the stage from OpenSL ES to AudioTrack?

AFAIK, OpenSL ES does not do any resampling or mixing but passes streams untouched to AudioTrack, while AudioTrack do the mixing and necessary resamplings dynamically to the rate set for the different devices in the policy conf (so the rate of the mix changes when there's a device change)

Ideally it would be great if Pulse can pass its streams to OpenSL ES untouched as well (or only do conversion when a specific stream exceeds the hard limits in OpenSL ES). However, you can't have a sink that does not do mixing so we need to have a spec for the mix, but there's almost no reason we want to prevent users from being able to set the spec just like what they do for other sink modules, especially when you can't have it keep track of the ultimate AudioTrack mix rate.

@twaik I am not sure how the issue (created by me) is relevant either. It was merely about a problem (and the reason is still unknown) with high rates in OpenAL. What does it have to do with Pulse or your sink module?

I am not sure what you mean by "disabled for PCM buffers"
When OpenSL source is "PCM buffers" (i.e. void buffer[2048]; ) Audiotrack expects it to be already resampled.

If that's the case, things would break if there's a change of device when one uses the pulse sink to get to OpenSL ES? Then the hard-coding is merely a partial solution?

@tomty89 hardcoding was a solution only for sound glitches. Pulseaudio on top of Android doesn't handle device connections, only sound. I just have connected and disconnected bluetooth headset during pulseaudio playback and nothing bad happened.

@twaik it seems like you still don't get what I am talking about. I know Pulse doesn't handle the device connection and routing, that's exactly the reason I don't get why you fetch PROPERTY_OUTPUT_SAMPLE_RATE and hard-code it as the sink rate, coz (I assume, again I am not sure what it is exactly) it changes with device and you can't make pulse keep track of that, so why don't you just allow user to set the sink rate?

@tomty89 Sorry, English is foreign language for me.
PROPERTY_OUTPUT_SAMPLE_RATE is just an environment variable can be exported by Termux if config file can not be rewritten for some reason.
PROPERTY_OUTPUT_SAMPLE_RATE is not Android variable or something. Android has its own property mechanism accessible by getprop and setprop commands.

so why don't you just allow user to set the sink rate?

I did it a few hours ago. Changes are waiting to be integrated into Termux.

PROPERTY_OUTPUT_SAMPLE_RATE is just an environment variable can be exported by Termux if config file can not be rewritten for some reason.

Huh? Reason like what? You can even have per-user pulse confs in $HOME/.config/pulse, not to mention that you don't even need root to change the general ones in $PREFIX/etc in the Termux case.

Though I guess it's okay if you are just adding a way to force it (I assume when it is unset forceFormat would be 0 and hence ss.rate would not be forced in that case. It's just that the way won't ever be useful.

I think the best solution is get termux-app to pass the device configuration as in get PROPERTY_OUTPUT_SAMPLE_RATE from audiomanager

It doesn't seem like @its-pointless shares the same idea with you (or me) though.

I think there is nothing wrong with making the sink rate configurable using a Pulseaudio configuration file, but it is important to have a way to pass the optimum sample rate _and chunk size_ from Java side to pulse. The reason is that, for devices that support it, only this combination of sample rate and chunk size has a chance of enabling 'fast mixer path', reduced latency audio processing. Since there are no getprop properties or NDK API that I know of from which these values can be obtained, the only way seems to be to pass them in from the Termux app itself.

Thanks to all who worked on this while I was busy with other things, much appreciated!

Added a new link to a video of a talk on high-performance audio on Android from 2017. It explains the requirements which must be met for 'fast mixer path' audio processing (starting at 13:50 in the video).

@ackalker First of all, is there even a way to get them? Second, is it even possible to have a pulse sink keep track of them? Coz they won't be a per-phone fixed values.

Since there are no getprop properties that I know of from which these values can be obtained, the only way seems to be to pass them in from the Termux app itself.

If you can not obtain them, what are you even going to pass? And why is it even related to the Termux app?

each device has a preferred chunk size sample rate. they generally resample to get that value. By only feeding it the correct values it skips the resample steps.
You can get that information from android os using termux-app on java side but not ndk side. Resson i want env variable as opposed to configuration is that this csn be done at runtime automatically. If we use configurarion file only it requires user changing it directly. Its anndroid, people aren't going to do it unless they research it.

@tomty89 See these lines in .../AudioBufferSize.java.
The values are obtained via properties of AudioManager instance, not global Android system properties which you can access using getprop. So the only way to obtain these values appears to be from Java code.

yep thanks for saying what i was struggling to say coffeeless ackalker

anyway twaik here is errors
/home/builder/.termux-build/libpulseaudio/src/src/modules/sles/module-sles-sink.c:173:17: error: non-void function 'pa_init_sles_player' should return a value [-Wreturn-type]
if (s == NULL) return;
^
/home/builder/.termux-build/libpulseaudio/src/src/modules/sles/module-sles-sink.c:196:14: warning: comparison of unsigned expression < 0 is always false [-Wtautological-compare]
if (sl_rate < 0) {
~~~ ^ ~
/home/builder/.termux-build/libpulseaudio/src/src/modules/sles/module-sles-sink.c:311:8: warning: duplicate 'static' declaration specifier [-Wduplicate-decl-specifier]
static static PA2SLrate(uint32_t rate){
^~~
/home/builder/.termux-build/libpulseaudio/src/src/modules/sles/module-sles-sink.c:311:15: warning: type specifier missing, defaults to 'int' [-Wimplicit-int]
static static PA2SLrate(uint32_t rate){
~~~~~ ^
/home/builder/.termux-build/libpulseaudio/src/src/modules/sles/module-sles-sink.c:312:8: error: use of undeclared identifier 'forceFormat'
if (!(forceFormat >= 8000 && <= 192000)) return -1;
^
/home/builder/.termux-build/libpulseaudio/src/src/modules/sles/module-sles-sink.c:312:31: error: expected expression
if (!(forceFormat >= 8000 && <= 192000)) return -1;
^
/home/builder/.termux-build/libpulseaudio/src/src/modules/sles/module-sles-sink.c:377:32: error: expected expression
if (forceFormat >= 8000 && <= 192000)
^
3 warnings and 4 errors generated.

I think it shouldn't be too hard to add something like an AudioParamsAPI.java to Termux:API app, then add a termux-audio-params script to the termux-api package which can be called in a wrapper script for pulseaudio to set environment variables to the right values.

there is a latency test in pulseaudio src/tests.
technically if the test is good enough we could have a script trying to find if any values are significantly lower to indicate the optimum output.

Interesting!

@ackalker Having the API thing is fine, but having the env var hack in the module is silly. It won't stick with the fast mixer path when there's a output device (hence "optimum rate") change, unless you
can keep track of that with a daemon or so.

So yeah it would be cool to have the termux-audio-params you proposed, so that users can have a general and straight-forward way to find out the optimum rates of the audio devices, but the best magic you can have is, a script that runs it and notify (reload) the sles sink module with whatever it gets. You don't need env vars for that, let alone hard-coding some names and magic in the module.

Instead of that env var hack, users would need / want to decide which device they care most in terms of the rate / fast mixer thing and set its optimum rate as default and alternate sample rate in daemon.conf (or as the rate param in default.pa). As for the script, most users probably won't even bother to use it.

P.S. Seriously Android audio makes no sense and this fast mixer thing is almost like a hoax to me. It's basically AudioTrack saying "if you want low latency, don't pass the resampling job to me! (Coz my resampler sucks?)"

Let's make it concrete. Say when I had Pulse started, I have Bluetooth audio connected (44.1kHz), after a while I have it disconnected and go back to wired audio (48kHz), how do you think the env var hard-coding or even the API thing is going to help making sure the sink rate stick with those rates respectively at different points?

We are talking about changing backend capabilities over time, because we are talking about violating an abstract of multiple devices. (OpenSL ES is not even the abstract, but another layer over it.)

Any attempt in making the sink aware of what's behind the scene is illogical, and yet the fast mixer thing requires us to do so, that's why I call it a hoax.

ALSA and Pulseaudio are integrated into the kernel. Your opinion is requested.

Most people have one audio out device they use. This is literally how android works and also... termux-api or java land could update it when new connection happens thus allowing a restart of thr daemon if need be. You are also thinking in terms of unix rather than what we actually are, an android app. this would be absurd in terms of unix or a rational or sensible os. Android is not like that.

ALSA and Pulseaudio are integrated into the kernel

Wrong. Kernel have ALSA subsystem, but Pulseaudio is userspace app.

@its-pointless

Most people have one audio out device they use.

Seriously? Even after I mentioned Bluetooth and USB Audio? Not to mention all those vendor-specific internal DAC switch solution and so.

termux-api or java land could update it when new connection happens thus allowing a restart of thr daemon if need be

Exactly, if you really want to implement it, you don't over-simplify the reality, but first of all make sure we can have something that keeps track of the rate and that the sink spec will be updated, but not just some ugly hack that aasume the rate is static. Otherwise we simply shouldn't have AudioTrack / fast mixer thing in mind. I don't think from the Android perspective we are ever suppose to anyway. The backend of the sink module is OpenSL ES, technically the limits of the layer/library is all we should care.

You are also thinking in terms of unix rather than what we actually are, an android app. this would be absurd in terms of unix or a rational or sensible os. Android is not like that.

I think you misunderstood my point. All I am saying is that the fast mixer thing isn't something that we should try to achieve, because it's doomed to be partial / ugly.

update it when new connection happens

Android redirects sound automatically when audio device is connected or disconnected. OpenSL ES does not provide tools to redirect sound manually except stream profiles (ring, system, voice, notification).

anyway its not compiling still
/home/builder/.termux-build/libpulseaudio/src/src/modules/sles/module-sles-sink.c:312:8: error: use of undeclared identifier 'forceFormat'
if (!(forceFormat >= 8000 && <= 192000)) return -1;
^
/home/builder/.termux-build/libpulseaudio/src/src/modules/sles/module-sles-sink.c:312:31: error: expected expression
if (!(forceFormat >= 8000 && <= 192000)) return -1;
^
/home/builder/.termux-build/libpulseaudio/src/src/modules/sles/module-sles-sink.c:377:32: error: expected expression
if (forceFormat >= 8000 && <= 192000)
^
i got it compiling but i did it by touching what the comment says to not touch.... so you probably want to do it properly.

Fixed compiling errors.
I written "Do not touch" for people who wants to get sample format and channel count from config. Format and sample are hardcoded into sles initialization process and I got segfaults during experiments, so I decided hardcoding is necessary.

@tomty89

PulseAudio supports any sample rate between 1 Hz and 192000 Hz.

I didn't check if it is right for OpenSL ES so I added support only for rates supported by most audio I/O devices as said in OpenSLES.h

/** These macros specify the commonly used sampling rates (in milliHertz) supported by most audio I/O devices. */

#define SL_SAMPLINGRATE_8       ((SLuint32) 8000000)
#define SL_SAMPLINGRATE_11_025      ((SLuint32) 11025000)
#define SL_SAMPLINGRATE_12      ((SLuint32) 12000000)
#define SL_SAMPLINGRATE_16      ((SLuint32) 16000000)
#define SL_SAMPLINGRATE_22_05       ((SLuint32) 22050000)
#define SL_SAMPLINGRATE_24      ((SLuint32) 24000000)
#define SL_SAMPLINGRATE_32      ((SLuint32) 32000000)
#define SL_SAMPLINGRATE_44_1        ((SLuint32) 44100000)
#define SL_SAMPLINGRATE_48      ((SLuint32) 48000000)
#define SL_SAMPLINGRATE_64      ((SLuint32) 64000000)
#define SL_SAMPLINGRATE_88_2        ((SLuint32) 88200000)
#define SL_SAMPLINGRATE_96      ((SLuint32) 96000000)
#define SL_SAMPLINGRATE_192     ((SLuint32) 192000000)

@twaik from what I can tell with mpv -v -ao opensles --audio-samplerate=$RATE $SOMEAUDIOFILE, $RATE can be any integer between 8000 and 192000. What I meant was, you don't need a switch for the conversion. See the linked line of mpv in my last comment.

@twaik another problem:
https://gist.github.com/twaik/0eaee765c712ddd03fc9dc4eaac09cfc#file-module-sles-sink-c-L376
See this:

$ cat test.c
#include <stdio.h>
#include <stdlib.h>

int main()
{
  int forceFormat = atoi(getenv("PROPERTY_OUTPUT_SAMPLE_RATE"));
  if (forceFormat)
    printf("%i\n", forceFormat);
}
$ cat test-.c
#include <stdio.h>
#include <stdlib.h>

int main()
{
  char* forceFormat = getenv("PROPERTY_OUTPUT_SAMPLE_RATE");
  if (forceFormat)
    printf("%i\n", atoi(forceFormat));
}



md5-91db42c1b8805ee1cc1d3c3d77b2ccbc



$ cc test.c -o test
$ cc test-.c -o test-
$ export PROPERTY_OUTPUT_SAMPLE_RATE=48000
$ ./test
48000
$ ./test-
48000
$ unset PROPERTY_OUTPUT_SAMPLE_RATE
$ ./test
Segmentation fault (core dumped)
$ ./test-
$

P.S. But seriously, you should really just get rid of this whole forceFormat env var thing.

Fixed. What is wrong with PA_SINK_LATENCY|PA_SINK_DYNAMIC_LATENCY?

@twaik
That line has nothing wrong, it was a mislink. I was just talking about the forceFormat line.

But no, it's not fixed. It segfaults with your new code as well. Why do you even involve strlen anyway? It's not fixing anything or making any sense.

$ cat sigh.c
#include <stdio.h>
#include <stdlib.h>

int main() {
  char *forceFormat_c = getenv("PROPERTY_OUTPUT_SAMPLE_RATE");
  int forceFormat = 0;
    if (strlen(forceFormat_c) > 4)
      forceFormat = atoi(forceFormat_c); //8000 is 4 symbols
    if (forceFormat >= 8000 && forceFormat <= 192000)
      printf("%i", forceFormat);
}
$ cc sigh.c
$ ./a.out
Segmentation fault (core dumped)
$

When PROPERTY_OUTPUT_SAMPLE_RATE is not set, getenv returns a null pointer, so you cannot just call a function like atoi (or strlen) on what getenv returns without checking if that's the case. Isn't it clearly enough shown in my code and tests?

Isn't it clearly enough shown in my code and tests?

Sorry, didn't compile it on my host. I was busy on second project. Looks like segfault is fixed.

I've used cmus + pulseaudio with the sles sink for a week or two now.
I've noticed that pulseaudio --daemon stop everytime cmus reaches the end of its playlist.
Is this the expected behaviour?
Logging pulseaudio -v I see (first line happens immediately after music stops)

I: [pulseaudio] sink-input.c: Freeing input 0 "playback"
I: [pulseaudio] client.c: Freed 0 "C* Music Player"
I: [pulseaudio] protocol-native.c: Connection died.
I: [pulseaudio] module-suspend-on-idle.c: Sink OpenSL_ES_sink idle for too long, suspending ...
I: [pulseaudio] core.c: We are idle, quitting...
I: [pulseaudio] main.c: Daemon shutdown initiated.
I: [pulseaudio] module.c: Unloading "module-sles-sink" (index: #16).
I: [pulseaudio] module-device-restore.c: Restoring volume for sink auto_null: front-left: 65536 / 100%,   front-right: 65536 / 100%
I: [pulseaudio] sink.c: Created sink 2 "auto_null" with sample spec s16le 2ch 44100Hz and channel map front-left,front-right
I: [pulseaudio] sink.c:     device.description = "Dummy Output"
I: [pulseaudio] sink.c:     device.class = "abstract"
I: [pulseaudio] sink.c:     device.icon_name = "audio-card"
I: [pulseaudio] source.c: Created source 2 "auto_null.monitor" with sample spec s16le 2ch 44100Hz and channel map front-left,front-right
I: [pulseaudio] source.c:     device.description = "Monitor of Dummy Output"
I: [pulseaudio] source.c:     device.class = "monitor"
I: [pulseaudio] source.c:     device.icon_name = "audio-input-microphone"
I: [pulseaudio] core.c: default_source: OpenSL_ES_sink.monitor -> auto_null.monitor
I: [pulseaudio] core.c: default_sink: OpenSL_ES_sink -> auto_null
I: [pulseaudio] module.c: Loaded "module-null-sink" (index: #17; argument: "sink_name=auto_null sink_properties='device.description="Dummy Output"'").
I: [pulseaudio] sink.c: Freeing sink 1 "OpenSL_ES_sink"
I: [pulseaudio] source.c: Freeing source 1 "OpenSL_ES_sink.monitor"
I: [pulseaudio] module.c: Unloaded "module-sles-sink" (index: #16).
I: [pulseaudio] module.c: Unloading "module-filter-apply" (index: #15).
### LOTS OF UNLOADING ###
W: [pulseaudio] module.c: After module unload, module 'module-null-sink' was still loaded!
I: [pulseaudio] module.c: Unloading "module-null-sink" (index: #17).
I: [pulseaudio] core.c: default_sink: auto_null -> (unset)
I: [pulseaudio] core.c: default_source: auto_null.monitor -> (unset)
I: [pulseaudio] sink.c: Freeing sink 2 "auto_null"
I: [pulseaudio] source.c: Freeing source 2 "auto_null.monitor"
I: [pulseaudio] module.c: Unloaded "module-null-sink" (index: #17).
I: [pulseaudio] main.c: Daemon terminated.

I've also experienced that termux has crashed three times during the last weeks (hasn't happened to me before), but I haven't logged the event yet so can't say for sure that it is related to pulseaudio.

I: [pulseaudio] module-suspend-on-idle.c: Sink OpenSL_ES_sink idle for too long, suspending ...
I: [pulseaudio] core.c: We are idle, quitting...
I: [pulseaudio] main.c: Daemon shutdown initiated.

Pulseaudio waits 20 seconds after last client disconnected and shuts down.
This behaviour can be disabled with "--exit-idle-time=-1" cmdline argument.
Linux Deploy project uses this cmdline:
pulseaudio -D -n --exit-idle-time=-1 --disallow-exit=true --disable-shm=true --use-pid-file=false --log-target=stderr --dl-search-path=/data/user/0/ru.meefik.linuxdeploy/libs -F /data/user/0/ru.meefik.linuxdeploy/etc/pulseaudio.conf

@twaik great, thanks for the info!

@its-pointless I think it should be in Termux release

yeah im just wondering the best way to do it. a seperate script or replace the current script with this?
should executing the script a second time kill the current one or not?

I think you should try both of them.

K iv got pull request up. Fixed a typo you made. Altered the conf in default settings. And changed how its configured so PA_BINARY actually points to the binary...

anyway i got Fast mix working. if you have root you can use /system/bin/dumpsys media.audio_flinger (will have to adjust LD_LIBRARY_PATH same as scripts we use for mpv and pulseaudio etc)

"FastMixer command=MIX_WRITE writeSequence=175587 framesWritten=21070320"
i used 44100 on this device i think 48000 worked as well on this device.
now do i think this is something we should go out of our way to support and setup this? no.

its also interesting in that i think it makes things slightly worse with IO usage causing skips etc. So it may be beneficial to avoid this for somethings.

The site says that fast mixer enables only for certain interfaces not involving signal processing. I changed the sink source.

Also this site says that the code can extract frames count that HAL buffer can hold.

AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
String framesPerBuffer = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
int framesPerBufferInt = Integer.parseInt(framesPerBuffer); // Convert to int
if (framesPerBufferInt == 0) framesPerBufferInt = 256; // Use default
am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
int framesPerBufferInt = Integer.parseInt(framesPerBuffer); // Convert to int
if (framesPerBufferInt == 0) framesPerBufferInt = 256; // Use default

I will add the code accepting the parameter later. It will be able to accept "PROPERTY_OUTPUT_FRAMES_PER_BUFFER" environment variable or "fbp" field in config file.

I just checked dumpsys and it says pulseaudio already uses fast mixer. I think the reason of high latency is the reason of this message:
shared memfd open() failed: Function not implemented
Memfd is disabled in Android kernels, but it can be easily reimplemented using ashmem (1, 2).
fake memfd_create function call implementation should be placed here

@its-pointless there's only one optimal rate per device, because it's the determined mix rate and Android does not do dynamic mix rate like Pulse does (the alternate sample rate thing). The mix rate would be the highest supported rate listed in the policy conf for a device (if unspecified simply the highest supported).

So if the optimal rate is a/the requirement, you can only get fast mixer at one rate per device.

Again device here doesn't mean a phone or so, but audio device(s), like internal audio device(s), usb audio devices, bluetooth audio. Each of them has its own mix rate.

... i know

i used 44100 on this device i think 48000 worked as well on this device.

Its interesting isn't it?
what is meant to happen and ehat appeared to happen diverged. Its why i mentioned it. It literally said it using fast...
if something that shouldn't happen hsppen should i not mention it?

What you havent accounted for... perhaps devices enable fast on more thah one frequency for compatibility. Again.., its why i mentioned it.

@its-pointless Was merely trying to clear things up. It might not have been a diversion either. I cannot tell for sure that no vendor by some means make its Android to route streams of different rate to different device or so. Even on my phone I can see audio of different apps being routed to different pcmNps (of the same "card") and they are being opened at different rate.

What you quoted doesn't seem like an indication of Termux/pulseaudio being a fast track either (according to @twaik 's link it should be some PID lines with F or not)

I don't have a rooted device. All I can check is logcat output on the AUDIO_OUTPUT_FLAG_FAST thing, which seems like a requirement of fast track (but not a guarantee). On my phone the logcat output does not divert from my theory.

you may be right in that regard.

I think first of all we need one correct way (command, script or programm) which will tell us that pulseaudio uses fast track.

*Maybe not the pulseaudio but PID uses fast track

F 3 yes 19493 3 00000001 00000003 31 480 A 1 48000 0 0 000B8B30 0xb0a11000 0x0 0x400 1152*
F 1 no 1339 1 00000001 00000003 24 13248 S 1 48000 -inf -inf 000033C0 0xb0a11000 0x0 0x600 0
F 2 no 807 1 00000001 00000003 26 3596 S 1 48000 -inf -inf 00001C18 0xb0a11000 0x0 0x600 0

That would indicate fast ?

it only prints the F when i use 48000
the yes indicates if its active
...
https://googlesamples.github.io/android-audio-high-performance/guides/audio-output-latency.html

What the commads prints that? My version of dumpsys media.audio_flinger prints another text.

that command. What version of android you using?

LineageOS 13. Device Samsung Galaxy S4 mini serranodsdd.

i did a small termux-api thing
output
{
"PROPERTY_OUTPUT_SAMPLE_RATE": "48000",
"PROPERTY_OUTPUT_FRAMES_PER_BUFFER": "1920"
}
i should ask if those values seem right?

also did the same thing on my android tv
{
"PROPERTY_OUTPUT_SAMPLE_RATE": "48000",
"PROPERTY_OUTPUT_FRAMES_PER_BUFFER": "2048"
}

Can I see the code?
By the way, another guy noticed the memory leak.

After playing music from debian for 2-3 hours I got just 20 mb avaible memory and when I killed the android pulseaudio it restored me 2,3 Gb avaible memory !

I need some time for investigation and improving the patch.
Offtop: what are you thinking about Xorg for Termux?

the code is java.
X can only work on android via that xserver by pelya and vnc.
@vishalbiswas got both working ages ago. Its just the extra work and bug fixing etc... is annoying and we don't have enough maintainers to also do x...

Looks like memory leak is fixed.

X can only work on android via that xserver by pelya and vnc.

I have Xorg running in bionic environment. But I have some problems with dlopen. dlopen(RTLD_LAZY) acts like dlopen(RTLD_NOW). I can share the code if you help me to fix that,

Im geting buffer underruns when android uses fast mixer

I'm still working on it. Pulseaudio clears buffer with pa_memblock_unref call. There are sound glitches if you use pa_memblock_unref and memory gobbling if you are not. I'll try to implement something like memblock unref queue to avoid underrun.

also i don't think rtld_lazy is supported in android. Limitations of the linker.

Yes, I know. I have seen @fornwall's bug report in android ndk repo. Just noticed. I used another way of linking in Xorg. And it works now.

Thank you all for the wonderful contribution! Thrilled to think if being able to use cmus inside Termux. Using Nexus 5X Android 8.1.0, installing cmus pulls libpulseaudio as a dependency. Installed pulseaudio myself, but I think the cross compilation somehow went wrong.

Failed to execute process '/data/data/com.termux/files/usr/bin/pulseaudio'. Reason:
exec: Exec format error
The file '/data/data/com.termux/files/usr/bin/pulseaudio' is marked as an executable but could not be run by the operating system.

Can you give me the device's /proc/cpuinfo content?

ummm is gitter not good enough?
anyway sent email...

pulseaudio cannot launch when I am trying to launch cmus.

u0_a117@grandpplte:/data/data/com.termux/files/home $ cmus
Failed to create random directory /data/local/tmp/pulse-twhmntjLOFgq: Permission denied

I fixed the error with chmod on /data/local/tmp but cmus still writes Error: opening audio device: internal error

You have lauch pulseadudio daemon yourself before

Not in Termux.

Of course X can work , just check of MaruOS .It even display out to external screen over HDMI.
I'm interested to help you to get X working inside your termux , but I dnt understand what you wanna do .
You wanna display X on your phone screen or over HDMI ?You wanna use a real mouse keyboard to control X11 apps or you wanna use touch of phone display?

X can work in termux its been done before i even got R gui working. It will be slow though using xsdl or vnc. Im very skeptical of getting opengl X apps working in a reasonable in android since opengl android drivers are very different to ones that work in x. What can be done though is to have opengl draw in an android app. You might be able to get opengl output of mpv on your device using a shared memory and integrating in termux with termux exposing opengl api.
Integrating that into a software output X doing kludge of some kind might kind of work but it will likely be terrible to actually use.

I can't really help since i do not do opengl. I just researched this a few years ago out of curiosity.

Yes about 3d acceleration .... Maybe a modified X11 GLX that comunicate with a android app ? So the android app to receive comamnds from X11 GLX and give back result ?

unless something has changed in last few years i doubt it can be made to work. Maybe with entirely open source drivers you could get something. I could be wrong though. Im no expert.

Somebody maybe tried a new version of libhybris and tell us if it works for 3d acceleration ...

Libhybris don't have Xorg driver. I found a way to share native window between Android app and executable without linking to libgui.so (the lib does not use Surfaceflinger directly). It can be handled with Android's window manager (the window will be drawn on applications SurfaceTexture) and will not draw the window on top of screen (i.e. not like these programs). I will publish source soon. For now I am adding autoconf stuff to the lib.

MaruOS have an mflinger daemon - it just reads dummy driver's current screen content and draws it onto a Surfaceflinger native window using lock(1) and unlockAndPost(0) calls.

Oke twaik that's great . I 'm exited to test your way to do it.

I think discussing X* and OpenGL/GLES stuff will be more appropriate here.

Looks like libpulse tries to connect to unix socket in path "$PULSE_RUNTIME_PATH/pulse/native:$XDG_RUNTIME_DIR/pulse/native:$HOME/pulse/native". You can add loading module-native-protocol-unix with $XDG_RUNTIME_DIR/ exported in termux and exporting PULSE_SERVER or patching pulseaudio to get audio work.

Hello,

everything seems to work much better now, as I had sound immediatly in Termux with mps-youtube / youtube-dl / mpv. Cmus is also working.
The only downside is, that the sound is not passing through an application, that alters the overall sound - it is called Viper4Android, a system-wide equalizer. Usually all apps on the system are passed through that app and I was a little surprised, that Termux somehow finds a way around it. It is actually the only way for me to listen to music on my phone, as I think the standard sound of any cellphone is really terrible.

Is there any way to pass the sound coming from Termux through that app as well?
Thanks for your help, any tip in which direction to search would be really nice.

Not really, unless you implement a pulse sink that makes use of the AudioTrack JNI (like VLC does for one of its outputs), and manage to run the pulse from the java layer (maybe by implementing some Termux API, idk).

Was this page helpful?
0 / 5 - 0 ratings

Related issues

bbtdev picture bbtdev  路  3Comments

roalyr picture roalyr  路  3Comments

thurask picture thurask  路  3Comments

roycebank picture roycebank  路  3Comments

Zuccace picture Zuccace  路  3Comments