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

Side by Side Diff: media/audio/android/opensles_input.cc

Issue 23296008: Adding audio unit tests for Android (Closed) Base URL: http://git.chromium.org/chromium/src.git@master
Patch Set: Feedback from tommi@ Created 7 years, 3 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
« no previous file with comments | « media/audio/android/opensles_input.h ('k') | media/audio/android/opensles_output.cc » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "media/audio/android/opensles_input.h" 5 #include "media/audio/android/opensles_input.h"
6 6
7 #include "base/logging.h" 7 #include "base/logging.h"
8 #include "media/audio/android/audio_manager_android.h" 8 #include "media/audio/android/audio_manager_android.h"
9 9
10 #define LOG_ON_FAILURE_AND_RETURN(op, ...) \ 10 #define LOG_ON_FAILURE_AND_RETURN(op, ...) \
11 do { \ 11 do { \
12 SLresult err = (op); \ 12 SLresult err = (op); \
13 if (err != SL_RESULT_SUCCESS) { \ 13 if (err != SL_RESULT_SUCCESS) { \
14 DLOG(ERROR) << #op << " failed: " << err; \ 14 DLOG(ERROR) << #op << " failed: " << err; \
15 return __VA_ARGS__; \ 15 return __VA_ARGS__; \
16 } \ 16 } \
17 } while (0) 17 } while (0)
18 18
19 namespace media { 19 namespace media {
20 20
21 OpenSLESInputStream::OpenSLESInputStream(AudioManagerAndroid* audio_manager, 21 OpenSLESInputStream::OpenSLESInputStream(AudioManagerAndroid* audio_manager,
22 const AudioParameters& params) 22 const AudioParameters& params)
23 : audio_manager_(audio_manager), 23 : audio_manager_(audio_manager),
24 callback_(NULL), 24 callback_(NULL),
25 recorder_(NULL), 25 recorder_(NULL),
26 simple_buffer_queue_(NULL), 26 simple_buffer_queue_(NULL),
27 active_queue_(0), 27 active_buffer_(0),
28 buffer_size_bytes_(0), 28 buffer_size_bytes_(0),
29 started_(false) { 29 started_(false) {
30 DVLOG(2) << "OpenSLESInputStream::OpenSLESInputStream()";
30 format_.formatType = SL_DATAFORMAT_PCM; 31 format_.formatType = SL_DATAFORMAT_PCM;
31 format_.numChannels = static_cast<SLuint32>(params.channels()); 32 format_.numChannels = static_cast<SLuint32>(params.channels());
32 // Provides sampling rate in milliHertz to OpenSLES. 33 // Provides sampling rate in milliHertz to OpenSLES.
33 format_.samplesPerSec = static_cast<SLuint32>(params.sample_rate() * 1000); 34 format_.samplesPerSec = static_cast<SLuint32>(params.sample_rate() * 1000);
34 format_.bitsPerSample = params.bits_per_sample(); 35 format_.bitsPerSample = params.bits_per_sample();
35 format_.containerSize = params.bits_per_sample(); 36 format_.containerSize = params.bits_per_sample();
36 format_.endianness = SL_BYTEORDER_LITTLEENDIAN; 37 format_.endianness = SL_BYTEORDER_LITTLEENDIAN;
37 if (format_.numChannels == 1) 38 if (format_.numChannels == 1)
38 format_.channelMask = SL_SPEAKER_FRONT_CENTER; 39 format_.channelMask = SL_SPEAKER_FRONT_CENTER;
39 else if (format_.numChannels == 2) 40 else if (format_.numChannels == 2)
40 format_.channelMask = SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT; 41 format_.channelMask = SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT;
41 else 42 else
42 NOTREACHED() << "Unsupported number of channels: " << format_.numChannels; 43 NOTREACHED() << "Unsupported number of channels: " << format_.numChannels;
43 44
44 buffer_size_bytes_ = params.GetBytesPerBuffer(); 45 buffer_size_bytes_ = params.GetBytesPerBuffer();
45 46
46 memset(&audio_data_, 0, sizeof(audio_data_)); 47 memset(&audio_data_, 0, sizeof(audio_data_));
47 } 48 }
48 49
49 OpenSLESInputStream::~OpenSLESInputStream() { 50 OpenSLESInputStream::~OpenSLESInputStream() {
51 DVLOG(2) << "OpenSLESInputStream::~OpenSLESInputStream()";
52 DCHECK(thread_checker_.CalledOnValidThread());
50 DCHECK(!recorder_object_.Get()); 53 DCHECK(!recorder_object_.Get());
51 DCHECK(!engine_object_.Get()); 54 DCHECK(!engine_object_.Get());
52 DCHECK(!recorder_); 55 DCHECK(!recorder_);
53 DCHECK(!simple_buffer_queue_); 56 DCHECK(!simple_buffer_queue_);
54 DCHECK(!audio_data_[0]); 57 DCHECK(!audio_data_[0]);
55 } 58 }
56 59
57 bool OpenSLESInputStream::Open() { 60 bool OpenSLESInputStream::Open() {
61 DVLOG(2) << "OpenSLESInputStream::Open()";
62 DCHECK(thread_checker_.CalledOnValidThread());
58 if (engine_object_.Get()) 63 if (engine_object_.Get())
59 return false; 64 return false;
60 65
61 if (!CreateRecorder()) 66 if (!CreateRecorder())
62 return false; 67 return false;
63 68
64 SetupAudioBuffer(); 69 SetupAudioBuffer();
70 active_buffer_ = 0;
65 71
66 return true; 72 return true;
67 } 73 }
68 74
69 void OpenSLESInputStream::Start(AudioInputCallback* callback) { 75 void OpenSLESInputStream::Start(AudioInputCallback* callback) {
76 DVLOG(2) << "OpenSLESInputStream::Start()";
77 DCHECK(thread_checker_.CalledOnValidThread());
70 DCHECK(callback); 78 DCHECK(callback);
79 DCHECK(!callback_);
tommi (sloooow) - chröme 2013/08/30 14:37:13 nit: keep the dcheck behind the lock
henrika (OOO until Aug 14) 2013/08/30 14:44:27 Done.
henrika (OOO until Aug 14) 2013/09/02 09:36:15 Actually, this will not work since we don't reset
tommi (sloooow) - chröme 2013/09/02 09:49:41 That's either a design flaw or we simply have bugs
71 DCHECK(recorder_); 80 DCHECK(recorder_);
72 DCHECK(simple_buffer_queue_); 81 DCHECK(simple_buffer_queue_);
73 if (started_) 82 if (started_)
74 return; 83 return;
75 84
76 // Enable the flags before streaming. 85 base::AutoLock lock(lock_);
77 callback_ = callback; 86 callback_ = callback;
78 active_queue_ = 0;
79 started_ = true;
80 87
81 SLresult err = SL_RESULT_UNKNOWN_ERROR; 88 SLAndroidSimpleBufferQueueState buffer_queue_state;
82 // Enqueues |kNumOfQueuesInBuffer| zero buffers to get the ball rolling. 89 SLresult err = (*simple_buffer_queue_)->GetState(simple_buffer_queue_,
83 for (int i = 0; i < kNumOfQueuesInBuffer; ++i) { 90 &buffer_queue_state);
91 if (SL_RESULT_SUCCESS != err) {
92 HandleError(err);
93 return;
94 }
95
96 // Number of free buffers in the queue.
97 int num_free_buffers = kMaxNumOfBuffersInQueue - buffer_queue_state.count;
98 DCHECK(num_free_buffers == kMaxNumOfBuffersInQueue || num_free_buffers == 0);
99
100 // Enqueues |num_free_buffers| zero buffers to get the ball rolling.
101 // |num_free_buffers| can be zero if Stop has been called followed by a
102 // new call to Start. We do clear the queue in Stop, and the buffer state
103 // should then be cleared, but for some reason it is not. Using this
104 // approach enables call sequences like: Start, Stop, Start, even if there
105 // might be some old data remaining at the second call to Start.
106 for (int i = 0; i < num_free_buffers; ++i) {
84 err = (*simple_buffer_queue_)->Enqueue( 107 err = (*simple_buffer_queue_)->Enqueue(
85 simple_buffer_queue_, 108 simple_buffer_queue_,
86 audio_data_[i], 109 audio_data_[i],
87 buffer_size_bytes_); 110 buffer_size_bytes_);
88 if (SL_RESULT_SUCCESS != err) { 111 if (SL_RESULT_SUCCESS != err) {
89 HandleError(err); 112 HandleError(err);
90 return; 113 return;
91 } 114 }
92 } 115 }
93 116
94 // Start the recording by setting the state to |SL_RECORDSTATE_RECORDING|. 117 // Start the recording by setting the state to |SL_RECORDSTATE_RECORDING|.
95 err = (*recorder_)->SetRecordState(recorder_, SL_RECORDSTATE_RECORDING); 118 err = (*recorder_)->SetRecordState(recorder_, SL_RECORDSTATE_RECORDING);
96 if (SL_RESULT_SUCCESS != err) 119 if (SL_RESULT_SUCCESS != err) {
97 HandleError(err); 120 HandleError(err);
121 }
122
123 started_ = true;
tommi (sloooow) - chröme 2013/08/30 14:37:13 if we failed to set the state to 'recording' I don
henrika (OOO until Aug 14) 2013/08/30 14:44:27 Done.
98 } 124 }
99 125
100 void OpenSLESInputStream::Stop() { 126 void OpenSLESInputStream::Stop() {
127 DVLOG(2) << "OpenSLESInputStream::Stop()";
128 DCHECK(thread_checker_.CalledOnValidThread());
101 if (!started_) 129 if (!started_)
102 return; 130 return;
103 131
132 base::AutoLock lock(lock_);
133
104 // Stop recording by setting the record state to |SL_RECORDSTATE_STOPPED|. 134 // Stop recording by setting the record state to |SL_RECORDSTATE_STOPPED|.
105 LOG_ON_FAILURE_AND_RETURN( 135 LOG_ON_FAILURE_AND_RETURN(
106 (*recorder_)->SetRecordState(recorder_, 136 (*recorder_)->SetRecordState(recorder_,
107 SL_RECORDSTATE_STOPPED)); 137 SL_RECORDSTATE_STOPPED));
108 138
109 // Clear the buffer queue to get rid of old data when resuming recording. 139 // Clear the buffer queue to get rid of old data when resuming recording.
110 LOG_ON_FAILURE_AND_RETURN( 140 LOG_ON_FAILURE_AND_RETURN(
111 (*simple_buffer_queue_)->Clear(simple_buffer_queue_)); 141 (*simple_buffer_queue_)->Clear(simple_buffer_queue_));
112 142
113 started_ = false; 143 started_ = false;
114 } 144 }
115 145
116 void OpenSLESInputStream::Close() { 146 void OpenSLESInputStream::Close() {
147 DVLOG(2) << "OpenSLESInputStream::Close()";
148 DCHECK(thread_checker_.CalledOnValidThread());
149
117 // Stop the stream if it is still recording. 150 // Stop the stream if it is still recording.
118 Stop(); 151 Stop();
152 {
153 base::AutoLock lock(lock_);
119 154
120 // Explicitly free the player objects and invalidate their associated 155 if (callback_) {
121 // interfaces. They have to be done in the correct order. 156 callback_->OnClose(this);
122 recorder_object_.Reset(); 157 callback_ = NULL;
123 engine_object_.Reset(); 158 }
124 simple_buffer_queue_ = NULL;
125 recorder_ = NULL;
126 159
127 ReleaseAudioBuffer(); 160 // Destroy the buffer queue recorder object and invalidate all associated
161 // interfaces.
162 recorder_object_.Reset();
163 simple_buffer_queue_ = NULL;
164 recorder_ = NULL;
165
166 // Destroy the engine object. We don't store any associated interface for
167 // this object.
168 engine_object_.Reset();
169 ReleaseAudioBuffer();
170 }
128 171
129 audio_manager_->ReleaseInputStream(this); 172 audio_manager_->ReleaseInputStream(this);
130 } 173 }
131 174
132 double OpenSLESInputStream::GetMaxVolume() { 175 double OpenSLESInputStream::GetMaxVolume() {
133 NOTIMPLEMENTED(); 176 NOTIMPLEMENTED();
134 return 0.0; 177 return 0.0;
135 } 178 }
136 179
137 void OpenSLESInputStream::SetVolume(double volume) { 180 void OpenSLESInputStream::SetVolume(double volume) {
138 NOTIMPLEMENTED(); 181 NOTIMPLEMENTED();
139 } 182 }
140 183
141 double OpenSLESInputStream::GetVolume() { 184 double OpenSLESInputStream::GetVolume() {
142 NOTIMPLEMENTED(); 185 NOTIMPLEMENTED();
143 return 0.0; 186 return 0.0;
144 } 187 }
145 188
146 void OpenSLESInputStream::SetAutomaticGainControl(bool enabled) { 189 void OpenSLESInputStream::SetAutomaticGainControl(bool enabled) {
147 NOTIMPLEMENTED(); 190 NOTIMPLEMENTED();
148 } 191 }
149 192
150 bool OpenSLESInputStream::GetAutomaticGainControl() { 193 bool OpenSLESInputStream::GetAutomaticGainControl() {
151 NOTIMPLEMENTED(); 194 NOTIMPLEMENTED();
152 return false; 195 return false;
153 } 196 }
154 197
155 bool OpenSLESInputStream::CreateRecorder() { 198 bool OpenSLESInputStream::CreateRecorder() {
199 DCHECK(thread_checker_.CalledOnValidThread());
200 DCHECK(!engine_object_.Get());
201 DCHECK(!recorder_object_.Get());
202 DCHECK(!recorder_);
203 DCHECK(!simple_buffer_queue_);
204
156 // Initializes the engine object with specific option. After working with the 205 // Initializes the engine object with specific option. After working with the
157 // object, we need to free the object and its resources. 206 // object, we need to free the object and its resources.
158 SLEngineOption option[] = { 207 SLEngineOption option[] = {
159 { SL_ENGINEOPTION_THREADSAFE, static_cast<SLuint32>(SL_BOOLEAN_TRUE) } 208 { SL_ENGINEOPTION_THREADSAFE, static_cast<SLuint32>(SL_BOOLEAN_TRUE) }
160 }; 209 };
161 LOG_ON_FAILURE_AND_RETURN(slCreateEngine(engine_object_.Receive(), 210 LOG_ON_FAILURE_AND_RETURN(slCreateEngine(engine_object_.Receive(),
162 1, 211 1,
163 option, 212 option,
164 0, 213 0,
165 NULL, 214 NULL,
(...skipping 15 matching lines...) Expand all
181 // Audio source configuration. 230 // Audio source configuration.
182 SLDataLocator_IODevice mic_locator = { 231 SLDataLocator_IODevice mic_locator = {
183 SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT, 232 SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT,
184 SL_DEFAULTDEVICEID_AUDIOINPUT, NULL 233 SL_DEFAULTDEVICEID_AUDIOINPUT, NULL
185 }; 234 };
186 SLDataSource audio_source = { &mic_locator, NULL }; 235 SLDataSource audio_source = { &mic_locator, NULL };
187 236
188 // Audio sink configuration. 237 // Audio sink configuration.
189 SLDataLocator_AndroidSimpleBufferQueue buffer_queue = { 238 SLDataLocator_AndroidSimpleBufferQueue buffer_queue = {
190 SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, // Locator type. 239 SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, // Locator type.
191 static_cast<SLuint32>(kNumOfQueuesInBuffer) // Number of buffers. 240 static_cast<SLuint32>(kMaxNumOfBuffersInQueue) // Number of buffers.
192 }; 241 };
193 SLDataSink audio_sink = { &buffer_queue, &format_ }; 242 SLDataSink audio_sink = { &buffer_queue, &format_ };
194 243
195 // Create an audio recorder. 244 // Create an audio recorder.
196 const SLInterfaceID interface_id[] = { 245 const SLInterfaceID interface_id[] = {
197 SL_IID_ANDROIDSIMPLEBUFFERQUEUE, 246 SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
198 SL_IID_ANDROIDCONFIGURATION 247 SL_IID_ANDROIDCONFIGURATION
199 }; 248 };
200 const SLboolean interface_required[] = { 249 const SLboolean interface_required[] = {
201 SL_BOOLEAN_TRUE, 250 SL_BOOLEAN_TRUE,
202 SL_BOOLEAN_TRUE 251 SL_BOOLEAN_TRUE
203 }; 252 };
253
204 // Create AudioRecorder and specify SL_IID_ANDROIDCONFIGURATION. 254 // Create AudioRecorder and specify SL_IID_ANDROIDCONFIGURATION.
205 LOG_ON_FAILURE_AND_RETURN( 255 LOG_ON_FAILURE_AND_RETURN(
206 (*engine)->CreateAudioRecorder(engine, 256 (*engine)->CreateAudioRecorder(engine,
207 recorder_object_.Receive(), 257 recorder_object_.Receive(),
208 &audio_source, 258 &audio_source,
209 &audio_sink, 259 &audio_sink,
210 arraysize(interface_id), 260 arraysize(interface_id),
211 interface_id, 261 interface_id,
212 interface_required), 262 interface_required),
213 false); 263 false);
214 264
215 SLAndroidConfigurationItf recorder_config; 265 SLAndroidConfigurationItf recorder_config;
216 LOG_ON_FAILURE_AND_RETURN( 266 LOG_ON_FAILURE_AND_RETURN(
217 recorder_object_->GetInterface(recorder_object_.Get(), 267 recorder_object_->GetInterface(recorder_object_.Get(),
218 SL_IID_ANDROIDCONFIGURATION, 268 SL_IID_ANDROIDCONFIGURATION,
219 &recorder_config), 269 &recorder_config),
220 false); 270 false);
221 271
272 // Uses the main microphone tuned for audio communications.
222 SLint32 stream_type = SL_ANDROID_RECORDING_PRESET_VOICE_COMMUNICATION; 273 SLint32 stream_type = SL_ANDROID_RECORDING_PRESET_VOICE_COMMUNICATION;
223 LOG_ON_FAILURE_AND_RETURN( 274 LOG_ON_FAILURE_AND_RETURN(
224 (*recorder_config)->SetConfiguration(recorder_config, 275 (*recorder_config)->SetConfiguration(recorder_config,
225 SL_ANDROID_KEY_RECORDING_PRESET, 276 SL_ANDROID_KEY_RECORDING_PRESET,
226 &stream_type, sizeof(SLint32)), 277 &stream_type, sizeof(SLint32)),
227 false); 278 false);
228 279
229 // Realize the recorder object in synchronous mode. 280 // Realize the recorder object in synchronous mode.
230 LOG_ON_FAILURE_AND_RETURN( 281 LOG_ON_FAILURE_AND_RETURN(
231 recorder_object_->Realize(recorder_object_.Get(), 282 recorder_object_->Realize(recorder_object_.Get(),
(...skipping 26 matching lines...) Expand all
258 } 309 }
259 310
260 void OpenSLESInputStream::SimpleBufferQueueCallback( 311 void OpenSLESInputStream::SimpleBufferQueueCallback(
261 SLAndroidSimpleBufferQueueItf buffer_queue, void* instance) { 312 SLAndroidSimpleBufferQueueItf buffer_queue, void* instance) {
262 OpenSLESInputStream* stream = 313 OpenSLESInputStream* stream =
263 reinterpret_cast<OpenSLESInputStream*>(instance); 314 reinterpret_cast<OpenSLESInputStream*>(instance);
264 stream->ReadBufferQueue(); 315 stream->ReadBufferQueue();
265 } 316 }
266 317
267 void OpenSLESInputStream::ReadBufferQueue() { 318 void OpenSLESInputStream::ReadBufferQueue() {
268 if (!started_) 319 base::AutoLock lock(lock_);
320
321 // Verify that we are in a recording state.
322 SLuint32 state;
323 SLresult err = (*recorder_)->GetRecordState(recorder_, &state);
324 if (SL_RESULT_SUCCESS != err) {
325 HandleError(err);
269 return; 326 return;
327 }
328 if (state != SL_RECORDSTATE_RECORDING) {
329 DLOG(WARNING) << "Received callback in non-recording state";
330 return;
331 }
270 332
271 // TODO(xians): Get an accurate delay estimation. 333 // TODO(xians): Get an accurate delay estimation.
272 callback_->OnData(this, 334 callback_->OnData(this,
273 audio_data_[active_queue_], 335 audio_data_[active_buffer_],
274 buffer_size_bytes_, 336 buffer_size_bytes_,
275 buffer_size_bytes_, 337 buffer_size_bytes_,
276 0.0); 338 0.0);
277 339
278 // Done with this buffer. Send it to device for recording. 340 // Done with this buffer. Send it to device for recording.
279 SLresult err = (*simple_buffer_queue_)->Enqueue( 341 err = (*simple_buffer_queue_)->Enqueue(
tommi (sloooow) - chröme 2013/08/30 14:37:13 I just noticed that a buffer_queue pointer is supp
henrika (OOO until Aug 14) 2013/08/30 14:44:27 Offline discussions => we decided that I could ign
280 simple_buffer_queue_, 342 simple_buffer_queue_,
281 audio_data_[active_queue_], 343 audio_data_[active_buffer_],
282 buffer_size_bytes_); 344 buffer_size_bytes_);
283 if (SL_RESULT_SUCCESS != err) 345 if (SL_RESULT_SUCCESS != err)
284 HandleError(err); 346 HandleError(err);
285 347
286 active_queue_ = (active_queue_ + 1) % kNumOfQueuesInBuffer; 348 active_buffer_ = (active_buffer_ + 1) % kMaxNumOfBuffersInQueue;
287 } 349 }
288 350
289 void OpenSLESInputStream::SetupAudioBuffer() { 351 void OpenSLESInputStream::SetupAudioBuffer() {
352 DCHECK(thread_checker_.CalledOnValidThread());
290 DCHECK(!audio_data_[0]); 353 DCHECK(!audio_data_[0]);
291 for (int i = 0; i < kNumOfQueuesInBuffer; ++i) { 354 for (int i = 0; i < kMaxNumOfBuffersInQueue; ++i) {
292 audio_data_[i] = new uint8[buffer_size_bytes_]; 355 audio_data_[i] = new uint8[buffer_size_bytes_];
293 } 356 }
294 } 357 }
295 358
296 void OpenSLESInputStream::ReleaseAudioBuffer() { 359 void OpenSLESInputStream::ReleaseAudioBuffer() {
360 DCHECK(thread_checker_.CalledOnValidThread());
297 if (audio_data_[0]) { 361 if (audio_data_[0]) {
298 for (int i = 0; i < kNumOfQueuesInBuffer; ++i) { 362 for (int i = 0; i < kMaxNumOfBuffersInQueue; ++i) {
299 delete [] audio_data_[i]; 363 delete [] audio_data_[i];
300 audio_data_[i] = NULL; 364 audio_data_[i] = NULL;
301 } 365 }
302 } 366 }
303 } 367 }
304 368
305 void OpenSLESInputStream::HandleError(SLresult error) { 369 void OpenSLESInputStream::HandleError(SLresult error) {
306 DLOG(FATAL) << "OpenSLES Input error " << error; 370 DLOG(ERROR) << "OpenSLES Input error " << error;
307 if (callback_) 371 if (callback_)
308 callback_->OnError(this); 372 callback_->OnError(this);
309 } 373 }
310 374
311 } // namespace media 375 } // namespace media
OLDNEW
« no previous file with comments | « media/audio/android/opensles_input.h ('k') | media/audio/android/opensles_output.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698