Update of /cvsroot/audacity/lib-src/portaudio-v19/src/hostapi/asio
In directory
23jxhf1.ch3.sourceforge.com:/tmp/cvs-serv29383/lib-src/portaudio-v19/src/hostapi/asio
Modified Files:
pa_asio.cpp
Log Message:
Update portaudio-v19 to current portaudio SVN version, r1414
Index: pa_asio.cpp
===================================================================
RCS file: /cvsroot/audacity/lib-src/portaudio-v19/src/hostapi/asio/pa_asio.cpp,v
retrieving revision 1.8
retrieving revision 1.9
diff -u -d -r1.8 -r1.9
--- pa_asio.cpp 31 Dec 2008 15:38:35 -0000 1.8
+++ pa_asio.cpp 25 May 2009 21:40:16 -0000 1.9
@@ -1620,6 +1620,7 @@
HANDLE completedBuffersPlayedEvent;
bool streamFinishedCallbackCalled;
+ int isStopped;
volatile int isActive;
volatile bool zeroOutput; /* all future calls to the callback will output
silence */
@@ -1754,6 +1755,98 @@
}
+static bool IsUsingExternalClockSource()
+{
+ bool result = false;
+ ASIOError asioError;
+ ASIOClockSource clocks[32];
+ long numSources=32;
+
+ /* davidv: listing ASIO Clock sources. there is an ongoing investigation by
+ me about whether or not to call ASIOSetSampleRate if an external Clock
is
+ used. A few drivers expected different things here */
+
+ asioError = ASIOGetClockSources(clocks, &numSources);
+ if( asioError != ASE_OK ){
+ PA_DEBUG(("ERROR: ASIOGetClockSources: %s\n",
PaAsio_GetAsioErrorText(asioError) ));
+ }else{
+ PA_DEBUG(("INFO ASIOGetClockSources listing %d clocks\n", numSources
));
+ for (int i=0;i<numSources;++i){
+ PA_DEBUG(("ASIOClockSource%d %s current:%d\n", i, clocks[i].name,
clocks[i].isCurrentSource ));
+
+ if (clocks[i].isCurrentSource)
+ result = true;
+ }
+ }
+
+ return result;
+}
+
+
+static PaError ValidateAndSetSampleRate( double sampleRate )
+{
+ PaError result = paNoError;
+ ASIOError asioError;
+
+ // check that the device supports the requested sample rate
+
+ asioError = ASIOCanSampleRate( sampleRate );
+ PA_DEBUG(("ASIOCanSampleRate(%f):%d\n", sampleRate, asioError ));
+
+ if( asioError != ASE_OK )
+ {
+ result = paInvalidSampleRate;
+ PA_DEBUG(("ERROR: ASIOCanSampleRate: %s\n",
PaAsio_GetAsioErrorText(asioError) ));
+ goto error;
+ }
+
+ // retrieve the current sample rate, we only change to the requested
+ // sample rate if the device is not already in that rate.
+
+ ASIOSampleRate oldRate;
+ asioError = ASIOGetSampleRate(&oldRate);
+ if( asioError != ASE_OK )
+ {
+ result = paInvalidSampleRate;
+ PA_DEBUG(("ERROR: ASIOGetSampleRate: %s\n",
PaAsio_GetAsioErrorText(asioError) ));
+ goto error;
+ }
+ PA_DEBUG(("ASIOGetSampleRate:%f\n",oldRate));
+
+ if (oldRate != sampleRate){
+ /* Set sample rate */
+
+ PA_DEBUG(("before ASIOSetSampleRate(%f)\n",sampleRate));
+
+ /*
+ If you have problems with some drivers when externally clocked,
+ try switching on the following line and commenting out the one
after it.
+ See IsUsingExternalClockSource() for more info.
+ */
+ //if( IsUsingExternalClockSource() ){
+ if( false ){
+ asioError = ASIOSetSampleRate( 0 );
+ }else{
+ asioError = ASIOSetSampleRate( sampleRate );
+ }
+ if( asioError != ASE_OK )
+ {
+ result = paInvalidSampleRate;
+ PA_DEBUG(("ERROR: ASIOSetSampleRate: %s\n",
PaAsio_GetAsioErrorText(asioError) ));
+ goto error;
+ }
+ PA_DEBUG(("after ASIOSetSampleRate(%f)\n",sampleRate));
+ }
+ else
+ {
+ PA_DEBUG(("No Need to change SR\n"));
+ }
+
+error:
+ return result;
+}
+
+
/* see pa_hostapi.h for a list of validity guarantees made about OpenStream
parameters */
static PaError OpenStream( struct PaUtilHostApiRepresentation *hostApi,
@@ -1785,7 +1878,6 @@
PaAsioDriverInfo *driverInfo;
int *inputChannelSelectors = 0;
int *outputChannelSelectors = 0;
- bool isExternal = false;
/* Are we using blocking i/o interface? */
int usingBlockingIo = ( !streamCallback ) ? TRUE : FALSE;
@@ -1807,6 +1899,8 @@
return paDeviceUnavailable;
}
+ assert( theAsioStream == 0 );
+
if( inputParameters && outputParameters )
{
/* full duplex ASIO stream must use the same device for input and
output */
@@ -1914,76 +2008,9 @@
}
}
-
- /* davidv: listing ASIO Clock sources, there is an ongoing investigation by
- me about whether or not call ASIOSetSampleRate if an external Clock is
- used. A few drivers expected different things here */
- {
- ASIOClockSource clocks[32];
- long numSources=32;
- asioError = ASIOGetClockSources(clocks, &numSources);
- if( asioError != ASE_OK ){
- PA_DEBUG(("ERROR: ASIOGetClockSources: %s\n",
PaAsio_GetAsioErrorText(asioError) ));
- }else{
- PA_DEBUG(("INFO ASIOGetClockSources listing %d clocks\n",
numSources ));
- for (int i=0;i<numSources;++i){
- PA_DEBUG(("ASIOClockSource%d %s current:%d\n",
i,clocks[i].name, clocks[i].isCurrentSource ));
-
- /*
- If you have problems with some drivers when externally
clocked,
- uncomment the next two lines
- */
- //if (clocks[i].isCurrentSource)
- // isExternal = true;
- }
- }
- }
-
- // check that the device supports the requested sample rate
-
- asioError = ASIOCanSampleRate( sampleRate );
- PA_DEBUG(("ASIOCanSampleRate(%f):%d\n",sampleRate, asioError ));
-
- if( asioError != ASE_OK )
- {
- result = paInvalidSampleRate;
- PA_DEBUG(("ERROR: ASIOCanSampleRate: %s\n",
PaAsio_GetAsioErrorText(asioError) ));
- goto error;
- }
-
-
- // retrieve the current sample rate, we only change to the requested
- // sample rate if the device is not already in that rate.
-
- ASIOSampleRate oldRate;
- asioError = ASIOGetSampleRate(&oldRate);
- if( asioError != ASE_OK )
- {
- result = paInvalidSampleRate;
- PA_DEBUG(("ERROR: ASIOGetSampleRate: %s\n",
PaAsio_GetAsioErrorText(asioError) ));
+ result = ValidateAndSetSampleRate( sampleRate );
+ if( result != paNoError )
goto error;
- }
- PA_DEBUG(("ASIOGetSampleRate:%f\n",oldRate));
-
- if (oldRate != sampleRate){
-
- PA_DEBUG(("before ASIOSetSampleRate(%f)\n",sampleRate));
-
- asioError = ASIOSetSampleRate( isExternal?0:sampleRate );
- /* Set sample rate */
- if( asioError != ASE_OK )
- {
- result = paInvalidSampleRate;
- PA_DEBUG(("ERROR: ASIOSetSampleRate: %s\n",
PaAsio_GetAsioErrorText(asioError) ));
- goto error;
- }
- PA_DEBUG(("after ASIOSetSampleRate(%f)\n",sampleRate));
- }
- else
- {
- PA_DEBUG(("No Need to change SR\n"));
- }
-
/*
IMPLEMENT ME:
@@ -2544,10 +2571,12 @@
stream->inputChannelCount = inputChannelCount;
stream->outputChannelCount = outputChannelCount;
stream->postOutput = driverInfo->postOutput;
+ stream->isStopped = 1;
stream->isActive = 0;
-
+
asioHostApi->openAsioDeviceIndex = asioDeviceIndex;
+ theAsioStream = stream;
*s = (PaStream*)stream;
return result;
@@ -2655,6 +2684,8 @@
ASIODisposeBuffers();
UnloadAsioDriver();
+ theAsioStream = 0;
+
return result;
}
@@ -3156,16 +3187,19 @@
if( result == paNoError )
{
- theAsioStream = stream;
+ assert( theAsioStream == stream ); /* theAsioStream should be set
correctly in OpenStream */
+
+ /* initialize these variables before the callback has a chance to be
invoked */
+ stream->isStopped = 0;
+ stream->isActive = 1;
+ stream->streamFinishedCallbackCalled = false;
+
asioError = ASIOStart();
- if( asioError == ASE_OK )
- {
- stream->isActive = 1;
- stream->streamFinishedCallbackCalled = false;
- }
- else
+ if( asioError != ASE_OK )
{
- theAsioStream = 0;
+ stream->isStopped = 1;
+ stream->isActive = 0;
+
result = paUnanticipatedHostError;
PA_ASIO_SET_LAST_ASIO_ERROR( asioError );
}
@@ -3174,6 +3208,18 @@
return result;
}
+static void EnsureCallbackHasCompleted( PaAsioStream *stream )
+{
+ // make sure that the callback is not still in-flight after ASIOStop()
+ // returns. This has been observed to happen on the Hoontech DSP24 for
+ // example.
+ int count = 2000; // only wait for 2 seconds, rather than hanging.
+ while( stream->reenterCount != -1 && count > 0 )
+ {
+ Sleep(1);
+ --count;
+ }
+}
static PaError StopStream( PaStream *s )
{
@@ -3224,7 +3270,7 @@
length is longer than the asio buffer size then that should
be taken into account.
*/
- if( WaitForSingleObject( theAsioStream->completedBuffersPlayedEvent,
+ if( WaitForSingleObject( stream->completedBuffersPlayedEvent,
(DWORD)(stream->streamRepresentation.streamInfo.outputLatency
* 1000. * 4.) )
== WAIT_TIMEOUT )
{
@@ -3233,13 +3279,17 @@
}
asioError = ASIOStop();
- if( asioError != ASE_OK )
+ if( asioError == ASE_OK )
+ {
+ EnsureCallbackHasCompleted( stream );
+ }
+ else
{
result = paUnanticipatedHostError;
PA_ASIO_SET_LAST_ASIO_ERROR( asioError );
}
- theAsioStream = 0;
+ stream->isStopped = 1;
stream->isActive = 0;
if( !stream->streamFinishedCallbackCalled )
@@ -3251,7 +3301,6 @@
return result;
}
-
static PaError AbortStream( PaStream *s )
{
PaError result = paNoError;
@@ -3261,31 +3310,17 @@
stream->zeroOutput = true;
asioError = ASIOStop();
- if( asioError != ASE_OK )
+ if( asioError == ASE_OK )
{
- result = paUnanticipatedHostError;
- PA_ASIO_SET_LAST_ASIO_ERROR( asioError );
+ EnsureCallbackHasCompleted( stream );
}
else
{
- // make sure that the callback is not still in-flight when ASIOStop()
- // returns. This has been observed to happen on the Hoontech DSP24 for
- // example.
- int count = 2000; // only wait for 2 seconds, rather than hanging.
- while( theAsioStream->reenterCount != -1 && count > 0 )
- {
- Sleep(1);
- --count;
- }
+ result = paUnanticipatedHostError;
+ PA_ASIO_SET_LAST_ASIO_ERROR( asioError );
}
- /* it is questionable whether we should zero theAsioStream if ASIOStop()
- returns an error, because the callback could still be active. We assume
- not - this is based on the fact that ASIOStop is unlikely to fail
- if the callback is running - it's more likely to fail because the
- callback is not running. */
-
- theAsioStream = 0;
+ stream->isStopped = 1;
stream->isActive = 0;
if( !stream->streamFinishedCallbackCalled )
@@ -3300,9 +3335,9 @@
static PaError IsStreamStopped( PaStream *s )
{
- //PaAsioStream *stream = (PaAsioStream*)s;
- (void) s; /* unused parameter */
- return theAsioStream == 0;
+ PaAsioStream *stream = (PaAsioStream*)s;
+
+ return stream->isStopped;
}
@@ -3435,11 +3470,8 @@
/* If block processing has stopped, abort! */
if( blockingState->stopFlag ) { return result =
paStreamIsStopped; }
- /* if a timeout is encountered, continue, perhaps we
should give up eventually */
- continue;
- /* To give up eventually, we may increase the time out
- period and return an error if it fails anyway. */
- /* retrun result = paTimedOut; */
+ /* If a timeout is encountered, give up eventually. */
+ return result = paTimedOut;
}
}
/* Now, the ring buffer contains the required amount of data
@@ -3609,11 +3641,9 @@
/* If block processing has stopped, abort! */
if( blockingState->stopFlag ) { return result =
paStreamIsStopped; }
- /* if a timeout is encountered, continue, perhaps we
should give up eventually */
- continue;
- /* To give up eventually, we may increase the time out
- period and return an error if it fails anyway. */
- /* retrun result = paTimedOut; */
+
+ /* If a timeout is encountered, give up eventually. */
+ return result = paTimedOut;
}
}
/* Now, the ring buffer contains the required amount of free
@@ -3703,6 +3733,118 @@
}
+/* This routine will be called by the PortAudio engine when audio is needed.
+** It may called at interrupt level on some machines so don't do anything
+** that could mess up the system like calling malloc() or free().
+*/
+static int BlockingIoPaCallback(const void *inputBuffer
,
+ void *outputBuffer
,
+ unsigned long
framesPerBuffer,
+ const PaStreamCallbackTimeInfo *timeInfo
,
+ PaStreamCallbackFlags statusFlags
,
+ void *userData
)
+{
+ PaError result = paNoError; /* Initial return value. */
+ PaAsioStream *stream = *(PaAsioStream**)userData; /* The PA ASIO stream. */
+ PaAsioStreamBlockingState *blockingState = stream->blockingState; /*
Persume blockingState is valid, otherwise the callback wouldn't be running. */
+
+ /* Get a pointer to the stream's blocking i/o buffer processor. */
+ PaUtilBufferProcessor *pBp = &blockingState->bufferProcessor;
+ PaUtilRingBuffer *pRb = NULL;
+
+ /* If output data has been requested. */
+ if( stream->outputChannelCount )
+ {
+ /* If the callback input argument signalizes a output underflow,
+ make sure the WriteStream() function knows about it, too! */
+ if( statusFlags & paOutputUnderflowed ) {
+ blockingState->outputUnderflowFlag = TRUE;
+ }
+
+ /* Access the corresponding ring buffer. */
+ pRb = &blockingState->writeRingBuffer;
+
+ /* If the blocking i/o buffer contains enough output data, */
+ if( PaUtil_GetRingBufferReadAvailable(pRb) >= (long) framesPerBuffer )
+ {
+ /* Extract the requested data from the ring buffer. */
+ PaUtil_ReadRingBuffer( pRb, outputBuffer, framesPerBuffer );
+ }
+ else /* If no output data is available :-( */
+ {
+ /* Signalize a write-buffer underflow. */
+ blockingState->outputUnderflowFlag = TRUE;
+
+ /* Fill the output buffer with silence. */
+ (*pBp->outputZeroer)( outputBuffer, 1, pBp->outputChannelCount *
framesPerBuffer );
+
+ /* If playback is to be stopped */
+ if( blockingState->stopFlag &&
PaUtil_GetRingBufferReadAvailable(pRb) < (long) framesPerBuffer )
+ {
+ /* Extract all the remaining data from the ring buffer,
+ whether it is a complete data block or not. */
+ PaUtil_ReadRingBuffer( pRb, outputBuffer,
PaUtil_GetRingBufferReadAvailable(pRb) );
+ }
+ }
+
+ /* Set blocking i/o event? */
+ if( blockingState->writeBuffersRequestedFlag &&
PaUtil_GetRingBufferWriteAvailable(pRb) >= (long)
blockingState->writeBuffersRequested )
+ {
+ /* Reset buffer request. */
+ blockingState->writeBuffersRequestedFlag = FALSE;
+ blockingState->writeBuffersRequested = 0;
+ /* Signalize that requested buffers are ready. */
+ SetEvent( blockingState->writeBuffersReadyEvent );
+ /* What do we do if SetEvent() returns zero, i.e. the event
+ could not be set? How to return errors from within the
+ callback? - S.Fischer */
+ }
+ }
+
+ /* If input data has been supplied. */
+ if( stream->inputChannelCount )
+ {
+ /* If the callback input argument signalizes a input overflow,
+ make sure the ReadStream() function knows about it, too! */
+ if( statusFlags & paInputOverflowed ) {
+ blockingState->inputOverflowFlag = TRUE;
+ }
+
+ /* Access the corresponding ring buffer. */
+ pRb = &blockingState->readRingBuffer;
+
+ /* If the blocking i/o buffer contains not enough input buffers */
+ if( PaUtil_GetRingBufferWriteAvailable(pRb) < (long) framesPerBuffer )
+ {
+ /* Signalize a read-buffer overflow. */
+ blockingState->inputOverflowFlag = TRUE;
+
+ /* Remove some old data frames from the buffer. */
+ PaUtil_AdvanceRingBufferReadIndex( pRb, framesPerBuffer );
+ }
+
+ /* Insert the current input data into the ring buffer. */
+ PaUtil_WriteRingBuffer( pRb, inputBuffer, framesPerBuffer );
+
+ /* Set blocking i/o event? */
+ if( blockingState->readFramesRequestedFlag &&
PaUtil_GetRingBufferReadAvailable(pRb) >= (long)
blockingState->readFramesRequested )
+ {
+ /* Reset buffer request. */
+ blockingState->readFramesRequestedFlag = FALSE;
+ blockingState->readFramesRequested = 0;
+ /* Signalize that requested buffers are ready. */
+ SetEvent( blockingState->readFramesReadyEvent );
+ /* What do we do if SetEvent() returns zero, i.e. the event
+ could not be set? How to return errors from within the
+ callback? - S.Fischer */
+ /** @todo report an error with PA_DEBUG */
+ }
+ }
+
+ return paContinue;
+}
+
+
PaError PaAsio_ShowControlPanel( PaDeviceIndex device, void* systemSpecific )
{
PaError result = paNoError;
@@ -3874,119 +4016,51 @@
}
-
-
-
-
-
-
-/* This routine will be called by the PortAudio engine when audio is needed.
-** It may called at interrupt level on some machines so don't do anything
-** that could mess up the system like calling malloc() or free().
+/* NOTE: the following functions are ASIO-stream specific, and are called
directly
+ by client code. We need to check for many more error conditions here
because
+ we don't have the benefit of pa_front.c's parameter checking.
*/
-static int BlockingIoPaCallback(const void *inputBuffer
,
- void *outputBuffer
,
- unsigned long
framesPerBuffer,
- const PaStreamCallbackTimeInfo *timeInfo
,
- PaStreamCallbackFlags statusFlags
,
- void *userData
)
+
+static PaError GetAsioStreamPointer( PaAsioStream **stream, PaStream *s )
{
- PaError result = paNoError; /* Initial return value. */
- PaAsioStream *stream = *(PaAsioStream**)userData; /* The PA ASIO stream.
*/ /* This is a pointer to "theAsioStream", see OpenStream(). */
- PaAsioStreamBlockingState *blockingState = stream->blockingState; /*
Persume blockingState is valid, otherwise the callback wouldn't be running. */
+ PaError result;
+ PaUtilHostApiRepresentation *hostApi;
+ PaAsioHostApiRepresentation *asioHostApi;
+
+ result = PaUtil_ValidateStreamPointer( s );
+ if( result != paNoError )
+ return result;
- /* Get a pointer to the stream's blocking i/o buffer processor. */
- PaUtilBufferProcessor *pBp = &blockingState->bufferProcessor;
- PaUtilRingBuffer *pRb = NULL;
+ result = PaUtil_GetHostApiRepresentation( &hostApi, paASIO );
+ if( result != paNoError )
+ return result;
- /* If output data has been requested. */
- if( stream->outputChannelCount )
+ asioHostApi = (PaAsioHostApiRepresentation*)hostApi;
+
+ if( PA_STREAM_REP( s )->streamInterface ==
&asioHostApi->callbackStreamInterface
+ || PA_STREAM_REP( s )->streamInterface ==
&asioHostApi->blockingStreamInterface )
{
- /* If the callback input argument signalizes a output underflow,
- make sure the WriteStream() function knows about it, too! */
- if( statusFlags & paOutputUnderflowed ) {
- blockingState->outputUnderflowFlag = TRUE;
- }
-
- /* Access the corresponding ring buffer. */
- pRb = &blockingState->writeRingBuffer;
-
- /* If the blocking i/o buffer contains enough output data, */
- if( PaUtil_GetRingBufferReadAvailable(pRb) >= (long) framesPerBuffer )
- {
- /* Extract the requested data from the ring buffer. */
- PaUtil_ReadRingBuffer( pRb, outputBuffer, framesPerBuffer );
- }
- else /* If no output data is available :-( */
- {
- /* Signalize a write-buffer underflow. */
- blockingState->outputUnderflowFlag = TRUE;
-
- /* Fill the output buffer with silence. */
- (*pBp->outputZeroer)( outputBuffer, 1, pBp->outputChannelCount *
framesPerBuffer );
-
- /* If playback is to be stopped */
- if( blockingState->stopFlag &&
PaUtil_GetRingBufferReadAvailable(pRb) < (long) framesPerBuffer )
- {
- /* Extract all the remaining data from the ring buffer,
- whether it is a complete data block or not. */
- PaUtil_ReadRingBuffer( pRb, outputBuffer,
PaUtil_GetRingBufferReadAvailable(pRb) );
- }
- }
-
- /* Set blocking i/o event? */
- if( blockingState->writeBuffersRequestedFlag &&
PaUtil_GetRingBufferWriteAvailable(pRb) >= (long)
blockingState->writeBuffersRequested )
- {
- /* Reset buffer request. */
- blockingState->writeBuffersRequestedFlag = FALSE;
- blockingState->writeBuffersRequested = 0;
- /* Signalize that requested buffers are ready. */
- SetEvent( blockingState->writeBuffersReadyEvent );
- /* What do we do if SetEvent() returns zero, i.e. the event
- could not be set? How to return errors from within the
- callback? - S.Fischer */
- }
+ /* s is an ASIO stream */
+ *stream = (PaAsioStream *)s;
+ return paNoError;
}
-
- /* If input data has been supplied. */
- if( stream->inputChannelCount )
+ else
{
- /* If the callback input argument signalizes a input overflow,
- make sure the ReadStream() function knows about it, too! */
- if( statusFlags & paInputOverflowed ) {
- blockingState->inputOverflowFlag = TRUE;
- }
-
- /* Access the corresponding ring buffer. */
- pRb = &blockingState->readRingBuffer;
-
- /* If the blocking i/o buffer contains not enough input buffers */
- if( PaUtil_GetRingBufferWriteAvailable(pRb) < (long) framesPerBuffer )
- {
- /* Signalize a read-buffer overflow. */
- blockingState->inputOverflowFlag = TRUE;
+ return paIncompatibleStreamHostApi;
+ }
+}
- /* Remove some old data frames from the buffer. */
- PaUtil_AdvanceRingBufferReadIndex( pRb, framesPerBuffer );
- }
- /* Insert the current input data into the ring buffer. */
- PaUtil_WriteRingBuffer( pRb, inputBuffer, framesPerBuffer );
+PaError PaAsio_SetStreamSampleRate( PaStream* s, double sampleRate )
+{
+ PaAsioStream *stream;
+ PaError result = GetAsioStreamPointer( &stream, s );
+ if( result != paNoError )
+ return result;
- /* Set blocking i/o event? */
- if( blockingState->readFramesRequestedFlag &&
PaUtil_GetRingBufferReadAvailable(pRb) >= (long)
blockingState->readFramesRequested )
- {
- /* Reset buffer request. */
- blockingState->readFramesRequestedFlag = FALSE;
- blockingState->readFramesRequested = 0;
- /* Signalize that requested buffers are ready. */
- SetEvent( blockingState->readFramesReadyEvent );
- /* What do we do if SetEvent() returns zero, i.e. the event
- could not be set? How to return errors from within the
- callback? - S.Fischer */
- /** @todo report an error with PA_DEBUG */
- }
- }
+ if( stream != theAsioStream )
+ return paBadStreamPtr;
- return paContinue;
+ return ValidateAndSetSampleRate( sampleRate );
}
+
------------------------------------------------------------------------------
Register Now for Creativity and Technology (CaT), June 3rd, NYC. CaT
is a gathering of tech-side developers & brand creativity professionals. Meet
the minds behind Google Creative Lab, Visual Complexity, Processing, &
iPhoneDevCamp asthey present alongside digital heavyweights like Barbarian
Group, R/GA, & Big Spaceship. http://www.creativitycat.com
_______________________________________________
Audacity-cvs mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/audacity-cvs