audiortp.cpp 16.8 KB
Newer Older
jpbl's avatar
jpbl committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
 *  Copyright (C) 2004-2005 Savoir-Faire Linux inc.
 *  Author: Yan Morin <yan.morin@savoirfairelinux.com>
 *  Author: Laurielle Lea <laurielle.lea@savoirfairelinux.com>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include <cstdio>
#include <cstdlib>
#include <ccrtp/rtp.h>
#include <assert.h>
#include <string>
26
#include <cstring>
jpbl's avatar
jpbl committed
27
28
29
30
31
32
33
34
35

#include "../global.h"
#include "../manager.h"
#include "codecDescriptor.h"
#include "audiortp.h"
#include "audiolayer.h"
#include "ringbuffer.h"
#include "../user_cfg.h"
#include "../sipcall.h"
36
#include <samplerate.h>
jpbl's avatar
jpbl committed
37
38
39
40
41
42
43
44
45
46
47
48
49
50

////////////////////////////////////////////////////////////////////////////////
// AudioRtp                                                          
////////////////////////////////////////////////////////////////////////////////
AudioRtp::AudioRtp ()
{
  _RTXThread = 0;
}

AudioRtp::~AudioRtp (void) {
  delete _RTXThread; _RTXThread = 0;
}

int 
yanmorin's avatar
   
yanmorin committed
51
AudioRtp::createNewSession (SIPCall *ca) {
jpbl's avatar
jpbl committed
52
53
54
55
  ost::MutexLock m(_threadMutex);

  // something should stop the thread before...
  if ( _RTXThread != 0 ) { 
56
    _debug("! ARTP Failure: Thread already exists..., stopping it\n");
57
58
    delete _RTXThread; _RTXThread = 0;
    //return -1; 
jpbl's avatar
jpbl committed
59
60
61
62
  }

  // Start RTP Send/Receive threads
  _symmetric = Manager::instance().getConfigInt(SIGNALISATION,SYMMETRIC) ? true : false;
63
  _RTXThread = new AudioRtpRTX (ca, _symmetric);
jpbl's avatar
jpbl committed
64
65
66

  try {
    if (_RTXThread->start() != 0) {
67
      _debug("! ARTP Failure: unable to start RTX Thread\n");
jpbl's avatar
jpbl committed
68
69
70
      return -1;
    }
  } catch(...) {
71
    _debugException("! ARTP Failure: when trying to start a thread");
jpbl's avatar
jpbl committed
72
73
74
75
76
77
78
79
80
81
82
83
84
85
    throw;
  }
  return 0;
}

	
void
AudioRtp::closeRtpSession () {
  ost::MutexLock m(_threadMutex);
  // This will make RTP threads finish.
  // _debug("Stopping AudioRTP\n");
  try {
    delete _RTXThread; _RTXThread = 0;
  } catch(...) {
86
    _debugException("! ARTP Exception: when stopping audiortp\n");
jpbl's avatar
jpbl committed
87
88
89
90
91
92
93
    throw;
  }
}

////////////////////////////////////////////////////////////////////////////////
// AudioRtpRTX Class                                                          //
////////////////////////////////////////////////////////////////////////////////
94
AudioRtpRTX::AudioRtpRTX (SIPCall *sipcall, bool sym) {
jpbl's avatar
jpbl committed
95
96
97
98
  setCancel(cancelDeferred);
  time = new ost::Time();
  _ca = sipcall;
  _sym = sym;
99
100
101
102
103
104
105
106
107
108
  // AudioRtpRTX should be close if we change sample rate

  _receiveDataDecoded = new int16[RTP_20S_48KHZ_MAX];
  _sendDataEncoded   =  new unsigned char[RTP_20S_8KHZ_MAX];

  // we estimate that the number of format after a conversion 8000->48000 is expanded to 6 times
  _dataAudioLayer = new SFLDataFormat[RTP_20S_48KHZ_MAX];
  _floatBuffer8000  = new float32[RTP_20S_8KHZ_MAX];
  _floatBuffer48000 = new float32[RTP_20S_48KHZ_MAX];
  _intBuffer8000  = new int16[RTP_20S_8KHZ_MAX];
jpbl's avatar
jpbl committed
109
110

  // TODO: Change bind address according to user settings.
yanmorin's avatar
   
yanmorin committed
111
  // TODO: this should be the local ip not the external (router) IP
yanmorin's avatar
   
yanmorin committed
112
  std::string localipConfig = _ca->getLocalIp(); // _ca->getLocalIp();
jpbl's avatar
jpbl committed
113
114
115
116
117
118
119
120
121
122
123
  ost::InetHostAddress local_ip(localipConfig.c_str());

  if (!_sym) {
    _sessionRecv = new ost::RTPSession(local_ip, _ca->getLocalAudioPort());
    _sessionSend = new ost::RTPSession(local_ip, _ca->getLocalAudioPort());
    _session = NULL;
  } else {
    _session = new ost::SymmetricRTPSession (local_ip, _ca->getLocalAudioPort());
    _sessionRecv = NULL;
    _sessionSend = NULL;
  }
124

jpbl's avatar
jpbl committed
125
126
127
128
129
130
131
132
}

AudioRtpRTX::~AudioRtpRTX () {
  _start.wait();

  try {
    this->terminate();
  } catch(...) {
133
    _debugException("! ARTP: Thread destructor didn't terminate correctly");
jpbl's avatar
jpbl committed
134
135
136
    throw;
  }
  //_debug("terminate audiortprtx ended...\n");
137
  _ca = 0;
jpbl's avatar
jpbl committed
138
139

  if (!_sym) {
140
141
    delete _sessionRecv; _sessionRecv = 0;
    delete _sessionSend; _sessionSend = 0;
jpbl's avatar
jpbl committed
142
  } else {
143
    delete _session;     _session = 0;
jpbl's avatar
jpbl committed
144
145
  }

146
147
148
149
  delete [] _intBuffer8000; _intBuffer8000 = 0;
  delete [] _floatBuffer48000; _floatBuffer48000 = 0;
  delete [] _floatBuffer8000; _floatBuffer8000 = 0;
  delete [] _dataAudioLayer; _dataAudioLayer = 0;
jpbl's avatar
jpbl committed
150

151
152
  delete [] _sendDataEncoded; _sendDataEncoded = 0;
  delete [] _receiveDataDecoded; _receiveDataDecoded = 0;
jpbl's avatar
jpbl committed
153

154
155

  delete time; time = NULL;
jpbl's avatar
jpbl committed
156
157
158
159
160
161
162
163
164
}

void
AudioRtpRTX::initAudioRtpSession (void) 
{
  try {
    if (_ca == 0) { return; }

    //_debug("Init audio RTP session\n");
yanmorin's avatar
   
yanmorin committed
165
    ost::InetHostAddress remote_ip(_ca->getRemoteIp().c_str());
jpbl's avatar
jpbl committed
166
    if (!remote_ip) {
167
      _debug("! ARTP Thread Error: Target IP address [%s] is not correct!\n", _ca->getRemoteIp().data());
jpbl's avatar
jpbl committed
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
      return;
    }

    // Initialization
    if (!_sym) {
      _sessionRecv->setSchedulingTimeout (10000);
      _sessionRecv->setExpireTimeout(1000000);

      _sessionSend->setSchedulingTimeout(10000);
      _sessionSend->setExpireTimeout(1000000);
    } else {
      _session->setSchedulingTimeout(10000);
      _session->setExpireTimeout(1000000);
    }

    if (!_sym) {
yanmorin's avatar
   
yanmorin committed
184
185
      if ( !_sessionRecv->addDestination(remote_ip, (unsigned short) _ca->getRemoteAudioPort()) ) {
        _debug("AudioRTP Thread Error: could not connect to port %d\n",  _ca->getRemoteAudioPort());
jpbl's avatar
jpbl committed
186
187
        return;
      }
yanmorin's avatar
   
yanmorin committed
188
      if (!_sessionSend->addDestination (remote_ip, (unsigned short) _ca->getRemoteAudioPort())) {
189
        _debug("! ARTP Thread Error: could not connect to port %d\n",  _ca->getRemoteAudioPort());
jpbl's avatar
jpbl committed
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
        return;
      }

      AudioCodec* audiocodec = _ca->getAudioCodec();
      bool payloadIsSet = false;
      if (audiocodec) {
        if (audiocodec->hasDynamicPayload()) {
          payloadIsSet = _sessionRecv->setPayloadFormat(ost::DynamicPayloadFormat((ost::PayloadType) audiocodec->getPayload(), audiocodec->getClockRate()));
        } else {
          payloadIsSet= _sessionRecv->setPayloadFormat(ost::StaticPayloadFormat((ost::StaticPayloadType) audiocodec->getPayload()));
          payloadIsSet = _sessionSend->setPayloadFormat(ost::StaticPayloadFormat((ost::StaticPayloadType) audiocodec->getPayload()));
        }
      }
      _sessionSend->setMark(true);
    } else {

      //_debug("AudioRTP Thread: Added session destination %s:%d\n", remote_ip.getHostname(), (unsigned short) _ca->getRemoteSdpAudioPort());

yanmorin's avatar
   
yanmorin committed
208
      if (!_session->addDestination (remote_ip, (unsigned short) _ca->getRemoteAudioPort())) {
jpbl's avatar
jpbl committed
209
210
211
212
213
214
215
216
217
218
219
220
221
222
        return;
      }

      AudioCodec* audiocodec = _ca->getAudioCodec();
      bool payloadIsSet = false;
      if (audiocodec) {
        if (audiocodec->hasDynamicPayload()) {
          payloadIsSet = _session->setPayloadFormat(ost::DynamicPayloadFormat((ost::PayloadType) audiocodec->getPayload(), audiocodec->getClockRate()));
        } else {
          payloadIsSet = _session->setPayloadFormat(ost::StaticPayloadFormat((ost::StaticPayloadType) audiocodec->getPayload()));
        }
      }
    }
  } catch(...) {
223
    _debugException("! ARTP Failure: initialisation failed");
jpbl's avatar
jpbl committed
224
225
226
227
228
    throw;
  }
}

void
229
AudioRtpRTX::sendSessionFromMic(int timestamp)
jpbl's avatar
jpbl committed
230
{
231
232
233
234
235
  // STEP:
  //   1. get data from mic
  //   2. convert it to int16 - good sample, good rate
  //   3. encode it
  //   4. send it
jpbl's avatar
jpbl committed
236
  try {
yanmorin's avatar
yanmorin committed
237
    if (_ca==0) { _debug(" !ARTP: No call associated (mic)\n"); return; } // no call, so we do nothing
jpbl's avatar
jpbl committed
238
    AudioLayer* audiolayer = Manager::instance().getAudioDriver();
yanmorin's avatar
yanmorin committed
239
    if (!audiolayer) { _debug(" !ARTP: No audiolayer available for mic\n"); return; }
jpbl's avatar
jpbl committed
240
241

    AudioCodec* audiocodec = _ca->getAudioCodec();
yanmorin's avatar
yanmorin committed
242
    if (!audiocodec) { _debug(" !ARTP: No audiocodec available for mic\n"); return; }
jpbl's avatar
jpbl committed
243

244
245
246
247
248
249
    // we have to get 20ms of data from the mic *20/1000 = /50
    // rate/50 shall be lower than RTP_20S_48KHZ_MAX
    int maxBytesToGet = audiolayer->getSampleRate()/50*sizeof(SFLDataFormat);

    // available bytes inside ringbuffer
    int availBytesFromMic = audiolayer->canGetMic();
jpbl's avatar
jpbl committed
250
251

    // take the lower
252
    int bytesAvail = (availBytesFromMic < maxBytesToGet) ? availBytesFromMic : maxBytesToGet;
yanmorin's avatar
yanmorin committed
253
    //_debug("available = %d, maxBytesToGet = %d\n", availBytesFromMic, maxBytesToGet);
jpbl's avatar
jpbl committed
254
255

    // Get bytes from micRingBuffer to data_from_mic
256
257
258
259
260
261
262
    int nbSample = audiolayer->getMic(_dataAudioLayer, bytesAvail) / sizeof(SFLDataFormat);

    int16* toSIP = 0;
    if (audiolayer->getSampleRate() != audiocodec->getClockRate() && nbSample) {
       SRC_DATA src_data;
       #ifdef DATAFORMAT_IS_FLOAT   
          src_data.data_in = _dataAudioLayer;
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
/*        _debug("inb: %d %f %f %f %f %f %f %f %f %f %f %f %f\n", nbSample, _dataAudioLayer[0], 
_dataAudioLayer[1 ],
_dataAudioLayer[2 ],
_dataAudioLayer[3 ],
_dataAudioLayer[4 ],
_dataAudioLayer[5 ],
_dataAudioLayer[6 ],
_dataAudioLayer[7 ],
_dataAudioLayer[8 ],
_dataAudioLayer[9 ],
_dataAudioLayer[10],
_dataAudioLayer[11],
_dataAudioLayer[12],
_dataAudioLayer[13],
_dataAudioLayer[14],
_dataAudioLayer[15]
);*/
280
281
282
283
284
285
286
287
288
289
290
291
       #else
          src_short_to_float_array(_dataAudioLayer, _floatBuffer48000, nbSample);
          src_data.data_in = _floatBuffer48000; 
       #endif
       double factord = (double)audiocodec->getClockRate()/audiolayer->getSampleRate();
       src_data.src_ratio = factord;
       src_data.input_frames = nbSample;
       src_data.output_frames = RTP_20S_8KHZ_MAX;
       src_data.data_out = _floatBuffer8000;
       src_simple (&src_data, SRC_SINC_BEST_QUALITY/*SRC_SINC_MEDIUM_QUALITY*/, 1); // 1 = channel
       nbSample = src_data.output_frames_gen;
       //if (nbSample > RTP_20S_8KHZ_MAX) { _debug("Alert from mic, nbSample %d is bigger than expected %d\n", nbSample, RTP_20S_8KHZ_MAX); }
292
293
294
       /*_debug("ina: %d %f %f\n", nbSample, _floatBuffer8000[0], 
_floatBuffer8000[1 ]
);*/
295
296
297
298
299
300
301
302
303
304
305
       src_float_to_short_array (_floatBuffer8000, _intBuffer8000, nbSample);
       toSIP = _intBuffer8000;
    } else {
      #ifdef DATAFORMAT_IS_FLOAT
        // convert _receiveDataDecoded to float inside _receiveData
        src_float_to_short_array(_dataAudioLayer, _intBuffer8000, nbSample);
        toSIP = _intBuffer8000;
       //if (nbSample > RTP_20S_8KHZ_MAX) { _debug("Alert from mic, nbSample %d is bigger than expected %d\n", nbSample, RTP_20S_8KHZ_MAX); }
      #else
        toSIP = _dataAudioLayer; // int to int
      #endif
jpbl's avatar
jpbl committed
306
307
    }

308
    if ( nbSample < (RTP_20S_8KHZ_MAX - 10) ) { // if only 10 is missing, it's ok
jpbl's avatar
jpbl committed
309
      // fill end with 0...
310
311
312
      //_debug("begin: %p, nbSample: %d\n", toSIP, nbSample);
      //_debug("has to fill: %d chars at %p\n", (RTP_20S_8KHZ_MAX-nbSample)*sizeof(int16), toSIP + nbSample);
      memset(toSIP + nbSample, 0, (RTP_20S_8KHZ_MAX-nbSample)*sizeof(int16));
yanmorin's avatar
yanmorin committed
313
      nbSample = RTP_20S_8KHZ_MAX;
jpbl's avatar
jpbl committed
314
    }
yanmorin's avatar
yanmorin committed
315
    //_debug("AR: Nb sample: %d int, [0]=%d [1]=%d [2]=%d\n", nbSample, toSIP[0], toSIP[1], toSIP[2]);
jpbl's avatar
jpbl committed
316

317
318
319
320
321
322
323
    // for the mono: range = 0 to RTP_FRAME2SEND * sizeof(int16)
    // codecEncode(char *dest, int16* src, size in bytes of the src)
    int compSize = audiocodec->codecEncode(_sendDataEncoded, toSIP, nbSample*sizeof(int16));

    // encode divise by two
    // Send encoded audio sample over the network
    if (compSize > RTP_20S_8KHZ_MAX) { _debug("! ARTP: %d should be %d\n", compSize, RTP_20S_8KHZ_MAX);}
324
    timestamp += time->getSecond();
325
326
327
328
    if (!_sym) {
      _sessionSend->putData(timestamp, _sendDataEncoded, compSize);
    } else {
      _session->putData(timestamp, _sendDataEncoded, compSize);
jpbl's avatar
jpbl committed
329
    }
330
    toSIP = 0;
jpbl's avatar
jpbl committed
331
  } catch(...) {
332
    _debugException("! ARTP: sending failed");
jpbl's avatar
jpbl committed
333
334
    throw;
  }
335

jpbl's avatar
jpbl committed
336
337
338
}

void
339
AudioRtpRTX::receiveSessionForSpkr (int& countTime)
jpbl's avatar
jpbl committed
340
341
342
343
{
  if (_ca == 0) { return; }
  try {
    AudioLayer* audiolayer = Manager::instance().getAudioDriver();
344
345
    if (!audiolayer) { return; }

jpbl's avatar
jpbl committed
346
347
348
349
350
351
352
353
354
355
356
357
    const ost::AppDataUnit* adu = NULL;
    // Get audio data stream

    if (!_sym) {
      adu = _sessionRecv->getData(_sessionRecv->getFirstTimestamp());
    } else {
      adu = _session->getData(_session->getFirstTimestamp());
    }
    if (adu == NULL) {
      return;
    }

358
359
360
361
362
363
364
365
366
    int payload = adu->getType(); // codec type
    unsigned char* data  = (unsigned char*)adu->getData(); // data in char
    unsigned int size    = adu->getSize(); // size in char

    if ( size > RTP_20S_8KHZ_MAX ) {
      _debug("We have received from RTP a packet larger than expected: %s VS %s\n", size, RTP_20S_8KHZ_MAX);
      _debug("The packet size has been cropped\n");
      size=RTP_20S_8KHZ_MAX;
    }
jpbl's avatar
jpbl committed
367
368
369
370
371

    // Decode data with relevant codec
    AudioCodec* audiocodec = _ca->getCodecMap().getCodec((CodecType)payload);
    if (audiocodec != 0) {
      // codecDecode(int16 *dest, char* src, size in bytes of the src)
372
      // decode multiply by two, so the number of byte should be double
jpbl's avatar
jpbl committed
373
      // size shall be RTP_FRAME2SEND or lower
374
      int expandedSize = audiocodec->codecDecode(_receiveDataDecoded, data, size);
jpbl's avatar
jpbl committed
375
      int nbInt16      = expandedSize/sizeof(int16);
376
377
378
379
      if (nbInt16 > RTP_20S_8KHZ_MAX) {
        _debug("We have decoded a RTP packet larger than expected: %s VS %s. crop\n", nbInt16, RTP_20S_8KHZ_MAX);
        nbInt16=RTP_20S_8KHZ_MAX;
      }
jpbl's avatar
jpbl committed
380

381
382
383
      SFLDataFormat* toAudioLayer;
      int nbSample = nbInt16;
      int nbSampleMaxRate = nbInt16 * 6; // TODO: change it
jpbl's avatar
jpbl committed
384

385
      if ( audiolayer->getSampleRate() != audiocodec->getClockRate() && nbSample) {
jpbl's avatar
jpbl committed
386
        // convert here
387
        double         factord = (double)audiolayer->getSampleRate()/audiocodec->getClockRate();
jpbl's avatar
jpbl committed
388
389

        SRC_DATA src_data;
390
391
392
393
        src_data.data_in = _floatBuffer8000;
        src_data.data_out = _floatBuffer48000;
        src_data.input_frames = nbSample;
        src_data.output_frames = nbSampleMaxRate;
jpbl's avatar
jpbl committed
394
        src_data.src_ratio = factord;
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
        src_short_to_float_array(_receiveDataDecoded, _floatBuffer8000, nbSample);
        src_simple (&src_data, SRC_SINC_BEST_QUALITY/*SRC_SINC_MEDIUM_QUALITY*/, 1); // 1=mono channel
       
        nbSample = ( src_data.output_frames_gen > RTP_20S_48KHZ_MAX) ? RTP_20S_48KHZ_MAX : src_data.output_frames_gen;
        #ifdef DATAFORMAT_IS_FLOAT
          toAudioLayer = _floatBuffer48000;
	#else
          src_float_to_short_array(_floatBuffer48000, _dataAudioLayer, nbSample);
	  toAudioLayer = _dataAudioLayer;
	#endif
	
      } else {
        nbSample = nbInt16;
        #ifdef DATAFORMAT_IS_FLOAT
      	  // convert _receiveDataDecoded to float inside _receiveData
          src_short_to_float_array(_receiveDataDecoded, _floatBuffer8000, nbSample);
	  toAudioLayer = _floatBuffer8000;
        #else
	  toAudioLayer = _receiveDataDecoded; // int to int
        #endif
jpbl's avatar
jpbl committed
415
      }
416
417
      audiolayer->putMain(toAudioLayer, nbSample * sizeof(SFLDataFormat));
      //_debug("ARTP: %d\n", nbSample * sizeof(SFLDataFormat));
jpbl's avatar
jpbl committed
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433

      // Notify (with a beep) an incoming call when there is already a call 
      countTime += time->getSecond();
      if (Manager::instance().incomingCallWaiting() > 0) {
        countTime = countTime % 500; // more often...
        if (countTime == 0) {
          Manager::instance().notificationIncomingCall();
        }
      }

    } else {
      countTime += time->getSecond();
    }

    delete adu; adu = NULL;
  } catch(...) {
434
    _debugException("! ARTP: receiving failed");
jpbl's avatar
jpbl committed
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
    throw;
  }
}

void
AudioRtpRTX::run () {
  //mic, we receive from soundcard in stereo, and we send encoded
  //encoding before sending
  AudioLayer *audiolayer = Manager::instance().getAudioDriver();

  try {
    // Init the session
    initAudioRtpSession();

    // start running the packet queue scheduler.
    //_debug("AudioRTP Thread started\n");
    if (!_sym) {
      _sessionRecv->startRunning();
      _sessionSend->startRunning();
    } else {
      _session->startRunning();
      //_debug("Session is now: %d active\n", _session->isActive());
    }

    int timestamp = 0; // for mic
    int countTime = 0; // for receive
    // TODO: get frameSize from user config 
    int frameSize = 20; // 20ms frames
    TimerPort::setTimer(frameSize);

    audiolayer->flushMic();
    audiolayer->startStream();
    _start.post();
468
    _debug("- ARTP Action: Start\n");
jpbl's avatar
jpbl committed
469
470
471
472
    while (!testCancel()) {
      ////////////////////////////
      // Send session
      ////////////////////////////
473
474
      sendSessionFromMic(timestamp);
      timestamp += RTP_20S_8KHZ_MAX;
jpbl's avatar
jpbl committed
475
476
477
478

      ////////////////////////////
      // Recv session
      ////////////////////////////
479
      receiveSessionForSpkr(countTime);
jpbl's avatar
jpbl committed
480
481
482
483
484
485
486
487
488

      // Let's wait for the next transmit cycle
      Thread::sleep(TimerPort::getTimer());
      TimerPort::incTimer(frameSize); // 'frameSize' ms
    }
    //_debug("stop stream for audiortp loop\n");
    audiolayer->stopStream();
  } catch(std::exception &e) {
    _start.post();
489
    _debug("! ARTP: Stop %s\n", e.what());
jpbl's avatar
jpbl committed
490
491
492
    throw;
  } catch(...) {
    _start.post();
493
    _debugException("* ARTP Action: Stop");
jpbl's avatar
jpbl committed
494
495
496
497
498
499
    throw;
  }
}


// EOF