# HG changeset patch # User Denis Lila # Date 1308583404 14400 # Node ID b746d080787eadd7f3fad57b5a20567da00c7f56 # Parent 26295314f6d60fc8e3676097cd8bbcded34e62b9 PR734: Fix pulse-java latency problem. 2011-06-20 Denis Lila * pulseaudio/src/java/org/classpath/icedtea/pulseaudio/PulseAudioDataLine.java (bufferSize): Remove. (getBufferSize): Return stream.getBufferSize(). * pulseaudio/src/java/org/classpath/icedtea/pulseaudio/PulseAudioSourceDataLine.java (connectLine): Improve formatting. * pulseaudio/src/java/org/classpath/icedtea/pulseaudio/PulseAudioTargetDataLine.java (connectLine): Set up flags to adjust the latency, if needed. * pulseaudio/src/java/org/classpath/icedtea/pulseaudio/Stream.java (bufAttr, bufAttrMutex): New members. (setBufAttr, bufferAttrCallback): New methods. They both set bufAttr. (getBufferSize): Return the current buffer size. (connectForRecording): Add a flags argument to allow callers to chose the flags. (stateCallback): When the stream is ready, set the buffer attributes to the actual ones used by the server. * pulseaudio/src/native/org_classpath_icedtea_pulseaudio_Stream.c (buf_attr_changed_callback): New function. (Java_org_classpath_icedtea_pulseaudio_Stream_native_1pa_1stream_1new): Set the buffer attribute callback. diff -r 26295314f6d6 -r b746d080787e ChangeLog --- a/ChangeLog Thu Jun 16 11:11:35 2011 -0400 +++ b/ChangeLog Mon Jun 20 11:23:24 2011 -0400 @@ -1,3 +1,25 @@ +2011-06-20 Denis Lila + + * pulseaudio/src/java/org/classpath/icedtea/pulseaudio/PulseAudioDataLine.java + (bufferSize): Remove. + (getBufferSize): Return stream.getBufferSize(). + * pulseaudio/src/java/org/classpath/icedtea/pulseaudio/PulseAudioSourceDataLine.java + (connectLine): Improve formatting. + * pulseaudio/src/java/org/classpath/icedtea/pulseaudio/PulseAudioTargetDataLine.java + (connectLine): Set up flags to adjust the latency, if needed. + * pulseaudio/src/java/org/classpath/icedtea/pulseaudio/Stream.java + (bufAttr, bufAttrMutex): New members. + (setBufAttr, bufferAttrCallback): New methods. They both set bufAttr. + (getBufferSize): Return the current buffer size. + (connectForRecording): Add a flags argument to allow callers to chose the + flags. + (stateCallback): When the stream is ready, set the buffer attributes to + the actual ones used by the server. + * pulseaudio/src/native/org_classpath_icedtea_pulseaudio_Stream.c + (buf_attr_changed_callback): New function. + (Java_org_classpath_icedtea_pulseaudio_Stream_native_1pa_1stream_1new): + Set the buffer attribute callback. + 2011-06-16 Denis Lila * pulseaudio/src/java/org/classpath/icedtea/pulseaudio/PulseAudioDataLine.java diff -r 26295314f6d6 -r b746d080787e pulseaudio/src/java/org/classpath/icedtea/pulseaudio/PulseAudioDataLine.java --- a/pulseaudio/src/java/org/classpath/icedtea/pulseaudio/PulseAudioDataLine.java Thu Jun 16 11:11:35 2011 -0400 +++ b/pulseaudio/src/java/org/classpath/icedtea/pulseaudio/PulseAudioDataLine.java Mon Jun 20 11:23:24 2011 -0400 @@ -74,7 +74,6 @@ protected AudioFormat defaultFormat = null; protected boolean sendEvents = true; - protected int bufferSize = 0; // the total number of frames played since this line was opened protected long framesSinceOpen = 0; @@ -265,7 +264,6 @@ throw e; } - this.bufferSize = bufferSize; try { semaphore.acquire(); synchronized (eventLoop.threadLock) { @@ -442,7 +440,7 @@ if (!isOpen()) { return DEFAULT_BUFFER_SIZE; } - return bufferSize; + return stream.getBufferSize(); } @Override diff -r 26295314f6d6 -r b746d080787e pulseaudio/src/java/org/classpath/icedtea/pulseaudio/PulseAudioSourceDataLine.java --- a/pulseaudio/src/java/org/classpath/icedtea/pulseaudio/PulseAudioSourceDataLine.java Thu Jun 16 11:11:35 2011 -0400 +++ b/pulseaudio/src/java/org/classpath/icedtea/pulseaudio/PulseAudioSourceDataLine.java Mon Jun 20 11:23:24 2011 -0400 @@ -116,10 +116,13 @@ @Override protected void connectLine(int bufferSize, Stream masterStream) throws LineUnavailableException { - StreamBufferAttributes bufferAttributes = new StreamBufferAttributes( - - bufferSize, bufferSize / 4, bufferSize / 8, - ((bufferSize / 10) > 100 ? bufferSize / 10 : 100), 0); + StreamBufferAttributes bufferAttributes = + new StreamBufferAttributes( + bufferSize, + bufferSize / 4, + bufferSize / 8, + Math.max(bufferSize / 10, 100), + 0); if (masterStream != null) { synchronized (eventLoop.threadLock) { diff -r 26295314f6d6 -r b746d080787e pulseaudio/src/java/org/classpath/icedtea/pulseaudio/PulseAudioTargetDataLine.java --- a/pulseaudio/src/java/org/classpath/icedtea/pulseaudio/PulseAudioTargetDataLine.java Thu Jun 16 11:11:35 2011 -0400 +++ b/pulseaudio/src/java/org/classpath/icedtea/pulseaudio/PulseAudioTargetDataLine.java Mon Jun 20 11:23:24 2011 -0400 @@ -133,11 +133,42 @@ @Override protected void connectLine(int bufferSize, Stream masterStream) throws LineUnavailableException { - int fragmentSize = bufferSize / 10 > 500 ? bufferSize / 10 : 500; - StreamBufferAttributes bufferAttributes = new StreamBufferAttributes( - bufferSize, 0, 0, 0, fragmentSize); + int fs = currentFormat.getFrameSize(); + float fr = currentFormat.getFrameRate(); + int bps = (int)(fs*fr); // bytes per second. + + // if 2 seconds' worth of data can fit in the buffer of the specified + // size, we don't have to adjust the latency. Otherwise we do, so as + // to avoid overruns. + long flags = Stream.FLAG_START_CORKED; + StreamBufferAttributes bufferAttributes; + if (bps*2 < bufferSize) { + // pulse audio completely ignores our fragmentSize attribute unless + // ADJUST_LATENCY is set, so we just leave it at -1. + bufferAttributes = new StreamBufferAttributes(bufferSize, -1, -1, -1, -1); + } else { + flags |= Stream.FLAG_ADJUST_LATENCY; + // in this case, the pulse audio docs: + // http://www.pulseaudio.org/wiki/LatencyControl + // say every field (including bufferSize) must be initialized + // to -1 except fragmentSize. + // XXX: but in my tests, it just sets it to about 4MB, which + // effectively makes it impossible to allocate a small buffer + // and nothing bad happens (yet) when you don't set it to -1 + // so we just leave it at bufferSize. + // XXX: the java api has no way to specify latency, which probably + // means it should be as low as possible. Right now this method's + // primary concern is avoiding dropouts, and if the user-provided + // buffer size is large enough, we leave the latency up to pulse + // audio (which sets it to something extremely high - about 2 + // seconds). We might want to always set a low latency. + int fragmentSize = bufferSize/2; + fragmentSize = Math.max((fragmentSize/fs)*fs, fs); + bufferAttributes = new StreamBufferAttributes(bufferSize, -1, -1, -1, fragmentSize); + } + synchronized (eventLoop.threadLock) { - stream.connectForRecording(Stream.DEFAULT_DEVICE, bufferAttributes); + stream.connectForRecording(Stream.DEFAULT_DEVICE, flags, bufferAttributes); } } diff -r 26295314f6d6 -r b746d080787e pulseaudio/src/java/org/classpath/icedtea/pulseaudio/Stream.java --- a/pulseaudio/src/java/org/classpath/icedtea/pulseaudio/Stream.java Thu Jun 16 11:11:35 2011 -0400 +++ b/pulseaudio/src/java/org/classpath/icedtea/pulseaudio/Stream.java Mon Jun 20 11:23:24 2011 -0400 @@ -173,6 +173,9 @@ private Format format; private float cachedVolume; + private StreamBufferAttributes bufAttr = new StreamBufferAttributes(0,0,0,0,0); + private static final Object bufAttrMutex = new Object(); + private List stateListeners; private List writeListeners; private List readListeners; @@ -462,6 +465,23 @@ return native_pa_stream_get_device_index(); } + private void setBufAttr() { + synchronized(bufAttrMutex) { + bufAttr = native_pa_stream_get_buffer_attr(); + } + } + + @SuppressWarnings("unused") + private void bufferAttrCallback() { + setBufAttr(); + } + + int getBufferSize() { + synchronized (bufAttrMutex) { + return bufAttr.getMaxLength(); + } + } + /** * * @return the name of the sink or source this stream is connected to in the @@ -513,7 +533,7 @@ * @throws LineUnavailableException * */ - void connectForRecording(String deviceName, + void connectForRecording(String deviceName, long flags, StreamBufferAttributes bufferAttributes) throws LineUnavailableException { @@ -524,7 +544,7 @@ bufferAttributes.getPreBuffering(), bufferAttributes.getMinimumRequest(), bufferAttributes.getFragmentSize(), - FLAG_START_CORKED, null, null + flags, null, null ); if (returnValue < 0) { throw new LineUnavailableException( @@ -608,6 +628,11 @@ */ @SuppressWarnings("unused") private void stateCallback() { + synchronized(EventLoop.getEventLoop().threadLock) { + if (getState() == Stream.STATE_READY) { + setBufAttr(); + } + } synchronized (stateListeners) { for (StateListener listener : stateListeners) { listener.update(); diff -r 26295314f6d6 -r b746d080787e pulseaudio/src/native/org_classpath_icedtea_pulseaudio_Stream.c --- a/pulseaudio/src/native/org_classpath_icedtea_pulseaudio_Stream.c Thu Jun 16 11:11:35 2011 -0400 +++ b/pulseaudio/src/native/org_classpath_icedtea_pulseaudio_Stream.c Mon Jun 20 11:23:24 2011 -0400 @@ -169,7 +169,6 @@ } - // requires pulseaudio 0.9.11 :( static void stream_started_callback(pa_stream *stream, void *userdata) { // printf("stream_started_callback called\n"); @@ -240,6 +239,20 @@ } +static void buf_attr_changed_callback(pa_stream *stream, void *userdata) { + java_context* context = userdata; + assert(stream); + assert(context); + assert(context->env); + assert(context->obj); + + if (pa_stream_get_state(stream) == PA_STREAM_CREATING) { + callJavaVoidMethod(context->env, context->obj, "bufferAttrCallback"); + } else { + callJavaVoidMethod(pulse_thread_env, context->obj, "bufferAttrCallback"); + } +} + // used to set stream flags and states. #define SET_STREAM_ENUM(env, clz, java_prefix, state_name) \ SET_JAVA_STATIC_LONG_FIELD_TO_PA_ENUM(env, clz, java_prefix, STREAM, state_name) @@ -355,7 +368,7 @@ pa_stream_set_latency_update_callback (stream, stream_latency_update_callback, j_context); pa_stream_set_moved_callback (stream, stream_moved_callback, j_context); pa_stream_set_suspended_callback (stream, stream_suspended_callback, j_context); - + pa_stream_set_buffer_attr_callback(stream, buf_attr_changed_callback, j_context); } /*