Home | History | Annotate | Download | only in libaudiojni
      1 /*
      2  * Copyright (C) 2015 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_NDEBUG 0
     18 #define LOG_TAG "audio-track-native"
     19 
     20 #include "Blob.h"
     21 #include "Gate.h"
     22 #include "sl-utils.h"
     23 
     24 #include <deque>
     25 #include <utils/Errors.h>
     26 
     27 // Select whether to use STL shared pointer or to use Android strong pointer.
     28 // We really don't promote any sharing of this object for its lifetime, but nevertheless could
     29 // change the shared pointer value on the fly if desired.
     30 #define USE_SHARED_POINTER
     31 
     32 #ifdef USE_SHARED_POINTER
     33 #include <memory>
     34 template <typename T> using shared_pointer = std::shared_ptr<T>;
     35 #else
     36 #include <utils/RefBase.h>
     37 template <typename T> using shared_pointer = android::sp<T>;
     38 #endif
     39 
     40 using namespace android;
     41 
     42 // Must be kept in sync with Java android.media.cts.AudioTrackNative.WriteFlags
     43 enum {
     44     WRITE_FLAG_BLOCKING = (1 << 0),
     45 };
     46 
     47 // TODO: Add a single buffer blocking write mode which does not require additional memory.
     48 // TODO: Add internal buffer memory (e.g. use circular buffer, right now mallocs on heap).
     49 
     50 class AudioTrackNative
     51 #ifndef USE_SHARED_POINTER
     52         : public RefBase // android strong pointers require RefBase
     53 #endif
     54 {
     55 public:
     56     AudioTrackNative() :
     57         mEngineObj(NULL),
     58         mEngine(NULL),
     59         mOutputMixObj(NULL),
     60         mPlayerObj(NULL),
     61         mPlay(NULL),
     62         mBufferQueue(NULL),
     63         mPlayState(SL_PLAYSTATE_STOPPED),
     64         mNumBuffers(0)
     65     { }
     66 
     67     ~AudioTrackNative() {
     68         close();
     69     }
     70 
     71     typedef std::lock_guard<std::recursive_mutex> auto_lock;
     72 
     73     status_t open(jint numChannels, jint channelMask,
     74                   jint sampleRate, jboolean useFloat, jint numBuffers) {
     75         close();
     76         auto_lock l(mLock);
     77         mEngineObj = OpenSLEngine();
     78         if (mEngineObj == NULL) {
     79             ALOGW("cannot create OpenSL ES engine");
     80             return INVALID_OPERATION;
     81         }
     82 
     83         SLresult res;
     84         for (;;) {
     85             /* Get the SL Engine Interface which is implicit */
     86             res = (*mEngineObj)->GetInterface(mEngineObj, SL_IID_ENGINE, (void *)&mEngine);
     87             if (res != SL_RESULT_SUCCESS) break;
     88 
     89             // Create Output Mix object to be used by player
     90             res = (*mEngine)->CreateOutputMix(
     91                     mEngine, &mOutputMixObj, 0 /* numInterfaces */,
     92                     NULL /* pInterfaceIds */, NULL /* pInterfaceRequired */);
     93             if (res != SL_RESULT_SUCCESS) break;
     94 
     95             // Realizing the Output Mix object in synchronous mode.
     96             res = (*mOutputMixObj)->Realize(mOutputMixObj, SL_BOOLEAN_FALSE /* async */);
     97             if (res != SL_RESULT_SUCCESS) break;
     98 
     99             /* Setup the data source structure for the buffer queue */
    100             SLDataLocator_BufferQueue bufferQueue;
    101             bufferQueue.locatorType = SL_DATALOCATOR_BUFFERQUEUE;
    102             bufferQueue.numBuffers = numBuffers;
    103             mNumBuffers = numBuffers;
    104 
    105             /* Setup the format of the content in the buffer queue */
    106 
    107             SLAndroidDataFormat_PCM_EX pcm;
    108             pcm.formatType = useFloat ? SL_ANDROID_DATAFORMAT_PCM_EX : SL_DATAFORMAT_PCM;
    109             pcm.numChannels = numChannels;
    110             pcm.sampleRate = sampleRate * 1000;
    111             pcm.bitsPerSample = useFloat ?
    112                     SL_PCMSAMPLEFORMAT_FIXED_32 : SL_PCMSAMPLEFORMAT_FIXED_16;
    113             pcm.containerSize = pcm.bitsPerSample;
    114             pcm.channelMask = channelMask;
    115             pcm.endianness = SL_BYTEORDER_LITTLEENDIAN;
    116             // additional
    117             pcm.representation = useFloat ? SL_ANDROID_PCM_REPRESENTATION_FLOAT
    118                                     : SL_ANDROID_PCM_REPRESENTATION_SIGNED_INT;
    119             SLDataSource audioSource;
    120             audioSource.pFormat = (void *)&pcm;
    121             audioSource.pLocator = (void *)&bufferQueue;
    122 
    123             /* Setup the data sink structure */
    124             SLDataLocator_OutputMix locator_outputmix;
    125             locator_outputmix.locatorType = SL_DATALOCATOR_OUTPUTMIX;
    126             locator_outputmix.outputMix = mOutputMixObj;
    127 
    128             SLDataSink audioSink;
    129             audioSink.pLocator = (void *)&locator_outputmix;
    130             audioSink.pFormat = NULL;
    131 
    132             SLboolean required[1];
    133             SLInterfaceID iidArray[1];
    134             required[0] = SL_BOOLEAN_TRUE;
    135             iidArray[0] = SL_IID_BUFFERQUEUE;
    136 
    137             res = (*mEngine)->CreateAudioPlayer(mEngine, &mPlayerObj,
    138                     &audioSource, &audioSink, 1 /* numInterfaces */, iidArray, required);
    139             if (res != SL_RESULT_SUCCESS) break;
    140 
    141             res = (*mPlayerObj)->Realize(mPlayerObj, SL_BOOLEAN_FALSE /* async */);
    142             if (res != SL_RESULT_SUCCESS) break;
    143 
    144             res = (*mPlayerObj)->GetInterface(mPlayerObj, SL_IID_PLAY, (void*)&mPlay);
    145             if (res != SL_RESULT_SUCCESS) break;
    146 
    147             res = (*mPlayerObj)->GetInterface(
    148                     mPlayerObj, SL_IID_BUFFERQUEUE, (void*)&mBufferQueue);
    149             if (res != SL_RESULT_SUCCESS) break;
    150 
    151             /* Setup to receive buffer queue event callbacks */
    152             res = (*mBufferQueue)->RegisterCallback(mBufferQueue, BufferQueueCallback, this);
    153             if (res != SL_RESULT_SUCCESS) break;
    154 
    155             // success
    156             break;
    157         }
    158         if (res != SL_RESULT_SUCCESS) {
    159             close(); // should be safe to close even with lock held
    160             ALOGW("open error %s", android::getSLErrStr(res));
    161             return INVALID_OPERATION;
    162         }
    163         return OK;
    164     }
    165 
    166     void close() {
    167         SLObjectItf engineObj;
    168         SLObjectItf outputMixObj;
    169         SLObjectItf playerObj;
    170         {
    171             auto_lock l(mLock);
    172             if (mPlay != NULL && mPlayState != SL_PLAYSTATE_STOPPED) {
    173                 (void)stop();
    174             }
    175             // once stopped, we can unregister the callback
    176             if (mBufferQueue != NULL) {
    177                 (void)(*mBufferQueue)->RegisterCallback(
    178                         mBufferQueue, NULL /* callback */, NULL /* *pContext */);
    179             }
    180             (void)flush();
    181             engineObj = mEngineObj;
    182             outputMixObj = mOutputMixObj;
    183             playerObj = mPlayerObj;
    184             // clear out interfaces and objects
    185             mPlay = NULL;
    186             mBufferQueue = NULL;
    187             mEngine = NULL;
    188             mPlayerObj = NULL;
    189             mOutputMixObj = NULL;
    190             mEngineObj = NULL;
    191             mPlayState = SL_PLAYSTATE_STOPPED;
    192         }
    193         // destroy without lock
    194         if (playerObj != NULL) {
    195             (*playerObj)->Destroy(playerObj);
    196         }
    197         if (outputMixObj != NULL) {
    198             (*outputMixObj)->Destroy(outputMixObj);
    199         }
    200         if (engineObj != NULL) {
    201             CloseSLEngine(engineObj);
    202         }
    203     }
    204 
    205     status_t setPlayState(SLuint32 playState) {
    206         auto_lock l(mLock);
    207         if (mPlay == NULL) {
    208             return INVALID_OPERATION;
    209         }
    210         SLresult res = (*mPlay)->SetPlayState(mPlay, playState);
    211         if (res != SL_RESULT_SUCCESS) {
    212             ALOGW("setPlayState %d error %s", playState, android::getSLErrStr(res));
    213             return INVALID_OPERATION;
    214         }
    215         mPlayState = playState;
    216         return OK;
    217     }
    218 
    219     SLuint32 getPlayState() {
    220         auto_lock l(mLock);
    221         if (mPlay == NULL) {
    222             return SL_PLAYSTATE_STOPPED;
    223         }
    224         SLuint32 playState;
    225         SLresult res = (*mPlay)->GetPlayState(mPlay, &playState);
    226         if (res != SL_RESULT_SUCCESS) {
    227             ALOGW("getPlayState error %s", android::getSLErrStr(res));
    228             return SL_PLAYSTATE_STOPPED;
    229         }
    230         return playState;
    231     }
    232 
    233     status_t getPositionInMsec(int64_t *position) {
    234         auto_lock l(mLock);
    235         if (mPlay == NULL) {
    236             return INVALID_OPERATION;
    237         }
    238         if (position == NULL) {
    239             return BAD_VALUE;
    240         }
    241         SLuint32 pos;
    242         SLresult res = (*mPlay)->GetPosition(mPlay, &pos);
    243         if (res != SL_RESULT_SUCCESS) {
    244             ALOGW("getPosition error %s", android::getSLErrStr(res));
    245             return INVALID_OPERATION;
    246         }
    247         // only lower 32 bits valid
    248         *position = pos;
    249         return OK;
    250     }
    251 
    252     status_t start() {
    253         return setPlayState(SL_PLAYSTATE_PLAYING);
    254     }
    255 
    256     status_t pause() {
    257         return setPlayState(SL_PLAYSTATE_PAUSED);
    258     }
    259 
    260     status_t stop() {
    261         return setPlayState(SL_PLAYSTATE_STOPPED);
    262     }
    263 
    264     status_t flush() {
    265         auto_lock l(mLock);
    266         status_t result = OK;
    267         if (mBufferQueue != NULL) {
    268             SLresult res = (*mBufferQueue)->Clear(mBufferQueue);
    269             if (res != SL_RESULT_SUCCESS) {
    270                 return INVALID_OPERATION;
    271             }
    272         }
    273 
    274         // possible race if the engine is in the callback
    275         // safety is only achieved if the player is paused or stopped.
    276         mDeliveredQueue.clear();
    277         return result;
    278     }
    279 
    280     status_t write(const void *buffer, size_t size, bool isBlocking = false) {
    281         std::lock_guard<std::mutex> rl(mWriteLock);
    282         // not needed if we assume that a single thread is doing the reading
    283         // or we always operate in non-blocking mode.
    284 
    285         {
    286             auto_lock l(mLock);
    287             if (mBufferQueue == NULL) {
    288                 return INVALID_OPERATION;
    289             }
    290             if (mDeliveredQueue.size() < mNumBuffers) {
    291                 auto b = std::make_shared<BlobReadOnly>(buffer, size, false /* byReference */);
    292                 mDeliveredQueue.emplace_back(b);
    293                 (*mBufferQueue)->Enqueue(mBufferQueue, b->mData, b->mSize);
    294                 return size;
    295             }
    296             if (!isBlocking) {
    297                 return 0;
    298             }
    299             mWriteReady.closeGate(); // we're full.
    300         }
    301         if (mWriteReady.wait()) {
    302             auto_lock l(mLock);
    303             if (mDeliveredQueue.size() < mNumBuffers) {
    304                 auto b = std::make_shared<BlobReadOnly>(buffer, size, false /* byReference */);
    305                 mDeliveredQueue.emplace_back(b);
    306                 (*mBufferQueue)->Enqueue(mBufferQueue, b->mData, b->mSize);
    307                 return size;
    308             }
    309         }
    310         ALOGW("unable to deliver write");
    311         return 0;
    312     }
    313 
    314     void logBufferState() {
    315         auto_lock l(mLock);
    316         SLBufferQueueState state;
    317         SLresult res = (*mBufferQueue)->GetState(mBufferQueue, &state);
    318         CheckErr(res);
    319         ALOGD("logBufferState state.count:%d  state.playIndex:%d", state.count, state.playIndex);
    320     }
    321 
    322     size_t getBuffersPending() {
    323         auto_lock l(mLock);
    324         return mDeliveredQueue.size();
    325     }
    326 
    327 private:
    328     void bufferQueueCallback(SLBufferQueueItf queueItf) {
    329         auto_lock l(mLock);
    330         if (queueItf != mBufferQueue) {
    331             ALOGW("invalid buffer queue interface, ignoring");
    332             return;
    333         }
    334         // logBufferState();
    335 
    336         // remove from delivered queue
    337         if (mDeliveredQueue.size()) {
    338             mDeliveredQueue.pop_front();
    339         } else {
    340             ALOGW("no delivered data!");
    341         }
    342         if (!mWriteReady.isOpen()) {
    343             mWriteReady.openGate();
    344         }
    345     }
    346 
    347     static void BufferQueueCallback(SLBufferQueueItf queueItf, void *pContext) {
    348         // naked native track
    349         AudioTrackNative *track = (AudioTrackNative *)pContext;
    350         track->bufferQueueCallback(queueItf);
    351     }
    352 
    353     SLObjectItf          mEngineObj;
    354     SLEngineItf          mEngine;
    355     SLObjectItf          mOutputMixObj;
    356     SLObjectItf          mPlayerObj;
    357     SLPlayItf            mPlay;
    358     SLBufferQueueItf     mBufferQueue;
    359     SLuint32             mPlayState;
    360     SLuint32             mNumBuffers;
    361     std::recursive_mutex mLock;           // monitor lock - locks public API methods and callback.
    362                                           // recursive since it may call itself through API.
    363     std::mutex           mWriteLock;      // write lock - for blocking mode, prevents multiple
    364                                           // writer threads from overlapping writes.  this is
    365                                           // generally unnecessary as writes occur from
    366                                           // one thread only.  acquire this before mLock.
    367     Gate                 mWriteReady;
    368     std::deque<std::shared_ptr<BlobReadOnly>> mDeliveredQueue; // delivered to mBufferQueue
    369 };
    370 
    371 /* Java static methods.
    372  *
    373  * These are not directly exposed to the user, so we can assume a valid "jtrack" handle
    374  * to be passed in.
    375  */
    376 
    377 extern "C" jint Java_android_media_cts_AudioTrackNative_nativeTest(
    378     JNIEnv * /* env */, jclass /* clazz */,
    379     jint numChannels, jint channelMask, jint sampleRate, jboolean useFloat,
    380     jint msecPerBuffer, jint numBuffers)
    381 {
    382     AudioTrackNative track;
    383     const size_t frameSize = numChannels * (useFloat ? sizeof(float) : sizeof(int16_t));
    384     const size_t framesPerBuffer = msecPerBuffer * sampleRate / 1000;
    385 
    386     status_t res;
    387     void *buffer = calloc(framesPerBuffer * numBuffers, frameSize);
    388     for (;;) {
    389         res = track.open(numChannels, channelMask, sampleRate, useFloat, numBuffers);
    390         if (res != OK) break;
    391 
    392         for (int i = 0; i < numBuffers; ++i) {
    393             track.write((char *)buffer + i * (framesPerBuffer * frameSize),
    394                     framesPerBuffer * frameSize);
    395         }
    396 
    397         track.logBufferState();
    398         res = track.start();
    399         if (res != OK) break;
    400 
    401         size_t buffers;
    402         while ((buffers = track.getBuffersPending()) > 0) {
    403             // ALOGD("outstanding buffers: %zu", buffers);
    404             usleep(5 * 1000 /* usec */);
    405         }
    406         res = track.stop();
    407         break;
    408     }
    409     track.close();
    410     free(buffer);
    411     return res;
    412 }
    413 
    414 extern "C" jlong Java_android_media_cts_AudioTrackNative_nativeCreateTrack(
    415     JNIEnv * /* env */, jclass /* clazz */)
    416 {
    417     return (jlong)(new shared_pointer<AudioTrackNative>(new AudioTrackNative()));
    418 }
    419 
    420 extern "C" void Java_android_media_cts_AudioTrackNative_nativeDestroyTrack(
    421     JNIEnv * /* env */, jclass /* clazz */, jlong jtrack)
    422 {
    423     delete (shared_pointer<AudioTrackNative> *)jtrack;
    424 }
    425 
    426 extern "C" jint Java_android_media_cts_AudioTrackNative_nativeOpen(
    427     JNIEnv * /* env */, jclass /* clazz */, jlong jtrack,
    428     jint numChannels, jint channelMask, jint sampleRate,
    429     jboolean useFloat, jint numBuffers)
    430 {
    431     auto track = *(shared_pointer<AudioTrackNative> *)jtrack;
    432     if (track.get() == NULL) {
    433         return (jint)INVALID_OPERATION;
    434     }
    435     return (jint) track->open(numChannels,
    436                               channelMask,
    437                               sampleRate,
    438                               useFloat == JNI_TRUE,
    439                               numBuffers);
    440 }
    441 
    442 extern "C" void Java_android_media_cts_AudioTrackNative_nativeClose(
    443     JNIEnv * /* env */, jclass /* clazz */, jlong jtrack)
    444 {
    445     auto track = *(shared_pointer<AudioTrackNative> *)jtrack;
    446     if (track.get() != NULL) {
    447         track->close();
    448     }
    449 }
    450 
    451 extern "C" jint Java_android_media_cts_AudioTrackNative_nativeStart(
    452     JNIEnv * /* env */, jclass /* clazz */, jlong jtrack)
    453 {
    454     auto track = *(shared_pointer<AudioTrackNative> *)jtrack;
    455     if (track.get() == NULL) {
    456         return (jint)INVALID_OPERATION;
    457     }
    458     return (jint)track->start();
    459 }
    460 
    461 extern "C" jint Java_android_media_cts_AudioTrackNative_nativeStop(
    462     JNIEnv * /* env */, jclass /* clazz */, jlong jtrack)
    463 {
    464     auto track = *(shared_pointer<AudioTrackNative> *)jtrack;
    465     if (track.get() == NULL) {
    466         return (jint)INVALID_OPERATION;
    467     }
    468     return (jint)track->stop();
    469 }
    470 
    471 extern "C" jint Java_android_media_cts_AudioTrackNative_nativePause(
    472     JNIEnv * /* env */, jclass /* clazz */, jlong jtrack)
    473 {
    474     auto track = *(shared_pointer<AudioTrackNative> *)jtrack;
    475     if (track.get() == NULL) {
    476         return (jint)INVALID_OPERATION;
    477     }
    478     return (jint)track->pause();
    479 }
    480 
    481 extern "C" jint Java_android_media_cts_AudioTrackNative_nativeFlush(
    482     JNIEnv * /* env */, jclass /* clazz */, jlong jtrack)
    483 {
    484     auto track = *(shared_pointer<AudioTrackNative> *)jtrack;
    485     if (track.get() == NULL) {
    486         return (jint)INVALID_OPERATION;
    487     }
    488     return (jint)track->flush();
    489 }
    490 
    491 extern "C" jint Java_android_media_cts_AudioTrackNative_nativeGetPositionInMsec(
    492     JNIEnv *env, jclass /* clazz */, jlong jtrack, jlongArray jPosition)
    493 {
    494     auto track = *(shared_pointer<AudioTrackNative> *)jtrack;
    495     if (track.get() == NULL) {
    496         return (jint)INVALID_OPERATION;
    497     }
    498     int64_t pos;
    499     status_t res = track->getPositionInMsec(&pos);
    500     if (res != OK) {
    501         return res;
    502     }
    503     jlong *nPostition = (jlong *) env->GetPrimitiveArrayCritical(jPosition, NULL /* isCopy */);
    504     if (nPostition == NULL) {
    505         ALOGE("Unable to get array for nativeGetPositionInMsec()");
    506         return BAD_VALUE;
    507     }
    508     nPostition[0] = (jlong)pos;
    509     env->ReleasePrimitiveArrayCritical(jPosition, nPostition, 0 /* mode */);
    510     return OK;
    511 }
    512 
    513 extern "C" jint Java_android_media_cts_AudioTrackNative_nativeGetBuffersPending(
    514     JNIEnv * /* env */, jclass /* clazz */, jlong jtrack)
    515 {
    516     auto track = *(shared_pointer<AudioTrackNative> *)jtrack;
    517     if (track.get() == NULL) {
    518         return (jint)0;
    519     }
    520     return (jint)track->getBuffersPending();
    521 }
    522 
    523 template <typename T>
    524 static inline jint writeToTrack(jlong jtrack, const T *data,
    525     jint offsetInSamples, jint sizeInSamples, jint writeFlags)
    526 {
    527     auto track = *(shared_pointer<AudioTrackNative> *)jtrack;
    528     if (track.get() == NULL) {
    529         return (jint)INVALID_OPERATION;
    530     }
    531 
    532     const bool isBlocking = writeFlags & WRITE_FLAG_BLOCKING;
    533     const size_t sizeInBytes = sizeInSamples * sizeof(T);
    534     ssize_t ret = track->write(data + offsetInSamples, sizeInBytes, isBlocking);
    535     return (jint)(ret > 0 ? ret / sizeof(T) : ret);
    536 }
    537 
    538 template <typename T>
    539 static inline jint writeArray(JNIEnv *env, jclass /* clazz */, jlong jtrack,
    540         T javaAudioData, jint offsetInSamples, jint sizeInSamples, jint writeFlags)
    541 {
    542     if (javaAudioData == NULL) {
    543         return (jint)INVALID_OPERATION;
    544     }
    545 
    546     auto cAudioData = envGetArrayElements(env, javaAudioData, NULL /* isCopy */);
    547     if (cAudioData == NULL) {
    548         ALOGE("Error retrieving source of audio data to play");
    549         return (jint)BAD_VALUE;
    550     }
    551 
    552     jint ret = writeToTrack(jtrack, cAudioData, offsetInSamples, sizeInSamples, writeFlags);
    553     envReleaseArrayElements(env, javaAudioData, cAudioData, 0 /* mode */);
    554     return ret;
    555 }
    556 
    557 extern "C" jint Java_android_media_cts_AudioTrackNative_nativeWriteByteArray(
    558     JNIEnv *env, jclass clazz, jlong jtrack,
    559     jbyteArray byteArray, jint offsetInSamples, jint sizeInSamples, jint writeFlags)
    560 {
    561     ALOGV("nativeWriteByteArray(%p, %d, %d, %d)",
    562             byteArray, offsetInSamples, sizeInSamples, writeFlags);
    563     return writeArray(env, clazz, jtrack, byteArray, offsetInSamples, sizeInSamples, writeFlags);
    564 }
    565 
    566 extern "C" jint Java_android_media_cts_AudioTrackNative_nativeWriteShortArray(
    567     JNIEnv *env, jclass clazz, jlong jtrack,
    568     jshortArray shortArray, jint offsetInSamples, jint sizeInSamples, jint writeFlags)
    569 {
    570     ALOGV("nativeWriteShortArray(%p, %d, %d, %d)",
    571             shortArray, offsetInSamples, sizeInSamples, writeFlags);
    572     return writeArray(env, clazz, jtrack, shortArray, offsetInSamples, sizeInSamples, writeFlags);
    573 }
    574 
    575 extern "C" jint Java_android_media_cts_AudioTrackNative_nativeWriteFloatArray(
    576     JNIEnv *env, jclass clazz, jlong jtrack,
    577     jfloatArray floatArray, jint offsetInSamples, jint sizeInSamples, jint writeFlags)
    578 {
    579     ALOGV("nativeWriteFloatArray(%p, %d, %d, %d)",
    580             floatArray, offsetInSamples, sizeInSamples, writeFlags);
    581     return writeArray(env, clazz, jtrack, floatArray, offsetInSamples, sizeInSamples, writeFlags);
    582 }
    583