Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,7 @@ dependencies {
exclude group: 'org.apache.logging.log4j'
}

implementation project(path: ':librespot-android-decoder-tremolo')
implementation project(':librespot-android-decoder-tremolo')
implementation project(':librespot-android-sink')
implementation 'uk.uuid.slf4j:slf4j-android:1.7.30-0'
}
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:usesCleartextTraffic="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Librespotandroid">
Expand Down
45 changes: 39 additions & 6 deletions app/src/main/java/xyz/gianlu/librespot/android/MainActivity.java
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
package xyz.gianlu.librespot.android;

import android.os.Bundle;
import android.util.Log;

import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;

import com.spotify.connectstate.Connect;

import java.io.IOException;
import java.security.GeneralSecurityException;

import xyz.gianlu.librespot.audio.format.SuperAudioFormat;
import xyz.gianlu.librespot.core.Session;
import xyz.gianlu.librespot.mercury.MercuryClient;
import xyz.gianlu.librespot.player.Player;
import xyz.gianlu.librespot.player.PlayerConfiguration;
import xyz.gianlu.librespot.player.codecs.Codecs;
import xyz.gianlu.librespot.player.codecs.TremoloVorbisCodec;

public class MainActivity extends AppCompatActivity {
private static final String TAG = "Main";

static {
Codecs.replaceCodecs(SuperAudioFormat.VORBIS, TremoloVorbisCodec.class);
Expand All @@ -25,15 +31,42 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

new Thread(() -> {
Session session;
try {
Session session = new Session.Builder()
.userPass("test123", "test123")
.create();
Session.Configuration conf = new Session.Configuration.Builder()
.setStoreCredentials(false)
.setCacheEnabled(false).build();
session = new Session.Builder(conf)
.setPreferredLocale("en")
.setDeviceType(Connect.DeviceType.SMARTPHONE)
.setDeviceName("librespot-java")
.userPass("user", "password")
.setDeviceId(null).create();

System.out.println("Logged in as: " + session.apWelcome().getCanonicalUsername());
} catch (IOException | GeneralSecurityException | Session.SpotifyAuthenticationException | MercuryClient.MercuryException ex) {
ex.printStackTrace();
Log.i(TAG, "Logged in as: " + session.apWelcome().getCanonicalUsername());
} catch (IOException |
GeneralSecurityException |
Session.SpotifyAuthenticationException |
MercuryClient.MercuryException ex) {
Log.e(TAG, "Session creation failed: ", ex);
return;
}

PlayerConfiguration configuration = new PlayerConfiguration.Builder()
.setOutput(PlayerConfiguration.AudioOutput.CUSTOM)
.setOutputClass("xyz.gianlu.librespot.android.sink.AndroidSinkOutput")
.setOutputClassParams(new String[0])
.build();

Player player = new Player(configuration, session);
try {
player.waitReady();
} catch (InterruptedException ex) {
return;
}

player.load("spotify:album:5m4VYOPoIpkV0XgOiRKkWC", true, false);

}).start();
}
}
Empty file modified gradlew
100644 → 100755
Empty file.
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package xyz.gianlu.librespot.player.codecs.tremolo;

import android.util.Log;

import org.jetbrains.annotations.NotNull;

import java.io.IOException;
Expand All @@ -9,8 +11,13 @@
/**
* Created by M. Lehmann on 15.11.2016.
*/
@SuppressWarnings("unused")
public class OggDecodingInputStream extends InputStream {
private static final int BUFFER_SIZE = 4096;
private static final int SEEK_SET = 0;
private static final int SEEK_CUR = 1;
private static final int SEEK_END = 2;
private final static String TAG = OggDecodingInputStream.class.getName();

static {
System.loadLibrary("tremolo");
Expand Down Expand Up @@ -47,6 +54,48 @@ public OggDecodingInputStream(@NotNull SeekableInputStream oggInputStream) throw

private native void close(long handle);

private int writeOgg(int size) {
byte[] bytes = new byte[Math.min(size, BUFFER_SIZE)];
try {
int read = oggInputStream.read(bytes);
if (read > -1) {
jniBuffer.put(bytes);
jniBuffer.flip();
return read;
}

return 0;
} catch (Exception ex) {
Log.e(TAG, "Internal writeOgg failed.", ex);
return -1;
}
}

private int seekOgg(long offset, int whence) {
try {
if (whence == SEEK_SET)
oggInputStream.seek(offset);
else if (whence == SEEK_CUR)
oggInputStream.seek(oggInputStream.tell() + offset);
else if (whence == SEEK_END)
oggInputStream.seek(oggInputStream.length() + offset);

return 0;
} catch (Exception ex) {
Log.e(TAG, "Internal seekOgg failed.", ex);
return -1;
}
}

private int tellOgg() {
try {
return (int) oggInputStream.tell();
} catch (Exception ex) {
Log.e(TAG, "Internal tellOgg failed.", ex);
return -1;
}
}

@Override
public synchronized int read() {
jniBuffer.clear();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,51 @@ public final class AndroidSinkOutput implements SinkOutput {
private AudioTrack track;
private float lastVolume = -1;

public AndroidSinkOutput() {
}

@Override
public boolean start(@NotNull OutputAudioFormat format) throws SinkException {
AudioTrack.Builder builder = new AudioTrack.Builder();
builder.setAudioFormat(new AudioFormat.Builder()
.setSampleRate((int) format.getSampleRate())
.build());
int pcmEncoding = format.getSampleSizeInBits() == 16 ? AudioFormat.ENCODING_PCM_16BIT : AudioFormat.ENCODING_PCM_FLOAT;
int channelConfig = format.getChannels() == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO;
int sampleRate = (int) format.getSampleRate();
int minBufferSize = AudioTrack.getMinBufferSize(
sampleRate,
channelConfig,
pcmEncoding
);

AudioFormat audioFormat = new AudioFormat.Builder()
.setEncoding(pcmEncoding)
.setSampleRate(sampleRate)
.build();

try {
track = new AudioTrack.Builder()
.setBufferSizeInBytes(minBufferSize)
.setAudioFormat(audioFormat)
.setTransferMode(AudioTrack.MODE_STREAM)
.build();
} catch (UnsupportedOperationException e) {
throw new SinkException("AudioTrack creation failed in Sink: ", e.getCause());
}

track = builder.build();
if (lastVolume != -1) track.setVolume(lastVolume);
return true;
}

@Override
public void write(byte[] buffer, int offset, int len) throws IOException {
track.write(buffer, offset, len, AudioTrack.WRITE_BLOCKING);
int outcome = track.write(buffer, offset, len, AudioTrack.WRITE_BLOCKING);
switch (outcome) {
case AudioTrack.ERROR:
throw new IOException("Generic Operation Failure while writing Track");
case AudioTrack.ERROR_BAD_VALUE:
throw new IOException("Invalid value used while writing Track");
case AudioTrack.ERROR_DEAD_OBJECT:
throw new IOException("Track Object has died in the meantime");
case AudioTrack.ERROR_INVALID_OPERATION:
throw new IOException("Failure due to improper use of Track Object methods");
default:
track.play();
}
}

@Override
Expand Down