Skip to content

Commit

Permalink
Add vorbis comments support to flac extractor
Browse files Browse the repository at this point in the history
Decode and add vorbis comments from the flac file to metadata.

 google#5527
  • Loading branch information
vavadhani committed Jul 9, 2019
1 parent 65d9c11 commit 77e1e4c
Show file tree
Hide file tree
Showing 9 changed files with 394 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;

/**
* JNI wrapper for the libflac Flac decoder.
Expand Down Expand Up @@ -151,6 +152,12 @@ public FlacStreamInfo decodeStreamInfo() throws IOException, InterruptedExceptio
return streamInfo;
}

/** Decodes and consumes the Vorbis Comment section from the FLAC stream. */
@Nullable
public ArrayList<String> decodeVorbisComment() throws IOException, InterruptedException {
return flacDecodeVorbisComment(nativeDecoderContext);
}

/**
* Decodes and consumes the next frame from the FLAC stream into the given byte buffer. If any IO
* error occurs, resets the stream and input to the given {@code retryPosition}.
Expand Down Expand Up @@ -269,6 +276,9 @@ private int readFromExtractorInput(
private native FlacStreamInfo flacDecodeMetadata(long context)
throws IOException, InterruptedException;

private native ArrayList<String> flacDecodeVorbisComment(long context)
throws IOException, InterruptedException;

private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer)
throws IOException, InterruptedException;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.metadata.vorbis.VorbisCommentDecoder;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.FlacStreamInfo;
import com.google.android.exoplayer2.util.MimeTypes;
Expand All @@ -42,6 +43,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
Expand Down Expand Up @@ -91,6 +93,7 @@ public final class FlacExtractor implements Extractor {
private @MonotonicNonNull OutputFrameHolder outputFrameHolder;

@Nullable private Metadata id3Metadata;
@Nullable private Metadata vorbisMetadata;
@Nullable private FlacBinarySearchSeeker binarySearchSeeker;

/** Constructs an instance with flags = 0. */
Expand Down Expand Up @@ -224,11 +227,16 @@ private void decodeStreamInfo(ExtractorInput input) throws InterruptedException,
}

streamInfoDecoded = true;
vorbisMetadata = decodeVorbisComment(input);
if (this.streamInfo == null) {
this.streamInfo = streamInfo;
binarySearchSeeker =
outputSeekMap(decoderJni, streamInfo, input.getLength(), extractorOutput);
outputFormat(streamInfo, id3MetadataDisabled ? null : id3Metadata, trackOutput);
Metadata metadata = id3MetadataDisabled ? null : id3Metadata;
if (vorbisMetadata != null) {
metadata = vorbisMetadata.copyWithAppendedEntriesFrom(metadata);
}
outputFormat(streamInfo, metadata, trackOutput);
outputBuffer.reset(streamInfo.maxDecodedFrameSize());
outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data));
}
Expand Down Expand Up @@ -262,6 +270,19 @@ private static boolean peekFlacSignature(ExtractorInput input)
return Arrays.equals(header, FLAC_SIGNATURE);
}

@Nullable
private Metadata decodeVorbisComment(ExtractorInput input)
throws InterruptedException, IOException {
try {
ArrayList<String> vorbisCommentList = decoderJni.decodeVorbisComment();
return new VorbisCommentDecoder().decodeVorbisComments(vorbisCommentList);
} catch (IOException e) {
decoderJni.reset(0);
input.setRetryPosition(0, e);
throw e;
}
}

/**
* Outputs a {@link SeekMap} and returns a {@link FlacBinarySearchSeeker} if one is required to
* handle seeks.
Expand Down
26 changes: 26 additions & 0 deletions extensions/flac/src/main/jni/flac_jni.cc
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,32 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) {
streamInfo.total_samples);
}

DECODER_FUNC(jobject, flacDecodeVorbisComment, jlong jContext) {
Context *context = reinterpret_cast<Context *>(jContext);
context->source->setFlacDecoderJni(env, thiz);

VorbisComment vorbisComment = context->parser->getVorbisComment();

if (vorbisComment.numComments == 0) {
return NULL;
} else {
jclass java_util_ArrayList = env->FindClass("java/util/ArrayList");

jmethodID java_util_ArrayList_ = env->GetMethodID(java_util_ArrayList, "<init>", "(I)V");
jmethodID java_util_ArrayList_add = env->GetMethodID(java_util_ArrayList, "add",
"(Ljava/lang/Object;)Z");

jobject result = env->NewObject(java_util_ArrayList, java_util_ArrayList_,
vorbisComment.numComments);
for (FLAC__uint32 i = 0; i < vorbisComment.numComments; ++i) {
jstring element = env->NewStringUTF(vorbisComment.metadataArray[i]);
env->CallBooleanMethod(result, java_util_ArrayList_add, element);
env->DeleteLocalRef(element);
}
return result;
}
}

DECODER_FUNC(jint, flacDecodeToBuffer, jlong jContext, jobject jOutputBuffer) {
Context *context = reinterpret_cast<Context *>(jContext);
context->source->setFlacDecoderJni(env, thiz);
Expand Down
28 changes: 28 additions & 0 deletions extensions/flac/src/main/jni/flac_parser.cc
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,30 @@ void FLACParser::metadataCallback(const FLAC__StreamMetadata *metadata) {
case FLAC__METADATA_TYPE_SEEKTABLE:
mSeekTable = &metadata->data.seek_table;
break;
case FLAC__METADATA_TYPE_VORBIS_COMMENT:
if (!mVorbisCommentValid) {
FLAC__uint32 count = 0;
const FLAC__StreamMetadata_VorbisComment *vc =
&metadata->data.vorbis_comment;
mVorbisCommentValid = true;
mVorbisComment.metadataArray =
(char **) malloc(vc->num_comments * sizeof(char *));
for (FLAC__uint32 i = 0; i < vc->num_comments; ++i) {
FLAC__StreamMetadata_VorbisComment_Entry *vce = &vc->comments[i];
if (vce->entry != NULL) {
mVorbisComment.metadataArray[count] =
(char *) malloc((vce->length + 1) * sizeof(char));
memcpy(mVorbisComment.metadataArray[count], vce->entry,
vce->length);
mVorbisComment.metadataArray[count][vce->length] = '\0';
count++;
}
}
mVorbisComment.numComments = count;
} else {
ALOGE("FLACParser::metadataCallback unexpected VORBISCOMMENT");
}
break;
default:
ALOGE("FLACParser::metadataCallback unexpected type %u", metadata->type);
break;
Expand Down Expand Up @@ -233,13 +257,15 @@ FLACParser::FLACParser(DataSource *source)
mCurrentPos(0LL),
mEOF(false),
mStreamInfoValid(false),
mVorbisCommentValid(false),
mWriteRequested(false),
mWriteCompleted(false),
mWriteBuffer(NULL),
mErrorStatus((FLAC__StreamDecoderErrorStatus)-1) {
ALOGV("FLACParser::FLACParser");
memset(&mStreamInfo, 0, sizeof(mStreamInfo));
memset(&mWriteHeader, 0, sizeof(mWriteHeader));
memset(&mVorbisComment, 0, sizeof(mVorbisComment));
}

FLACParser::~FLACParser() {
Expand All @@ -266,6 +292,8 @@ bool FLACParser::init() {
FLAC__METADATA_TYPE_STREAMINFO);
FLAC__stream_decoder_set_metadata_respond(mDecoder,
FLAC__METADATA_TYPE_SEEKTABLE);
FLAC__stream_decoder_set_metadata_respond(mDecoder,
FLAC__METADATA_TYPE_VORBIS_COMMENT);
FLAC__StreamDecoderInitStatus initStatus;
initStatus = FLAC__stream_decoder_init_stream(
mDecoder, read_callback, seek_callback, tell_callback, length_callback,
Expand Down
14 changes: 14 additions & 0 deletions extensions/flac/src/main/jni/include/flac_parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@

typedef int status_t;

typedef struct VorbisComment_ {
int numComments;
char **metadataArray;
} VorbisComment;

class FLACParser {
public:
FLACParser(DataSource *source);
Expand Down Expand Up @@ -71,6 +76,7 @@ class FLACParser {
mEOF = false;
if (newPosition == 0) {
mStreamInfoValid = false;
mVorbisCommentValid = false;
FLAC__stream_decoder_reset(mDecoder);
} else {
FLAC__stream_decoder_flush(mDecoder);
Expand All @@ -96,6 +102,10 @@ class FLACParser {
FLAC__STREAM_DECODER_END_OF_STREAM;
}

VorbisComment getVorbisComment() {
return mVorbisComment;
}

private:
DataSource *mDataSource;

Expand All @@ -116,6 +126,8 @@ class FLACParser {
const FLAC__StreamMetadata_SeekTable *mSeekTable;
uint64_t firstFrameOffset;

bool mVorbisCommentValid;

// cached when a decoded PCM block is "written" by libFLAC parser
bool mWriteRequested;
bool mWriteCompleted;
Expand All @@ -129,6 +141,8 @@ class FLACParser {
FLACParser(const FLACParser &);
FLACParser &operator=(const FLACParser &);

VorbisComment mVorbisComment;

// FLAC parser callbacks as C++ instance methods
FLAC__StreamDecoderReadStatus readCallback(FLAC__byte buffer[],
size_t *bytes);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.metadata.vorbis;

import androidx.annotation.Nullable;
import com.google.android.exoplayer2.metadata.Metadata;
import java.util.ArrayList;

/** Decodes vorbis comments */
public class VorbisCommentDecoder {

private static final String SEPARATOR = "=";

/**
* Decodes an {@link ArrayList} of vorbis comments.
*
* @param metadataStringList An {@link ArrayList} containing vorbis comments as {@link String}
* @return A {@link Metadata} structure with the vorbis comments as its entries.
*/
public Metadata decodeVorbisComments(@Nullable ArrayList<String> metadataStringList) {
if (metadataStringList == null || metadataStringList.size() == 0) {
return null;
}

ArrayList<VorbisCommentFrame> vorbisCommentFrames = new ArrayList<>();
VorbisCommentFrame vorbisCommentFrame;

for (String commentEntry : metadataStringList) {
String[] keyValue;

keyValue = commentEntry.split(SEPARATOR);
if (keyValue.length != 2) {
/* Could not parse this comment, no key value pair found */
continue;
}
vorbisCommentFrame = new VorbisCommentFrame(keyValue[0], keyValue[1]);
vorbisCommentFrames.add(vorbisCommentFrame);
}

if (vorbisCommentFrames.size() > 0) {
return new Metadata(vorbisCommentFrames);
} else {
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.metadata.vorbis;

import static com.google.android.exoplayer2.util.Util.castNonNull;

import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.metadata.Metadata;

/** Base class for Vorbis Comment Frames. */
public class VorbisCommentFrame implements Metadata.Entry {

/** The frame key and value */
public final String key;

public final String value;

/**
* @param key The key
* @param value Value corresponding to the key
*/
public VorbisCommentFrame(String key, String value) {
this.key = key;
this.value = value;
}

/* package */ VorbisCommentFrame(Parcel in) {
this.key = castNonNull(in.readString());
this.value = castNonNull(in.readString());
}

@Override
public String toString() {
return key;
}

@Override
public int describeContents() {
return 0;
}

// Parcelable implementation.
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(key);
dest.writeString(value);
}

@Override
public boolean equals(@Nullable Object obj) {
if ((obj != null) && (obj.getClass() == this.getClass())) {
if (this == obj) {
return true;
} else {
VorbisCommentFrame compareFrame = (VorbisCommentFrame) obj;
if (this.key.equals(compareFrame.key) && this.value.equals(compareFrame.value)) {
return true;
}
}
}
return false;
}

@Override
public int hashCode() {
int result = 17;

result = 31 * result + key.hashCode();
result = 31 * result + value.hashCode();

return result;
}

public static final Parcelable.Creator<VorbisCommentFrame> CREATOR =
new Parcelable.Creator<VorbisCommentFrame>() {

@Override
public VorbisCommentFrame createFromParcel(Parcel in) {
return new VorbisCommentFrame(in);
}

@Override
public VorbisCommentFrame[] newArray(int size) {
return new VorbisCommentFrame[size];
}
};
}
Loading

0 comments on commit 77e1e4c

Please sign in to comment.