ogg stream

master
Hannes Achleitner MAC 2014-01-03 12:43:48 +01:00 committed by Hannes Achleitner
parent c13c69b558
commit 3a8daa69a5
15 changed files with 757 additions and 1 deletions

View File

@ -6,6 +6,7 @@ include $(addprefix $(LOCAL_PATH)/, $(addsuffix /Android.mk, \
libogg \
libvorbis \
libvorbis-jni \
libvorbis-stream \
))

View File

@ -0,0 +1,19 @@
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := vorbis-stream
LOCAL_CFLAGS += -I$(LOCAL_PATH)/../include -ffast-math -fsigned-char
ifeq ($(TARGET_ARCH),arm)
LOCAL_CFLAGS += -march=armv6 -marm -mfloat-abi=softfp -mfpu=vfp
endif
LOCAL_SHARED_LIBRARIES := libogg libvorbis
LOCAL_SRC_FILES := \
vorbis-fileoutputstream.c \
vorbis-fileinputstream.c \
jni-util.c
include $(BUILD_SHARED_LIBRARY)

View File

@ -0,0 +1,28 @@
/* Programmer: Nicholas Wertzberger
* Email: wertnick@gmail.com
*
*/
#include <jni.h>
#include <stdlib.h>
#include <stdio.h>
#include <stream/util.h>
/*
* http://java.sun.com/docs/books/jni/html/exceptions.html
*/
void
JNU_ThrowByName(JNIEnv *env, const char *name, const char *msg, const int code)
{
char buf [45];
sprintf(buf, "%35s: %d", msg, code);
jclass cls = (*env)->FindClass(env, name);
/* if cls is NULL, an exception has already been thrown */
if (cls != NULL) {
(*env)->ThrowNew(env, cls, buf);
}
/* free the local ref */
(*env)->DeleteLocalRef(env, cls);
}

View File

@ -0,0 +1,198 @@
/**
* A java interface (a la inputstream) to read ogg vorbis files.
* http://svn.xiph.org/trunk/vorbis/examples/vorbisfile_example.c
*/
#include <jni.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <math.h>
#include <vorbis/vorbisfile.h>
#include <stream/util.h>
/* This is arbitrary, If you don't like it, change it */
#define MAX_INPUTSTREAMS 8
struct input_stream {
FILE * fh;
OggVorbis_File vf;
int section;
int length;
};
static struct input_stream input_streams[MAX_INPUTSTREAMS];
jint Java_org_xiph_vorbis_stream_VorbisFileInputStream_create(
JNIEnv* env,
jobject this,
jstring path,
jobject info
)
{
int ret; /* Debugging variable */
jfieldID channels_field, sample_rate_field, length_field; /* JNI field ID */
jclass cls = (*env)->GetObjectClass(env, info);
int stream_idx;
struct input_stream * iptr;
vorbis_info * vi;
/* Find an unused input_stream */
for (stream_idx = 0; stream_idx < MAX_INPUTSTREAMS; stream_idx++) {
if (input_streams[stream_idx].fh == NULL) {
const jbyte * pchars = (*env)->GetStringUTFChars(env, path, NULL);
if (pchars == NULL) {
/* Exception Already thrown */
return;
}
/* We found one! */
iptr = &input_streams[stream_idx];
iptr->fh = fopen(pchars, "r");
if (iptr->fh == NULL) {
JNU_ThrowByName(env, "java/io/IOException", "Error Creating File Handle", 0);
return;
}
(*env)->ReleaseStringUTFChars(env, path, pchars);
break;
}
}
if (stream_idx == MAX_INPUTSTREAMS) {
JNU_ThrowByName(env, "java/io/IOException",
"Too Many Vorbis InputStreams", stream_idx);
return;
}
/* Open the stream */
ret = ov_open(iptr->fh, &iptr->vf, NULL, 0);
if (ret < 0) {
JNU_ThrowByName(env, "java/io/IOException",
"Vorbis File Corrupt", ret);
fclose(iptr->fh);
iptr->fh = NULL;
return;
}
channels_field = (*env)->GetFieldID(env, cls, "channels", "I");
sample_rate_field = (*env)->GetFieldID(env, cls, "sampleRate", "I");
length_field = (*env)->GetFieldID(env, cls, "length", "J");
if (channels_field == NULL || sample_rate_field == NULL) {
JNU_ThrowByName(env, "java/lang/Exception",
"Native Field Misnamed", 0);
ov_clear(&iptr->vf);
fclose(iptr->fh);
iptr->fh = NULL;
return;
}
vi = ov_info(&iptr->vf, -1);
iptr->section = 0;
iptr->length = ov_pcm_total(&iptr->vf, -1);
/* Populate basic stream info into the VorbisInfo object. */
(*env)->SetIntField(env, info, channels_field, vi->channels);
(*env)->SetIntField(env, info, sample_rate_field, vi->rate);
(*env)->SetLongField(env, info, length_field, iptr->length);
return stream_idx;
}
jint Java_org_xiph_vorbis_stream_VorbisFileInputStream_readStreamIdx(
JNIEnv* env,
jobject this,
jint sidx,
jshortArray pcm,
jint offset,
jint length
)
{
long ret;
struct input_stream * iptr = &input_streams[sidx];
jshort * pcmShorts = (*env)->GetShortArrayElements(env, pcm, NULL);
int maxLength = (*env)->GetArrayLength(env,pcm);
/* Do the battery of validation checks. */
if (offset + length > maxLength) {
JNU_ThrowByName(env, "java/lang/ArrayIndexOutOfBoundsException",
"No data was written to the buffer",
offset + length - 1);
return;
}
if (sidx >= MAX_INPUTSTREAMS || sidx < 0 || iptr->fh == NULL) {
JNU_ThrowByName(env, "java/io/IOException", "Invalid Stream Index", sidx);
return;
}
if (length > 0) {
ret = ov_read(&iptr->vf, (char *)(pcmShorts + offset), length, 0, 2, 1, &iptr->section);
/* -1 is EOF */
if (ret == 0) {
ret = -1;
}
else if (ret < 0) {
if (ret == OV_EBADLINK) {
JNU_ThrowByName(env, "java/io/IOException", "Corrupt bitstream section!", iptr->section);
return;
}
}
}
else {
ret = 0;
}
/* Apparently sample rates can change inside the stream... We may need to account for that. */
(*env)->ReleaseShortArrayElements(env, pcm, pcmShorts, 0);
return ret >> 1;
}
jlong Java_org_xiph_vorbis_stream_VorbisFileInputStream_skipStreamIdx(
JNIEnv* env,
jobject this,
jint sidx,
jlong offset
)
{
struct input_stream * iptr = &input_streams[sidx];
long ret;
if (sidx >= MAX_INPUTSTREAMS || sidx < 0 || iptr->fh == NULL) {
JNU_ThrowByName(env, "java/io/IOException", "Invalid Stream Index", sidx);
return;
}
ret = ov_pcm_seek_lap(&iptr->vf, offset);
if (ret == OV_EREAD) {
JNU_ThrowByName(env, "java/io/IOException", "Read ERROR", ret);
return;
}
else if (ret != 0){
JNU_ThrowByName(env, "java/io/IOException", "Vorbis Seek Error code: ", ret);
return;
}
return ret;
}
void Java_org_xiph_vorbis_stream_VorbisFileInputStream_closeStreamIdx(
JNIEnv* env,
jobject this,
jint sidx
)
{
struct input_stream * iptr = &input_streams[sidx];
if (sidx >= MAX_INPUTSTREAMS || sidx < 0 || iptr->fh == NULL) {
JNU_ThrowByName(env, "java/io/IOException", "Invalid Stream Index", sidx);
return;
}
ov_clear(&iptr->vf);
fclose(iptr->fh);
iptr->fh = NULL;
}

View File

@ -0,0 +1,295 @@
/* Programmer: Nicholas Wertzberger
*
* The Java interface (a la outputstream) for vorbis encoding. This acts
* roughly the way I would expect a Java OutputStream to act. I didn't bother
* trying to SWIG anythign around between the native world and java. In fact,
* I jsut have a statically allocate array to store data in here.
*
* http://svn.xiph.org/trunk/vorbis/examples/encoder_example.c
*/
#include <jni.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <math.h>
#include <errno.h>
#include <vorbis/vorbisenc.h>
#include <stream/util.h>
/* I really don't want to figure out what vorbis is storing in their structs.
* Let's just store it all in this here array and call it good.
*/
/* This is arbitrary, If you don't like it, change it */
#define MAX_OUTPUTSTREAMS 4
#define MAX_VORBIS_CHUNKSIZE 1024
struct output_stream {
FILE * fh;
vorbis_info vi;
vorbis_comment vc;
vorbis_dsp_state vd;
vorbis_block vb;
ogg_stream_state os;
ogg_page og;
ogg_packet op;
int channels;
};
static struct output_stream output_streams[MAX_OUTPUTSTREAMS];
/* Based on code from:
* http://svn.xiph.org/trunk/vorbis/examples/encoder_example.c
* Returns a pointer to the stream struct related to that current vorbis file.
*/
jint Java_org_xiph_vorbis_stream_VorbisFileOutputStream_create(
JNIEnv* env,
jobject this,
jstring path,
jobject info
)
{
/* Configuration structs */
struct output_stream * optr = NULL;
/* JNI field ID's */
jfieldID channels_field, sample_rate_field, quality_field;
jclass cls = (*env)->GetObjectClass(env, info);
/* packet stream structs */
ogg_packet header;
ogg_packet header_comm;
ogg_packet header_code;
int ret; /* Return code storage for function calls */
int eos = 0; /* End of Stream */
int stream_idx;
int sample_rate;
float quality;
/* Find an unused output_stream */
for (stream_idx = 0; stream_idx < MAX_OUTPUTSTREAMS; stream_idx++) {
if (output_streams[stream_idx].fh == NULL) {
const jbyte * pchars = (*env)->GetStringUTFChars(env, path, NULL);
if (pchars == NULL) {
/* Exception Already thrown */
return;
}
/* We found one! */
optr = &output_streams[stream_idx];
optr->fh = fopen(pchars, "w");
if (optr->fh == NULL) {
char * message = "Error Creating File Handle. ";
JNU_ThrowByName(env, "java/io/IOException", message, errno);
return;
}
(*env)->ReleaseStringUTFChars(env, path, pchars);
break;
}
}
if (stream_idx == MAX_OUTPUTSTREAMS) {
JNU_ThrowByName(env, "java/io/IOException",
"Too Many Vorbis OutputStreams", stream_idx);
return;
}
/* Step 1. According to documented workflow.
* http://xiph.org/vorbis/doc/libvorbis/overview.html
*/
vorbis_info_init(&optr->vi);
/* TODO: make these options passed in. We definitely don't need stereo
* most of the time.
*/
channels_field = (*env)->GetFieldID(env, cls, "channels", "I");
sample_rate_field = (*env)->GetFieldID(env, cls, "sampleRate", "I");
quality_field = (*env)->GetFieldID(env, cls, "quality", "F");
optr->channels = (*env)->GetIntField(env, info, channels_field);
sample_rate = (*env)->GetIntField(env, info, sample_rate_field);
quality = (*env)->GetFloatField(env, info, quality_field);
/* TODO: Optimize this for speed more? */
ret = vorbis_encode_init_vbr(&optr->vi,optr->channels,sample_rate,quality);
if (ret) {
JNU_ThrowByName(env, "java/io/IOException", "Bad Encoding options", ret);
fclose(optr->fh);
return;
}
/* Step 2. */
vorbis_analysis_init(&optr->vd, &optr->vi);
vorbis_block_init(&optr->vd, &optr->vb);
/* Step 3. */
vorbis_comment_init(&optr->vc);
/* A 0 means all is well. */
srand(time(NULL));
ogg_stream_init(&optr->os, rand());
ret = vorbis_analysis_headerout(&optr->vd, &optr->vc, &header, &header_comm,
&header_code);
if (ret) {
JNU_ThrowByName(env, "java/io/IOException", "header init error", ret);
ogg_stream_clear(&optr->os);
vorbis_block_clear(&optr->vb);
vorbis_dsp_clear(&optr->vd);
vorbis_comment_clear(&optr->vc);
vorbis_info_clear(&optr->vi);
fclose(optr->fh);
optr->fh = NULL;
return;
}
ogg_stream_packetin(&optr->os, &header); /* placed in its own page */
ogg_stream_packetin(&optr->os, &header_comm);
ogg_stream_packetin(&optr->os, &header_code);
/* This ensures the actual
* audio data will start on a new page, as per spec
*/
while (1) {
int result = ogg_stream_flush(&optr->os, &optr->og);
if (result == 0)
break;
/* TODO: Tie this into the file handle passed in... Or whatever */
fwrite(optr->og.header, 1, optr->og.header_len, optr->fh);
fwrite(optr->og.body, 1, optr->og.body_len, optr->fh);
}
return stream_idx;
}
/* Write out to the file handle
*
*/
jint Java_org_xiph_vorbis_stream_VorbisFileOutputStream_writeStreamIdx(
JNIEnv* env,
jobject this,
jint sidx,
jshortArray pcm,
jint offset,
jint length
)
{
jshort * pcmShorts = (*env)->GetShortArrayElements(env, pcm, NULL);
int maxLength = (*env)->GetArrayLength(env,pcm);
struct output_stream * optr = &output_streams[sidx];
int channels;
int i,j;
int eos = 0;
if (offset + length > maxLength) {
JNU_ThrowByName(env, "java/lang/ArrayIndexOutOfBoundsException",
"No data was read from the buffer",
offset + length - 1);
return;
}
if (sidx >= MAX_OUTPUTSTREAMS || sidx < 0 || optr->fh == NULL) {
JNU_ThrowByName(env, "java/io/IOException", "Invalid Stream Index",
sidx);
return;
}
channels = optr->channels;
while (length > 0) {
/* Data to encode:
* According to this: http://xiph.org/vorbis/doc/libvorbis/vorbis_analysis_buffer.html
*
* A "reasonable" chunk size is 1024. Due to some sampling issues, we
* are going to force this to be the max size.
*/
int chunksize = length;
if (chunksize > MAX_VORBIS_CHUNKSIZE) chunksize = MAX_VORBIS_CHUNKSIZE;
/* expose the buffer to submit data */
float ** buffer = vorbis_analysis_buffer(&optr->vd, chunksize);
/* uninterleave samples */
for (i = 0; i < chunksize / channels; i++) {
for (j = 0; j < channels; j++) {
buffer[j][i] = pcmShorts[i*channels + j + offset] / 32768.f;
}
}
/* tell the library how much we actually submitted */
vorbis_analysis_wrote(&optr->vd, i);
length -= i*channels;
offset += i*channels;
/* vorbis does some data preanalysis, then divvies up blocks for
more involved (potentially parallel) processing. Get a single
block for encoding now */
while (vorbis_analysis_blockout(&optr->vd, &optr->vb) == 1) {
/* analysis, assume we want to use bitrate management */
vorbis_analysis(&optr->vb, NULL);
vorbis_bitrate_addblock(&optr->vb);
while (vorbis_bitrate_flushpacket(&optr->vd, &optr->op)) {
/* weld the packet into the bitstream */
ogg_stream_packetin(&optr->os, &optr->op);
/* write out pages (if any) */
while (!eos) {
int result = ogg_stream_pageout(&optr->os, &optr->og);
if (result == 0)
break;
fwrite(optr->og.header, 1, optr->og.header_len, optr->fh);
fwrite(optr->og.body, 1, optr->og.body_len, optr->fh);
/* this could be set above, but for illustrative purposes, I do
it here (to show that vorbis does know where the stream ends) */
if (ogg_page_eos(&optr->og))
eos = 1;
}
}
}
}
(*env)->ReleaseShortArrayElements(env, pcm, pcmShorts, JNI_ABORT);
}
/*
* Clean up stream info.
*/
void Java_org_xiph_vorbis_stream_VorbisFileOutputStream_closeStreamIdx(
JNIEnv* env,
jobject this,
jint sidx
)
{
struct output_stream * optr = &output_streams[sidx];
if (sidx >= MAX_OUTPUTSTREAMS || sidx < 0 || optr->fh == NULL) {
JNU_ThrowByName(env, "java/io/IOException", "Invalid Stream Index", sidx);
return;
}
vorbis_analysis_wrote(&optr->vd, 0);
while (vorbis_analysis_blockout(&optr->vd, &optr->vb) == 1) {
vorbis_analysis(&optr->vb, NULL);
vorbis_bitrate_addblock(&optr->vb);
while (vorbis_bitrate_flushpacket(&optr->vd, &optr->op)) {
ogg_stream_packetin(&optr->os, &optr->op);
}
}
while (ogg_stream_pageout(&optr->os, &optr->og) > 0) {
fwrite(optr->og.header, 1, optr->og.header_len, optr->fh);
fwrite(optr->og.body, 1, optr->og.body_len, optr->fh);
}
ogg_stream_clear(&optr->os);
vorbis_block_clear(&optr->vb);
vorbis_dsp_clear(&optr->vd);
vorbis_comment_clear(&optr->vc);
vorbis_info_clear(&optr->vi);
fclose(optr->fh);
optr->fh = NULL;
}

View File

@ -1,7 +1,6 @@
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := libvorbis
LOCAL_CFLAGS += -I$(LOCAL_PATH)/../include -ffast-math -fsigned-char
ifeq ($(TARGET_ARCH),arm)

Binary file not shown.

BIN
libs/armeabi/libvorbis-stream.so Executable file

Binary file not shown.

BIN
libs/mips/libvorbis-stream.so Executable file

Binary file not shown.

BIN
libs/x86/libvorbis-stream.so Executable file

Binary file not shown.

View File

@ -0,0 +1,23 @@
package org.xiph.vorbis.stream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
public abstract class AudioInputStream extends InputStream implements Closeable {
public abstract void close() throws IOException;
public int read(short[] pcmBuffer) throws IOException {
return this.read(pcmBuffer, 0, pcmBuffer.length);
}
public abstract int read(short[] pcmBuffer, int offset, int length) throws IOException;
@Override
public int read() throws IOException {
short buf[] = new short[1];
if ( this.read(buf, 0, 1) == -1) return -1;
else return buf[0];
}
}

View File

@ -0,0 +1,27 @@
package org.xiph.vorbis.stream;
import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
public abstract class AudioOutputStream extends OutputStream implements Closeable {
@Override
public void write(final int buf) throws IOException {
short[] buffer = new short[1];
buffer[0] = (short) buf;
this.write(buffer, 0, buffer.length);
}
public void write(final short[] buffer) throws IOException {
this.write(buffer, 0, buffer.length);
}
public abstract void write(final short[] buffer, int offset, int length)
throws IOException;
public abstract int getSampleRate();
@Override
public abstract void close() throws IOException;
}

View File

@ -0,0 +1,70 @@
package org.xiph.vorbis.stream;
import java.io.IOException;
public class VorbisFileInputStream extends AudioInputStream {
private final VorbisInfo info;
public VorbisInfo getInfo() {
return info;
}
private final int oggStreamIdx;
static {
System.loadLibrary("ogg");
System.loadLibrary("vorbis");
System.loadLibrary("vorbis-stream");
}
/**
* Opens a file for reading and parses any comments out of the file header.
*/
public VorbisFileInputStream(String fname) throws IOException {
info = new VorbisInfo();
oggStreamIdx = this.create(fname, info);
}
@Override
public void close() throws IOException {
this.closeStreamIdx(oggStreamIdx);
}
/**
* Returns interleaved PCM data from the vorbis stream.
*
* @param pcmBuffer
* @param offset
* @param length
* @return
* @throws IOException
*/
@Override
public int read(short[] pcmBuffer, int offset, int length) throws IOException {
return this.readStreamIdx(oggStreamIdx, pcmBuffer, offset, length);
}
private native int create(String fname, VorbisInfo info) throws IOException;
private native void closeStreamIdx(int sidx) throws IOException;
/**
* This just returns all the channels interleaved together. I assume this is how android wants it.
*
* @param pcm
* @return
* @throws IOException
*/
private native int readStreamIdx(int sidx, short[] pcm, int offset, int size) throws IOException;
/**
* Skips over the number of samples specified. This skip doesn't account for channels.
*
* @param samples
* @return
* @throws IOException
*/
private native long skipStreamIdx(int sidx, long samples) throws IOException;
}

View File

@ -0,0 +1,62 @@
package org.xiph.vorbis.stream;
import java.io.IOException;
/**
* Converts incoming PCM Audio Data into OGG data into a file. This will be implemented using the open source BSD-licensed stuff from Xiph.org.
*
* NOTE: This implementation has a limitation of MAX_STREAMS concurrent output streams. When i wrote this, it was set to 8. Check in
* vorbis-fileoutputstream.c to see what it is set to.
*
*/
public class VorbisFileOutputStream extends AudioOutputStream {
// The index into native memory where the ogg stream info is stored.
private final int oggStreamIdx;
private VorbisInfo info;
private static final int VORBIS_BLOCK_SIZE = 1024;
static {
System.loadLibrary("ogg");
System.loadLibrary("vorbis");
System.loadLibrary("vorbis-stream");
}
public VorbisFileOutputStream(String fname, VorbisInfo s) throws IOException {
info = s;
oggStreamIdx = this.create(fname, s);
}
public VorbisFileOutputStream(String fname) throws IOException {
oggStreamIdx = this.create(fname, new VorbisInfo());
}
@Override
public void close() throws IOException {
this.closeStreamIdx(this.oggStreamIdx);
}
/**
* Write PCM data to ogg. This assumes that you pass your streams in interleaved.
*
* @param buffer
* @param offset
* @param length
* @return
* @throws IOException
*/
@Override
public void write(final short[] buffer, int offset, int length) throws IOException {
this.writeStreamIdx(this.oggStreamIdx, buffer, offset, length);
}
private native int writeStreamIdx(int idx, short[] pcmdata, int offset, int size) throws IOException;
private native void closeStreamIdx(int idx) throws IOException;
private native int create(String path, VorbisInfo s) throws IOException;
@Override
public int getSampleRate() {
return info.sampleRate;
}
}

View File

@ -0,0 +1,34 @@
package org.xiph.vorbis.stream;
/**
* A class used to pass vorbis file info to the encoder and find out what kind of file was received by the decoder.
*
* Currently, the vorbis file encoder/decoder only supports 16-bit samples. There are no plans by me to ever add 8-bit. Defaults are set to whatever I
* wanted to use for the project i wrote this for.
*
*/
public class VorbisInfo {
/**
* The number of channels to be encoded. For your sake, here are the official channel positions for the first five according to Xiph.org. one
* channel - the stream is monophonic two channels - the stream is stereo. channel order: left, right three channels - the stream is a 1d-surround
* encoding. channel order: left, center, right four channels - the stream is quadraphonic surround. channel order: front left, front right, rear
* left, rear right five channels - the stream is five-channel surround. channel order: front left, center, front right, rear left, rear right
*/
public int channels = 1;
/**
* The number of samples per second of pcm data.
*/
public int sampleRate = 44100;
/**
* The recording quality of the encoding. This is not currently set for the decoder. The range goes from -.1 (worst) to 1 (best)
*/
public float quality = 0.4f;
/**
* the total number of samples from the recording. This field means nothing to the encoder.
*/
public long length;
}