Index: src/host/RtAudio/RtAudio.cpp --- src/host/RtAudio/RtAudio.cpp.orig +++ src/host/RtAudio/RtAudio.cpp @@ -109,6 +109,7 @@ const char* rtaudio_api_names[][2] = { { "alsa" , "ALSA" }, { "pulse" , "Pulse" }, { "oss" , "OpenSoundSystem" }, + { "sndio" , "sndio" }, { "jack" , "Jack" }, { "core" , "CoreAudio" }, { "wasapi" , "WASAPI" }, @@ -135,6 +136,9 @@ extern "C" const RtAudio::Api rtaudio_compiled_apis[] #if defined(__LINUX_OSS__) RtAudio::LINUX_OSS, #endif +#if defined(__OPENBSD_SNDIO__) + RtAudio::OPENBSD_SNDIO, +#endif #if defined(__WINDOWS_ASIO__) RtAudio::WINDOWS_ASIO, #endif @@ -216,6 +220,10 @@ void RtAudio :: openRtApi( RtAudio::Api api ) if ( api == LINUX_OSS ) rtapi_ = new RtApiOss(); #endif +#if defined(__OPENBSD_SNDIO__) + if ( api == OPENBSD_SNDIO ) + rtapi_ = new RtApiSndio(); +#endif #if defined(__WINDOWS_ASIO__) if ( api == WINDOWS_ASIO ) rtapi_ = new RtApiAsio(); @@ -10387,6 +10395,594 @@ static void *ossCallbackHandler( void *ptr ) //******************** End of __LINUX_OSS__ *********************// #endif +#if defined(__OPENBSD_SNDIO__) + +#include +#include +#include +#include + +static void *sndioCallbackHandler( void *ptr ); + +struct SndioHandle { + struct sio_hdl *handle; + bool xrun[2]; // [0]=output underflow, [1]=input overflow + + SndioHandle() : handle(nullptr) { xrun[0] = false; xrun[1] = false; } +}; + +static void sndioXrunCb( void *arg ) +{ + SndioHandle *h = (SndioHandle *) arg; + h->xrun[0] = true; + h->xrun[1] = true; +} + +RtApiSndio :: RtApiSndio() {} + +RtApiSndio :: ~RtApiSndio() +{ + if ( stream_.state != STREAM_CLOSED ) closeStream(); +} + +unsigned int RtApiSndio :: getDeviceCount( void ) +{ + return 1; +} + +RtAudio::DeviceInfo RtApiSndio :: getDeviceInfo( unsigned int device ) +{ + RtAudio::DeviceInfo info; + info.probed = false; + + if ( device != 0 ) { + errorText_ = "RtApiSndio::getDeviceInfo: invalid device ID!"; + error( RTAUDIO_INVALID_DEVICE ); + return info; + } + + // Advertise default capabilities; actual negotiation happens in probeDeviceOpen. + // We do not require sndiod to be running at probe time. + info.name = "sndio default"; + info.outputChannels = 2; + info.inputChannels = 2; + info.duplexChannels = 2; + info.isDefaultOutput = true; + info.isDefaultInput = true; + + // sndio accepts most standard sample rates + static const unsigned int rates[] = { + 8000, 11025, 16000, 22050, 32000, 44100, 48000, 88200, 96000, 0 + }; + for ( int i = 0; rates[i]; i++ ) { + info.sampleRates.push_back( rates[i] ); + if ( rates[i] == 48000 ) + info.preferredSampleRate = 48000; + } + + info.nativeFormats = RTAUDIO_SINT16 | RTAUDIO_SINT32; + info.probed = true; + return info; +} + +bool RtApiSndio :: probeDeviceOpen( unsigned int device, StreamMode mode, + unsigned int channels, + unsigned int firstChannel, + unsigned int sampleRate, + RtAudioFormat format, + unsigned int *bufferSize, + RtAudio::StreamOptions *options ) +{ + if ( device != 0 ) { + errorText_ = "RtApiSndio::probeDeviceOpen: only device 0 is supported."; + return FAILURE; + } + if ( firstChannel != 0 ) { + errorText_ = "RtApiSndio::probeDeviceOpen: channel offset not supported."; + return FAILURE; + } + + SndioHandle *handle = (SndioHandle *) stream_.apiHandle; + + // When adding INPUT to an already-open OUTPUT stream (going duplex), reuse the + // existing play-only sndio handle instead of closing and reopening as + // SIO_PLAY|SIO_REC. sndiod's default sub-device ("-m play -s default") is + // play-only: a duplex open succeeds but the record clock never ticks, so + // sio_write stalls after the initial pre-fill. Keeping the play-only handle + // lets the poll-with-timeout path in callbackEvent() substitute silence for + // input without blocking. True duplex recording requires a sndiod sub-device + // configured with "-m play,rec" or "-m rec". + if ( mode == INPUT && stream_.mode == OUTPUT && handle && handle->handle ) { + // Set up INPUT bookkeeping using the same format/params as OUTPUT. + stream_.deviceFormat[INPUT] = stream_.deviceFormat[OUTPUT]; + stream_.nUserChannels[INPUT] = channels; + stream_.nDeviceChannels[INPUT] = channels; + stream_.deviceInterleaved[INPUT] = true; + + stream_.doConvertBuffer[INPUT] = false; + stream_.doByteSwap[INPUT] = false; + if ( stream_.userFormat != stream_.deviceFormat[INPUT] ) + stream_.doConvertBuffer[INPUT] = true; + if ( stream_.nUserChannels[INPUT] < stream_.nDeviceChannels[INPUT] ) + stream_.doConvertBuffer[INPUT] = true; + if ( stream_.userInterleaved != stream_.deviceInterleaved[INPUT] && + stream_.nUserChannels[INPUT] > 1 ) + stream_.doConvertBuffer[INPUT] = true; + + unsigned long bufBytes = stream_.nUserChannels[INPUT] * stream_.bufferSize * + formatBytes( stream_.userFormat ); + stream_.userBuffer[INPUT] = (char *) calloc( bufBytes, 1 ); + if ( !stream_.userBuffer[INPUT] ) { + errorText_ = "RtApiSndio::probeDeviceOpen: error allocating input buffer."; + return FAILURE; + } + + if ( stream_.doConvertBuffer[INPUT] ) { + unsigned long devBytes = stream_.nDeviceChannels[INPUT] * + formatBytes( stream_.deviceFormat[INPUT] ) * + stream_.bufferSize; + if ( !stream_.deviceBuffer || devBytes > + stream_.nDeviceChannels[OUTPUT] * formatBytes( stream_.deviceFormat[OUTPUT] ) + * stream_.bufferSize ) { + if ( stream_.deviceBuffer ) free( stream_.deviceBuffer ); + stream_.deviceBuffer = (char *) calloc( devBytes, 1 ); + if ( !stream_.deviceBuffer ) { + free( stream_.userBuffer[INPUT] ); + stream_.userBuffer[INPUT] = nullptr; + errorText_ = "RtApiSndio::probeDeviceOpen: error allocating device buffer."; + return FAILURE; + } + } + setConvertInfo( INPUT, firstChannel ); + } + + stream_.device[INPUT] = device; + stream_.mode = DUPLEX; + return SUCCESS; + } + + // Normal path: open a new sndio device handle. + unsigned int sioMode = ( mode == OUTPUT ) ? SIO_PLAY : SIO_REC; struct sio_hdl *hdl = sio_open( SIO_DEVANY, sioMode, 0 ); if ( !hdl ) { + errorText_ = "RtApiSndio::probeDeviceOpen: error opening sndio device."; + goto error; + } + + // Map RtAudio format to sndio encoding + stream_.userFormat = format; + { + unsigned int bits, bps; + + if ( format == RTAUDIO_SINT8 ) { + bits = 8; bps = 1; + stream_.deviceFormat[mode] = RTAUDIO_SINT8; + } else if ( format == RTAUDIO_SINT16 ) { + bits = 16; bps = 2; + stream_.deviceFormat[mode] = RTAUDIO_SINT16; + } else if ( format == RTAUDIO_SINT24 ) { + bits = 24; bps = 4; + stream_.deviceFormat[mode] = RTAUDIO_SINT24; + } else if ( format == RTAUDIO_SINT32 ) { + bits = 32; bps = 4; + stream_.deviceFormat[mode] = RTAUDIO_SINT32; + } else { + // Float formats not natively supported: use SINT16, RtAudio converts + bits = 16; bps = 2; + stream_.deviceFormat[mode] = RTAUDIO_SINT16; + } + + struct sio_par par; + sio_initpar( &par ); + par.bits = bits; + par.bps = bps; + par.sig = 1; + par.le = SIO_LE_NATIVE; + par.rate = sampleRate; + par.round = *bufferSize; + par.appbufsz = *bufferSize * 4; + par.pchan = ( mode == OUTPUT ) ? channels : 0; + par.rchan = ( mode == INPUT ) ? channels : 0; if ( !sio_setpar( hdl, &par ) ) { + sio_close( hdl ); + errorText_ = "RtApiSndio::probeDeviceOpen: error setting stream parameters."; + return FAILURE; + } if ( !sio_getpar( hdl, &par ) ) { + sio_close( hdl ); + errorText_ = "RtApiSndio::probeDeviceOpen: error getting stream parameters."; + return FAILURE; + } + + if ( par.rate != sampleRate ) { + sio_close( hdl ); + errorStream_ << "RtApiSndio::probeDeviceOpen: sample rate " << sampleRate + << " not supported (got " << par.rate << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + *bufferSize = par.round; + stream_.bufferSize = *bufferSize; + stream_.nBuffers = par.appbufsz / par.round; + stream_.sampleRate = sampleRate; + } // end format/par scope + + stream_.nUserChannels[mode] = channels; + stream_.nDeviceChannels[mode] = channels; + + stream_.userInterleaved = true; + stream_.deviceInterleaved[mode] = true; + if ( options && options->flags & RTAUDIO_NONINTERLEAVED ) + stream_.userInterleaved = false; + + stream_.doConvertBuffer[mode] = false; + stream_.doByteSwap[mode] = false; + if ( stream_.userFormat != stream_.deviceFormat[mode] ) + stream_.doConvertBuffer[mode] = true; + if ( stream_.nUserChannels[mode] < stream_.nDeviceChannels[mode] ) + stream_.doConvertBuffer[mode] = true; + if ( stream_.userInterleaved != stream_.deviceInterleaved[mode] && + stream_.nUserChannels[mode] > 1 ) + stream_.doConvertBuffer[mode] = true; + + // Allocate or reuse SndioHandle + if ( !handle ) { + try { + handle = new SndioHandle; + } + catch ( std::bad_alloc& ) { + sio_close( hdl ); + errorText_ = "RtApiSndio::probeDeviceOpen: error allocating SndioHandle."; + goto error; + } + stream_.apiHandle = (void *) handle; + } + handle->handle = hdl; + sio_onxrun( hdl, sndioXrunCb, handle ); + + // Allocate user buffer + { + unsigned long bufBytes = stream_.nUserChannels[mode] * *bufferSize * + formatBytes( stream_.userFormat ); + stream_.userBuffer[mode] = (char *) calloc( bufBytes, 1 ); + if ( !stream_.userBuffer[mode] ) { + errorText_ = "RtApiSndio::probeDeviceOpen: error allocating user buffer."; + goto error; + } + } + + if ( stream_.doConvertBuffer[mode] ) { + bool makeBuffer = true; + unsigned long bufBytes = stream_.nDeviceChannels[mode] * + formatBytes( stream_.deviceFormat[mode] ); + if ( mode == INPUT ) { + if ( stream_.mode == OUTPUT && stream_.deviceBuffer ) { + unsigned long bytesOut = stream_.nDeviceChannels[0] * + formatBytes( stream_.deviceFormat[0] ); + if ( bufBytes <= bytesOut ) makeBuffer = false; + } + } + if ( makeBuffer ) { + bufBytes *= *bufferSize; + if ( stream_.deviceBuffer ) free( stream_.deviceBuffer ); + stream_.deviceBuffer = (char *) calloc( bufBytes, 1 ); + if ( !stream_.deviceBuffer ) { + errorText_ = "RtApiSndio::probeDeviceOpen: error allocating device buffer."; + goto error; + } + } + } + + stream_.device[mode] = device; + stream_.state = STREAM_STOPPED; + + if ( stream_.doConvertBuffer[mode] ) setConvertInfo( mode, firstChannel ); + + // Setup mode and callback thread + if ( stream_.mode == OUTPUT && mode == INPUT ) { + stream_.mode = DUPLEX; + } else { + stream_.mode = mode; + + stream_.callbackInfo.object = (void *) this; + + pthread_attr_t attr; + pthread_attr_init( &attr ); + pthread_attr_setdetachstate( &attr, PTHREAD_CREATE_JOINABLE ); + pthread_attr_setschedpolicy( &attr, SCHED_OTHER ); + + stream_.callbackInfo.isRunning = true; + int result = pthread_create( &stream_.callbackInfo.thread, &attr, + sndioCallbackHandler, &stream_.callbackInfo ); + pthread_attr_destroy( &attr ); + if ( result ) { + stream_.callbackInfo.isRunning = false; + errorText_ = "RtApiSndio::probeDeviceOpen: error creating callback thread!"; + goto error; + } + } + + return SUCCESS; + + error: + // If a callback thread is already running (e.g., from a prior OUTPUT pass + // that succeeded before this INPUT/DUPLEX pass failed), stop it now so the + // thread doesn't access the handle after we free it below. + if ( stream_.callbackInfo.isRunning ) { + stream_.callbackInfo.isRunning = false; + // Thread spins in 5ms nanosleep when state != RUNNING; it will exit promptly. + pthread_join( stream_.callbackInfo.thread, nullptr ); + } + if ( handle ) { + if ( handle->handle ) sio_close( handle->handle ); + delete handle; + stream_.apiHandle = nullptr; + } + for ( int i = 0; i < 2; i++ ) { + if ( stream_.userBuffer[i] ) { + free( stream_.userBuffer[i] ); + stream_.userBuffer[i] = nullptr; + } + } + if ( stream_.deviceBuffer ) { + free( stream_.deviceBuffer ); + stream_.deviceBuffer = nullptr; + } + stream_.state = STREAM_CLOSED; + return FAILURE; +} + +void RtApiSndio :: closeStream() +{ + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApiSndio::closeStream(): no open stream to close!"; + error( RTAUDIO_WARNING ); + return; + } + + SndioHandle *handle = (SndioHandle *) stream_.apiHandle; + stream_.callbackInfo.isRunning = false; + + MUTEX_LOCK( &stream_.mutex ); + stream_.state = STREAM_CLOSED; + MUTEX_UNLOCK( &stream_.mutex ); + + // Join FIRST (thread may be in sio_write with poll timeout ~23ms). + // Then call sio_stop/sio_close with no concurrent sio I/O. pthread_join( stream_.callbackInfo.thread, nullptr ); + if ( handle ) { + if ( handle->handle ) { sio_stop( handle->handle ); sio_close( handle->handle ); } + delete handle; + stream_.apiHandle = nullptr; + } + + for ( int i = 0; i < 2; i++ ) { + if ( stream_.userBuffer[i] ) { + free( stream_.userBuffer[i] ); + stream_.userBuffer[i] = nullptr; + } + } + if ( stream_.deviceBuffer ) { + free( stream_.deviceBuffer ); + stream_.deviceBuffer = nullptr; + } + + clearStreamInfo(); +} + +RtAudioErrorType RtApiSndio :: startStream() +{ + if ( stream_.state != STREAM_STOPPED ) { + if ( stream_.state == STREAM_RUNNING ) + errorText_ = "RtApiSndio::startStream(): stream already running!"; + else + errorText_ = "RtApiSndio::startStream(): stream is stopping or closed!"; + return error( RTAUDIO_WARNING ); + } + + SndioHandle *handle = (SndioHandle *) stream_.apiHandle; + + MUTEX_LOCK( &stream_.mutex ); if ( !sio_start( handle->handle ) ) { + MUTEX_UNLOCK( &stream_.mutex ); + errorText_ = "RtApiSndio::startStream(): error starting sndio stream."; + return error( RTAUDIO_SYSTEM_ERROR ); + } stream_.state = STREAM_RUNNING; + MUTEX_UNLOCK( &stream_.mutex ); + return RTAUDIO_NO_ERROR; +} + +RtAudioErrorType RtApiSndio :: stopStream() +{ + if ( stream_.state != STREAM_RUNNING && stream_.state != STREAM_STOPPING ) { + if ( stream_.state == STREAM_STOPPED ) + errorText_ = "RtApiSndio::stopStream(): stream already stopped!"; + else + errorText_ = "RtApiSndio::stopStream(): stream is closed!"; + return error( RTAUDIO_WARNING ); + } + + MUTEX_LOCK( &stream_.mutex ); + + if ( stream_.state == STREAM_STOPPED ) { + MUTEX_UNLOCK( &stream_.mutex ); + return RTAUDIO_NO_ERROR; + } + + SndioHandle *handle = (SndioHandle *) stream_.apiHandle; + + // Do NOT call sio_stop() here: stopStream() may be called from any thread, + // including while the callback thread holds the sio handle in sio_write(). + // sio is not thread-safe; concurrent sio_stop()+sio_write() causes hangs. + // Instead, just set state=STOPPED; closeStream() calls sio_stop() after + // pthread_join() ensures no concurrent sio I/O. + stream_.state = STREAM_STOPPED; + MUTEX_UNLOCK( &stream_.mutex ); + return RTAUDIO_NO_ERROR; +} + +RtAudioErrorType RtApiSndio :: abortStream() +{ + if ( stream_.state != STREAM_RUNNING ) { + if ( stream_.state == STREAM_STOPPED ) + errorText_ = "RtApiSndio::abortStream(): stream already stopped!"; + else + errorText_ = "RtApiSndio::abortStream(): stream is stopping or closed!"; + return error( RTAUDIO_WARNING ); + } + + MUTEX_LOCK( &stream_.mutex ); + // Same thread-safety rule as stopStream(): no sio_stop() here. + // closeStream() will call sio_stop() after pthread_join(). + stream_.state = STREAM_STOPPED; + MUTEX_UNLOCK( &stream_.mutex ); + return RTAUDIO_NO_ERROR; +} + +void RtApiSndio :: callbackEvent() +{ + SndioHandle *handle = (SndioHandle *) stream_.apiHandle; + + if ( stream_.state == STREAM_STOPPED || stream_.state == STREAM_CLOSED ) { + // Spin-wait briefly when stopped to avoid busy-looping + struct timespec ts = { 0, 5000000 }; // 5 ms + nanosleep( &ts, nullptr ); + return; + } + + if ( stream_.state != STREAM_RUNNING ) return; + + // Invoke user callback + int doStopStream = 0; + RtAudioCallback callback = (RtAudioCallback) stream_.callbackInfo.callback; + double streamTime = getStreamTime(); + RtAudioStreamStatus status = 0; + if ( stream_.mode != INPUT && handle->xrun[0] ) { + status |= RTAUDIO_OUTPUT_UNDERFLOW; + handle->xrun[0] = false; + } + if ( stream_.mode != OUTPUT && handle->xrun[1] ) { + status |= RTAUDIO_INPUT_OVERFLOW; + handle->xrun[1] = false; + } + + { static int _cb_pre=0; if(++_cb_pre<=3) { fprintf(stderr,"[sndio-dbg] invoking user callback #%d\n",_cb_pre); fflush(stderr); } } + doStopStream = callback( stream_.userBuffer[0], stream_.userBuffer[1], + stream_.bufferSize, streamTime, status, + stream_.callbackInfo.userData ); + if ( doStopStream == 2 ) { + abortStream(); + return; + } + + // Perform I/O outside the mutex so closeStream() can set state and call + // sio_stop() to unblock a thread waiting in sio_read/sio_write. + if ( stream_.state != STREAM_RUNNING ) { + RtApi::tickStreamTime(); + return; + } + + { + char *buffer; + int samples; + RtAudioFormat format; + size_t nbytes, written; + + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + if ( stream_.doConvertBuffer[0] ) { + buffer = stream_.deviceBuffer; + convertBuffer( buffer, stream_.userBuffer[0], stream_.convertInfo[0] ); + samples = stream_.bufferSize * stream_.nDeviceChannels[0]; + format = stream_.deviceFormat[0]; + } else { + buffer = stream_.userBuffer[0]; + samples = stream_.bufferSize * stream_.nUserChannels[0]; + format = stream_.userFormat; + } + if ( stream_.doByteSwap[0] ) + byteSwapBuffer( buffer, samples, format ); + + nbytes = samples * formatBytes( format ); + + // Use poll with a timeout so sio_write() cannot block indefinitely. + // This is critical for clean shutdown: stopStream() calls sio_stop() + // which stops the device clock, so POLLOUT may never fire again after + // that point. Two buffer periods is plenty under normal operation. + { + int timeoutMs = (int)( 2000.0 * stream_.bufferSize / stream_.sampleRate ) + 10; + struct pollfd pfd[8]; + { static int _cb_n=0; if(++_cb_n<=5||_cb_n%100==0) { fprintf(stderr,"[sndio-dbg] callbackEvent #%d pre-poll\n",_cb_n); fflush(stderr); } } + int nfds = sio_pollfd( handle->handle, pfd, POLLOUT ); + int ready = ( nfds > 0 ) ? poll( pfd, nfds, timeoutMs ) : 0; + written = 0; + { static int _pb=0; if(++_pb<=10||_pb%50==0) { int rev = (ready>0)?sio_revents(handle->handle,pfd):0; fprintf(stderr,"[sndio-dbg] poll#%d nfds=%d ready=%d rev=0x%x POLLOUT=%d\n",_pb,nfds,ready,rev,!!(rev&POLLOUT)); fflush(stderr); } } + if ( ready > 0 && ( sio_revents( handle->handle, pfd ) & POLLOUT ) ) + written = sio_write( handle->handle, buffer, nbytes ); + } + if ( written != nbytes ) { + handle->xrun[0] = true; + errorText_ = "RtApiSndio::callbackEvent: audio write error."; + error( RTAUDIO_WARNING ); + } + } + + if ( stream_.mode == INPUT || stream_.mode == DUPLEX ) { + if ( stream_.doConvertBuffer[1] ) { + buffer = stream_.deviceBuffer; + samples = stream_.bufferSize * stream_.nDeviceChannels[1]; + format = stream_.deviceFormat[1]; + } else { + buffer = stream_.userBuffer[1]; + samples = stream_.bufferSize * stream_.nUserChannels[1]; + format = stream_.userFormat; + } + + nbytes = samples * formatBytes( format ); + + // Use poll with a timeout so we don't block forever on play-only devices. + // Two buffer periods (in ms) is enough for normal operation. + int timeoutMs = (int)( 2000.0 * stream_.bufferSize / stream_.sampleRate ) + 10; + struct pollfd pfd[8]; + int nfds = sio_pollfd( handle->handle, pfd, POLLIN ); + int ready = ( nfds > 0 ) ? poll( pfd, nfds, timeoutMs ) : 0; + + size_t got = 0; + if ( ready > 0 && ( sio_revents( handle->handle, pfd ) & POLLIN ) ) + got = sio_read( handle->handle, buffer, nbytes ); + + if ( got != nbytes ) { + if ( got == 0 ) + memset( buffer, 0, nbytes ); // play-only device: substitute silence + else { + handle->xrun[1] = true; + errorText_ = "RtApiSndio::callbackEvent: audio read error."; + error( RTAUDIO_WARNING ); + } + } + if ( got > 0 ) { + if ( stream_.doByteSwap[1] ) + byteSwapBuffer( buffer, samples, format ); + if ( stream_.doConvertBuffer[1] ) + convertBuffer( stream_.userBuffer[1], stream_.deviceBuffer, stream_.convertInfo[1] ); + } + } + } + + RtApi::tickStreamTime(); + if ( doStopStream == 1 ) stopStream(); +} + +static void *sndioCallbackHandler( void *ptr ) +{ + CallbackInfo *info = (CallbackInfo *) ptr; + RtApiSndio *object = (RtApiSndio *) info->object; + bool *isRunning = &info->isRunning; + + while ( *isRunning == true ) { + pthread_testcancel(); + object->callbackEvent(); + } + + pthread_exit( nullptr ); +} + +//******************** End of __OPENBSD_SNDIO__ *********************// +#endif // *************************************************** // //