static int instream_start_pa(SoundIoPrivate *si, SoundIoInStreamPrivate *is) { SoundIoInStream *instream = &is->pub; SoundIoInStreamPulseAudio *ispa = &is->backend_data.pulseaudio; SoundIoPulseAudio *sipa = &si->backend_data.pulseaudio; pa_threaded_mainloop_lock(sipa->main_loop); pa_stream_flags_t flags = PA_STREAM_AUTO_TIMING_UPDATE; if (instream->software_latency > 0.0) flags = (pa_stream_flags_t) (flags|PA_STREAM_ADJUST_LATENCY); int err = pa_stream_connect_record(ispa->stream, instream->device->id, &ispa->buffer_attr, flags); if (err) { pa_threaded_mainloop_unlock(sipa->main_loop); return SoundIoErrorOpeningDevice; } while (!ispa->stream_ready) pa_threaded_mainloop_wait(sipa->main_loop); pa_operation *update_timing_info_op = pa_stream_update_timing_info(ispa->stream, timing_update_callback, si); if ((err = perform_operation(si, update_timing_info_op))) { pa_threaded_mainloop_unlock(sipa->main_loop); return err; } pa_threaded_mainloop_unlock(sipa->main_loop); return 0; }
/* Someone requested that the latency is shown */ static void sigusr1_signal_callback(pa_mainloop_api*m, pa_signal_event *e, int sig, void *userdata) { if (!stream) return; pa_operation_unref(pa_stream_update_timing_info(stream, stream_update_timing_callback, NULL)); }
static int instream_start_pa(struct SoundIoPrivate *si, struct SoundIoInStreamPrivate *is) { struct SoundIoInStream *instream = &is->pub; struct SoundIoInStreamPulseAudio *ispa = &is->backend_data.pulseaudio; struct SoundIoPulseAudio *sipa = &si->backend_data.pulseaudio; pa_threaded_mainloop_lock(sipa->main_loop); pa_stream_flags_t flags = (pa_stream_flags_t)(PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_INTERPOLATE_TIMING); int err = pa_stream_connect_record(ispa->stream, instream->device->id, &ispa->buffer_attr, flags); if (err) { pa_threaded_mainloop_unlock(sipa->main_loop); return SoundIoErrorOpeningDevice; } while (!SOUNDIO_ATOMIC_LOAD(ispa->stream_ready)) pa_threaded_mainloop_wait(sipa->main_loop); pa_operation *update_timing_info_op = pa_stream_update_timing_info(ispa->stream, timing_update_callback, si); if ((err = perform_operation(si, update_timing_info_op))) { pa_threaded_mainloop_unlock(sipa->main_loop); return err; } pa_threaded_mainloop_unlock(sipa->main_loop); return 0; }
void CAESinkPULSE::GetDelay(AEDelayStatus& status) { if (!m_IsAllocated) { status.SetDelay(0); return; } int error = 0; pa_usec_t latency = (pa_usec_t) -1; pa_threaded_mainloop_lock(m_MainLoop); if ((error = pa_stream_get_latency(m_Stream, &latency, NULL)) < 0) { if (error == -PA_ERR_NODATA) { WaitForOperation(pa_stream_update_timing_info(m_Stream, NULL,NULL), m_MainLoop, "Update Timing Information"); if ((error = pa_stream_get_latency(m_Stream, &latency, NULL)) < 0) { CLog::Log(LOGDEBUG, "GetDelay - Failed to get Latency %d", error); } } } if (error < 0 ) latency = (pa_usec_t) 0; pa_threaded_mainloop_unlock(m_MainLoop); status.SetDelay(latency / 1000000.0); }
static void stream_latency_cb(pa_stream *p, void *userdata) { #ifndef INTERPOLATE pa_operation *o; o = pa_stream_update_timing_info(p, NULL, NULL); pa_operation_unref(o); #endif }
static void time_event_callback(pa_mainloop_api *m, pa_time_event *e, const struct timeval *t, void *userdata) { if (stream && pa_stream_get_state(stream) == PA_STREAM_READY) { pa_operation *o; if (!(o = pa_stream_update_timing_info(stream, stream_update_timing_callback, NULL))) pa_log(_("pa_stream_update_timing_info() failed: %s"), pa_strerror(pa_context_errno(context))); else pa_operation_unref(o); } pa_context_rttime_restart(context, e, pa_rtclock_now() + TIME_EVENT_USEC); }
// Return the current latency in seconds static float get_delay(struct ao *ao) { /* This code basically does what pa_stream_get_latency() _should_ * do, but doesn't due to multiple known bugs in PulseAudio (at * PulseAudio version 2.1). In particular, the timing interpolation * mode (PA_STREAM_INTERPOLATE_TIMING) can return completely bogus * values, and the non-interpolating code has a bug causing too * large results at end of stream (so a stream never seems to finish). * This code can still return wrong values in some cases due to known * PulseAudio bugs that can not be worked around on the client side. * * We always query the server for latest timing info. This may take * too long to work well with remote audio servers, but at least * this should be enough to fix the normal local playback case. */ struct priv *priv = ao->priv; pa_threaded_mainloop_lock(priv->mainloop); if (!waitop(priv, pa_stream_update_timing_info(priv->stream, NULL, NULL))) { GENERIC_ERR_MSG(priv->context, "pa_stream_update_timing_info() failed"); return 0; } pa_threaded_mainloop_lock(priv->mainloop); const pa_timing_info *ti = pa_stream_get_timing_info(priv->stream); if (!ti) { pa_threaded_mainloop_unlock(priv->mainloop); GENERIC_ERR_MSG(priv->context, "pa_stream_get_timing_info() failed"); return 0; } const struct pa_sample_spec *ss = pa_stream_get_sample_spec(priv->stream); if (!ss) { pa_threaded_mainloop_unlock(priv->mainloop); GENERIC_ERR_MSG(priv->context, "pa_stream_get_sample_spec() failed"); return 0; } // data left in PulseAudio's main buffers (not written to sink yet) int64_t latency = pa_bytes_to_usec(ti->write_index - ti->read_index, ss); // since this info may be from a while ago, playback has progressed since latency -= ti->transport_usec; // data already moved from buffers to sink, but not played yet int64_t sink_latency = ti->sink_usec; if (!ti->playing) /* At the end of a stream, part of the data "left" in the sink may * be padding silence after the end; that should be subtracted to * get the amount of real audio from our stream. This adjustment * is missing from Pulseaudio's own get_latency calculations * (as of PulseAudio 2.1). */ sink_latency -= pa_bytes_to_usec(ti->since_underrun, ss); if (sink_latency > 0) latency += sink_latency; if (latency < 0) latency = 0; pa_threaded_mainloop_unlock(priv->mainloop); return latency / 1e6; }
void QPulseAudioThread::time_event_callback ( pa_mainloop_api*m, pa_time_event *e, const struct timeval *tv, void *userdata ) { struct timeval next; if ( stream && pa_stream_get_state ( stream ) == PA_STREAM_READY ) { pa_operation *o; if ( ! ( o = pa_stream_update_timing_info ( stream, stream_update_timing_callback, NULL ) ) ) fprintf ( stderr, "pa_stream_update_timing_info() failed: %s\n", pa_strerror ( pa_context_errno ( context ) ) ); else pa_operation_unref ( o ); } pa_gettimeofday ( &next ); pa_timeval_add ( &next, TIME_EVENT_USEC ); m->time_restart ( e, &next ); }
static int init(struct ao *ao, char *params) { struct pa_sample_spec ss; struct pa_channel_map map; char *devarg = NULL; char *host = NULL; char *sink = NULL; const char *version = pa_get_library_version(); struct priv *priv = talloc_zero(ao, struct priv); ao->priv = priv; if (params) { devarg = strdup(params); sink = strchr(devarg, ':'); if (sink) *sink++ = 0; if (devarg[0]) host = devarg; } priv->broken_pause = false; /* not sure which versions are affected, assume 0.9.11* to 0.9.14* * known bad: 0.9.14, 0.9.13 * known good: 0.9.9, 0.9.10, 0.9.15 * To test: pause, wait ca. 5 seconds, framestep and see if MPlayer * hangs somewhen. */ if (strncmp(version, "0.9.1", 5) == 0 && version[5] >= '1' && version[5] <= '4') { mp_msg(MSGT_AO, MSGL_WARN, "[pulse] working around probably broken pause functionality,\n" " see http://www.pulseaudio.org/ticket/440\n"); priv->broken_pause = true; } ss.channels = ao->channels; ss.rate = ao->samplerate; const struct format_map *fmt_map = format_maps; while (fmt_map->mp_format != ao->format) { if (fmt_map->mp_format == AF_FORMAT_UNKNOWN) { mp_msg(MSGT_AO, MSGL_V, "AO: [pulse] Unsupported format, using default\n"); fmt_map = format_maps; break; } fmt_map++; } ao->format = fmt_map->mp_format; ss.format = fmt_map->pa_format; if (!pa_sample_spec_valid(&ss)) { mp_msg(MSGT_AO, MSGL_ERR, "AO: [pulse] Invalid sample spec\n"); goto fail; } pa_channel_map_init_auto(&map, ss.channels, PA_CHANNEL_MAP_ALSA); ao->bps = pa_bytes_per_second(&ss); if (!(priv->mainloop = pa_threaded_mainloop_new())) { mp_msg(MSGT_AO, MSGL_ERR, "AO: [pulse] Failed to allocate main loop\n"); goto fail; } if (!(priv->context = pa_context_new(pa_threaded_mainloop_get_api( priv->mainloop), PULSE_CLIENT_NAME))) { mp_msg(MSGT_AO, MSGL_ERR, "AO: [pulse] Failed to allocate context\n"); goto fail; } pa_context_set_state_callback(priv->context, context_state_cb, ao); if (pa_context_connect(priv->context, host, 0, NULL) < 0) goto fail; pa_threaded_mainloop_lock(priv->mainloop); if (pa_threaded_mainloop_start(priv->mainloop) < 0) goto unlock_and_fail; /* Wait until the context is ready */ pa_threaded_mainloop_wait(priv->mainloop); if (pa_context_get_state(priv->context) != PA_CONTEXT_READY) goto unlock_and_fail; if (!(priv->stream = pa_stream_new(priv->context, "audio stream", &ss, &map))) goto unlock_and_fail; pa_stream_set_state_callback(priv->stream, stream_state_cb, ao); pa_stream_set_write_callback(priv->stream, stream_request_cb, ao); pa_stream_set_latency_update_callback(priv->stream, stream_latency_update_cb, ao); pa_buffer_attr bufattr = { .maxlength = -1, .tlength = pa_usec_to_bytes(1000000, &ss), .prebuf = -1, .minreq = -1, .fragsize = -1, }; if (pa_stream_connect_playback(priv->stream, sink, &bufattr, PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_AUTO_TIMING_UPDATE, NULL, NULL) < 0) goto unlock_and_fail; /* Wait until the stream is ready */ pa_threaded_mainloop_wait(priv->mainloop); if (pa_stream_get_state(priv->stream) != PA_STREAM_READY) goto unlock_and_fail; pa_threaded_mainloop_unlock(priv->mainloop); free(devarg); return 0; unlock_and_fail: if (priv->mainloop) pa_threaded_mainloop_unlock(priv->mainloop); fail: if (priv->context) GENERIC_ERR_MSG(priv->context, "Init failed"); free(devarg); uninit(ao, true); return -1; } static void cork(struct ao *ao, bool pause) { struct priv *priv = ao->priv; pa_threaded_mainloop_lock(priv->mainloop); priv->retval = 0; if (!waitop(priv, pa_stream_cork(priv->stream, pause, success_cb, ao)) || !priv->retval) GENERIC_ERR_MSG(priv->context, "pa_stream_cork() failed"); } // Play the specified data to the pulseaudio server static int play(struct ao *ao, void *data, int len, int flags) { struct priv *priv = ao->priv; /* For some reason Pulseaudio behaves worse if this is done after * the write - rapidly repeated seeks result in bogus increasing * reported latency. */ if (priv->did_reset) cork(ao, false); pa_threaded_mainloop_lock(priv->mainloop); if (pa_stream_write(priv->stream, data, len, NULL, 0, PA_SEEK_RELATIVE) < 0) { GENERIC_ERR_MSG(priv->context, "pa_stream_write() failed"); len = -1; } if (priv->did_reset) { priv->did_reset = false; if (!waitop(priv, pa_stream_update_timing_info(priv->stream, success_cb, ao)) || !priv->retval) GENERIC_ERR_MSG(priv->context, "pa_stream_UPP() failed"); } else pa_threaded_mainloop_unlock(priv->mainloop); return len; } // Reset the audio stream, i.e. flush the playback buffer on the server side static void reset(struct ao *ao) { // pa_stream_flush() works badly if not corked cork(ao, true); struct priv *priv = ao->priv; pa_threaded_mainloop_lock(priv->mainloop); priv->retval = 0; if (!waitop(priv, pa_stream_flush(priv->stream, success_cb, ao)) || !priv->retval) GENERIC_ERR_MSG(priv->context, "pa_stream_flush() failed"); priv->did_reset = true; }
static void audio_callback(void* data) { sa_stream_t* s = (sa_stream_t*)data; unsigned int bytes_per_frame = s->sample_spec.channels * pa_sample_size(&s->sample_spec); size_t buffer_size = s->sample_spec.rate * bytes_per_frame; char* buffer = malloc(buffer_size); while(1) { char* dst = buffer; size_t bytes_to_copy, bytes; pa_threaded_mainloop_lock(s->m); while(1) { if (s == NULL || s->stream == NULL) { if (s != NULL && s->m != NULL) pa_threaded_mainloop_unlock(s->m); goto free_buffer; } if ((bytes_to_copy = pa_stream_writable_size(s->stream)) == (size_t) -1) { fprintf(stderr, "pa_stream_writable_size() failed: %s", pa_strerror(pa_context_errno(s->context))); pa_threaded_mainloop_unlock(s->m); goto free_buffer; } if(bytes_to_copy > 0) break; pa_threaded_mainloop_wait(s->m); } pa_threaded_mainloop_unlock(s->m); if (bytes_to_copy > buffer_size) bytes_to_copy = buffer_size; bytes = bytes_to_copy; pthread_mutex_lock(&s->mutex); if (!s->thread_id) { pthread_mutex_unlock(&s->mutex); break; } /* * Consume data from the start of the buffer list. */ while (1) { unsigned int avail = s->bl_head->end - s->bl_head->start; assert(s->bl_head->start <= s->bl_head->end); if (avail >= bytes_to_copy) { /* * We have all we need in the head buffer, so just grab it and go. */ memcpy(dst, s->bl_head->data + s->bl_head->start, bytes_to_copy); s->bl_head->start += bytes_to_copy; break; } else { sa_buf* next = 0; /* * Copy what we can from the head and move on to the next buffer. */ memcpy(dst, s->bl_head->data + s->bl_head->start, avail); s->bl_head->start += avail; dst += avail; bytes_to_copy -= avail; /* * We want to free the now-empty buffer, but not if it's also the * current tail. If it is the tail, we don't have enough data to fill * the destination buffer, so we write less and give up. */ next = s->bl_head->next; if (next == NULL) { bytes = bytes-bytes_to_copy; break; } free(s->bl_head); s->bl_head = next; s->n_bufs--; } /* if (avail >= bytes_to_copy), else */ } /* while (1) */ if(bytes > 0) { pa_threaded_mainloop_lock(s->m); if (pa_stream_write(s->stream, buffer, bytes, NULL, 0, PA_SEEK_RELATIVE) < 0) { fprintf(stderr, "pa_stream_write() failed: %s", pa_strerror(pa_context_errno(s->context))); pa_threaded_mainloop_unlock(s->m); return; } pa_stream_update_timing_info(s->stream, NULL, NULL); s->bytes_written += bytes; pa_threaded_mainloop_unlock(s->m); } pthread_mutex_unlock(&s->mutex); } free_buffer: free(buffer); }
static int outstream_open_pa(struct SoundIoPrivate *si, struct SoundIoOutStreamPrivate *os) { struct SoundIoOutStreamPulseAudio *ospa = &os->backend_data.pulseaudio; struct SoundIoOutStream *outstream = &os->pub; if ((unsigned)outstream->layout.channel_count > PA_CHANNELS_MAX) return SoundIoErrorIncompatibleBackend; if (!outstream->name) outstream->name = "SoundIoOutStream"; struct SoundIoPulseAudio *sipa = &si->backend_data.pulseaudio; SOUNDIO_ATOMIC_STORE(ospa->stream_ready, false); SOUNDIO_ATOMIC_FLAG_TEST_AND_SET(ospa->clear_buffer_flag); assert(sipa->pulse_context); pa_threaded_mainloop_lock(sipa->main_loop); pa_sample_spec sample_spec; sample_spec.format = to_pulseaudio_format(outstream->format); sample_spec.rate = outstream->sample_rate; sample_spec.channels = outstream->layout.channel_count; pa_channel_map channel_map = to_pulseaudio_channel_map(&outstream->layout); ospa->stream = pa_stream_new(sipa->pulse_context, outstream->name, &sample_spec, &channel_map); if (!ospa->stream) { pa_threaded_mainloop_unlock(sipa->main_loop); outstream_destroy_pa(si, os); return SoundIoErrorNoMem; } pa_stream_set_state_callback(ospa->stream, playback_stream_state_callback, os); ospa->buffer_attr.maxlength = UINT32_MAX; ospa->buffer_attr.tlength = UINT32_MAX; ospa->buffer_attr.prebuf = 0; ospa->buffer_attr.minreq = UINT32_MAX; ospa->buffer_attr.fragsize = UINT32_MAX; int bytes_per_second = outstream->bytes_per_frame * outstream->sample_rate; if (outstream->software_latency > 0.0) { int buffer_length = outstream->bytes_per_frame * ceil_dbl_to_int(outstream->software_latency * bytes_per_second / (double)outstream->bytes_per_frame); ospa->buffer_attr.maxlength = buffer_length; ospa->buffer_attr.tlength = buffer_length; } pa_stream_flags_t flags = (pa_stream_flags_t)(PA_STREAM_START_CORKED | PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_INTERPOLATE_TIMING); int err = pa_stream_connect_playback(ospa->stream, outstream->device->id, &ospa->buffer_attr, flags, NULL, NULL); if (err) { pa_threaded_mainloop_unlock(sipa->main_loop); return SoundIoErrorOpeningDevice; } while (!SOUNDIO_ATOMIC_LOAD(ospa->stream_ready)) pa_threaded_mainloop_wait(sipa->main_loop); pa_operation *update_timing_info_op = pa_stream_update_timing_info(ospa->stream, timing_update_callback, si); if ((err = perform_operation(si, update_timing_info_op))) { pa_threaded_mainloop_unlock(sipa->main_loop); return err; } size_t writable_size = pa_stream_writable_size(ospa->stream); outstream->software_latency = ((double)writable_size) / (double)bytes_per_second; pa_threaded_mainloop_unlock(sipa->main_loop); return 0; }
int cubeb_stream_init(cubeb * context, cubeb_stream ** stream, char const * stream_name, cubeb_stream_params stream_params, unsigned int latency, cubeb_data_callback data_callback, cubeb_state_callback state_callback, void * user_ptr) { pa_sample_spec ss; cubeb_stream * stm; pa_operation * o; pa_buffer_attr battr; pa_channel_map map; if (stream_params.rate < 1 || stream_params.rate > 192000 || stream_params.channels < 1 || stream_params.channels > 32 || latency < 1 || latency > 2000) { return CUBEB_ERROR_INVALID_FORMAT; } switch (stream_params.format) { case CUBEB_SAMPLE_S16LE: ss.format = PA_SAMPLE_S16LE; break; case CUBEB_SAMPLE_S16BE: ss.format = PA_SAMPLE_S16BE; break; case CUBEB_SAMPLE_FLOAT32LE: ss.format = PA_SAMPLE_FLOAT32LE; break; case CUBEB_SAMPLE_FLOAT32BE: ss.format = PA_SAMPLE_FLOAT32BE; break; default: return CUBEB_ERROR_INVALID_FORMAT; } ss.rate = stream_params.rate; ss.channels = stream_params.channels; /* XXX check that this does the right thing for Vorbis and WaveEx */ pa_channel_map_init_auto(&map, ss.channels, PA_CHANNEL_MAP_DEFAULT); stm = calloc(1, sizeof(*stm)); assert(stm); stm->context = context; assert(stm->context); stm->data_callback = data_callback; stm->state_callback = state_callback; stm->user_ptr = user_ptr; stm->sample_spec = ss; battr.maxlength = -1; battr.tlength = pa_usec_to_bytes(latency * 1000, &stm->sample_spec); battr.prebuf = -1; battr.minreq = battr.tlength / 2; battr.fragsize = -1; pa_threaded_mainloop_lock(stm->context->mainloop); stm->stream = pa_stream_new(stm->context->context, stream_name, &ss, &map); pa_stream_set_state_callback(stm->stream, stream_state_callback, stm->context->mainloop); pa_stream_set_write_callback(stm->stream, stream_request_callback, stm); pa_stream_connect_playback(stm->stream, NULL, &battr, PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_START_CORKED, NULL, NULL); pa_threaded_mainloop_unlock(stm->context->mainloop); stream_state_wait(stm, PA_STREAM_READY); /* force a timing update now, otherwise timing info does not become valid until some point after initialization has completed. */ pa_threaded_mainloop_lock(stm->context->mainloop); o = pa_stream_update_timing_info(stm->stream, stream_success_callback, stm->context->mainloop); operation_wait(stm->context, o); pa_operation_unref(o); pa_threaded_mainloop_unlock(stm->context->mainloop); *stream = stm; return CUBEB_OK; }
/*! * \Write outgoing samples directly to pulseaudio server. * \param playdev Input. Device to which to play the samples. * \param nSamples Input. Number of samples to play. * \param cSamples Input. Sample buffer to play from. * \param report_latency Input. 1 to update \c quisk_sound_state.latencyPlay, 0 otherwise. * \param volume Input. Ratio in [0,1] by which to scale the played samples. */ void quisk_play_pulseaudio(struct sound_dev *dev, int nSamples, complex double *cSamples, int report_latency, double volume) { pa_stream *s = dev->handle; int i=0, n=0; void *fbuffer; int fbuffer_bytes = 0; if( !dev || nSamples <= 0) return; if (dev->cork_status) return; if (report_latency) { // Report the latency, if requested. pa_operation *o; pa_threaded_mainloop_lock(pa_ml); if (!(o = pa_stream_update_timing_info(s, stream_timing_callback, dev))) { printf("pa_stream_update_timing(): %s\n", pa_strerror(pa_context_errno(pa_stream_get_context(s)))); } else { while (pa_operation_get_state(o) == PA_OPERATION_RUNNING) pa_threaded_mainloop_wait(pa_ml); pa_operation_unref(o); } pa_threaded_mainloop_unlock(pa_ml); } fbuffer = pa_xmalloc(nSamples * dev->num_channels * dev->sample_bytes); // Convert from complex data to framebuffer if (dev->sample_bytes == 4) { float fi=0.f, fq=0.f; for(i = 0, n = 0; n < nSamples; i += (dev->num_channels * 4), ++n) { fi = (volume * creal(cSamples[n])) / CLIP32; fq = (volume * cimag(cSamples[n])) / CLIP32; memcpy(fbuffer + i + (dev->channel_I * 4), &fi, 4); memcpy(fbuffer + i + (dev->channel_Q * 4), &fq, 4); } } else if (dev->sample_bytes == 2) { int ii, qq; for(i = 0, n = 0; n < nSamples; i += (dev->num_channels * 2), ++n) { ii = (int)(volume * creal(cSamples[n]) / 65536); qq = (int)(volume * cimag(cSamples[n]) / 65536); memcpy(fbuffer + i + (dev->channel_I * 2), &ii, 2); memcpy(fbuffer + i + (dev->channel_Q * 2), &qq, 2); } } else { printf("Unknown sample size for %s", dev->name); exit(1); } fbuffer_bytes = nSamples * dev->num_channels * dev->sample_bytes; pa_threaded_mainloop_lock(pa_ml); size_t writable = pa_stream_writable_size(s); if (writable > 0) { if ( writable > 1024*1000 ) //sanity check to prevent pa_xmalloc from crashing on monitor streams writable = 1024*1000; if (fbuffer_bytes > writable) { if (quisk_sound_state.verbose_pulse) printf("Truncating write by %u bytes\n", fbuffer_bytes - (int)writable); fbuffer_bytes = writable; } pa_stream_write(dev->handle, fbuffer, (size_t)fbuffer_bytes, NULL, 0, PA_SEEK_RELATIVE); //printf("wrote %d to %s\n", writable, dev->name); } else { if (quisk_sound_state.verbose_pulse) printf("Can't write to stream %s. Dropping %d bytes\n", dev->name, fbuffer_bytes); } pa_threaded_mainloop_unlock(pa_ml); pa_xfree(fbuffer); fbuffer=NULL; }