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

Side by Side Diff: webrtc/modules/audio_device/audio_device_buffer.cc

Issue 2132613002: Adds data logging in native AudioDeviceBuffer class (Closed) Base URL: https://chromium.googlesource.com/external/webrtc.git@master
Patch Set: Created 4 years, 5 months 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
1 /* 1 /*
2 * Copyright (c) 2012 The WebRTC project authors. All Rights Reserved. 2 * Copyright (c) 2012 The WebRTC project authors. All Rights Reserved.
3 * 3 *
4 * Use of this source code is governed by a BSD-style license 4 * Use of this source code is governed by a BSD-style license
5 * that can be found in the LICENSE file in the root of the source 5 * that can be found in the LICENSE file in the root of the source
6 * tree. An additional intellectual property rights grant can be found 6 * tree. An additional intellectual property rights grant can be found
7 * in the file PATENTS. All contributing project authors may 7 * in the file PATENTS. All contributing project authors may
8 * be found in the AUTHORS file in the root of the source tree. 8 * be found in the AUTHORS file in the root of the source tree.
9 */ 9 */
10 10
11 #include "webrtc/modules/audio_device/audio_device_buffer.h" 11 #include "webrtc/modules/audio_device/audio_device_buffer.h"
12 12
13 #include "webrtc/base/bind.h"
13 #include "webrtc/base/checks.h" 14 #include "webrtc/base/checks.h"
14 #include "webrtc/base/logging.h" 15 #include "webrtc/base/logging.h"
15 #include "webrtc/base/format_macros.h" 16 #include "webrtc/base/format_macros.h"
17 #include "webrtc/base/timeutils.h"
16 #include "webrtc/modules/audio_device/audio_device_config.h" 18 #include "webrtc/modules/audio_device/audio_device_config.h"
17 #include "webrtc/system_wrappers/include/critical_section_wrapper.h"
18 19
19 namespace webrtc { 20 namespace webrtc {
20 21
21 static const int kHighDelayThresholdMs = 300; 22 static const int kHighDelayThresholdMs = 300;
22 static const int kLogHighDelayIntervalFrames = 500; // 5 seconds. 23 static const int kLogHighDelayIntervalFrames = 500; // 5 seconds.
23 24
25 static const char kTimerQueueName[] = "AudioDeviceBufferTimer";
26 static const size_t kTimerIntervalInSeconds = 10;
27 static const size_t kTimerIntervalInMilliseconds =
28 kTimerIntervalInSeconds * 1000;
29
24 AudioDeviceBuffer::AudioDeviceBuffer() 30 AudioDeviceBuffer::AudioDeviceBuffer()
25 : _critSect(*CriticalSectionWrapper::CreateCriticalSection()), 31 : _ptrCbAudioTransport(nullptr),
26 _critSectCb(*CriticalSectionWrapper::CreateCriticalSection()), 32 task_queue_(new rtc::TaskQueue(kTimerQueueName)),
27 _ptrCbAudioTransport(nullptr), 33 timer_has_started_(false),
28 _recSampleRate(0), 34 _recSampleRate(0),
29 _playSampleRate(0), 35 _playSampleRate(0),
30 _recChannels(0), 36 _recChannels(0),
31 _playChannels(0), 37 _playChannels(0),
32 _recChannel(AudioDeviceModule::kChannelBoth), 38 _recChannel(AudioDeviceModule::kChannelBoth),
33 _recBytesPerSample(0), 39 _recBytesPerSample(0),
34 _playBytesPerSample(0), 40 _playBytesPerSample(0),
35 _recSamples(0), 41 _recSamples(0),
36 _recSize(0), 42 _recSize(0),
37 _playSamples(0), 43 _playSamples(0),
38 _playSize(0), 44 _playSize(0),
39 _recFile(*FileWrapper::Create()), 45 _recFile(*FileWrapper::Create()),
40 _playFile(*FileWrapper::Create()), 46 _playFile(*FileWrapper::Create()),
41 _currentMicLevel(0), 47 _currentMicLevel(0),
42 _newMicLevel(0), 48 _newMicLevel(0),
43 _typingStatus(false), 49 _typingStatus(false),
44 _playDelayMS(0), 50 _playDelayMS(0),
45 _recDelayMS(0), 51 _recDelayMS(0),
46 _clockDrift(0), 52 _clockDrift(0),
47 // Set to the interval in order to log on the first occurrence. 53 // Set to the interval in order to log on the first occurrence.
48 high_delay_counter_(kLogHighDelayIntervalFrames) { 54 high_delay_counter_(kLogHighDelayIntervalFrames),
55 rec_callbacks_(0),
56 last_rec_callbacks_(0),
57 play_callbacks_(0),
58 last_play_callbacks_(0),
59 rec_samples_(0),
60 last_rec_samples_(0),
61 play_samples_(0),
62 last_play_samples_(0) {
49 LOG(INFO) << "AudioDeviceBuffer::ctor"; 63 LOG(INFO) << "AudioDeviceBuffer::ctor";
50 memset(_recBuffer, 0, kMaxBufferSizeBytes); 64 memset(_recBuffer, 0, kMaxBufferSizeBytes);
51 memset(_playBuffer, 0, kMaxBufferSizeBytes); 65 memset(_playBuffer, 0, kMaxBufferSizeBytes);
66 RTC_DCHECK(task_queue_);
52 } 67 }
53 68
54 AudioDeviceBuffer::~AudioDeviceBuffer() { 69 AudioDeviceBuffer::~AudioDeviceBuffer() {
55 LOG(INFO) << "AudioDeviceBuffer::~dtor"; 70 LOG(INFO) << "AudioDeviceBuffer::~dtor";
56 { 71 _recFile.Flush();
57 CriticalSectionScoped lock(&_critSect); 72 _recFile.CloseFile();
73 delete &_recFile;
58 74
59 _recFile.Flush(); 75 _playFile.Flush();
60 _recFile.CloseFile(); 76 _playFile.CloseFile();
61 delete &_recFile; 77 delete &_playFile;
62
63 _playFile.Flush();
64 _playFile.CloseFile();
65 delete &_playFile;
66 }
67
68 delete &_critSect;
69 delete &_critSectCb;
70 } 78 }
71 79
72 int32_t AudioDeviceBuffer::RegisterAudioCallback( 80 int32_t AudioDeviceBuffer::RegisterAudioCallback(
73 AudioTransport* audioCallback) { 81 AudioTransport* audioCallback) {
74 LOG(INFO) << __FUNCTION__; 82 LOG(INFO) << __FUNCTION__;
75 CriticalSectionScoped lock(&_critSectCb); 83 rtc::CritScope lock(&_critSectCb);
76 _ptrCbAudioTransport = audioCallback; 84 _ptrCbAudioTransport = audioCallback;
77 return 0; 85 return 0;
78 } 86 }
79 87
80 int32_t AudioDeviceBuffer::InitPlayout() { 88 int32_t AudioDeviceBuffer::InitPlayout() {
81 LOG(INFO) << __FUNCTION__; 89 LOG(INFO) << __FUNCTION__;
90 if (!timer_has_started_) {
stefan-webrtc 2016/07/07 15:22:13 Is this accessed on a single thread? I don't know
henrika_webrtc 2016/07/08 12:46:48 It is only accessed on one thread but it is a good
91 StartTimer();
92 timer_has_started_ = true;
93 }
82 return 0; 94 return 0;
83 } 95 }
84 96
85 int32_t AudioDeviceBuffer::InitRecording() { 97 int32_t AudioDeviceBuffer::InitRecording() {
86 LOG(INFO) << __FUNCTION__; 98 LOG(INFO) << __FUNCTION__;
99 if (!timer_has_started_) {
100 StartTimer();
101 timer_has_started_ = true;
102 }
87 return 0; 103 return 0;
88 } 104 }
89 105
90 int32_t AudioDeviceBuffer::SetRecordingSampleRate(uint32_t fsHz) { 106 int32_t AudioDeviceBuffer::SetRecordingSampleRate(uint32_t fsHz) {
91 LOG(INFO) << "SetRecordingSampleRate(" << fsHz << ")"; 107 LOG(INFO) << "SetRecordingSampleRate(" << fsHz << ")";
92 CriticalSectionScoped lock(&_critSect); 108 rtc::CritScope lock(&_critSect);
93 _recSampleRate = fsHz; 109 _recSampleRate = fsHz;
94 return 0; 110 return 0;
95 } 111 }
96 112
97 int32_t AudioDeviceBuffer::SetPlayoutSampleRate(uint32_t fsHz) { 113 int32_t AudioDeviceBuffer::SetPlayoutSampleRate(uint32_t fsHz) {
98 LOG(INFO) << "SetPlayoutSampleRate(" << fsHz << ")"; 114 LOG(INFO) << "SetPlayoutSampleRate(" << fsHz << ")";
99 CriticalSectionScoped lock(&_critSect); 115 rtc::CritScope lock(&_critSect);
100 _playSampleRate = fsHz; 116 _playSampleRate = fsHz;
101 return 0; 117 return 0;
102 } 118 }
103 119
104 int32_t AudioDeviceBuffer::RecordingSampleRate() const { 120 int32_t AudioDeviceBuffer::RecordingSampleRate() const {
105 return _recSampleRate; 121 return _recSampleRate;
106 } 122 }
107 123
108 int32_t AudioDeviceBuffer::PlayoutSampleRate() const { 124 int32_t AudioDeviceBuffer::PlayoutSampleRate() const {
109 return _playSampleRate; 125 return _playSampleRate;
110 } 126 }
111 127
112 int32_t AudioDeviceBuffer::SetRecordingChannels(size_t channels) { 128 int32_t AudioDeviceBuffer::SetRecordingChannels(size_t channels) {
113 CriticalSectionScoped lock(&_critSect); 129 rtc::CritScope lock(&_critSect);
114 _recChannels = channels; 130 _recChannels = channels;
115 _recBytesPerSample = 131 _recBytesPerSample =
116 2 * channels; // 16 bits per sample in mono, 32 bits in stereo 132 2 * channels; // 16 bits per sample in mono, 32 bits in stereo
117 return 0; 133 return 0;
118 } 134 }
119 135
120 int32_t AudioDeviceBuffer::SetPlayoutChannels(size_t channels) { 136 int32_t AudioDeviceBuffer::SetPlayoutChannels(size_t channels) {
121 CriticalSectionScoped lock(&_critSect); 137 rtc::CritScope lock(&_critSect);
122 _playChannels = channels; 138 _playChannels = channels;
123 // 16 bits per sample in mono, 32 bits in stereo 139 // 16 bits per sample in mono, 32 bits in stereo
124 _playBytesPerSample = 2 * channels; 140 _playBytesPerSample = 2 * channels;
125 return 0; 141 return 0;
126 } 142 }
127 143
128 int32_t AudioDeviceBuffer::SetRecordingChannel( 144 int32_t AudioDeviceBuffer::SetRecordingChannel(
129 const AudioDeviceModule::ChannelType channel) { 145 const AudioDeviceModule::ChannelType channel) {
130 CriticalSectionScoped lock(&_critSect); 146 rtc::CritScope lock(&_critSect);
131 147
132 if (_recChannels == 1) { 148 if (_recChannels == 1) {
133 return -1; 149 return -1;
134 } 150 }
135 151
136 if (channel == AudioDeviceModule::kChannelBoth) { 152 if (channel == AudioDeviceModule::kChannelBoth) {
137 // two bytes per channel 153 // two bytes per channel
138 _recBytesPerSample = 4; 154 _recBytesPerSample = 4;
139 } else { 155 } else {
140 // only utilize one out of two possible channels (left or right) 156 // only utilize one out of two possible channels (left or right)
(...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after
186 } 202 }
187 } 203 }
188 204
189 _playDelayMS = playDelayMs; 205 _playDelayMS = playDelayMs;
190 _recDelayMS = recDelayMs; 206 _recDelayMS = recDelayMs;
191 _clockDrift = clockDrift; 207 _clockDrift = clockDrift;
192 } 208 }
193 209
194 int32_t AudioDeviceBuffer::StartInputFileRecording( 210 int32_t AudioDeviceBuffer::StartInputFileRecording(
195 const char fileName[kAdmMaxFileNameSize]) { 211 const char fileName[kAdmMaxFileNameSize]) {
196 CriticalSectionScoped lock(&_critSect); 212 rtc::CritScope lock(&_critSect);
197 213
198 _recFile.Flush(); 214 _recFile.Flush();
199 _recFile.CloseFile(); 215 _recFile.CloseFile();
200 216
201 return _recFile.OpenFile(fileName, false) ? 0 : -1; 217 return _recFile.OpenFile(fileName, false) ? 0 : -1;
202 } 218 }
203 219
204 int32_t AudioDeviceBuffer::StopInputFileRecording() { 220 int32_t AudioDeviceBuffer::StopInputFileRecording() {
205 CriticalSectionScoped lock(&_critSect); 221 rtc::CritScope lock(&_critSect);
206 222
207 _recFile.Flush(); 223 _recFile.Flush();
208 _recFile.CloseFile(); 224 _recFile.CloseFile();
209 225
210 return 0; 226 return 0;
211 } 227 }
212 228
213 int32_t AudioDeviceBuffer::StartOutputFileRecording( 229 int32_t AudioDeviceBuffer::StartOutputFileRecording(
214 const char fileName[kAdmMaxFileNameSize]) { 230 const char fileName[kAdmMaxFileNameSize]) {
215 CriticalSectionScoped lock(&_critSect); 231 rtc::CritScope lock(&_critSect);
216 232
217 _playFile.Flush(); 233 _playFile.Flush();
218 _playFile.CloseFile(); 234 _playFile.CloseFile();
219 235
220 return _playFile.OpenFile(fileName, false) ? 0 : -1; 236 return _playFile.OpenFile(fileName, false) ? 0 : -1;
221 } 237 }
222 238
223 int32_t AudioDeviceBuffer::StopOutputFileRecording() { 239 int32_t AudioDeviceBuffer::StopOutputFileRecording() {
224 CriticalSectionScoped lock(&_critSect); 240 rtc::CritScope lock(&_critSect);
225 241
226 _playFile.Flush(); 242 _playFile.Flush();
227 _playFile.CloseFile(); 243 _playFile.CloseFile();
228 244
229 return 0; 245 return 0;
230 } 246 }
231 247
232 int32_t AudioDeviceBuffer::SetRecordedBuffer(const void* audioBuffer, 248 int32_t AudioDeviceBuffer::SetRecordedBuffer(const void* audioBuffer,
233 size_t nSamples) { 249 size_t nSamples) {
234 CriticalSectionScoped lock(&_critSect); 250 rtc::CritScope lock(&_critSect);
235 251
236 if (_recBytesPerSample == 0) { 252 if (_recBytesPerSample == 0) {
237 assert(false); 253 assert(false);
238 return -1; 254 return -1;
239 } 255 }
240 256
241 _recSamples = nSamples; 257 _recSamples = nSamples;
242 _recSize = _recBytesPerSample * nSamples; // {2,4}*nSamples 258 _recSize = _recBytesPerSample * nSamples; // {2,4}*nSamples
243 if (_recSize > kMaxBufferSizeBytes) { 259 if (_recSize > kMaxBufferSizeBytes) {
244 assert(false); 260 assert(false);
(...skipping 18 matching lines...) Expand all
263 ptr16In++; 279 ptr16In++;
264 ptr16In++; 280 ptr16In++;
265 } 281 }
266 } 282 }
267 283
268 if (_recFile.is_open()) { 284 if (_recFile.is_open()) {
269 // write to binary file in mono or stereo (interleaved) 285 // write to binary file in mono or stereo (interleaved)
270 _recFile.Write(&_recBuffer[0], _recSize); 286 _recFile.Write(&_recBuffer[0], _recSize);
271 } 287 }
272 288
289 ++rec_callbacks_;
290 rec_samples_ += nSamples;
291
273 return 0; 292 return 0;
274 } 293 }
275 294
276 int32_t AudioDeviceBuffer::DeliverRecordedData() { 295 int32_t AudioDeviceBuffer::DeliverRecordedData() {
277 CriticalSectionScoped lock(&_critSectCb); 296 rtc::CritScope lock(&_critSectCb);
278 // Ensure that user has initialized all essential members 297 // Ensure that user has initialized all essential members
279 if ((_recSampleRate == 0) || (_recSamples == 0) || 298 if ((_recSampleRate == 0) || (_recSamples == 0) ||
280 (_recBytesPerSample == 0) || (_recChannels == 0)) { 299 (_recBytesPerSample == 0) || (_recChannels == 0)) {
281 RTC_NOTREACHED(); 300 RTC_NOTREACHED();
282 return -1; 301 return -1;
283 } 302 }
284 303
285 if (!_ptrCbAudioTransport) { 304 if (!_ptrCbAudioTransport) {
286 LOG(LS_WARNING) << "Invalid audio transport"; 305 LOG(LS_WARNING) << "Invalid audio transport";
287 return 0; 306 return 0;
(...skipping 14 matching lines...) Expand all
302 } 321 }
303 322
304 int32_t AudioDeviceBuffer::RequestPlayoutData(size_t nSamples) { 323 int32_t AudioDeviceBuffer::RequestPlayoutData(size_t nSamples) {
305 uint32_t playSampleRate = 0; 324 uint32_t playSampleRate = 0;
306 size_t playBytesPerSample = 0; 325 size_t playBytesPerSample = 0;
307 size_t playChannels = 0; 326 size_t playChannels = 0;
308 327
309 // TOOD(henrika): improve bad locking model and make it more clear that only 328 // TOOD(henrika): improve bad locking model and make it more clear that only
310 // 10ms buffer sizes is supported in WebRTC. 329 // 10ms buffer sizes is supported in WebRTC.
311 { 330 {
312 CriticalSectionScoped lock(&_critSect); 331 rtc::CritScope lock(&_critSect);
313 332
314 // Store copies under lock and use copies hereafter to avoid race with 333 // Store copies under lock and use copies hereafter to avoid race with
315 // setter methods. 334 // setter methods.
316 playSampleRate = _playSampleRate; 335 playSampleRate = _playSampleRate;
317 playBytesPerSample = _playBytesPerSample; 336 playBytesPerSample = _playBytesPerSample;
318 playChannels = _playChannels; 337 playChannels = _playChannels;
319 338
320 // Ensure that user has initialized all essential members 339 // Ensure that user has initialized all essential members
321 if ((playBytesPerSample == 0) || (playChannels == 0) || 340 if ((playBytesPerSample == 0) || (playChannels == 0) ||
322 (playSampleRate == 0)) { 341 (playSampleRate == 0)) {
323 RTC_NOTREACHED(); 342 RTC_NOTREACHED();
324 return -1; 343 return -1;
325 } 344 }
326 345
327 _playSamples = nSamples; 346 _playSamples = nSamples;
328 _playSize = playBytesPerSample * nSamples; // {2,4}*nSamples 347 _playSize = playBytesPerSample * nSamples; // {2,4}*nSamples
329 RTC_CHECK_LE(_playSize, kMaxBufferSizeBytes); 348 RTC_CHECK_LE(_playSize, kMaxBufferSizeBytes);
330 RTC_CHECK_EQ(nSamples, _playSamples); 349 RTC_CHECK_EQ(nSamples, _playSamples);
331 } 350 }
332 351
333 size_t nSamplesOut(0); 352 size_t nSamplesOut(0);
334 353
335 CriticalSectionScoped lock(&_critSectCb); 354 rtc::CritScope lock(&_critSectCb);
336 355
337 // It is currently supported to start playout without a valid audio 356 // It is currently supported to start playout without a valid audio
338 // transport object. Leads to warning and silence. 357 // transport object. Leads to warning and silence.
339 if (!_ptrCbAudioTransport) { 358 if (!_ptrCbAudioTransport) {
340 LOG(LS_WARNING) << "Invalid audio transport"; 359 LOG(LS_WARNING) << "Invalid audio transport";
341 return 0; 360 return 0;
342 } 361 }
343 362
344 uint32_t res(0); 363 uint32_t res(0);
345 int64_t elapsed_time_ms = -1; 364 int64_t elapsed_time_ms = -1;
346 int64_t ntp_time_ms = -1; 365 int64_t ntp_time_ms = -1;
347 res = _ptrCbAudioTransport->NeedMorePlayData( 366 res = _ptrCbAudioTransport->NeedMorePlayData(
348 _playSamples, playBytesPerSample, playChannels, playSampleRate, 367 _playSamples, playBytesPerSample, playChannels, playSampleRate,
349 &_playBuffer[0], nSamplesOut, &elapsed_time_ms, &ntp_time_ms); 368 &_playBuffer[0], nSamplesOut, &elapsed_time_ms, &ntp_time_ms);
350 if (res != 0) { 369 if (res != 0) {
351 LOG(LS_ERROR) << "NeedMorePlayData() failed"; 370 LOG(LS_ERROR) << "NeedMorePlayData() failed";
352 } 371 }
353 372
373 ++play_callbacks_;
374 play_samples_ += nSamplesOut;
375
354 return static_cast<int32_t>(nSamplesOut); 376 return static_cast<int32_t>(nSamplesOut);
355 } 377 }
356 378
357 int32_t AudioDeviceBuffer::GetPlayoutData(void* audioBuffer) { 379 int32_t AudioDeviceBuffer::GetPlayoutData(void* audioBuffer) {
358 CriticalSectionScoped lock(&_critSect); 380 rtc::CritScope lock(&_critSect);
359 RTC_CHECK_LE(_playSize, kMaxBufferSizeBytes); 381 RTC_CHECK_LE(_playSize, kMaxBufferSizeBytes);
360 382
361 memcpy(audioBuffer, &_playBuffer[0], _playSize); 383 memcpy(audioBuffer, &_playBuffer[0], _playSize);
362 384
363 if (_playFile.is_open()) { 385 if (_playFile.is_open()) {
364 // write to binary file in mono or stereo (interleaved) 386 // write to binary file in mono or stereo (interleaved)
365 _playFile.Write(&_playBuffer[0], _playSize); 387 _playFile.Write(&_playBuffer[0], _playSize);
366 } 388 }
367 389
368 return static_cast<int32_t>(_playSamples); 390 return static_cast<int32_t>(_playSamples);
369 } 391 }
370 392
393 void AudioDeviceBuffer::StartTimer() {
394 task_queue_->PostDelayedTask(rtc::Bind(&AudioDeviceBuffer::LogStats, this),
395 kTimerIntervalInMilliseconds);
396 }
397
398 void AudioDeviceBuffer::LogStats() {
399 RTC_DCHECK(task_queue_->IsCurrent());
400
401 int32_t next_callback_time = rtc::Time32() + kTimerIntervalInMilliseconds;
stefan-webrtc 2016/07/07 15:22:13 I think it'd be better to use TimeMillis() and int
henrika_webrtc 2016/07/08 12:46:48 Done.
402
403 uint32_t diff_samples = rec_samples_ - last_rec_samples_;
404 uint32_t rate = diff_samples / kTimerIntervalInSeconds;
405 LOG(INFO) << "[REC:10 sec@" << _recSampleRate / 1000
406 << "kHz] callbacks: " << rec_callbacks_ - last_rec_callbacks_
407 << ", "
408 << "samples: " << diff_samples << ", "
409 << "rate: " << rate;
410
411 diff_samples = play_samples_ - last_play_samples_;
412 rate = diff_samples / kTimerIntervalInSeconds;
413 LOG(INFO) << "[PLAY:10 sec@" << _playSampleRate / 1000
414 << "kHz] callbacks: " << play_callbacks_ - last_play_callbacks_
415 << ", "
416 << "samples: " << diff_samples << ", "
417 << "rate: " << rate;
418
419 last_rec_callbacks_ = rec_callbacks_;
stefan-webrtc 2016/07/07 15:22:13 As mentioned offline, I think you have to protect
henrika_webrtc 2016/07/08 12:46:48 It actually does not complain. At least not in the
420 last_play_callbacks_ = play_callbacks_;
421 last_rec_samples_ = rec_samples_;
422 last_play_samples_ = play_samples_;
423
424 int32_t time_to_wait_ms = next_callback_time - rtc::Time32();
stefan-webrtc 2016/07/07 15:22:13 same here.
henrika_webrtc 2016/07/08 12:46:48 Done.
425 RTC_DCHECK_GT(time_to_wait_ms, 0) << "Invalid timer interval";
426
427 task_queue_->PostDelayedTask(rtc::Bind(&AudioDeviceBuffer::LogStats, this),
428 time_to_wait_ms);
429 }
430
371 } // namespace webrtc 431 } // namespace webrtc
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698