1 /*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 #define LOG_TAG "StreamHalLocal"
18 //#define LOG_NDEBUG 0
19
20 #include <audio_utils/Metadata.h>
21 #include <hardware/audio.h>
22 #include <media/AudioParameter.h>
23 #include <utils/Log.h>
24
25 #include "DeviceHalLocal.h"
26 #include "ParameterUtils.h"
27 #include "StreamHalLocal.h"
28
29 namespace android {
30 namespace CPP_VERSION {
31
StreamHalLocal(audio_stream_t * stream,sp<DeviceHalLocal> device)32 StreamHalLocal::StreamHalLocal(audio_stream_t *stream, sp<DeviceHalLocal> device)
33 : mDevice(device),
34 mStream(stream) {
35 // Instrument audio signal power logging.
36 // Note: This assumes channel mask, format, and sample rate do not change after creation.
37 if (mStream != nullptr /* && mStreamPowerLog.isUserDebugOrEngBuild() */) {
38 mStreamPowerLog.init(mStream->get_sample_rate(mStream),
39 mStream->get_channels(mStream),
40 mStream->get_format(mStream));
41 }
42 }
43
~StreamHalLocal()44 StreamHalLocal::~StreamHalLocal() {
45 mStream = 0;
46 mDevice.clear();
47 }
48
getBufferSize(size_t * size)49 status_t StreamHalLocal::getBufferSize(size_t *size) {
50 *size = mStream->get_buffer_size(mStream);
51 return OK;
52 }
53
getAudioProperties(audio_config_base_t * configBase)54 status_t StreamHalLocal::getAudioProperties(audio_config_base_t *configBase) {
55 configBase->sample_rate = mStream->get_sample_rate(mStream);
56 configBase->channel_mask = mStream->get_channels(mStream);
57 configBase->format = mStream->get_format(mStream);
58 return OK;
59 }
60
setParameters(const String8 & kvPairs)61 status_t StreamHalLocal::setParameters(const String8& kvPairs) {
62 return mStream->set_parameters(mStream, kvPairs.string());
63 }
64
getParameters(const String8 & keys,String8 * values)65 status_t StreamHalLocal::getParameters(const String8& keys, String8 *values) {
66 char *halValues = mStream->get_parameters(mStream, keys.string());
67 if (halValues != NULL) {
68 values->setTo(halValues);
69 free(halValues);
70 } else {
71 values->clear();
72 }
73 return OK;
74 }
75
addEffect(sp<EffectHalInterface>)76 status_t StreamHalLocal::addEffect(sp<EffectHalInterface>) {
77 LOG_ALWAYS_FATAL("Local streams can not have effects");
78 return INVALID_OPERATION;
79 }
80
removeEffect(sp<EffectHalInterface>)81 status_t StreamHalLocal::removeEffect(sp<EffectHalInterface>) {
82 LOG_ALWAYS_FATAL("Local streams can not have effects");
83 return INVALID_OPERATION;
84 }
85
standby()86 status_t StreamHalLocal::standby() {
87 return mStream->standby(mStream);
88 }
89
dump(int fd)90 status_t StreamHalLocal::dump(int fd) {
91 status_t status = mStream->dump(mStream, fd);
92 mStreamPowerLog.dump(fd);
93 return status;
94 }
95
setHalThreadPriority(int)96 status_t StreamHalLocal::setHalThreadPriority(int) {
97 // Don't need to do anything as local hal is executed by audioflinger directly
98 // on the same thread.
99 return OK;
100 }
101
StreamOutHalLocal(audio_stream_out_t * stream,sp<DeviceHalLocal> device)102 StreamOutHalLocal::StreamOutHalLocal(audio_stream_out_t *stream, sp<DeviceHalLocal> device)
103 : StreamHalLocal(&stream->common, device), mStream(stream) {
104 }
105
~StreamOutHalLocal()106 StreamOutHalLocal::~StreamOutHalLocal() {
107 mCallback.clear();
108 mDevice->closeOutputStream(mStream);
109 mStream = 0;
110 }
111
getFrameSize(size_t * size)112 status_t StreamOutHalLocal::getFrameSize(size_t *size) {
113 *size = audio_stream_out_frame_size(mStream);
114 return OK;
115 }
116
getLatency(uint32_t * latency)117 status_t StreamOutHalLocal::getLatency(uint32_t *latency) {
118 *latency = mStream->get_latency(mStream);
119 return OK;
120 }
121
setVolume(float left,float right)122 status_t StreamOutHalLocal::setVolume(float left, float right) {
123 if (mStream->set_volume == NULL) return INVALID_OPERATION;
124 return mStream->set_volume(mStream, left, right);
125 }
126
selectPresentation(int presentationId,int programId)127 status_t StreamOutHalLocal::selectPresentation(int presentationId, int programId) {
128 AudioParameter param;
129 param.addInt(String8(AudioParameter::keyPresentationId), presentationId);
130 param.addInt(String8(AudioParameter::keyProgramId), programId);
131 return setParameters(param.toString());
132 }
133
write(const void * buffer,size_t bytes,size_t * written)134 status_t StreamOutHalLocal::write(const void *buffer, size_t bytes, size_t *written) {
135 ssize_t writeResult = mStream->write(mStream, buffer, bytes);
136 if (writeResult > 0) {
137 *written = writeResult;
138 mStreamPowerLog.log(buffer, *written);
139 return OK;
140 } else {
141 *written = 0;
142 return writeResult;
143 }
144 }
145
getRenderPosition(uint32_t * dspFrames)146 status_t StreamOutHalLocal::getRenderPosition(uint32_t *dspFrames) {
147 return mStream->get_render_position(mStream, dspFrames);
148 }
149
getNextWriteTimestamp(int64_t * timestamp)150 status_t StreamOutHalLocal::getNextWriteTimestamp(int64_t *timestamp) {
151 if (mStream->get_next_write_timestamp == NULL) return INVALID_OPERATION;
152 return mStream->get_next_write_timestamp(mStream, timestamp);
153 }
154
setCallback(wp<StreamOutHalInterfaceCallback> callback)155 status_t StreamOutHalLocal::setCallback(wp<StreamOutHalInterfaceCallback> callback) {
156 if (mStream->set_callback == NULL) return INVALID_OPERATION;
157 status_t result = mStream->set_callback(mStream, StreamOutHalLocal::asyncCallback, this);
158 if (result == OK) {
159 mCallback = callback;
160 }
161 return result;
162 }
163
164 // static
asyncCallback(stream_callback_event_t event,void *,void * cookie)165 int StreamOutHalLocal::asyncCallback(stream_callback_event_t event, void*, void *cookie) {
166 // We act as if we gave a wp<StreamOutHalLocal> to HAL. This way we should handle
167 // correctly the case when the callback is invoked while StreamOutHalLocal's destructor is
168 // already running, because the destructor is invoked after the refcount has been atomically
169 // decremented.
170 wp<StreamOutHalLocal> weakSelf(static_cast<StreamOutHalLocal*>(cookie));
171 sp<StreamOutHalLocal> self = weakSelf.promote();
172 if (self == 0) return 0;
173 sp<StreamOutHalInterfaceCallback> callback = self->mCallback.promote();
174 if (callback == 0) return 0;
175 ALOGV("asyncCallback() event %d", event);
176 switch (event) {
177 case STREAM_CBK_EVENT_WRITE_READY:
178 callback->onWriteReady();
179 break;
180 case STREAM_CBK_EVENT_DRAIN_READY:
181 callback->onDrainReady();
182 break;
183 case STREAM_CBK_EVENT_ERROR:
184 callback->onError();
185 break;
186 default:
187 ALOGW("asyncCallback() unknown event %d", event);
188 break;
189 }
190 return 0;
191 }
192
supportsPauseAndResume(bool * supportsPause,bool * supportsResume)193 status_t StreamOutHalLocal::supportsPauseAndResume(bool *supportsPause, bool *supportsResume) {
194 *supportsPause = mStream->pause != NULL;
195 *supportsResume = mStream->resume != NULL;
196 return OK;
197 }
198
pause()199 status_t StreamOutHalLocal::pause() {
200 if (mStream->pause == NULL) return INVALID_OPERATION;
201 return mStream->pause(mStream);
202 }
203
resume()204 status_t StreamOutHalLocal::resume() {
205 if (mStream->resume == NULL) return INVALID_OPERATION;
206 return mStream->resume(mStream);
207 }
208
supportsDrain(bool * supportsDrain)209 status_t StreamOutHalLocal::supportsDrain(bool *supportsDrain) {
210 *supportsDrain = mStream->drain != NULL;
211 return OK;
212 }
213
drain(bool earlyNotify)214 status_t StreamOutHalLocal::drain(bool earlyNotify) {
215 if (mStream->drain == NULL) return INVALID_OPERATION;
216 return mStream->drain(mStream, earlyNotify ? AUDIO_DRAIN_EARLY_NOTIFY : AUDIO_DRAIN_ALL);
217 }
218
flush()219 status_t StreamOutHalLocal::flush() {
220 if (mStream->flush == NULL) return INVALID_OPERATION;
221 return mStream->flush(mStream);
222 }
223
getPresentationPosition(uint64_t * frames,struct timespec * timestamp)224 status_t StreamOutHalLocal::getPresentationPosition(uint64_t *frames, struct timespec *timestamp) {
225 if (mStream->get_presentation_position == NULL) return INVALID_OPERATION;
226 return mStream->get_presentation_position(mStream, frames, timestamp);
227 }
228
doUpdateSourceMetadata(const SourceMetadata & sourceMetadata)229 void StreamOutHalLocal::doUpdateSourceMetadata(const SourceMetadata& sourceMetadata) {
230 std::vector<playback_track_metadata> halTracks;
231 halTracks.reserve(sourceMetadata.tracks.size());
232 for (auto& metadata : sourceMetadata.tracks) {
233 playback_track_metadata halTrackMetadata;
234 playback_track_metadata_from_v7(&halTrackMetadata, &metadata);
235 halTracks.push_back(halTrackMetadata);
236 }
237 const source_metadata_t halMetadata = {
238 .track_count = halTracks.size(),
239 .tracks = halTracks.data(),
240 };
241 mStream->update_source_metadata(mStream, &halMetadata);
242 }
243
244 #if MAJOR_VERSION >= 7
doUpdateSourceMetadataV7(const SourceMetadata & sourceMetadata)245 void StreamOutHalLocal::doUpdateSourceMetadataV7(const SourceMetadata& sourceMetadata) {
246 const source_metadata_v7_t metadata {
247 .track_count = sourceMetadata.tracks.size(),
248 // const cast is fine as it is in a const structure
249 .tracks = const_cast<playback_track_metadata_v7*>(sourceMetadata.tracks.data()),
250 };
251 mStream->update_source_metadata_v7(mStream, &metadata);
252 }
253 #endif
254
updateSourceMetadata(const SourceMetadata & sourceMetadata)255 status_t StreamOutHalLocal::updateSourceMetadata(const SourceMetadata& sourceMetadata) {
256 #if MAJOR_VERSION < 7
257 if (mStream->update_source_metadata == nullptr) {
258 return INVALID_OPERATION;
259 }
260 doUpdateSourceMetadata(sourceMetadata);
261 #else
262 if (mDevice->version() < AUDIO_DEVICE_API_VERSION_3_2) {
263 if (mStream->update_source_metadata == nullptr) {
264 return INVALID_OPERATION;
265 }
266 doUpdateSourceMetadata(sourceMetadata);
267 } else {
268 if (mStream->update_source_metadata_v7 == nullptr) {
269 return INVALID_OPERATION;
270 }
271 doUpdateSourceMetadataV7(sourceMetadata);
272 }
273 #endif
274 return OK;
275 }
276
277
start()278 status_t StreamOutHalLocal::start() {
279 if (mStream->start == NULL) return INVALID_OPERATION;
280 return mStream->start(mStream);
281 }
282
stop()283 status_t StreamOutHalLocal::stop() {
284 if (mStream->stop == NULL) return INVALID_OPERATION;
285 return mStream->stop(mStream);
286 }
287
createMmapBuffer(int32_t minSizeFrames,struct audio_mmap_buffer_info * info)288 status_t StreamOutHalLocal::createMmapBuffer(int32_t minSizeFrames,
289 struct audio_mmap_buffer_info *info) {
290 if (mStream->create_mmap_buffer == NULL) return INVALID_OPERATION;
291 return mStream->create_mmap_buffer(mStream, minSizeFrames, info);
292 }
293
getMmapPosition(struct audio_mmap_position * position)294 status_t StreamOutHalLocal::getMmapPosition(struct audio_mmap_position *position) {
295 if (mStream->get_mmap_position == NULL) return INVALID_OPERATION;
296 return mStream->get_mmap_position(mStream, position);
297 }
298
getDualMonoMode(audio_dual_mono_mode_t * mode)299 status_t StreamOutHalLocal::getDualMonoMode(audio_dual_mono_mode_t* mode) {
300 if (mStream->get_dual_mono_mode == nullptr) return INVALID_OPERATION;
301 return mStream->get_dual_mono_mode(mStream, mode);
302 }
303
setDualMonoMode(audio_dual_mono_mode_t mode)304 status_t StreamOutHalLocal::setDualMonoMode(audio_dual_mono_mode_t mode) {
305 if (mStream->set_dual_mono_mode == nullptr) return INVALID_OPERATION;
306 return mStream->set_dual_mono_mode(mStream, mode);
307 }
308
getAudioDescriptionMixLevel(float * leveldB)309 status_t StreamOutHalLocal::getAudioDescriptionMixLevel(float* leveldB) {
310 if (mStream->get_audio_description_mix_level == nullptr) return INVALID_OPERATION;
311 return mStream->get_audio_description_mix_level(mStream, leveldB);
312 }
313
setAudioDescriptionMixLevel(float leveldB)314 status_t StreamOutHalLocal::setAudioDescriptionMixLevel(float leveldB) {
315 if (mStream->set_audio_description_mix_level == nullptr) return INVALID_OPERATION;
316 return mStream->set_audio_description_mix_level(mStream, leveldB);
317 }
318
getPlaybackRateParameters(audio_playback_rate_t * playbackRate)319 status_t StreamOutHalLocal::getPlaybackRateParameters(audio_playback_rate_t* playbackRate) {
320 if (mStream->get_playback_rate_parameters == nullptr) return INVALID_OPERATION;
321 return mStream->get_playback_rate_parameters(mStream, playbackRate);
322 }
323
setPlaybackRateParameters(const audio_playback_rate_t & playbackRate)324 status_t StreamOutHalLocal::setPlaybackRateParameters(const audio_playback_rate_t& playbackRate) {
325 if (mStream->set_playback_rate_parameters == nullptr) return INVALID_OPERATION;
326 return mStream->set_playback_rate_parameters(mStream, &playbackRate);
327 }
328
setEventCallback(const sp<StreamOutHalInterfaceEventCallback> & callback)329 status_t StreamOutHalLocal::setEventCallback(
330 const sp<StreamOutHalInterfaceEventCallback>& callback) {
331 if (mStream->set_event_callback == nullptr) {
332 return INVALID_OPERATION;
333 }
334 stream_event_callback_t asyncCallback =
335 callback == nullptr ? nullptr : StreamOutHalLocal::asyncEventCallback;
336 status_t result = mStream->set_event_callback(mStream, asyncCallback, this);
337 if (result == OK) {
338 mEventCallback = callback;
339 }
340 return result;
341 }
342
343 // static
asyncEventCallback(stream_event_callback_type_t event,void * param,void * cookie)344 int StreamOutHalLocal::asyncEventCallback(
345 stream_event_callback_type_t event, void *param, void *cookie) {
346 // We act as if we gave a wp<StreamOutHalLocal> to HAL. This way we should handle
347 // correctly the case when the callback is invoked while StreamOutHalLocal's destructor is
348 // already running, because the destructor is invoked after the refcount has been atomically
349 // decremented.
350 wp<StreamOutHalLocal> weakSelf(static_cast<StreamOutHalLocal*>(cookie));
351 sp<StreamOutHalLocal> self = weakSelf.promote();
352 if (self == nullptr) return 0;
353 sp<StreamOutHalInterfaceEventCallback> callback = self->mEventCallback.promote();
354 if (callback.get() == nullptr) return 0;
355 switch (event) {
356 case STREAM_EVENT_CBK_TYPE_CODEC_FORMAT_CHANGED:
357 // void* param is the byte string buffer from byte_string_from_audio_metadata().
358 // As the byte string buffer may have embedded zeroes, we cannot use strlen()
359 callback->onCodecFormatChanged(std::basic_string<uint8_t>(
360 (const uint8_t*)param,
361 audio_utils::metadata::dataByteStringLen((const uint8_t*)param)));
362 break;
363 default:
364 ALOGW("%s unknown event %d", __func__, event);
365 break;
366 }
367 return 0;
368 }
369
StreamInHalLocal(audio_stream_in_t * stream,sp<DeviceHalLocal> device)370 StreamInHalLocal::StreamInHalLocal(audio_stream_in_t *stream, sp<DeviceHalLocal> device)
371 : StreamHalLocal(&stream->common, device), mStream(stream) {
372 }
373
~StreamInHalLocal()374 StreamInHalLocal::~StreamInHalLocal() {
375 mDevice->closeInputStream(mStream);
376 mStream = 0;
377 }
378
getFrameSize(size_t * size)379 status_t StreamInHalLocal::getFrameSize(size_t *size) {
380 *size = audio_stream_in_frame_size(mStream);
381 return OK;
382 }
383
setGain(float gain)384 status_t StreamInHalLocal::setGain(float gain) {
385 return mStream->set_gain(mStream, gain);
386 }
387
read(void * buffer,size_t bytes,size_t * read)388 status_t StreamInHalLocal::read(void *buffer, size_t bytes, size_t *read) {
389 ssize_t readResult = mStream->read(mStream, buffer, bytes);
390 if (readResult > 0) {
391 *read = readResult;
392 mStreamPowerLog.log( buffer, *read);
393 return OK;
394 } else {
395 *read = 0;
396 return readResult;
397 }
398 }
399
getInputFramesLost(uint32_t * framesLost)400 status_t StreamInHalLocal::getInputFramesLost(uint32_t *framesLost) {
401 *framesLost = mStream->get_input_frames_lost(mStream);
402 return OK;
403 }
404
getCapturePosition(int64_t * frames,int64_t * time)405 status_t StreamInHalLocal::getCapturePosition(int64_t *frames, int64_t *time) {
406 if (mStream->get_capture_position == NULL) return INVALID_OPERATION;
407 return mStream->get_capture_position(mStream, frames, time);
408 }
409
doUpdateSinkMetadata(const SinkMetadata & sinkMetadata)410 void StreamInHalLocal::doUpdateSinkMetadata(const SinkMetadata& sinkMetadata) {
411 std::vector<record_track_metadata> halTracks;
412 halTracks.reserve(sinkMetadata.tracks.size());
413 for (auto& metadata : sinkMetadata.tracks) {
414 record_track_metadata halTrackMetadata;
415 record_track_metadata_from_v7(&halTrackMetadata, &metadata);
416 halTracks.push_back(halTrackMetadata);
417 }
418 const sink_metadata_t halMetadata = {
419 .track_count = halTracks.size(),
420 .tracks = halTracks.data(),
421 };
422 mStream->update_sink_metadata(mStream, &halMetadata);
423 }
424
425 #if MAJOR_VERSION >= 7
doUpdateSinkMetadataV7(const SinkMetadata & sinkMetadata)426 void StreamInHalLocal::doUpdateSinkMetadataV7(const SinkMetadata& sinkMetadata) {
427 const sink_metadata_v7_t halMetadata {
428 .track_count = sinkMetadata.tracks.size(),
429 // const cast is fine as it is in a const structure
430 .tracks = const_cast<record_track_metadata_v7*>(sinkMetadata.tracks.data()),
431 };
432 mStream->update_sink_metadata_v7(mStream, &halMetadata);
433 }
434 #endif
435
updateSinkMetadata(const SinkMetadata & sinkMetadata)436 status_t StreamInHalLocal::updateSinkMetadata(const SinkMetadata& sinkMetadata) {
437 #if MAJOR_VERSION < 7
438 if (mStream->update_sink_metadata == nullptr) {
439 return INVALID_OPERATION; // not supported by the HAL
440 }
441 doUpdateSinkMetadata(sinkMetadata);
442 #else
443 if (mDevice->version() < AUDIO_DEVICE_API_VERSION_3_2) {
444 if (mStream->update_sink_metadata == nullptr) {
445 return INVALID_OPERATION; // not supported by the HAL
446 }
447 doUpdateSinkMetadata(sinkMetadata);
448 } else {
449 if (mStream->update_sink_metadata_v7 == nullptr) {
450 return INVALID_OPERATION; // not supported by the HAL
451 }
452 doUpdateSinkMetadataV7(sinkMetadata);
453 }
454 #endif
455 return OK;
456 }
457
start()458 status_t StreamInHalLocal::start() {
459 if (mStream->start == NULL) return INVALID_OPERATION;
460 return mStream->start(mStream);
461 }
462
stop()463 status_t StreamInHalLocal::stop() {
464 if (mStream->stop == NULL) return INVALID_OPERATION;
465 return mStream->stop(mStream);
466 }
467
createMmapBuffer(int32_t minSizeFrames,struct audio_mmap_buffer_info * info)468 status_t StreamInHalLocal::createMmapBuffer(int32_t minSizeFrames,
469 struct audio_mmap_buffer_info *info) {
470 if (mStream->create_mmap_buffer == NULL) return INVALID_OPERATION;
471 return mStream->create_mmap_buffer(mStream, minSizeFrames, info);
472 }
473
getMmapPosition(struct audio_mmap_position * position)474 status_t StreamInHalLocal::getMmapPosition(struct audio_mmap_position *position) {
475 if (mStream->get_mmap_position == NULL) return INVALID_OPERATION;
476 return mStream->get_mmap_position(mStream, position);
477 }
478
479 #if MAJOR_VERSION == 2
getActiveMicrophones(std::vector<media::MicrophoneInfo> * microphones __unused)480 status_t StreamInHalLocal::getActiveMicrophones(
481 std::vector<media::MicrophoneInfo> *microphones __unused) {
482 return INVALID_OPERATION;
483 }
484 #elif MAJOR_VERSION >= 4
getActiveMicrophones(std::vector<media::MicrophoneInfo> * microphones)485 status_t StreamInHalLocal::getActiveMicrophones(std::vector<media::MicrophoneInfo> *microphones) {
486 if (mStream->get_active_microphones == NULL) return INVALID_OPERATION;
487 size_t actual_mics = AUDIO_MICROPHONE_MAX_COUNT;
488 audio_microphone_characteristic_t mic_array[AUDIO_MICROPHONE_MAX_COUNT];
489 status_t status = mStream->get_active_microphones(mStream, &mic_array[0], &actual_mics);
490 for (size_t i = 0; i < actual_mics; i++) {
491 media::MicrophoneInfo microphoneInfo = media::MicrophoneInfo(mic_array[i]);
492 microphones->push_back(microphoneInfo);
493 }
494 return status;
495 }
496 #endif
497
498 #if MAJOR_VERSION < 5
setPreferredMicrophoneDirection(audio_microphone_direction_t direction __unused)499 status_t StreamInHalLocal::setPreferredMicrophoneDirection(
500 audio_microphone_direction_t direction __unused) {
501 return INVALID_OPERATION;
502 }
503
setPreferredMicrophoneFieldDimension(float zoom __unused)504 status_t StreamInHalLocal::setPreferredMicrophoneFieldDimension(float zoom __unused) {
505 return INVALID_OPERATION;
506 }
507 #else
setPreferredMicrophoneDirection(audio_microphone_direction_t direction)508 status_t StreamInHalLocal::setPreferredMicrophoneDirection(audio_microphone_direction_t direction) {
509 if (mStream->set_microphone_direction == NULL) return INVALID_OPERATION;
510 return mStream->set_microphone_direction(mStream, direction);
511 }
512
setPreferredMicrophoneFieldDimension(float zoom)513 status_t StreamInHalLocal::setPreferredMicrophoneFieldDimension(float zoom) {
514 if (mStream->set_microphone_field_dimension == NULL) return INVALID_OPERATION;
515 return mStream->set_microphone_field_dimension(mStream, zoom);
516
517 }
518 #endif
519
520 } // namespace CPP_VERSION
521 } // namespace android
522
523
524