vincentjames501 / libvorbis-libogg-android

JNI Android wrapper for encoding and decoding ogg/vorbis bitstreams
67 stars 39 forks source link

pause option #7

Open QuickBrownFoxy opened 10 years ago

QuickBrownFoxy commented 10 years ago

I am trying to add pause function but somehow it doesn't I have altered writeVorbisData function in my class and check with isRecording(). If it state set to anythign than RECORDING I bypass

outputStream.write(vorbisData, 0, amountToWrite);

Altugh it seems encoding paused, final file has wrong length and even audacity can;t open it.

any ideas?

@Override public int writeVorbisData(byte[] vorbisData, int amountToWrite) { // If we have data to write and we are recording, write the data if (vorbisData != null && amountToWrite > 0 && outputStream != null) { if (isRecording()) {

                // AppHelper.Log(tag, "Current stata: " +currentState.get().name() + " RECORDING, writing data");
                try {
                    // Write the data to the output stream
                    outputStream.write(vorbisData, 0, amountToWrite);
                    payloadSize += amountToWrite;
                    return amountToWrite;
                } catch (IOException e) {
                    // Failed to write to the file
                    AppHelper.Log(tag, "Failed to write data to file, stopping recording");
                    stop();
                }
            } else {
                AppHelper.Log(tag, "Paused, not writing data");
            }
        }
        // Otherwise let the native encoder know we are done
        return 0;
    }
vincentjames501 commented 10 years ago

How are you stopping the recording process? Anytime the writeVorbisData method is called you should probably put the data into a separate buffer so that converted data isn't lost. Make sure you aren't feeding any data into the encoder as well that you don't actually want encoded.

QuickBrownFoxy commented 10 years ago

here is the modified version of your sample VorbisEncoder that implements pause, a seperate service uses this class to start pause and stop recording. See writeVorbisData To stop I just call stop(). I guess thats were the problem is? I can see no data fed to encoder on pause

/**
 * The VorbisRecorder is responsible for receiving raw pcm data from the
 * {@link AudioRecord} and feeding that data to the naitive
 * {@link VorbisEncoder}
 * <p/>
 * This class is primarily intended as a demonstration of how to work with the
 * JNI java interface {@link VorbisEncoder}
 * <p/>
 * User: vincent Date: 3/28/13 Time: 12:47 PM
 */
public class VorbisAudioRecorder implements BasicRecorder {
    private int mGain = 0;
    private int cAmplitude = 0;
    private long payloadSize;
    /**
     * Logging tag
     */
    private String tag = this.getClass().getName();

    /**
     * The encode feed to feed raw pcm and write vorbis data
     */
    private final EncodeFeed encodeFeed;

    /**
     * The audio recorder to pull raw pcm data from
     */
    private AudioRecord audioRecorder;

    /**
     * The current state of the recorder
     */
    private final AtomicReference<RecordingState> currentState = new AtomicReference<RecordingState>(RecordingState.STOPPED);

    /**
     * Helper class that implements {@link EncodeFeed} that will write the
     * processed vorbis data to a file and will read raw PCM data from an
     * {@link AudioRecord}
     */
    private class FileEncodeFeed implements EncodeFeed {
        /**
         * The file to write to
         */
        private final File fileToSaveTo;

        /**
         * The output stream to write the vorbis data to
         */
        private OutputStream outputStream;

        /**
         * Constructs a file encode feed to write the encoded vorbis output to
         * 
         * @param fileToSaveTo
         *            the file to save to
         */
        public FileEncodeFeed(File fileToSaveTo) {
            if (fileToSaveTo == null) {
                throw new IllegalArgumentException("File to save to must not be null");
            }
            this.fileToSaveTo = fileToSaveTo;
        }

        @Override
        public long readPCMData(byte[] pcmDataBuffer, int amountToRead) {
            // If we are no longer recording, return 0 to let the native encoder
            // know
            if (isStopped()) {
                return 0;
            }

            // Otherwise read from the audio recorder
            int read = audioRecorder.read(pcmDataBuffer, 0, amountToRead);
            switch (read) {
            case AudioRecord.ERROR_INVALID_OPERATION:
                AppHelper.Log(tag, "Invalid operation on AudioRecord object");
                return 0;
            case AudioRecord.ERROR_BAD_VALUE:
                AppHelper.Log(tag, "Invalid value returned from audio recorder");
                return 0;
            case -1:
                return 0;
            default:
                for (int i = 0; i < pcmDataBuffer.length / 2; i++) {
                    short curSample = AppHelper.getShortFromByte(pcmDataBuffer[i * 2], pcmDataBuffer[i * 2 + 1]);
                    if (mGain != 0) {
                        curSample *= Math.pow(10.0d, mGain / 20.0d);
                        if (curSample > 32767.0d) {
                            curSample = (short) (int) 32767.0d;
                        }
                        if (curSample < -32768.0d) {
                            curSample = (short) (int) -32768.0d;
                        }
                        byte[] a = AppHelper.getByteFromShort(curSample);
                        pcmDataBuffer[i * 2] = a[0];
                        pcmDataBuffer[i * 2 + 1] = a[1];

                    }
                    if (curSample > cAmplitude) {
                        cAmplitude = curSample;
                    }
                }
                return read;
            }
        }

        @Override
        public int writeVorbisData(byte[] vorbisData, int amountToWrite) {
            // If we have data to write and we are recording, write the data
            if (vorbisData != null && amountToWrite > 0 && outputStream != null) {
                if (isRecording()) {

                    // AppHelper.Log(tag, "Current stata: "
                    // +currentState.get().name() + " RECORDING, writing data");
                    try {
                        // Write the data to the output stream
                        outputStream.write(vorbisData, 0, amountToWrite);
                        payloadSize += amountToWrite;
                        return amountToWrite;
                    } catch (IOException e) {
                        // Failed to write to the file
                        AppHelper.Log(tag, "Failed to write data to file, stopping recording");
                        stop();
                    }
                } else {
                    AppHelper.Log(tag, "Paused, not writing data");
                }
            }
            // Otherwise let the native encoder know we are done
            return 0;
        }

        @Override
        public void stop() {
            AppHelper.Log(tag, "Stop called");
            if (isRecording() || currentState.get() == RecordingState.PAUSED) {
                // Set our state to stopped
                currentState.set(RecordingState.STOPPED);

                AppHelper.Log(tag, "Stopped. Closing file");
                // Close the output stream
                if (outputStream != null) {
                    try {
                        outputStream.close();
                    } catch (IOException e) {
                        AppHelper.Log(tag, "Failed to close output stream");
                    }
                    outputStream = null;
                }

                // Stop and clean up the audio recorder
                AppHelper.Log(tag, "Stopped. Stopping AudioRecorder");
                if (audioRecorder != null) {
                    try {
                        audioRecorder.stop();
                        audioRecorder.release();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }

        @Override
        public void start() {
            if (isStopped()) {

                try {
                    // Start recording
                    currentState.set(RecordingState.RECORDING);
                    audioRecorder.startRecording();

                    // Create the output stream
                    if (outputStream == null) {
                        try {
                            outputStream = new BufferedOutputStream(new FileOutputStream(fileToSaveTo));
                        } catch (FileNotFoundException e) {
                            AppHelper.Log(tag, "Failed to write to file");
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }

            }
        }
    }

    /**
     * Constructs a recorder that will record an ogg file
     * 
     * @param fileToSaveTo
     *            the file to save to
     */
    public VorbisAudioRecorder(File fileToSaveTo) {
        AppHelper.Log(tag, "Creating  OGG recorder");
        if (fileToSaveTo == null) {
            throw new IllegalArgumentException("File to play must not be null.");
        }

        // Delete the file if it exists
        if (fileToSaveTo.exists()) {
            fileToSaveTo.deleteOnExit();
        }

        this.encodeFeed = new FileEncodeFeed(fileToSaveTo);
    }

    /**
     * Constructs a vorbis recorder with a custom {@link EncodeFeed}
     * 
     * @param encodeFeed
     *            the custom {@link EncodeFeed}
     */
    public VorbisAudioRecorder(EncodeFeed encodeFeed) {
        if (encodeFeed == null) {
            throw new IllegalArgumentException("Encode feed must not be null.");
        }

        this.encodeFeed = encodeFeed;
    }

    /**
     * Starts the recording/encoding process
     * 
     * @param sampleRate
     *            the rate to sample the audio at, should be greater than
     *            <code>0</code>
     * @param numberOfChannels
     *            the nubmer of channels, must only be
     *            <code>1/code> or <code>2</code>
     * @param quality
     *            the quality at which to encode, must be between
     *            <code>-0.1</code> and <code>1.0</code>
     */
    public synchronized void startRecording(int audioSource, int sampleRate, int channelConfig, int audioFormat, float quality) {
        if (isStopped()) {
            if (sampleRate <= 0) {
                throw new IllegalArgumentException("Invalid sample rate, must be above 0");
            }
            if (quality < -0.1f || quality > 1.0f) {
                throw new IllegalArgumentException("Quality must be between -0.1 and 1.0");
            }

            // Creates the audio recorder
            int channelConfiguration = channelConfig;
            int numberOfChannels = 1;
            if (channelConfiguration == AudioFormat.CHANNEL_IN_STEREO) {
                numberOfChannels = 2;
            }

            int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfiguration, audioFormat);

            try {
                // *2 just to increase buffer size, sometimes it gets clumsy
                // when this is not in place
                audioRecorder = new AudioRecord(audioSource, sampleRate, channelConfiguration, audioFormat, bufferSize * 2);
            } catch (Exception e) {
                // if fails revoke to default
                bufferSize = AudioRecord.getMinBufferSize(44100, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
                audioRecorder = new AudioRecord(audioSource, 44100, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize);
                e.printStackTrace();
                AppHelper.Log(tag, "Audio Recorder failed with minBufferSize*2");
                // save these settings
                App.getAppSettings().saveSettingString(AppSettings.SAMPLERATE, "44100");
                App.getAppSettings().saveSettingInt(AppSettings.MAX_AUDIO_CHANNEL, AudioFormat.CHANNEL_IN_MONO);
            }

            // Starts the recording process
            new Thread(new AsyncEncoding(sampleRate, numberOfChannels, quality)).start();
        }
    }

    /**
     * Stops the audio recorder and notifies the {@link EncodeFeed}
     */
    public synchronized void stop() {
        encodeFeed.stop();
    }

    private class AsyncEncoding implements Runnable {

        private final long sampleRate;
        private final long numberOfchannels;
        private final float quality;

        public AsyncEncoding(long sampleRate, long numberOfChannels, float quality) {
            this.sampleRate = sampleRate;
            this.numberOfchannels = numberOfChannels;
            this.quality = quality;
        }

        @Override
        public void run() {
            // Start the native encoder
            try {
                int result = VorbisEncoder.startEncoding(sampleRate, numberOfchannels, quality, encodeFeed);
                if (result == EncodeFeed.SUCCESS) {
                    AppHelper.Log(tag, "Encoder successfully finished");
                }
            } catch (EncodeException e) {
                switch (e.getErrorCode()) {
                case EncodeException.ERROR_INITIALIZING:
                    AppHelper.Log(tag, "There was an error initializing the native encoder");
                    break;
                }
            }
        }
    }

    /**
     * Checks whether the recording is currently recording
     * 
     * @return <code>true</code> if recording, <code>false</code> otherwise
     */
    @Override
    public synchronized boolean isRecording() {
        return currentState.get() == RecordingState.RECORDING;
    }

    /**
     * Checks whether the recording is currently stopped (not recording)
     * 
     * @return <code>true</code> if stopped, <code>false</code> otherwise
     */
    public synchronized boolean isStopped() {
        return currentState.get() == RecordingState.STOPPED;
    }

    @Override
    public RecordingState getRecordingState() {
        return currentState.get();
    }

    @Override
    public void setOutputFile(String argPath) {
        // TODO Auto-generated method stub

    }

    @Override
    public int getMaxAmplitude() {
        if (currentState.get() == RecordingState.RECORDING || currentState.get() == RecordingState.PAUSED) {
            int result = cAmplitude;
            cAmplitude = 0;
            return result;
        } else {
            return 0;
        }
    }

    @Override
    public void prepare() {
        // TODO Auto-generated method stub

    }

    @Override
    public void release() {
        // TODO Auto-generated method stub

    }

    @Override
    public void reset() {
        // TODO Auto-generated method stub

    }

    @Override
    public long getLength() {
        return payloadSize;
    }

    @Override
    public void start(int audioSource, int sampleRate, int channelConfig, int audioFormat, float quality) {
        startRecording(audioSource, sampleRate, channelConfig, audioFormat, quality);

    }

    @Override
    public void resume() {
        currentState.set(RecordingState.RECORDING);

    }

    @Override
    public void pause() {
        currentState.set(RecordingState.PAUSED);

    }

    @Override
    public void setGain(int gain) {
        mGain = gain;

    }

    @Override
    public void start() {
        // TODO Auto-generated method stub

    }

    @Override
    public void start(int audioSource, int sampleRate, int channelConfig, int audioFormat, int bitrate) {
        // TODO Auto-generated method stub

    }
}
vincentjames501 commented 10 years ago

I think my sample may be wrong for implementing pause. When pause happens, the audioRecorder needs to get paused/stopped and the readPCMData needs to quit reading data from the record buffer.

QuickBrownFoxy commented 10 years ago

I have tried returning 0 from readPCMData but this time native encoder understands it as stop and stops encoding. I would try to implement pause in org_xiph_vorbis_encoder_VorbisEncoder.c like start and stop methods but I have no C knowledge

vincentjames501 commented 10 years ago

Yeah I think pause will have to be written in the C code portion. You can try to preform a block on the call to readPCMData using something like a http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/Lock.html. But this will hang that entire thread until the lock is unlocked. That could be a temporary workaround.

QuickBrownFoxy commented 10 years ago

I don't want to dive into it at the moment. Do you mind adding pause method as another to-do to this project? In the mean time I struggle a little bit more :) Thanks

QuickBrownFoxy commented 10 years ago

it won;t work from Java side. do you think you'll be able to find time and add pause method or shall I stop hoping :)

QuickBrownFoxy commented 10 years ago

While you are here. I have straggled with this all night without luck. What I have noticed is that, in my pause implementation from java side when I avoid writing to actual file on writeVorbisData method and resume at some point later (since native encoder gets fed from audioRecorder) native encoder holds recording time in somewhere and when you stopped encoding it writes whole time including time passed while paused to ogg and finalizes it.

As I've said I have no idea how or where the native encoder holds this information