Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(522)

Side by Side Diff: services/media/audio/platform/linux/alsa_output.cc

Issue 1419593007: Add an ALSA output to the motown audio server. (Closed) Base URL: https://github.com/domokit/mojo.git@change5
Patch Set: fixing android build Created 5 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include <limits>
6 #include <set>
7
8 #include "mojo/services/media/common/cpp/local_time.h"
9 #include "services/media/audio/platform/linux/alsa_output.h"
10
11 namespace mojo {
12 namespace media {
13 namespace audio {
14
15 static constexpr LocalDuration TARGET_LATENCY = local_time::from_msec(35);
16 static constexpr LocalDuration LOW_BUF_THRESH = local_time::from_msec(30);
17 static constexpr LocalDuration ERROR_RECOVERY_TIME = local_time::from_msec(300);
18 static constexpr LocalDuration WAIT_FOR_ALSA_DELAY = local_time::from_usec(500);
19 static const std::set<uint8_t> SUPPORTED_CHANNEL_COUNTS({ 1, 2 });
20 static const std::set<uint32_t> SUPPORTED_SAMPLE_RATES({
21 48000, 32000, 24000, 16000, 8000, 4000,
22 44100, 22050, 11025,
23 });
24
25 static inline bool IsRecoverableAlsaError(int error_code) {
26 switch (error_code) {
27 case -EINTR:
28 case -EPIPE:
29 case -ESTRPIPE:
30 return true;
31 default:
32 return false;
33 }
34 }
35
36 AudioOutputPtr CreateDefaultAlsaOutput(AudioOutputManager* manager) {
37 // TODO(johngro): Do better than this. If we really want to support
38 // Linux/ALSA as a platform, we should be creating one output for each
39 // physical output in the system, matching our configuration to the physical
40 // output's configuration, and disabling resampling at the ALSA level.
41 //
42 // If we could own the output entirely and bypass the mixer to achieve lower
43 // latency, that would be even better.
44 AudioOutputPtr audio_out(audio::AlsaOutput::New(manager));
45 if (!audio_out) { return nullptr; }
46
47 AlsaOutput* alsa_out = static_cast<AlsaOutput*>(audio_out.get());
48 DCHECK(alsa_out);
49
50 LpcmMediaTypeDetailsPtr config(LpcmMediaTypeDetails::New());
51 config->frames_per_second = 48000;
52 config->samples_per_frame = 2;
53 config->sample_format = LpcmSampleFormat::SIGNED_16;
54
55 if (alsa_out->Configure(config.Pass()) != MediaResult::OK) {
56 return nullptr;
57 }
58
59 return audio_out;
60 }
61
62 AlsaOutput::AlsaOutput(AudioOutputManager* manager)
63 : StandardOutputBase(manager) {}
64
65 AlsaOutput::~AlsaOutput() {
66 // We should have been cleaned up already, but in release builds, call cleanup
67 // anyway, just in case something got missed.
68 DCHECK(!alsa_device_);
69 Cleanup();
70 }
71
72 AudioOutputPtr AlsaOutput::New(AudioOutputManager* manager) {
73 return AudioOutputPtr(new AlsaOutput(manager));
74 }
75
76 MediaResult AlsaOutput::Configure(LpcmMediaTypeDetailsPtr config) {
77 if (!config) { return MediaResult::INVALID_ARGUMENT; }
78 if (output_format_) { return MediaResult::BAD_STATE; }
79
80 uint32_t bytes_per_sample;
81 switch (config->sample_format) {
82 case LpcmSampleFormat::UNSIGNED_8:
83 alsa_format_ = SND_PCM_FORMAT_U8;
84 silence_byte_ = 0x80;
85 bytes_per_sample = 1;
86 break;
87
88 case LpcmSampleFormat::SIGNED_16:
89 alsa_format_ = SND_PCM_FORMAT_S16;
90 silence_byte_ = 0x00;
91 bytes_per_sample = 2;
92 break;
93
94 case LpcmSampleFormat::SIGNED_24_IN_32:
95 default:
96 return MediaResult::UNSUPPORTED_CONFIG;
97 }
98
99 if (SUPPORTED_SAMPLE_RATES.find(config->frames_per_second) ==
100 SUPPORTED_SAMPLE_RATES.end()) {
101 return MediaResult::UNSUPPORTED_CONFIG;
102 }
103
104 if (SUPPORTED_CHANNEL_COUNTS.find(config->samples_per_frame) ==
105 SUPPORTED_CHANNEL_COUNTS.end()) {
106 return MediaResult::UNSUPPORTED_CONFIG;
107 }
108
109 // Compute the ratio between frames and local time ticks.
110 LinearTransform::Ratio sec_per_tick(LocalDuration::period::num,
111 LocalDuration::period::den);
112 LinearTransform::Ratio frames_per_sec(config->frames_per_second, 1);
113 bool is_precise = LinearTransform::Ratio::Compose(frames_per_sec,
114 sec_per_tick,
115 &frames_per_tick_);
116 DCHECK(is_precise);
117
118 // Figure out how many bytes there are per frame.
119 output_bytes_per_frame_ = bytes_per_sample * config->samples_per_frame;
120
121 // Success
122 output_format_ = config.Pass();
123 return MediaResult::OK;
124 }
125
126 MediaResult AlsaOutput::Init() {
127 static const char* kAlsaDevice = "default";
128
129 if (!output_format_) { return MediaResult::BAD_STATE; }
130 if (alsa_device_) { return MediaResult::BAD_STATE; }
131
132 snd_pcm_sframes_t res;
133 res = snd_pcm_open(&alsa_device_,
134 kAlsaDevice,
135 SND_PCM_STREAM_PLAYBACK,
136 SND_PCM_NONBLOCK);
137 if (res != 0) {
138 LOG(ERROR) << "Failed to open ALSA device \"" << kAlsaDevice << "\".";
139 return MediaResult::INTERNAL_ERROR;
140 }
141
142 res = snd_pcm_set_params(alsa_device_,
143 alsa_format_,
144 SND_PCM_ACCESS_RW_INTERLEAVED,
145 output_format_->samples_per_frame,
146 output_format_->frames_per_second,
147 0, // do not allow ALSA resample
148 local_time::to_usec<unsigned int>(TARGET_LATENCY));
149 if (res) {
150 LOG(ERROR) << "Failed to configure ALSA device \"" << kAlsaDevice << "\" "
151 << "(res = " << res << ")";
152 LOG(ERROR) << "Requested samples per frame: "
153 << output_format_->samples_per_frame;
154 LOG(ERROR) << "Requested frames per second: "
155 << output_format_->frames_per_second;
156 LOG(ERROR) << "Requested ALSA format : " << alsa_format_;
157 Cleanup();
158 return MediaResult::INTERNAL_ERROR;
159 }
160
161 // Figure out how big our mixing buffer needs to be, then allocate it.
162 res = snd_pcm_avail_update(alsa_device_);
163 if (res <= 0) {
164 LOG(ERROR) << "[" << this << "] : "
165 << "Fatal error (" << res
166 << ") attempting to determine ALSA buffer size.";
167 Cleanup();
168 return MediaResult::INTERNAL_ERROR;
169 }
170
171 mix_buf_frames_ = res;
172 mix_buf_.reset(new uint8_t[mix_buf_frames_ * output_bytes_per_frame_]);
173
174 return MediaResult::OK;
175 }
176
177 void AlsaOutput::Cleanup() {
178 if (alsa_device_) {
179 snd_pcm_close(alsa_device_);
180 alsa_device_ = nullptr;
181 }
182
183 mix_buf_ = nullptr;
184 mix_buf_frames_ = 0;
185 }
186
187 bool AlsaOutput::StartMixJob(MixJob* job, const LocalTime& process_start) {
188 DCHECK(job);
189
190 // Are we not primed? If so, fill a mix buffer with silence and send it to
191 // the alsa device. Schedule a callback for a short time in the future so
192 // ALSA has a chance to start the output and we can take our best guess of the
193 // function which maps output frames to local time.
194 if (!primed_) {
195 HandleAsUnderflow();
196 return false;
197 }
198
199 // Figure out how many frames of audio we need to produce in order to top off
200 // the buffer. If we are primed, but do not know the transformation between
201 // audio frames and local time ticks, do our best to figure it out in the
202 // process.
203 snd_pcm_sframes_t avail;
204 if (!local_to_output_known_) {
205 snd_pcm_sframes_t delay;
206
207 int res = snd_pcm_avail_delay(alsa_device_, &avail, &delay);
208 LocalTime now = LocalClock::now();
209
210 if (res < 0) {
211 HandleAlsaError(res);
212 return false;
213 }
214
215 DCHECK_GE(delay, 0);
216 int64_t now_ticks = now.time_since_epoch().count();
217 local_to_output_ = LinearTransform(now_ticks, frames_per_tick_, -delay);
218 local_to_output_known_ = true;
219 frames_sent_ = 0;
220 while (++local_to_output_gen_ == MixJob::INVALID_GENERATION) {}
221 } else {
222 avail = snd_pcm_avail_update(alsa_device_);
223 if (avail < 0) {
224 HandleAlsaError(avail);
225 return false;
226 }
227 }
228
229 // Compute the time that we think we will completely underflow, then back off
230 // from that by the low buffer threshold and use that to determine when we
231 // should mix again.
232 int64_t playout_time_ticks;
233 bool trans_ok = local_to_output_.DoReverseTransform(frames_sent_,
234 &playout_time_ticks);
235 DCHECK(trans_ok);
236 LocalTime playout_time = LocalTime(LocalDuration(playout_time_ticks));
237 LocalTime low_buf_time = playout_time - LOW_BUF_THRESH;
238
239 if (process_start >= low_buf_time) {
240 // Because of the way that ALSA consumes data and updates its internal
241 // bookkeeping, it is possible that we are past our low buffer threshold,
242 // but ALSA still thinks that there is no room to write new frames. If this
243 // is the case, just try again a short amount of time in the future.
244 DCHECK_GE(avail, 0);
245 if (!avail) {
246 SetNextSchedDelay(WAIT_FOR_ALSA_DELAY);
247 return false;
248 }
249
250 // Limit the amt that we queue to be no more than what ALSA will currently
251 // accept, or what it currently will take to fill us to our target latency.
252 //
253 // The playout target had better be ahead of the playout time, or we are
254 // almost certainly going to underflow. If this happens, for whatever
255 // reason, just try to send a full buffer and deal with the underflow when
256 // ALSA notices it.
257 int64_t fill_amt;
258 LocalTime playout_target = LocalClock::now() + TARGET_LATENCY;
259 if (playout_target > playout_time) {
260 fill_amt = (playout_target - playout_time).count();
261 } else {
262 fill_amt = TARGET_LATENCY.count();
263 }
264
265 DCHECK_GE(fill_amt, 0);
266 DCHECK_LE(fill_amt, std::numeric_limits<int32_t>::max());
267 fill_amt *= frames_per_tick_.numerator;
268 fill_amt += frames_per_tick_.denominator - 1;
269 fill_amt /= frames_per_tick_.denominator;
270
271 job->buf_frames = (avail < fill_amt) ? avail : fill_amt;
272 if (job->buf_frames > mix_buf_frames_) {
273 job->buf_frames = mix_buf_frames_;
274 }
275
276 job->buf = mix_buf_.get();
277 job->start_pts_of = frames_sent_;
278 job->local_to_output = &local_to_output_;
279 job->local_to_output_gen = local_to_output_gen_;
280
281 // TODO(johngro): optimize this if we can. The first buffer we mix can just
282 // put its samples directly into the output buffer, and does not need to
283 // accumulate and clip. In theory, we only need to put silence in the
284 // places where our outputs are not going to already overwrite.
285 FillMixBufWithSilence(job->buf_frames);
286 return true;
287 }
288
289 // Wait until its time to mix some more data.
290 SetNextSchedTime(low_buf_time);
291 return false;
292 }
293
294 bool AlsaOutput::FinishMixJob(const MixJob& job) {
295 DCHECK(job.buf == mix_buf_.get());
296 DCHECK(job.buf_frames);
297
298 // We should always be able to write all of the data that we mixed.
299 snd_pcm_sframes_t res;
300 res = snd_pcm_writei(alsa_device_, job.buf, job.buf_frames);
301 if (res != job.buf_frames) {
302 HandleAlsaError(res);
303 return false;
304 }
305
306 frames_sent_ += res;
307 return true;
308 }
309
310 void AlsaOutput::FillMixBufWithSilence(uint32_t frames) {
311 DCHECK(mix_buf_);
312 DCHECK(frames <= mix_buf_frames_);
313
314 // TODO(johngro): someday, this may not be this simple. Filling unsigned
315 // multibyte sample formats, or floating point formats, will require something
316 // more sophisticated than filling with a single byte pattern.
317 ::memset(mix_buf_.get(), silence_byte_, frames * output_bytes_per_frame_);
318 }
319
320 void AlsaOutput::HandleAsUnderflow() {
321 snd_pcm_sframes_t res;
322
323 // If we were already primed, then this is a legitimate underflow, not the
324 // startup case or recovery from some other error.
325 if (primed_) {
326 // TODO(johngro): come up with a way to properly throttle this. Also, add a
327 // friendly name to the output so the log helps to identify which output
328 // underflowed.
329 LOG(WARNING) << "[" << this << "] : underflow";
330 res = snd_pcm_recover(alsa_device_, -EPIPE, true);
331 if (res < 0) {
332 HandleAsError(res);
333 return;
334 }
335 }
336
337 // TODO(johngro): We don't actually have to fill up the entire lead time with
338 // silence. When we have better control of our thread priorities, prime this
339 // with the minimimum amt we can get away with and still be able to start
340 // mixing without underflowing.
341 FillMixBufWithSilence(mix_buf_frames_);
342 res = snd_pcm_writei(alsa_device_, mix_buf_.get(), mix_buf_frames_);
343
344 if (res < 0) {
345 HandleAsError(res);
346 return;
347 }
348
349 primed_ = true;
350 local_to_output_known_ = false;
351 SetNextSchedDelay(local_time::from_msec(1));
352 }
353
354 void AlsaOutput::HandleAsError(snd_pcm_sframes_t code) {
355 // TODO(johngro): Throttle this somehow.
356 LOG(WARNING) << "[" << this << "] : Attempting to recover from ALSA error "
357 << code;
358
359 if (IsRecoverableAlsaError(code)) {
360 snd_pcm_sframes_t new_code;
361
362 new_code = snd_pcm_recover(alsa_device_, code, true);
363 DCHECK(!new_code || (new_code == code));
364
365 // If we recovered, or we didn't and the original error was EINTR, schedule
366 // a retry time in the future and unwind.
367 //
368 // TODO(johngro): revisit the topic of errors we fail to snd_pcm_recover
369 // from. If we cannot recover from them, we should probably close and
370 // re-open the device. No matter what, we should put some form of limit on
371 // how many times we try before really giving up and shutting down the
372 // output for good. We also need to invent a good way to test these edge
373 // cases.
374 if (!new_code || (new_code == -EINTR)) {
375 primed_ = false;
376 local_to_output_known_ = false;
377 SetNextSchedDelay(ERROR_RECOVERY_TIME);
378 }
379 }
380
381 LOG(ERROR) << "[" << this << "] : Fatal ALSA error "
382 << code << ". Shutting down";
383 ShutdownSelf();
384 }
385
386 void AlsaOutput::HandleAlsaError(snd_pcm_sframes_t code) {
387 // ALSA signals an underflow by returning -EPIPE from jobs. If the error code
388 // is -EPIPE, treat this as an underflow and attempt to reprime the pipeline.
389 if (code == -EPIPE) {
390 HandleAsUnderflow();
391 } else {
392 HandleAsError(code);
393 }
394 }
395
396 } // namespace audio
397 } // namespace media
398 } // namespace mojo
399
OLDNEW
« no previous file with comments | « services/media/audio/platform/linux/alsa_output.h ('k') | services/media/audio/platform/stubs/alsa_output_stub.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698