summaryrefslogtreecommitdiff
diff options
authorJonathan Cave <jonathan.cave@canonical.com>2019-08-20 14:38:14 +0100
committerJonathan Cave <jonathan.cave@canonical.com>2019-08-20 14:38:14 +0100
commit300c637cf564ae009c55bb21d485d03999611544 (patch)
treea31d6534473b969ba4d31ec3722712382ca8ec68
parent7364f311b472055070884320b497cf21efc5aae5 (diff)
src: import alsa_test from p-p-snappy
-rw-r--r--src/EXECUTABLES1
-rw-r--r--src/Makefile6
-rw-r--r--src/alsa_test.cpp493
3 files changed, 498 insertions, 2 deletions
diff --git a/src/EXECUTABLES b/src/EXECUTABLES
index 1f505d9..943bfdf 100644
--- a/src/EXECUTABLES
+++ b/src/EXECUTABLES
@@ -1,2 +1,3 @@
+alsa_test
clocktest
threaded_memtest
diff --git a/src/Makefile b/src/Makefile
index 917e6f1..1978732 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -1,13 +1,15 @@
.PHONY:
-all: clocktest threaded_memtest
+all: alsa_test clocktest threaded_memtest
.PHONY: clean
clean:
- rm -f clocktest threaded_memtest
+ rm -f alsa_test clocktest threaded_memtest
threaded_memtest: CFLAGS += -pthread
threaded_memtest: CFLAGS += -Wno-unused-but-set-variable
clocktest: CFLAGS += -D_POSIX_C_SOURCE=199309L -D_BSD_SOURCE
clocktest: LDLIBS += -lrt
+alsa_test: CXXFLAGS += -std=c++11
+alsa_test: LDLIBS += -lasound -pthread
CFLAGS += -Wall
diff --git a/src/alsa_test.cpp b/src/alsa_test.cpp
new file mode 100644
index 0000000..5145c91
--- /dev/null
+++ b/src/alsa_test.cpp
@@ -0,0 +1,493 @@
+#include <algorithm>
+#include <cmath>
+#include <cstdint>
+#include <cstring>
+#include <iostream>
+#include <istream>
+#include <iterator>
+#include <limits>
+#include <map>
+#include <string>
+#include <sstream>
+#include <thread>
+#include <vector>
+#include <alsa/asoundlib.h>
+#include <complex>
+#include <valarray>
+
+typedef std::valarray<std::complex<float>> CArray;
+void fft(CArray& x)
+{
+ // almost the same implementation as one on rosetta code
+ const size_t N = x.size();
+ if (N <= 1) return;
+ CArray even = x[std::slice(0, N/2, 2)];
+ CArray odd = x[std::slice(1, N/2, 2)];
+ fft(even);
+ fft(odd);
+ for (size_t k = 0; k < N/2; ++k) {
+ auto t = std::polar(1.0f, -2 * float(M_PI) * k / N) * odd[k];
+ x[k] = even[k] + t;
+ x[k+N/2] = even[k] - t;
+ }
+}
+
+struct Logger {
+ enum class Level {normal, info, debug};
+ void set_level(Level new_lvl) {
+ level = new_lvl;
+ }
+ std::ostream& info() {
+ if (this->level >= Level::info) {
+ return std::cout;
+ } else {
+ return this->nullStream;
+ }
+ }
+ std::ostream& normal() {
+ return std::cout;
+ }
+ Logger() : level(Level::normal) {}
+ Logger(const Logger& l) {}
+
+private:
+ Level level;
+ struct NullStream : std::ostream {
+ template<typename T>
+ NullStream& operator<<(T const&) {}
+ };
+ NullStream nullStream;
+};
+
+Logger logger = Logger();
+
+std::vector<std::pair<std::string, std::string>> all_formats = {
+ {"float_44100", "Float32 encoded, 44100Hz sampling"},
+ {"float_48000", "Float32 encoded, 48000Hz sampling"},
+ {"int16_44100", "Signed Int16 encoded, 44100Hz sampling"},
+ {"int16_48000", "Signed Int16 encoded, 48000Hz sampling"},
+ {"uint16_44100", "Unsigned Int16 encoded, 44100 sampling"},
+ {"uint16_48000", "Unsigned Int16 encoded, 48000 sampling"}
+};
+
+namespace Alsa{
+
+using std::string;
+
+struct AlsaError: std::runtime_error {
+ explicit AlsaError(const string& what_arg) : runtime_error(what_arg) {}
+};
+
+template<class storage_type>
+struct Pcm {
+ enum class Mode {playback, capture};
+
+ Pcm() : Pcm{"default", Mode::playback} {}
+ Pcm(string device_name, Mode mode = Mode::playback) {
+ snd_pcm_stream_t stream_mode;
+ switch(mode) {
+ case Mode::playback:
+ stream_mode = SND_PCM_STREAM_PLAYBACK;
+ break;
+ case Mode::capture:
+ stream_mode = SND_PCM_STREAM_CAPTURE;
+ break;
+ }
+
+ int res = snd_pcm_open(&this->pcm_handle, device_name.c_str(),
+ stream_mode, 0 /* blocking */);
+ if (res < 0) {
+ auto ec = std::error_code(-res, std::system_category());
+ auto msg = string("Failed to open device: ") + string(device_name)
+ + string(". ") + string(snd_strerror(res));
+ throw std::system_error(ec, msg);
+ }
+ }
+ ~Pcm() {
+ snd_pcm_drain(this->pcm_handle);
+ snd_pcm_close(this->pcm_handle);
+ }
+ void drain() {
+ snd_pcm_drain(this->pcm_handle);
+ }
+ void set_params(const unsigned desired_rate) {
+ snd_pcm_hw_params_t *params = nullptr;
+ snd_pcm_hw_params_alloca(&params);
+ snd_pcm_hw_params_any(this->pcm_handle, params);
+ if (snd_pcm_hw_params_set_access(this->pcm_handle, params,
+ SND_PCM_ACCESS_RW_INTERLEAVED) < 0) {
+ throw AlsaError("Failed to set access mode");
+ }
+ if (snd_pcm_hw_params_set_channels(this->pcm_handle, params, 2) < 0) {
+ throw AlsaError("Failed to set the number of channels");
+ }
+ if (auto res = snd_pcm_hw_params_set_format(this->pcm_handle, params,
+ get_alsa_format()) < 0) {
+ throw AlsaError(string("Failed to set format") + string(
+ snd_strerror(res)));
+ }
+ this->rate = desired_rate;
+ // pick will determine how alsa picks value,
+ // 0: exact value, -1: closest smaller value, +1: closest bigger value
+ int pick = 0;
+ if (snd_pcm_hw_params_set_rate_near(this->pcm_handle, params,
+ &this->rate, &pick) < 0) {
+ throw AlsaError("Failed to set rate");
+ }
+ if (snd_pcm_hw_params(this->pcm_handle, params) < 0) {
+ throw AlsaError("Failed to write params to ALSA");
+ }
+ logger.info() << "got rate: " << rate << std::endl;
+ snd_pcm_uframes_t frames;
+ int dir;
+ auto res = snd_pcm_hw_params_get_period_size(params, &frames, &dir);
+ this->period = frames;
+ unsigned period_time;
+ snd_pcm_hw_params_get_period_time(params, &period_time, NULL);
+ logger.info() << "period_time: " << period_time << std::endl;
+ logger.info() << "state: " <<
+ snd_pcm_state_name(snd_pcm_state(this->pcm_handle)) << std::endl;
+ unsigned channs;
+ snd_pcm_hw_params_get_channels_max(params, &channs);
+ logger.info() << "no. of channels: " << channs << std::endl;
+ }
+ void sine(const float freq, const float duration, const float amplitude) const {
+ auto *buff = new storage_type[this->period * 2];
+ void *ugly_ptr = static_cast<void*>(buff);
+ unsigned t = 0;
+ while (t < float(this->rate) * duration) {
+ for (int i=0; i < this->period * 2; i+=2) {
+ auto sample = sin(2 * M_PI *((t + i/2) / (this->rate / freq)));
+ // we need to convert the sample to the target range, -1.0f should
+ // match the min_val and +1.0f should match the max_val
+ auto target_range = float(this->max_val()) - float(this->min_val());
+ sample = target_range * ((sample + 1.0f)/2.0f) + float(min_val());
+ // saturate/trim
+ if (sample > float(max_val()))
+ sample = max_val();
+ else if (sample < float(min_val()))
+ sample = min_val();
+ // set volume
+ sample *= amplitude;
+ buff[i] = sample;
+ buff[i+1] = buff[i]; // the other channel
+ }
+ auto res = snd_pcm_writei(this->pcm_handle, ugly_ptr, this->period);
+ if (res == -EPIPE) {
+ logger.info() << "Buffer underrun" << std::endl;
+ snd_pcm_prepare(this->pcm_handle);
+ }
+ t += this->period;
+ }
+ logger.info() << "state: " <<
+ snd_pcm_state_name(snd_pcm_state(this->pcm_handle)) << std::endl;
+ snd_pcm_start(this->pcm_handle);
+ delete[] buff;
+ }
+ void record(storage_type *buff, int buff_size /*in samples*/) {
+ auto *local_buff = new storage_type[this->period * 2];
+ int res;
+ snd_pcm_start(this->pcm_handle);
+ logger.info() << "state: " <<
+ snd_pcm_state_name(snd_pcm_state(this->pcm_handle)) << std::endl;
+
+ while(buff_size > 0) {
+ if (buff_size >= this->period * 2) {
+ void *ugly_ptr = static_cast<void*>(buff);
+ res = snd_pcm_readi(this->pcm_handle, ugly_ptr, this->period);
+ buff_size -= this->period * 2;
+ buff += this->period *2;
+ } else {
+ void *ugly_ptr = static_cast<void*>(local_buff);
+ res = snd_pcm_readi(this->pcm_handle, ugly_ptr, this->period);
+ std::memcpy(buff, local_buff, buff_size * sizeof(storage_type));
+ buff_size = 0;
+ }
+ }
+ delete[] local_buff;
+ }
+ void play(storage_type *buff, int buff_size) {
+ snd_pcm_prepare(this->pcm_handle);
+ while (buff_size > 0) {
+ void *ugly_ptr = static_cast<void*>(buff);
+ auto res = snd_pcm_writei(this->pcm_handle, ugly_ptr, this->period);
+ buff_size -= this->period *2;
+ buff += this->period * 2;
+ }
+ logger.info() << "state: " <<
+ snd_pcm_state_name(snd_pcm_state(this->pcm_handle)) << std::endl;
+ }
+
+
+private:
+ snd_pcm_format_t get_alsa_format();
+ bool is_little_endian() {
+ #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+ return true;
+ #else
+ return false;
+ #endif
+ }
+ storage_type min_val() const { return std::numeric_limits<storage_type>::min();}
+ storage_type max_val() const { return std::numeric_limits<storage_type>::max();}
+
+ snd_pcm_t *pcm_handle;
+ unsigned rate;
+ snd_pcm_uframes_t period;
+};
+
+template<>
+float Alsa::Pcm<float>::min_val() const { return -1.0f; }
+
+template<>
+float Alsa::Pcm<float>::max_val() const { return 1.0f; }
+
+template<>
+snd_pcm_format_t Alsa::Pcm<float>::get_alsa_format() {
+ return is_little_endian() ? SND_PCM_FORMAT_FLOAT_LE : SND_PCM_FORMAT_FLOAT_BE;
+}
+template<>
+snd_pcm_format_t Alsa::Pcm<double>::get_alsa_format() {
+ return is_little_endian() ? SND_PCM_FORMAT_FLOAT64_LE : SND_PCM_FORMAT_FLOAT64_BE;
+}
+template<>
+snd_pcm_format_t Alsa::Pcm<int16_t>::get_alsa_format() {
+ return is_little_endian() ? SND_PCM_FORMAT_S16_LE : SND_PCM_FORMAT_S16_BE;
+}
+template<>
+snd_pcm_format_t Alsa::Pcm<uint16_t>::get_alsa_format() {
+ return is_little_endian() ? SND_PCM_FORMAT_U16_LE : SND_PCM_FORMAT_U16_BE;
+}
+template<>
+snd_pcm_format_t Alsa::Pcm<int8_t>::get_alsa_format() {
+ return SND_PCM_FORMAT_S8;
+}
+template<>
+snd_pcm_format_t Alsa::Pcm<uint8_t>::get_alsa_format() {
+ return SND_PCM_FORMAT_U8;
+}
+
+struct Mixer {
+ Mixer(string card_name, string mixer_name) {
+ int res;
+ res = snd_mixer_open(&mixer_handle, 0);
+ if (res < 0) throw AlsaError("Failed to open an empty Mixer");
+ res = snd_mixer_attach(mixer_handle, card_name.c_str());
+ if (res < 0) throw AlsaError("Failed to attach HCTL to a Mixer");
+ res = snd_mixer_selem_register(mixer_handle, NULL, NULL);
+ if (res < 0) throw AlsaError("Failed to register a Mixer");
+ res = snd_mixer_load(mixer_handle);
+ if (res < 0) throw AlsaError("Failed to load a Mixer");
+ snd_mixer_selem_id_alloca(&sid);
+ snd_mixer_selem_id_set_index(sid, 0);
+ snd_mixer_selem_id_set_name(sid, mixer_name.c_str());
+ elem = snd_mixer_find_selem(mixer_handle, sid);
+ if (!elem) throw AlsaError(mixer_name + " mixer not found.");
+ }
+ ~Mixer() {
+ snd_mixer_close(mixer_handle);
+ }
+ void set_all_playback_volume(float volume) {
+ long min, max;
+ snd_mixer_selem_get_playback_volume_range(elem, &min, &max);
+ int new_vol = int(float(max) * volume);
+ snd_mixer_selem_set_playback_switch_all(elem, 1);
+ snd_mixer_selem_set_playback_volume_all(elem, new_vol);
+ }
+ void set_all_capture_volume(float volume) {
+ long min, max;
+ snd_mixer_selem_get_capture_volume_range(elem, &min, &max);
+ int new_vol = int(float(max) * volume);
+ snd_mixer_selem_set_capture_switch_all(elem, 1);
+ snd_mixer_selem_set_capture_volume_all(elem, new_vol);
+ }
+private:
+ snd_mixer_t *mixer_handle;
+ snd_mixer_selem_id_t *sid;
+ snd_mixer_elem_t* elem;
+};
+}; //namespace Alsa
+
+template<class storage_type>
+int playback_test(float duration, int sampling_rate, const char* capture_pcm, const char* playback_pcm) {
+ auto player = Alsa::Pcm<storage_type>();
+ player.set_params(sampling_rate);
+ player.sine(440, duration, 0.5f);
+}
+
+template<class storage_type>
+float dominant_freq(storage_type *buff, int buffsize, int rate) {
+ CArray data(buffsize);
+ for (int i=0; i < buffsize; i++) {
+ data[i] = std::complex<float>(buff[i], 0);
+ }
+ fft(data);
+ auto freqs = std::vector<float>(buffsize/2); // drop mirrored freqs
+ for (int i=0; i< buffsize / 2; i++){
+ freqs[i] = std::abs(data[i]);
+ }
+ auto it = std::max_element(freqs.begin(), freqs.end());
+ if (it != freqs.end()) {
+ return float(std::distance(freqs.begin(), it)) / (float(buffsize) / rate);
+ } else {
+ return 0.0f;
+ }
+}
+template<class storage_type>
+int loopback_test(float duration, int sampling_rate, const char* capture_pcm, const char* playback_pcm) {
+ int buffsize = static_cast<int>(ceil(float(sampling_rate * 2) * duration));
+ auto *buff = new storage_type[buffsize];
+ for (int attempt = 0; attempt < 3; ++attempt) {
+ for (int i=0; i<buffsize; i++) buff[i] = storage_type(0);
+ auto recorder = Alsa::Pcm<storage_type> (capture_pcm, Alsa::Pcm<storage_type>::Mode::capture);
+ recorder.set_params(sampling_rate);
+ std::thread rec_thread([&recorder, &buff, &buffsize]() mutable{
+ recorder.record(buff, buffsize);
+ });
+ const float test_freq = 440.0f;
+ auto player = Alsa::Pcm<storage_type>(playback_pcm);
+ player.set_params(sampling_rate);
+ player.sine(test_freq, duration, 0.5f);
+ player.drain();
+ rec_thread.join();
+ float dominant = dominant_freq<storage_type>(buff, buffsize, sampling_rate * 2);
+ if (dominant > 0.0f) {
+ //buff contains stereo samples, so the sampling rate can be considered 88200
+ logger.normal() << "Dominant frequency: " << dominant << std::endl;
+ // inverse-proportional to duration - the longer it runs,
+ // the more accurate the fft gets
+ float epsilon = 5 / duration + 1;
+ float deviation = abs(test_freq - dominant);
+ logger.normal() << "Deviation: " << deviation << std::endl;
+ if (deviation <= epsilon)
+ return 0;
+ }
+ }
+ return 1;
+}
+int list_formats(){
+ const char* env_var = std::getenv("ALSA_TEST_FORMATS");
+ std::vector<std::string> picked_formats;
+
+ if (env_var) {
+ std::stringstream ss(env_var);
+ std::istream_iterator<std::string> ss_iter(ss);
+ std::istream_iterator<std::string> end;
+ picked_formats = std::vector<std::string>(ss_iter, end);
+ } else {
+ // nothing specified in the envvar so let's just copy keys from all_formats
+ for (auto it: all_formats) {
+ picked_formats.push_back(it.first);
+ }
+ }
+
+ for (auto format: all_formats) {
+ if (find(picked_formats.begin(), picked_formats.end(), format.first) == picked_formats.end()) {
+ // format not picked
+ continue;
+ }
+ std::cout << "format: " << format.first << std::endl;
+ std::cout << "description: " << format.second << std::endl;
+ std::cout << std::endl;
+ }
+}
+
+void set_volumes(const std::string playback_pcm, const std::string capture_pcm) {
+ try {
+ auto playback_mixer = Alsa::Mixer(playback_pcm, "Master");
+ auto capture_mixer = Alsa::Mixer(capture_pcm, "Capture");
+ playback_mixer.set_all_playback_volume(0.75f);
+ capture_mixer.set_all_capture_volume(0.75f);
+ } catch(Alsa::AlsaError err) {
+ logger.normal() << "Failed to change volume: " << err.what() << std::endl;
+ // not being able to change the volume is not critical to the test
+ // and for some devices "Master" and "Caputre" mixers may not exist
+ // so let's just print warning if that's the case
+ }
+}
+
+int main(int argc, char *argv[]) {
+ std::vector<std::string> args{};
+ for (int i=0; i < argc; ++i) {
+ args.push_back(std::string(argv[i]));
+ }
+ if (std::find(args.begin(), args.end(), std::string("-v")) != args.end()) {
+ logger.set_level(Logger::Level::info);
+ }
+ auto format = std::string("int16_48000");
+ auto format_it = std::find(args.begin(), args.end(), std::string("--format"));
+ if (format_it != args.end()) { // not doing && because of sequence points
+ if (++format_it != args.end()) {
+ format = std::string(*format_it);
+ auto it = find_if(
+ all_formats.begin(), all_formats.end(),
+ [format](std::pair<std::string, std::string> p) {return p.first == format;});
+ if (it == all_formats.end()) {
+ std::cerr << "Unknown format: " << format << std::endl;
+ return 1;
+ }
+ }
+ }
+ auto sample_format = std::string(format.begin(), find(format.begin(), format.end(), '_'));
+ auto sampling_rate = atoi(
+ std::string(find(format.begin(), format.end(), '_') + 1, format.end()).c_str());
+ logger.info() << "Using format: " << sample_format <<
+ " and sampling rate: " << sampling_rate << std::endl;
+ std::map<std::string, int(*)(float, int, const char*, const char*)> scenarios;
+ if (sample_format == "float") {
+ scenarios["playback"] = playback_test<float>;
+ scenarios["loopback"] = loopback_test<float>;
+ }
+ else if (sample_format == "int16") {
+ scenarios["playback"] = playback_test<int16_t>;
+ scenarios["loopback"] = loopback_test<int16_t>;
+ }
+ else if (sample_format == "uint16") {
+ scenarios["playback"] = playback_test<uint16_t>;
+ scenarios["loopback"] = loopback_test<uint16_t>;
+ }
+ else {
+ assert(!"MISSING IF-ELSES FOR FORMATS");
+ }
+
+ if (args.size() < 2) {
+ std::cerr << "Required 'scenario' argument missing" << std::endl;
+ return 1;
+ }
+ float duration = 1.0f;
+ auto it = std::find(args.begin(), args.end(), std::string("-d"));
+ if (it != args.end()) { // not doing && because of sequence points
+ if (++it != args.end()) {
+ duration = std::stof(*it);
+ }
+ }
+ std::string capture_pcm{"default"};
+ it = std::find(args.begin(), args.end(), std::string("--capture-pcm"));
+ if (it != args.end()) {
+ if (++it != args.end()) {
+ capture_pcm = *it;
+ }
+ }
+ std::string playback_pcm{"default"};
+ it = std::find(args.begin(), args.end(), std::string("--playback-pcm"));
+ if (it != args.end()) { // not doing && because of sequence points
+ if (++it != args.end()) {
+ playback_pcm = *it;
+ }
+ }
+ set_volumes(playback_pcm, capture_pcm);
+ std::string scenario{args[1]};
+ if (scenario == "playback") {
+ return scenarios["playback"](duration, sampling_rate, capture_pcm.c_str(), playback_pcm.c_str());
+ }
+ else if (scenario == "loopback") {
+ return scenarios["loopback"](duration, sampling_rate, capture_pcm.c_str(), playback_pcm.c_str());
+ }
+ else if (scenario == "list-formats") {
+ return list_formats();
+ }
+ if (scenarios.find(args[1]) == scenarios.end()) {
+ std::cerr << args[1] << " scenario not found!" << std::endl;
+ return 1;
+ }
+}