blob: c6dc73a99ba6554fcea4e277311156f7c72d8f8c [file] [log] [blame]
/*
* 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);
}
};
// Accesses ALSA functions through our late-binding symbol table instead of
// directly. This way we don't have to link to libasound, which means our binary
// will load faster and we can run on strange systems that may not have
// libasound
#define LATE(sym) \
LATESYM_GET(AlsaSymbolTable, &alsa_->symbol_table_, sym)
// 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 = LATE(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 = LATE(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 = LATE(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 = LATE(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 = LATE(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.
LATE(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 = LATE(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 = LATE(snd_pcm_drop)(handle_);
if (err != 0) {
LOG(LS_ERROR) << "snd_pcm_drop(): " << GetError(err);
// Continue anyways.
}
err = LATE(snd_pcm_close)(handle_);
if (err != 0) {
LOG(LS_ERROR) << "snd_pcm_close(): " << GetError(err);
// Continue anyways.
}
handle_ = NULL;
}
return true;
}
AlsaSoundSystem *alsa() {
return alsa_;
}
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);
};
// Redefine for the next two classes.
#undef LATE
#define LATE(sym) \
LATESYM_GET(AlsaSymbolTable, &stream_.alsa()->symbol_table_, sym)
// 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 = LATE(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<char> 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 = LATE(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;
}
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);
};
// Redefine for the main class.
#undef LATE
#define LATE(sym) LATESYM_GET(AlsaSymbolTable, &symbol_table_, sym)
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 = LATE(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 = LATE(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 = LATE(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 = LATE(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 = LATE(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(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 = LATE(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(params.format < ARRAY_SIZE(kCricketFormatToAlsaFormatTable));
err = LATE(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 = LATE(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 = LATE(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 = LATE(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 LATE(snd_strerror)(err);
}
} // namespace cricket