aboutsummaryrefslogtreecommitdiff
path: root/webrtc/sound/alsasoundsystem.cc
diff options
context:
space:
mode:
Diffstat (limited to 'webrtc/sound/alsasoundsystem.cc')
-rw-r--r--webrtc/sound/alsasoundsystem.cc743
1 files changed, 743 insertions, 0 deletions
diff --git a/webrtc/sound/alsasoundsystem.cc b/webrtc/sound/alsasoundsystem.cc
new file mode 100644
index 0000000000..3cc77a988c
--- /dev/null
+++ b/webrtc/sound/alsasoundsystem.cc
@@ -0,0 +1,743 @@
+/*
+ * Copyright 2004 The WebRTC Project Authors. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+#include "webrtc/sound/alsasoundsystem.h"
+
+#include <algorithm>
+#include "webrtc/sound/sounddevicelocator.h"
+#include "webrtc/sound/soundinputstreaminterface.h"
+#include "webrtc/sound/soundoutputstreaminterface.h"
+#include "webrtc/base/common.h"
+#include "webrtc/base/logging.h"
+#include "webrtc/base/scoped_ptr.h"
+#include "webrtc/base/stringutils.h"
+#include "webrtc/base/timeutils.h"
+#include "webrtc/base/worker.h"
+
+namespace rtc {
+
+// Lookup table from the rtc format enum in soundsysteminterface.h to
+// ALSA's enums.
+static const snd_pcm_format_t kCricketFormatToAlsaFormatTable[] = {
+ // The order here must match the order in soundsysteminterface.h
+ SND_PCM_FORMAT_S16_LE,
+};
+
+// Lookup table for the size of a single sample of a given format.
+static const size_t kCricketFormatToSampleSizeTable[] = {
+ // The order here must match the order in soundsysteminterface.h
+ sizeof(int16_t), // 2
+};
+
+// Minimum latency we allow, in microseconds. This is more or less arbitrary,
+// but it has to be at least large enough to be able to buffer data during a
+// missed context switch, and the typical Linux scheduling quantum is 10ms.
+static const int kMinimumLatencyUsecs = 20 * 1000;
+
+// The latency we'll use for kNoLatencyRequirements (chosen arbitrarily).
+static const int kDefaultLatencyUsecs = kMinimumLatencyUsecs * 2;
+
+// We translate newlines in ALSA device descriptions to hyphens.
+static const char kAlsaDescriptionSearch[] = "\n";
+static const char kAlsaDescriptionReplace[] = " - ";
+
+class AlsaDeviceLocator : public SoundDeviceLocator {
+ public:
+ AlsaDeviceLocator(const std::string &name,
+ const std::string &device_name)
+ : SoundDeviceLocator(name, device_name) {
+ // The ALSA descriptions have newlines in them, which won't show up in
+ // a drop-down box. Replace them with hyphens.
+ rtc::replace_substrs(kAlsaDescriptionSearch,
+ sizeof(kAlsaDescriptionSearch) - 1,
+ kAlsaDescriptionReplace,
+ sizeof(kAlsaDescriptionReplace) - 1,
+ &name_);
+ }
+
+ SoundDeviceLocator *Copy() const override {
+ return new AlsaDeviceLocator(*this);
+ }
+};
+
+// Functionality that is common to both AlsaInputStream and AlsaOutputStream.
+class AlsaStream {
+ public:
+ AlsaStream(AlsaSoundSystem *alsa,
+ snd_pcm_t *handle,
+ size_t frame_size,
+ int wait_timeout_ms,
+ int flags,
+ int freq)
+ : alsa_(alsa),
+ handle_(handle),
+ frame_size_(frame_size),
+ wait_timeout_ms_(wait_timeout_ms),
+ flags_(flags),
+ freq_(freq) {
+ }
+
+ ~AlsaStream() {
+ Close();
+ }
+
+ // Waits for the stream to be ready to accept/return more data, and returns
+ // how much can be written/read, or 0 if we need to Wait() again.
+ snd_pcm_uframes_t Wait() {
+ snd_pcm_sframes_t frames;
+ // Ideally we would not use snd_pcm_wait() and instead hook snd_pcm_poll_*
+ // into PhysicalSocketServer, but PhysicalSocketServer is nasty enough
+ // already and the current clients of SoundSystemInterface do not run
+ // anything else on their worker threads, so snd_pcm_wait() is good enough.
+ frames = symbol_table()->snd_pcm_avail_update()(handle_);
+ if (frames < 0) {
+ LOG(LS_ERROR) << "snd_pcm_avail_update(): " << GetError(frames);
+ Recover(frames);
+ return 0;
+ } else if (frames > 0) {
+ // Already ready, so no need to wait.
+ return frames;
+ }
+ // Else no space/data available, so must wait.
+ int ready = symbol_table()->snd_pcm_wait()(handle_, wait_timeout_ms_);
+ if (ready < 0) {
+ LOG(LS_ERROR) << "snd_pcm_wait(): " << GetError(ready);
+ Recover(ready);
+ return 0;
+ } else if (ready == 0) {
+ // Timeout, so nothing can be written/read right now.
+ // We set the timeout to twice the requested latency, so continuous
+ // timeouts are indicative of a problem, so log as a warning.
+ LOG(LS_WARNING) << "Timeout while waiting on stream";
+ return 0;
+ }
+ // Else ready > 0 (i.e., 1), so it's ready. Get count.
+ frames = symbol_table()->snd_pcm_avail_update()(handle_);
+ if (frames < 0) {
+ LOG(LS_ERROR) << "snd_pcm_avail_update(): " << GetError(frames);
+ Recover(frames);
+ return 0;
+ } else if (frames == 0) {
+ // wait() said we were ready, so this ought to have been positive. Has
+ // been observed to happen in practice though.
+ LOG(LS_WARNING) << "Spurious wake-up";
+ }
+ return frames;
+ }
+
+ int CurrentDelayUsecs() {
+ if (!(flags_ & SoundSystemInterface::FLAG_REPORT_LATENCY)) {
+ return 0;
+ }
+
+ snd_pcm_sframes_t delay;
+ int err = symbol_table()->snd_pcm_delay()(handle_, &delay);
+ if (err != 0) {
+ LOG(LS_ERROR) << "snd_pcm_delay(): " << GetError(err);
+ Recover(err);
+ // We'd rather continue playout/capture with an incorrect delay than stop
+ // it altogether, so return a valid value.
+ return 0;
+ }
+ // The delay is in frames. Convert to microseconds.
+ return delay * rtc::kNumMicrosecsPerSec / freq_;
+ }
+
+ // Used to recover from certain recoverable errors, principally buffer overrun
+ // or underrun (identified as EPIPE). Without calling this the stream stays
+ // in the error state forever.
+ bool Recover(int error) {
+ int err;
+ err = symbol_table()->snd_pcm_recover()(
+ handle_,
+ error,
+ // Silent; i.e., no logging on stderr.
+ 1);
+ if (err != 0) {
+ // Docs say snd_pcm_recover returns the original error if it is not one
+ // of the recoverable ones, so this log message will probably contain the
+ // same error twice.
+ LOG(LS_ERROR) << "Unable to recover from \"" << GetError(error) << "\": "
+ << GetError(err);
+ return false;
+ }
+ if (error == -EPIPE && // Buffer underrun/overrun.
+ symbol_table()->snd_pcm_stream()(handle_) == SND_PCM_STREAM_CAPTURE) {
+ // For capture streams we also have to repeat the explicit start() to get
+ // data flowing again.
+ err = symbol_table()->snd_pcm_start()(handle_);
+ if (err != 0) {
+ LOG(LS_ERROR) << "snd_pcm_start(): " << GetError(err);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ bool Close() {
+ if (handle_) {
+ int err;
+ err = symbol_table()->snd_pcm_drop()(handle_);
+ if (err != 0) {
+ LOG(LS_ERROR) << "snd_pcm_drop(): " << GetError(err);
+ // Continue anyways.
+ }
+ err = symbol_table()->snd_pcm_close()(handle_);
+ if (err != 0) {
+ LOG(LS_ERROR) << "snd_pcm_close(): " << GetError(err);
+ // Continue anyways.
+ }
+ handle_ = NULL;
+ }
+ return true;
+ }
+
+ AlsaSymbolTable *symbol_table() {
+ return &alsa_->symbol_table_;
+ }
+
+ snd_pcm_t *handle() {
+ return handle_;
+ }
+
+ const char *GetError(int err) {
+ return alsa_->GetError(err);
+ }
+
+ size_t frame_size() {
+ return frame_size_;
+ }
+
+ private:
+ AlsaSoundSystem *alsa_;
+ snd_pcm_t *handle_;
+ size_t frame_size_;
+ int wait_timeout_ms_;
+ int flags_;
+ int freq_;
+
+ RTC_DISALLOW_COPY_AND_ASSIGN(AlsaStream);
+};
+
+// Implementation of an input stream. See soundinputstreaminterface.h regarding
+// thread-safety.
+class AlsaInputStream :
+ public SoundInputStreamInterface,
+ private rtc::Worker {
+ public:
+ AlsaInputStream(AlsaSoundSystem *alsa,
+ snd_pcm_t *handle,
+ size_t frame_size,
+ int wait_timeout_ms,
+ int flags,
+ int freq)
+ : stream_(alsa, handle, frame_size, wait_timeout_ms, flags, freq),
+ buffer_size_(0) {
+ }
+
+ ~AlsaInputStream() override {
+ bool success = StopReading();
+ // We need that to live.
+ VERIFY(success);
+ }
+
+ bool StartReading() override {
+ return StartWork();
+ }
+
+ bool StopReading() override {
+ return StopWork();
+ }
+
+ bool GetVolume(int *volume) override {
+ // TODO: Implement this.
+ return false;
+ }
+
+ bool SetVolume(int volume) override {
+ // TODO: Implement this.
+ return false;
+ }
+
+ bool Close() override {
+ return StopReading() && stream_.Close();
+ }
+
+ int LatencyUsecs() override {
+ return stream_.CurrentDelayUsecs();
+ }
+
+ private:
+ // Inherited from Worker.
+ void OnStart() override {
+ HaveWork();
+ }
+
+ // Inherited from Worker.
+ void OnHaveWork() override {
+ // Block waiting for data.
+ snd_pcm_uframes_t avail = stream_.Wait();
+ if (avail > 0) {
+ // Data is available.
+ size_t size = avail * stream_.frame_size();
+ if (size > buffer_size_) {
+ // Must increase buffer size.
+ buffer_.reset(new char[size]);
+ buffer_size_ = size;
+ }
+ // Read all the data.
+ snd_pcm_sframes_t read = stream_.symbol_table()->snd_pcm_readi()(
+ stream_.handle(),
+ buffer_.get(),
+ avail);
+ if (read < 0) {
+ LOG(LS_ERROR) << "snd_pcm_readi(): " << GetError(read);
+ stream_.Recover(read);
+ } else if (read == 0) {
+ // Docs say this shouldn't happen.
+ ASSERT(false);
+ LOG(LS_ERROR) << "No data?";
+ } else {
+ // Got data. Pass it off to the app.
+ SignalSamplesRead(buffer_.get(),
+ read * stream_.frame_size(),
+ this);
+ }
+ }
+ // Check for more data with no delay, after any pending messages are
+ // dispatched.
+ HaveWork();
+ }
+
+ // Inherited from Worker.
+ void OnStop() override {
+ // Nothing to do.
+ }
+
+ const char *GetError(int err) {
+ return stream_.GetError(err);
+ }
+
+ AlsaStream stream_;
+ rtc::scoped_ptr<char[]> buffer_;
+ size_t buffer_size_;
+
+ RTC_DISALLOW_COPY_AND_ASSIGN(AlsaInputStream);
+};
+
+// Implementation of an output stream. See soundoutputstreaminterface.h
+// regarding thread-safety.
+class AlsaOutputStream : public SoundOutputStreamInterface,
+ private rtc::Worker {
+ public:
+ AlsaOutputStream(AlsaSoundSystem *alsa,
+ snd_pcm_t *handle,
+ size_t frame_size,
+ int wait_timeout_ms,
+ int flags,
+ int freq)
+ : stream_(alsa, handle, frame_size, wait_timeout_ms, flags, freq) {
+ }
+
+ ~AlsaOutputStream() override {
+ bool success = DisableBufferMonitoring();
+ // We need that to live.
+ VERIFY(success);
+ }
+
+ bool EnableBufferMonitoring() override {
+ return StartWork();
+ }
+
+ bool DisableBufferMonitoring() override {
+ return StopWork();
+ }
+
+ bool WriteSamples(const void *sample_data, size_t size) override {
+ if (size % stream_.frame_size() != 0) {
+ // No client of SoundSystemInterface does this, so let's not support it.
+ // (If we wanted to support it, we'd basically just buffer the fractional
+ // frame until we get more data.)
+ ASSERT(false);
+ LOG(LS_ERROR) << "Writes with fractional frames are not supported";
+ return false;
+ }
+ snd_pcm_uframes_t frames = size / stream_.frame_size();
+ snd_pcm_sframes_t written = stream_.symbol_table()->snd_pcm_writei()(
+ stream_.handle(),
+ sample_data,
+ frames);
+ if (written < 0) {
+ LOG(LS_ERROR) << "snd_pcm_writei(): " << GetError(written);
+ stream_.Recover(written);
+ return false;
+ } else if (static_cast<snd_pcm_uframes_t>(written) < frames) {
+ // Shouldn't happen. Drop the rest of the data.
+ LOG(LS_ERROR) << "Stream wrote only " << written << " of " << frames
+ << " frames!";
+ return false;
+ }
+ return true;
+ }
+
+ bool GetVolume(int *volume) override {
+ // TODO: Implement this.
+ return false;
+ }
+
+ bool SetVolume(int volume) override {
+ // TODO: Implement this.
+ return false;
+ }
+
+ bool Close() override {
+ return DisableBufferMonitoring() && stream_.Close();
+ }
+
+ int LatencyUsecs() override {
+ return stream_.CurrentDelayUsecs();
+ }
+
+ private:
+ // Inherited from Worker.
+ void OnStart() override {
+ HaveWork();
+ }
+
+ // Inherited from Worker.
+ void OnHaveWork() override {
+ snd_pcm_uframes_t avail = stream_.Wait();
+ if (avail > 0) {
+ size_t space = avail * stream_.frame_size();
+ SignalBufferSpace(space, this);
+ }
+ HaveWork();
+ }
+
+ // Inherited from Worker.
+ void OnStop() override {
+ // Nothing to do.
+ }
+
+ const char *GetError(int err) {
+ return stream_.GetError(err);
+ }
+
+ AlsaStream stream_;
+
+ RTC_DISALLOW_COPY_AND_ASSIGN(AlsaOutputStream);
+};
+
+AlsaSoundSystem::AlsaSoundSystem() : initialized_(false) {}
+
+AlsaSoundSystem::~AlsaSoundSystem() {
+ // Not really necessary, because Terminate() doesn't really do anything.
+ Terminate();
+}
+
+bool AlsaSoundSystem::Init() {
+ if (IsInitialized()) {
+ return true;
+ }
+
+ // Load libasound.
+ if (!symbol_table_.Load()) {
+ // Very odd for a Linux machine to not have a working libasound ...
+ LOG(LS_ERROR) << "Failed to load symbol table";
+ return false;
+ }
+
+ initialized_ = true;
+
+ return true;
+}
+
+void AlsaSoundSystem::Terminate() {
+ if (!IsInitialized()) {
+ return;
+ }
+
+ initialized_ = false;
+
+ // We do not unload the symbol table because we may need it again soon if
+ // Init() is called again.
+}
+
+bool AlsaSoundSystem::EnumeratePlaybackDevices(
+ SoundDeviceLocatorList *devices) {
+ return EnumerateDevices(devices, false);
+}
+
+bool AlsaSoundSystem::EnumerateCaptureDevices(
+ SoundDeviceLocatorList *devices) {
+ return EnumerateDevices(devices, true);
+}
+
+bool AlsaSoundSystem::GetDefaultPlaybackDevice(SoundDeviceLocator **device) {
+ return GetDefaultDevice(device);
+}
+
+bool AlsaSoundSystem::GetDefaultCaptureDevice(SoundDeviceLocator **device) {
+ return GetDefaultDevice(device);
+}
+
+SoundOutputStreamInterface *AlsaSoundSystem::OpenPlaybackDevice(
+ const SoundDeviceLocator *device,
+ const OpenParams &params) {
+ return OpenDevice<SoundOutputStreamInterface>(
+ device,
+ params,
+ SND_PCM_STREAM_PLAYBACK,
+ &AlsaSoundSystem::StartOutputStream);
+}
+
+SoundInputStreamInterface *AlsaSoundSystem::OpenCaptureDevice(
+ const SoundDeviceLocator *device,
+ const OpenParams &params) {
+ return OpenDevice<SoundInputStreamInterface>(
+ device,
+ params,
+ SND_PCM_STREAM_CAPTURE,
+ &AlsaSoundSystem::StartInputStream);
+}
+
+const char *AlsaSoundSystem::GetName() const {
+ return "ALSA";
+}
+
+bool AlsaSoundSystem::EnumerateDevices(
+ SoundDeviceLocatorList *devices,
+ bool capture_not_playback) {
+ ClearSoundDeviceLocatorList(devices);
+
+ if (!IsInitialized()) {
+ return false;
+ }
+
+ const char *type = capture_not_playback ? "Input" : "Output";
+ // dmix and dsnoop are only for playback and capture, respectively, but ALSA
+ // stupidly includes them in both lists.
+ const char *ignore_prefix = capture_not_playback ? "dmix:" : "dsnoop:";
+ // (ALSA lists many more "devices" of questionable interest, but we show them
+ // just in case the weird devices may actually be desirable for some
+ // users/systems.)
+ const char *ignore_default = "default";
+ const char *ignore_null = "null";
+ const char *ignore_pulse = "pulse";
+ // The 'pulse' entry has a habit of mysteriously disappearing when you query
+ // a second time. Remove it from our list. (GIPS lib did the same thing.)
+ int err;
+
+ void **hints;
+ err = symbol_table_.snd_device_name_hint()(-1, // All cards
+ "pcm", // Only PCM devices
+ &hints);
+ if (err != 0) {
+ LOG(LS_ERROR) << "snd_device_name_hint(): " << GetError(err);
+ return false;
+ }
+
+ for (void **list = hints; *list != NULL; ++list) {
+ char *actual_type = symbol_table_.snd_device_name_get_hint()(*list, "IOID");
+ if (actual_type) { // NULL means it's both.
+ bool wrong_type = (strcmp(actual_type, type) != 0);
+ free(actual_type);
+ if (wrong_type) {
+ // Wrong type of device (i.e., input vs. output).
+ continue;
+ }
+ }
+
+ char *name = symbol_table_.snd_device_name_get_hint()(*list, "NAME");
+ if (!name) {
+ LOG(LS_ERROR) << "Device has no name???";
+ // Skip it.
+ continue;
+ }
+
+ // Now check if we actually want to show this device.
+ if (strcmp(name, ignore_default) != 0 &&
+ strcmp(name, ignore_null) != 0 &&
+ strcmp(name, ignore_pulse) != 0 &&
+ !rtc::starts_with(name, ignore_prefix)) {
+
+ // Yes, we do.
+ char *desc = symbol_table_.snd_device_name_get_hint()(*list, "DESC");
+ if (!desc) {
+ // Virtual devices don't necessarily have descriptions. Use their names
+ // instead (not pretty!).
+ desc = name;
+ }
+
+ AlsaDeviceLocator *device = new AlsaDeviceLocator(desc, name);
+
+ devices->push_back(device);
+
+ if (desc != name) {
+ free(desc);
+ }
+ }
+
+ free(name);
+ }
+
+ err = symbol_table_.snd_device_name_free_hint()(hints);
+ if (err != 0) {
+ LOG(LS_ERROR) << "snd_device_name_free_hint(): " << GetError(err);
+ // Continue and return true anyways, since we did get the whole list.
+ }
+
+ return true;
+}
+
+bool AlsaSoundSystem::GetDefaultDevice(SoundDeviceLocator **device) {
+ if (!IsInitialized()) {
+ return false;
+ }
+ *device = new AlsaDeviceLocator("Default device", "default");
+ return true;
+}
+
+inline size_t AlsaSoundSystem::FrameSize(const OpenParams &params) {
+ ASSERT(static_cast<int>(params.format) <
+ ARRAY_SIZE(kCricketFormatToSampleSizeTable));
+ return kCricketFormatToSampleSizeTable[params.format] * params.channels;
+}
+
+template <typename StreamInterface>
+StreamInterface *AlsaSoundSystem::OpenDevice(
+ const SoundDeviceLocator *device,
+ const OpenParams &params,
+ snd_pcm_stream_t type,
+ StreamInterface *(AlsaSoundSystem::*start_fn)(
+ snd_pcm_t *handle,
+ size_t frame_size,
+ int wait_timeout_ms,
+ int flags,
+ int freq)) {
+
+ if (!IsInitialized()) {
+ return NULL;
+ }
+
+ StreamInterface *stream;
+ int err;
+
+ const char *dev = static_cast<const AlsaDeviceLocator *>(device)->
+ device_name().c_str();
+
+ snd_pcm_t *handle = NULL;
+ err = symbol_table_.snd_pcm_open()(
+ &handle,
+ dev,
+ type,
+ // No flags.
+ 0);
+ if (err != 0) {
+ LOG(LS_ERROR) << "snd_pcm_open(" << dev << "): " << GetError(err);
+ return NULL;
+ }
+ LOG(LS_VERBOSE) << "Opening " << dev;
+ ASSERT(handle); // If open succeeded, handle ought to be valid
+
+ // Compute requested latency in microseconds.
+ int latency;
+ if (params.latency == kNoLatencyRequirements) {
+ latency = kDefaultLatencyUsecs;
+ } else {
+ // kLowLatency is 0, so we treat it the same as a request for zero latency.
+ // Compute what the user asked for.
+ latency = rtc::kNumMicrosecsPerSec *
+ params.latency /
+ params.freq /
+ FrameSize(params);
+ // And this is what we'll actually use.
+ latency = std::max(latency, kMinimumLatencyUsecs);
+ }
+
+ ASSERT(static_cast<int>(params.format) <
+ ARRAY_SIZE(kCricketFormatToAlsaFormatTable));
+
+ err = symbol_table_.snd_pcm_set_params()(
+ handle,
+ kCricketFormatToAlsaFormatTable[params.format],
+ // SoundSystemInterface only supports interleaved audio.
+ SND_PCM_ACCESS_RW_INTERLEAVED,
+ params.channels,
+ params.freq,
+ 1, // Allow ALSA to resample.
+ latency);
+ if (err != 0) {
+ LOG(LS_ERROR) << "snd_pcm_set_params(): " << GetError(err);
+ goto fail;
+ }
+
+ err = symbol_table_.snd_pcm_prepare()(handle);
+ if (err != 0) {
+ LOG(LS_ERROR) << "snd_pcm_prepare(): " << GetError(err);
+ goto fail;
+ }
+
+ stream = (this->*start_fn)(
+ handle,
+ FrameSize(params),
+ // We set the wait time to twice the requested latency, so that wait
+ // timeouts should be rare.
+ 2 * latency / rtc::kNumMicrosecsPerMillisec,
+ params.flags,
+ params.freq);
+ if (stream) {
+ return stream;
+ }
+ // Else fall through.
+
+ fail:
+ err = symbol_table_.snd_pcm_close()(handle);
+ if (err != 0) {
+ LOG(LS_ERROR) << "snd_pcm_close(): " << GetError(err);
+ }
+ return NULL;
+}
+
+SoundOutputStreamInterface *AlsaSoundSystem::StartOutputStream(
+ snd_pcm_t *handle,
+ size_t frame_size,
+ int wait_timeout_ms,
+ int flags,
+ int freq) {
+ // Nothing to do here but instantiate the stream.
+ return new AlsaOutputStream(
+ this, handle, frame_size, wait_timeout_ms, flags, freq);
+}
+
+SoundInputStreamInterface *AlsaSoundSystem::StartInputStream(
+ snd_pcm_t *handle,
+ size_t frame_size,
+ int wait_timeout_ms,
+ int flags,
+ int freq) {
+ // Output streams start automatically once enough data has been written, but
+ // input streams must be started manually or else snd_pcm_wait() will never
+ // return true.
+ int err;
+ err = symbol_table_.snd_pcm_start()(handle);
+ if (err != 0) {
+ LOG(LS_ERROR) << "snd_pcm_start(): " << GetError(err);
+ return NULL;
+ }
+ return new AlsaInputStream(
+ this, handle, frame_size, wait_timeout_ms, flags, freq);
+}
+
+inline const char *AlsaSoundSystem::GetError(int err) {
+ return symbol_table_.snd_strerror()(err);
+}
+
+} // namespace rtc