/* * libjingle * Copyright 2004--2010, Google Inc. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * 3. The name of the author may not be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "talk/sound/alsasoundsystem.h" #include "talk/base/common.h" #include "talk/base/logging.h" #include "talk/base/scoped_ptr.h" #include "talk/base/stringutils.h" #include "talk/base/timeutils.h" #include "talk/base/worker.h" #include "talk/sound/sounddevicelocator.h" #include "talk/sound/soundinputstreaminterface.h" #include "talk/sound/soundoutputstreaminterface.h" namespace cricket { // Lookup table from the cricket 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. talk_base::replace_substrs(kAlsaDescriptionSearch, sizeof(kAlsaDescriptionSearch) - 1, kAlsaDescriptionReplace, sizeof(kAlsaDescriptionReplace) - 1, &name_); } virtual SoundDeviceLocator *Copy() const { 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 * talk_base::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_; DISALLOW_COPY_AND_ASSIGN(AlsaStream); }; // Implementation of an input stream. See soundinputstreaminterface.h regarding // thread-safety. class AlsaInputStream : public SoundInputStreamInterface, private talk_base::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) { } virtual ~AlsaInputStream() { bool success = StopReading(); // We need that to live. VERIFY(success); } virtual bool StartReading() { return StartWork(); } virtual bool StopReading() { return StopWork(); } virtual bool GetVolume(int *volume) { // TODO: Implement this. return false; } virtual bool SetVolume(int volume) { // TODO: Implement this. return false; } virtual bool Close() { return StopReading() && stream_.Close(); } virtual int LatencyUsecs() { return stream_.CurrentDelayUsecs(); } private: // Inherited from Worker. virtual void OnStart() { HaveWork(); } // Inherited from Worker. virtual void OnHaveWork() { // 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. virtual void OnStop() { // Nothing to do. } const char *GetError(int err) { return stream_.GetError(err); } AlsaStream stream_; talk_base::scoped_array buffer_; size_t buffer_size_; DISALLOW_COPY_AND_ASSIGN(AlsaInputStream); }; // Implementation of an output stream. See soundoutputstreaminterface.h // regarding thread-safety. class AlsaOutputStream : public SoundOutputStreamInterface, private talk_base::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) { } virtual ~AlsaOutputStream() { bool success = DisableBufferMonitoring(); // We need that to live. VERIFY(success); } virtual bool EnableBufferMonitoring() { return StartWork(); } virtual bool DisableBufferMonitoring() { return StopWork(); } virtual bool WriteSamples(const void *sample_data, size_t size) { 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(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; } virtual bool GetVolume(int *volume) { // TODO: Implement this. return false; } virtual bool SetVolume(int volume) { // TODO: Implement this. return false; } virtual bool Close() { return DisableBufferMonitoring() && stream_.Close(); } virtual int LatencyUsecs() { return stream_.CurrentDelayUsecs(); } private: // Inherited from Worker. virtual void OnStart() { HaveWork(); } // Inherited from Worker. virtual void OnHaveWork() { snd_pcm_uframes_t avail = stream_.Wait(); if (avail > 0) { size_t space = avail * stream_.frame_size(); SignalBufferSpace(space, this); } HaveWork(); } // Inherited from Worker. virtual void OnStop() { // Nothing to do. } const char *GetError(int err) { return stream_.GetError(err); } AlsaStream stream_; 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( device, params, SND_PCM_STREAM_PLAYBACK, &AlsaSoundSystem::StartOutputStream); } SoundInputStreamInterface *AlsaSoundSystem::OpenCaptureDevice( const SoundDeviceLocator *device, const OpenParams ¶ms) { return OpenDevice( 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 && !talk_base::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(params.format) < ARRAY_SIZE(kCricketFormatToSampleSizeTable)); return kCricketFormatToSampleSizeTable[params.format] * params.channels; } template 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(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 = talk_base::kNumMicrosecsPerSec * params.latency / params.freq / FrameSize(params); // And this is what we'll actually use. latency = talk_base::_max(latency, kMinimumLatencyUsecs); } ASSERT(static_cast(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 / talk_base::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 cricket