diff options
Diffstat (limited to 'webrtc/sound/alsasoundsystem.cc')
-rw-r--r-- | webrtc/sound/alsasoundsystem.cc | 743 |
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 ¶ms) { + return OpenDevice<SoundOutputStreamInterface>( + device, + params, + SND_PCM_STREAM_PLAYBACK, + &AlsaSoundSystem::StartOutputStream); +} + +SoundInputStreamInterface *AlsaSoundSystem::OpenCaptureDevice( + const SoundDeviceLocator *device, + const OpenParams ¶ms) { + 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 ¶ms) { + 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 ¶ms, + 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 |