Commit f1c4be83 authored by Daniel Sonck's avatar Daniel Sonck
Browse files

Merge pull request #36 in THFM/fm.touhou.touhoufm from feature/THFMA-20 to development

* commit '4db4d949':
  [THFMA-20] Implement new song info
parents c6125036 4db4d949
......@@ -8,13 +8,13 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.1'
classpath 'com.android.tools.build:gradle:3.5.0'
}
}
plugins {
id 'com.android.application'
id 'com.github.triplet.play' version '2.1.0'
id 'com.github.triplet.play' version '2.4.1'
}
apply from: 'version.gradle'
......@@ -38,12 +38,12 @@ dependencies {
testImplementation 'org.powermock:powermock-module-junit4:1.6.5'
testImplementation 'org.powermock:powermock-api-mockito:1.6.5'
implementation "com.android.support:support-v4:27.0.2"
implementation "com.android.support:support-v13:27.0.2"
implementation "com.android.support:cardview-v7:27.0.2"
implementation "com.android.support:support-v4:28.0.0"
implementation "com.android.support:support-v13:28.0.0"
implementation "com.android.support:cardview-v7:28.0.0"
implementation 'com.android.support:appcompat-v7:27.0.2'
implementation 'com.android.support.constraint:constraint-layout:1.0.2'
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'org.java-websocket:Java-WebSocket:1.3.7'
implementation 'com.facebook.fresco:fresco:1.8.0'
}
......@@ -58,11 +58,11 @@ List<String> dirs = [
android {
compileSdkVersion 27
compileSdkVersion 28
defaultConfig {
minSdkVersion 14
targetSdkVersion 27
targetSdkVersion 28
applicationId "fm.touhou.touhoufm"
versionCode gitVersionCode
......@@ -99,6 +99,7 @@ android {
externalNativeBuild {
cmake {
version '3.10.2'
path 'src/main/cpp/CMakeLists.txt'
}
}
......
......@@ -22,6 +22,7 @@
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<application
android:allowBackup="true"
......
......@@ -2,7 +2,7 @@
# This ensures that a certain set of CMake features is available to
# your build.
cmake_minimum_required(VERSION 3.4.1)
cmake_minimum_required(VERSION 3.10.2)
project(native-opus)
......
......@@ -31,8 +31,10 @@ import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import fm.touhou.touhoufm.service.players.RadioPlayer;
import fm.touhou.touhoufm.utils.AudioPacket;
import fm.touhou.touhoufm.utils.BasePacket;
import fm.touhou.touhoufm.utils.MetaPacket;
import fm.touhou.touhoufm.utils.OpusDecoder;
import fm.touhou.touhoufm.utils.RtpPacket;
public class RtpReceiver extends Thread {
private static final String TAG = RtpReceiver.class.getName();
......@@ -59,7 +61,11 @@ public class RtpReceiver extends Thread {
private RingBuffer mRingBuffer;
public RtpReceiver(RingBuffer buffer) {
private MetaListener metaListener;
public RtpReceiver(RingBuffer buffer, MetaListener metaListener) {
this.metaListener = metaListener;
mRingBuffer = buffer;
mOpusDecoder = new OpusDecoder();
......@@ -176,32 +182,40 @@ public class RtpReceiver extends Thread {
DatagramPacket recvPacket = receiveDatagramPacket();
// Decode the RTP packet
RtpPacket rtpPacket = new RtpPacket(recvPacket.getData(), recvPacket.getLength());
BasePacket rtpPacket = new BasePacket(recvPacket.getData(), recvPacket.getLength());
if(rtpPacket.getSystem() == BasePacket.SYSTEM_AUDIO) {
if(rtpPacket.getType() == BasePacket.TYPE_AUDIO_OPUS) {
AudioPacket audioPacket = rtpPacket.toAudio();
// Get the payload out of the RTP packet
byte[] opus = new byte[audioPacket.getAudioSize()];
audioPacket.getAudio(opus);
if(rtpPacket.getPayloadType() == 120) {
// Get the payload out of the RTP packet
byte[] opus = new byte[rtpPacket.getPayloadLength()];
rtpPacket.getPayload(opus);
int sequenceNumber = rtpPacket.getSequenceNumber();
int sequenceNumber = rtpPacket.getSequenceNumber();
if (mLastSeqNum > sequenceNumber && mLastSeqNum - sequenceNumber < 128) {
Log.w(TAG, "Received out-of-order packet");
} else if (mLastSeqNum == sequenceNumber) {
Log.w(TAG, "Received duplicate packet");
} else {
if (mLastSeqNum > sequenceNumber && mLastSeqNum - sequenceNumber < 128) {
Log.w(TAG, "Received out-of-order packet");
} else if (mLastSeqNum == sequenceNumber) {
Log.w(TAG, "Received duplicate packet");
} else {
mLastSeqNum = sequenceNumber;
mLastSeqNum = sequenceNumber;
// Decode the opus data into pcm frames
int samps = mOpusDecoder.decode(opus, pcmFrames);
// Decode the opus data into pcm frames
mOpusDecoder.decode(opus, pcmFrames);
// Write the audio data to the buffer
if (mRingBuffer.write(pcmFrames, samps*2) < 0) {
// If the write returns -1, this means the buffer is aborted and we should stop
interrupt();
}
// Write the audio data to the buffer
if (mRingBuffer.write(pcmFrames, pcmFrames.length) < 0) {
// If the write returns -1, this means the buffer is aborted and we should stop
interrupt();
}
}
if(rtpPacket.getType() == BasePacket.TYPE_META) {
MetaPacket metaPacket = rtpPacket.toMeta();
metaListener.newMeta(metaPacket.getField(), metaPacket.getContents());
}
} else {
Log.w(TAG, "Invalid RTP packet received");
......@@ -271,4 +285,8 @@ public class RtpReceiver extends Thread {
private void shutDown() {
mOpusDecoder.close();
}
public interface MetaListener {
void newMeta(int field, String value);
}
}
......@@ -48,10 +48,12 @@ import java.util.Date;
import java.util.Iterator;
import fm.touhou.touhoufm.R;
import fm.touhou.touhoufm.radio.RtpReceiver;
import fm.touhou.touhoufm.service.PlaybackInfoListener;
import fm.touhou.touhoufm.service.PlayerAdapter;
import fm.touhou.touhoufm.ui.MainActivity;
import fm.touhou.touhoufm.utils.APIClient;
import fm.touhou.touhoufm.utils.MetaPacket;
import static fm.touhou.touhoufm.utils.APIClient.INFO_ALBUM;
import static fm.touhou.touhoufm.utils.APIClient.INFO_ARTIST;
......@@ -62,7 +64,7 @@ import static fm.touhou.touhoufm.utils.APIClient.INFO_TITLE;
* Exposes the functionality of the {@link MediaPlayer} and implements the {@link PlayerAdapter}
* so that {@link MainActivity} can control music playback.
*/
public final class MediaPlayerAdapter extends PlayerAdapter implements APIClient.APIListener {
public final class MediaPlayerAdapter extends PlayerAdapter implements RtpReceiver.MetaListener {
private RadioPlayer mMediaPlayer;
private PlaybackInfoListener mPlaybackInfoListener;
......@@ -70,7 +72,9 @@ public final class MediaPlayerAdapter extends PlayerAdapter implements APIClient
private int mState;
private long mSongStart = new Date().getTime();
private final APIClient mApiClient;
String songTitle, songArtist, songCircle, songAlbum;
private Context mContext;
public MediaPlayerAdapter(Context context, PlaybackInfoListener listener) {
......@@ -80,12 +84,13 @@ public final class MediaPlayerAdapter extends PlayerAdapter implements APIClient
MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, "-1");
String songTitle = mContext.getString(R.string.unknown_song);
songTitle = mContext.getString(R.string.unknown_song);
builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, songTitle);
builder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, mContext.getString(R.string.unknown_album));
String songArtist = mContext.getString(R.string.unknown_artist);
songAlbum = mContext.getString(R.string.unknown_album);
builder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, songAlbum);
songArtist = mContext.getString(R.string.unknown_artist);
builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, songArtist);
String songCircle = mContext.getString(R.string.unknown_circle);
songCircle = mContext.getString(R.string.unknown_circle);
builder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, songCircle);
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, songTitle);
......@@ -95,8 +100,6 @@ public final class MediaPlayerAdapter extends PlayerAdapter implements APIClient
mPlaybackInfoListener.onMetadataChange(mCurrentMedia);
mApiClient = new APIClient(this);
mApiClient.connect();
}
/**
......@@ -108,7 +111,7 @@ public final class MediaPlayerAdapter extends PlayerAdapter implements APIClient
*/
private void initializeMediaPlayer() {
if (mMediaPlayer == null) {
mMediaPlayer = new RadioPlayer();
mMediaPlayer = new RadioPlayer(this);
}
}
......@@ -126,7 +129,6 @@ public final class MediaPlayerAdapter extends PlayerAdapter implements APIClient
}
private void release() {
mApiClient.disconnect();
if (mMediaPlayer != null) {
mMediaPlayer.release();
mMediaPlayer = null;
......@@ -164,8 +166,8 @@ public final class MediaPlayerAdapter extends PlayerAdapter implements APIClient
stateBuilder.setActions(getAvailableActions());
stateBuilder.setState(mState,
new Date().getTime() - mSongStart,
1.0f,
SystemClock.elapsedRealtime());
1.0f,
SystemClock.elapsedRealtime());
mPlaybackInfoListener.onPlaybackStateChange(stateBuilder.build());
}
......@@ -190,26 +192,26 @@ public final class MediaPlayerAdapter extends PlayerAdapter implements APIClient
@PlaybackStateCompat.Actions
private long getAvailableActions() {
long actions = PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID
| PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH;
| PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH;
switch (mState) {
case PlaybackStateCompat.STATE_STOPPED:
actions |= PlaybackStateCompat.ACTION_PLAY
| PlaybackStateCompat.ACTION_PAUSE;
| PlaybackStateCompat.ACTION_PAUSE;
break;
case PlaybackStateCompat.STATE_PLAYING:
actions |= PlaybackStateCompat.ACTION_STOP
| PlaybackStateCompat.ACTION_PAUSE
| PlaybackStateCompat.ACTION_SEEK_TO;
| PlaybackStateCompat.ACTION_PAUSE
| PlaybackStateCompat.ACTION_SEEK_TO;
break;
case PlaybackStateCompat.STATE_PAUSED:
actions |= PlaybackStateCompat.ACTION_PLAY
| PlaybackStateCompat.ACTION_STOP;
| PlaybackStateCompat.ACTION_STOP;
break;
default:
actions |= PlaybackStateCompat.ACTION_PLAY
| PlaybackStateCompat.ACTION_PLAY_PAUSE
| PlaybackStateCompat.ACTION_STOP
| PlaybackStateCompat.ACTION_PAUSE;
| PlaybackStateCompat.ACTION_PLAY_PAUSE
| PlaybackStateCompat.ACTION_STOP
| PlaybackStateCompat.ACTION_PAUSE;
}
return actions;
}
......@@ -222,87 +224,36 @@ public final class MediaPlayerAdapter extends PlayerAdapter implements APIClient
}
@Override
public void onProgress(JSONObject message) {
}
public void newMeta(int field, String value) {
switch (field) {
case MetaPacket
.FIELD_SONG_TITLE:
songTitle = value;
break;
case MetaPacket.FIELD_ALBUM_TITLE:
songAlbum = value;
break;
case MetaPacket.FIELD_SONG_ARTIST:
songArtist = value;
break;
case MetaPacket.FIELD_ALBUM_ARTIST:
songCircle = value;
break;
}
@Override
public void onSongInfo(JSONObject mMessage) {
MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, songTitle);
builder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, songAlbum);
builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, songArtist);
builder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, songCircle);
boolean newData = false;
StringBuilder id = new StringBuilder("-");
String songTitle = "", songArtist = "", songCircle = "";
try {
if(mMessage.has("duration")) {
double length = mMessage.getDouble("duration") * 1000;
builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, (long)length);
}
if(mMessage.has("position")) {
double position = mMessage.getDouble("position") * 1000;
mSongStart = new Date().getTime() - (long)position;
}
JSONObject array = mMessage.getJSONObject("tags");
Iterator<String> keys = array.keys();
while (keys.hasNext()) {
String key = keys.next();
switch (key) {
case "AlId":
id.insert(0,array.getString(key));
newData = true;
break;
case "Track":
id.append(array.getString(key));
newData = true;
break;
case "AlbumImage":
builder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, "https://www.touhou.fm/" + array.getString(key));
newData = true;
break;
case INFO_TITLE:
songTitle = array.getString(key);
builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, songTitle);
newData = true;
break;
case INFO_ALBUM:
builder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, array.getString(key));
newData = true;
break;
case INFO_ARTIST:
songArtist = array.getString(key);
builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, songArtist);
newData = true;
break;
case INFO_CIRCLE:
songCircle = array.getString(key);
builder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, songCircle);
newData = true;
break;
}
}
} catch (JSONException e) {
e.printStackTrace();
}
if(newData) {
builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id.toString());
builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, String.format("%s-%s-%s-%s", songTitle, songAlbum, songArtist, songCircle));
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, songTitle);
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, String.format(mContext.getString(R.string.artist_circle_format), songArtist, songCircle));
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, songTitle);
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, String.format(mContext.getString(R.string.artist_circle_format), songArtist, songCircle));
mCurrentMedia = builder.build();
mPlaybackInfoListener.onMetadataChange(mCurrentMedia);
mCurrentMedia = builder.build();
mPlaybackInfoListener.onMetadataChange(mCurrentMedia);
setNewState(mState);
}
}
}
......@@ -20,6 +20,7 @@
package fm.touhou.touhoufm.service.players;
import android.media.AudioTrack;
import android.provider.MediaStore;
import fm.touhou.touhoufm.radio.AndroidPlayer;
import fm.touhou.touhoufm.radio.RingBuffer;
......@@ -33,8 +34,8 @@ import static fm.touhou.touhoufm.radio.RtpReceiver.SAMPLE_RATE;
public class RadioPlayer {
private static final String TAG = RadioPlayer.class.getSimpleName();
public static final String STREAM_HOST = "touhou.fm";
public static final int STREAM_PORT = 1234;
public static final String STREAM_HOST = "strm.touhou.fm";
public static final int STREAM_PORT = 1235;
private RingBuffer mRingBuffer;
......@@ -43,6 +44,12 @@ public class RadioPlayer {
private int mMinBufSize = AudioTrack.getMinBufferSize(SAMPLE_RATE, CHANNEL_OUT_STEREO, ENCODING_PCM_16BIT);
RtpReceiver.MetaListener mListener;
public RadioPlayer(RtpReceiver.MetaListener listener) {
mListener = listener;
}
void startMusic() {
if (mRingBuffer == null) {
mRingBuffer = new RingBuffer(mMinBufSize * 16, mMinBufSize * 2, FRAME_SIZE * 4);
......@@ -50,7 +57,7 @@ public class RadioPlayer {
mAudioPlayer = new AndroidPlayer(mRingBuffer, mMinBufSize);
mAudioPlayer.start();
mRtpReceiver = new RtpReceiver(mRingBuffer);
mRtpReceiver = new RtpReceiver(mRingBuffer, mListener);
mRtpReceiver.start();
}
}
......
......@@ -146,7 +146,6 @@ public class MainActivity extends AppCompatActivity {
MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST));
mAlbumTextView.setText(String.format(getString(R.string.album_format), mediaMetadata.getString(
MediaMetadataCompat.METADATA_KEY_ALBUM)));
mAlbumArt.setImageURI(Uri.parse(mediaMetadata.getString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI)));
mSeekBarAudio.onMetadataChanged(mediaMetadata);
}
}
......
package fm.touhou.touhoufm.utils;
import java.nio.ByteBuffer;
import static java.lang.System.arraycopy;
public class AudioPacket {
private static final int HEADER_SIZE = 16;
private long timestamp;
private int position;
private int length;
private byte[] header;
private byte[] audio;
private AudioPacket() {
timestamp = 0;
position = 0;
length = 0;
header = new byte[HEADER_SIZE];
}
public AudioPacket(long timestamp, int position, int length) {
this();
this.timestamp = timestamp;
this.position = position;
this.length = length;
ByteBuffer buffer = ByteBuffer.wrap(header);
buffer.putLong(this.timestamp).putInt(this.position).putInt(this.length);
}
public AudioPacket(byte[] packet) {
this();
if(packet.length >= HEADER_SIZE) {
arraycopy(packet, 0, header, 0, HEADER_SIZE);
audio = new byte[packet.length - header.length];
arraycopy(packet, HEADER_SIZE ,audio, 0, packet.length - HEADER_SIZE);
ByteBuffer buffer = ByteBuffer.wrap(header);
timestamp = buffer.getLong();
position = buffer.getInt();
length = buffer.getInt();
}
}
public int getPacket(byte[] packet) {
arraycopy(header, 0, packet, 0, HEADER_SIZE);
arraycopy(audio, 0, packet, HEADER_SIZE, audio.length);
return (header.length + audio.length);
}
public void getAudio(byte[] audio) {
arraycopy(this.audio, 0, audio, 0,this.audio.length);
}
public int getAudioSize() {
return audio.length;
}
public long getTimestamp() {
return timestamp;
}
public int getPosition() {
return position;
}
public int getLength() {
return length;
}
}
package fm.touhou.touhoufm.utils;
import static java.lang.System.arraycopy;
public class BasePacket {
private static final int HEADER_SIZE = 4;
public static final int VERSION_1_0 = 0;
public static final int SYSTEM_SESSION = (0);
public static final int SYSTEM_AUDIO = (1);
public static final int TYPE_NONE=(0);
public static final int TYPE_AUDIO_OPUS=(1);
public static final int TYPE_META=(2);
private int version;
private int system;
private int type;
private int seq;
private byte[] header;
private byte[] payload;
private BasePacket() {
version = VERSION_1_0;
system = SYSTEM_SESSION;
type = TYPE_NONE;
seq = 0;
header = new byte[HEADER_SIZE];
}
public BasePacket(int v, int s, int t, int seq, byte[] data) {
this();
version = v;
system = s;
type = t;
this.seq = seq;
this.payload = data;
header[0] = (byte)(version << 4 | system & 0x0F);
header[1] = (byte)(type);
header[2] = (byte)(this.seq >> 8);
header[3] = (byte)(this.seq & 0xFF);
payload = new byte[data.length];
arraycopy(data, 0, payload, 0, payload.length);
}
public BasePacket(byte[] packet, int length) {
this();
if(packet.length >= HEADER_SIZE) {
arraycopy(packet, 0, header, 0, HEADER_SIZE);