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

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: 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 TGT_LATENCY = local_time::from_msec(35);
jeffbrown 2015/11/04 20:33:42 TGT is pretty opaque as acronyms go, prefer TARGET
johngro 2015/11/06 20:22:07 Done.
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 static const std::set<int> RECOVERABLE_ALSA_ERRORS({
25 -EINTR, -EPIPE, -ESTRPIPE,
jeffbrown 2015/11/04 20:33:42 Kind of odd to put these in a set<> given that it'
johngro 2015/11/06 20:22:07 It just makes the code a bit more readable and mai
26 });
27
28 AlsaOutput::AlsaOutput(AudioOutputManager* manager)
29 : StandardOutputBase(manager) { }
30
31 AlsaOutput::~AlsaOutput() {
32 // We should have been cleaned up already, but in release builds, call cleanup
33 // anyway, just in case something got missed.
jeffbrown 2015/11/04 20:33:42 Why not just CHECK in release mode? Yeah we'll cr
johngro 2015/11/06 20:22:07 A call to cleanup might (someday) do more than jus
34 DCHECK(!alsa_device_);
35 Cleanup();
36 }
37
38 AudioOutputPtr AlsaOutput::New(AudioOutputManager* manager) {
39 return AudioOutputPtr(new AlsaOutput(manager));
40 }
41
42 MediaResult AlsaOutput::Configure(LpcmMediaTypeDetailsPtr config) {
43 if (!config) { return MediaResult::INVALID_ARGUMENT; }
44 if (output_format_) { return MediaResult::BAD_STATE; }
45
46 uint32_t bytes_per_sample;
47 switch (config->sample_format) {
48 case LpcmSampleFormat::UNSIGNED_8:
49 alsa_format_ = SND_PCM_FORMAT_U8;
50 silence_byte_ = 0x80;
51 bytes_per_sample = 1;
52 break;
53
54 case LpcmSampleFormat::SIGNED_16:
55 alsa_format_ = SND_PCM_FORMAT_S16;
56 silence_byte_ = 0x00;
57 bytes_per_sample = 2;
58 break;
59
60 case LpcmSampleFormat::SIGNED_24_IN_32:
61 default:
62 return MediaResult::UNSUPPORTED_CONFIG;
63 }
64
65 if (SUPPORTED_SAMPLE_RATES.find(config->frames_per_second) ==
66 SUPPORTED_SAMPLE_RATES.end()) {
67 return MediaResult::UNSUPPORTED_CONFIG;
68 }
69
70 if (SUPPORTED_CHANNEL_COUNTS.find(config->samples_per_frame) ==
71 SUPPORTED_CHANNEL_COUNTS.end()) {
72 return MediaResult::UNSUPPORTED_CONFIG;
73 }
74
75 // Compute the ratio between frames and local time ticks.
76 LinearTransform::Ratio sec_per_tick(LocalDuration::period::num,
77 LocalDuration::period::den);
78 LinearTransform::Ratio frames_per_sec(config->frames_per_second, 1);
79 bool is_precise = LinearTransform::Ratio::Compose(frames_per_sec,
80 sec_per_tick,
81 &frames_per_tick_);
82 DCHECK(is_precise);
83
84 // Figure out how many bytes there are per frame.
85 output_bytes_per_frame_ = bytes_per_sample * config->samples_per_frame;
86
87 // Success
88 output_format_ = config.Pass();
89 return MediaResult::OK;
90 }
91
92 MediaResult AlsaOutput::Init() {
93 if (!output_format_) { return MediaResult::BAD_STATE; }
94 if (alsa_device_) { return MediaResult::BAD_STATE; }
95
96 snd_pcm_sframes_t res;
97 res = snd_pcm_open(&alsa_device_,
98 "default",
99 SND_PCM_STREAM_PLAYBACK,
100 SND_PCM_NONBLOCK);
101 if (res != 0) {
102 // TODO(johngro): log something here?
jeffbrown 2015/11/04 20:33:42 Yeah. LOG(ERROR) << "You're hosed." ;)
johngro 2015/11/06 20:22:07 Done.
103 DCHECK(!alsa_device_);
jeffbrown 2015/11/04 20:33:42 This DCHECK seems unnecessary given your test at t
johngro 2015/11/06 20:22:06 Assuming that ALSA never has any bugs, but yes...
104 return MediaResult::INTERNAL_ERROR;
105 }
106
107 res = snd_pcm_set_params(alsa_device_,
108 alsa_format_,
109 SND_PCM_ACCESS_RW_INTERLEAVED,
110 output_format_->samples_per_frame,
111 output_format_->frames_per_second,
112 1, // allow ALSA resample
jeffbrown 2015/11/04 20:33:42 Hmm. Guess we'll disable this in the future.
johngro 2015/11/06 20:22:07 how about right now? Not sure how this got set TB
113 local_time::to_usec<unsigned int>(TGT_LATENCY));
114 if (res) {
115 // TODO(johngro): log something here?
jeffbrown 2015/11/04 20:33:42 Please do. ALSA is such a pain to debug when brin
johngro 2015/11/06 20:22:07 Done.
116 Cleanup();
117 return MediaResult::INTERNAL_ERROR;
118 }
119
120 // Figure out how big our mixing buffer needs to be, then allocate it.
121 res = snd_pcm_avail_update(alsa_device_);
122 if (res <= 0) {
123 LOG(ERROR) << "[" << this << "] : "
124 << "Fatal error (" << res
125 << ") attempting to determine ALSA buffer size.";
126 Cleanup();
127 return MediaResult::INTERNAL_ERROR;
128 }
129
130 mix_buf_frames_ = res;
131 mix_buf_bytes_ = res * output_bytes_per_frame_;
132 mix_buf_.reset(new uint8_t[mix_buf_bytes_]);
133
134 return MediaResult::OK;
135 }
136
137 void AlsaOutput::Cleanup() {
138 if (alsa_device_) {
139 snd_pcm_close(alsa_device_);
140 alsa_device_ = nullptr;
141 }
142
143 mix_buf_ = nullptr;
144 mix_buf_bytes_ = 0;
145 mix_buf_frames_ = 0;
146 }
147
148 bool AlsaOutput::StartMixJob(MixJob* job, const LocalTime& process_start) {
149 DCHECK(job);
150
151 // Are we not primed? If so, fill a mix buffer with silence and send it to
152 // the alsa device. Schedule a callback for a short time in the future so
153 // ALSA has a chance to start the output and we can take our best guess of the
154 // function which maps output frames to local time.
155 if (!primed_) {
156 HandleAsUnderflow();
157 return false;
158 }
159
160 // Figure out how many frames of audio we need to produce in order to top off
161 // the buffer. If we are primed, but do not know the transformation between
162 // audio frames and local time ticks, do our best to figure it out in the
163 // process.
164 snd_pcm_sframes_t avail;
165 if (!local_to_output_known_) {
166 snd_pcm_sframes_t delay;
167
168 int res = snd_pcm_avail_delay(alsa_device_, &avail, &delay);
169 LocalTime now = LocalClock::now();
170
171 if (res < 0) {
172 HandleAlsaError(res);
173 return false;
174 }
175
176 DCHECK_GE(delay, 0);
177 int64_t now_ticks = now.time_since_epoch().count();
jeffbrown 2015/11/04 20:33:42 Looking at this conversion to int64_t, I wonder wh
johngro 2015/11/06 20:22:07 yeah, an adapter might be a good idea. If we end
178 local_to_output_ = LinearTransform(now_ticks, frames_per_tick_, -delay);
179 local_to_output_known_ = true;
180 frames_sent_ = 0;
181 while (++local_to_output_gen_ == MixJob::INVALID_GENERATION) {}
jeffbrown 2015/11/04 20:33:42 A little weird but ok.
johngro 2015/11/06 20:22:07 Acknowledged. I could wrap this in a method if yo
182 } else {
183 avail = snd_pcm_avail_update(alsa_device_);
184 if (avail < 0) {
185 HandleAlsaError(avail);
186 return false;
187 }
188 }
189
190 // Compute the time that we think we will completely underflow, then back off
191 // from that by the low buffer threshold and use that to determine when we
192 // should mix again.
193 int64_t playout_time_ticks;
194 bool trans_ok = local_to_output_.DoReverseTransform(frames_sent_,
jeffbrown 2015/11/04 20:33:42 Am I correct in thinking that the reverse transfor
johngro 2015/11/06 20:22:07 No, outputs cannot be paused. They are either run
195 &playout_time_ticks);
196 DCHECK(trans_ok);
jeffbrown 2015/11/04 20:33:43 Ahh, here's our check for singularity. Odd though
johngro 2015/11/06 20:22:07 This also checks for overflow, not just singularit
197 LocalTime playout_time = LocalTime(LocalDuration(playout_time_ticks));
198 LocalTime low_buf_time = playout_time - LOW_BUF_THRESH;
199
200 if (process_start >= low_buf_time) {
201 // Because of the way that ALSA consumes data and updates its internal
202 // bookkeeping, it is possible that we are past our low buffer threshold,
203 // but ALSA still thinks that there is no room to write new frames. If this
204 // is the case, just try again a short amt of time in the future.
jeffbrown 2015/11/04 20:33:42 amt -> amount
johngro 2015/11/06 20:22:07 Done.
205 DCHECK_GE(avail, 0);
206 if (!avail) {
207 SetNextSchedDelay(WAIT_FOR_ALSA_DELAY);
208 return false;
209 }
210
211 // Limit the amt that we queue to be no more than what ALSA will currently
212 // accept, or what it currently will take to fill us to our target latency.
213 //
214 // The playout target had better be ahead of the playout time, or we are
215 // almost certainly going to underflow. If this happens, for whatever
216 // reason, just try to send a full buffer and deal with the underflow when
217 // ALSA notices it.
218 int64_t fill_amt;
219 LocalTime playout_target = LocalClock::now() + TGT_LATENCY;
220 if (playout_target > playout_time) {
221 fill_amt = (playout_target - playout_time).count();
222 } else {
223 fill_amt = TGT_LATENCY.count();
224 }
225
226 DCHECK_GE(fill_amt, 0);
227 DCHECK_LE(fill_amt, std::numeric_limits<int32_t>::max());
jeffbrown 2015/11/04 20:33:42 Are we guaranteed that numerator <= denominator?
johngro 2015/11/06 20:22:07 We should be. If N > D, it implies that our audio
228 fill_amt *= frames_per_tick_.numerator;
jeffbrown 2015/11/04 20:33:43 I might recommend that we create a for rounded sca
johngro 2015/11/06 20:22:07 good idea. See above; its the type of thing which
229 fill_amt += frames_per_tick_.denominator - 1;
230 fill_amt /= frames_per_tick_.denominator;
231
232 job->buf_frames = (avail < fill_amt) ? avail : fill_amt;
233 if (job->buf_frames > mix_buf_frames_) {
234 job->buf_frames = mix_buf_frames_;
235 }
236
237 job->buf = mix_buf_.get();
238 job->start_pts_of = frames_sent_;
239 job->local_to_output = &local_to_output_;
240 job->local_to_output_gen = local_to_output_gen_;
241
242 // TODO(johngro): optimize this if we can. The first buffer we mix can just
243 // put its samples directly into the output buffer, and does not need to
244 // accumulate and clip. In theory, we only need to put silence in the
245 // places where our outputs are not going to already overwrite.
246 ::memset(job->buf,
247 silence_byte_,
jeffbrown 2015/11/04 20:33:43 Technically silence might not always be a byte. A
johngro 2015/11/06 20:22:07 Acknowledged. This only works because we (current
248 job->buf_frames * output_bytes_per_frame_);
249 return true;
250 }
251
252 // Wait until its time to mix some more data.
253 SetNextSchedTime(low_buf_time);
254 return false;
255 }
256
257 bool AlsaOutput::FinishMixJob(const MixJob& job) {
258 DCHECK(job.buf == mix_buf_.get());
259 DCHECK(job.buf_frames);
260
261 // We should always be able to write all of the data that we mixed.
262 snd_pcm_sframes_t res;
263 res = snd_pcm_writei(alsa_device_, job.buf, job.buf_frames);
264 if (res != job.buf_frames) {
265 HandleAlsaError(res);
266 return false;
267 }
268
269 frames_sent_ += res;
270 return true;
271 }
272
273 void AlsaOutput::HandleAsUnderflow() {
274 snd_pcm_sframes_t res;
275
276 // If we were already primed, then this is a legitimate underflow, not the
277 // startup case or recovery from some other error.
278 if (primed_) {
279 // TODO(johngro): come up with a way to properly throttle this. Also, add a
280 // friendly name to the output so the log helps to identify which output
281 // underflowed.
282 LOG(WARNING) << "[" << this << "] : underflow";
283 res = snd_pcm_recover(alsa_device_, -EPIPE, true);
284 if (res < 0) {
285 HandleAsError(res);
286 return;
287 }
288 }
289
290 // TODO(johngro): We don't actually have to fill up the entire lead time with
291 // silence. When we have better control of our thread priorities, prime this
292 // with the minimimum amt we can get away with and still be able to start
293 // mixing without underflowing.
294 DCHECK(mix_buf_);
295 ::memset(mix_buf_.get(), silence_byte_, mix_buf_bytes_);
jeffbrown 2015/11/04 20:33:42 This has shown up twice now. Consider making a fu
johngro 2015/11/06 20:22:07 Sure; this (moving to frames only) means performin
296 res = snd_pcm_writei(alsa_device_, mix_buf_.get(), mix_buf_frames_);
297
298 if (res < 0) {
299 HandleAsError(res);
300 return;
301 }
302
303 primed_ = true;
304 local_to_output_known_ = false;
305 SetNextSchedDelay(local_time::from_msec(1));
306 }
307
308 void AlsaOutput::HandleAsError(snd_pcm_sframes_t code) {
jeffbrown 2015/11/04 20:33:42 The name of this function is a little too similar
johngro 2015/11/06 20:22:07 Permission to say HandleALSAError instead of Handl
309 // TODO(johngro): Throttle this somehow.
310 LOG(WARNING) << "[" << this << "] : Attempting to recover from ALSA error "
311 << code;
312
313 if (RECOVERABLE_ALSA_ERRORS.find(code) != RECOVERABLE_ALSA_ERRORS.end()) {
jeffbrown 2015/11/04 20:33:42 If we got EINTR then the correct response was to r
johngro 2015/11/06 20:22:07 I'm not sure I understand your request here. The
314 code = snd_pcm_recover(alsa_device_, code, true);
315 if (!code) {
316 primed_ = false;
317 local_to_output_known_ = false;
318 SetNextSchedDelay(ERROR_RECOVERY_TIME);
319 }
320 }
321
322 LOG(ERROR) << "[" << this << "] : Fatal ALSA error "
323 << code << ". Shutting down";
324 ShutdownSelf();
325 }
326
327 void AlsaOutput::HandleAlsaError(snd_pcm_sframes_t code) {
328 // ALSA signals an underflow by returning -EPIPE from jobs. If the error code
329 // is -EPIPE, treat this as an underflow and attempt to reprime the pipeline.
330 if (code == -EPIPE) {
331 HandleAsUnderflow();
332 } else {
333 HandleAsError(code);
334 }
335 }
336
337 } // namespace audio
338 } // namespace media
339 } // namespace mojo
340
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698