From cf3a31200119efe96ede1293cb4e71b1e32c0396 Mon Sep 17 00:00:00 2001 From: Henner Date: Sat, 24 Apr 2021 22:34:47 +0200 Subject: [PATCH 1/2] Make Sink work --- app/build.gradle | 3 ++ app/src/main/AndroidManifest.xml | 1 + .../librespot/android/MainActivity.java | 39 +++++++++++++--- gradlew | 0 .../android/sink/AndroidSinkOutput.java | 45 +++++++++++++++---- 5 files changed, 72 insertions(+), 16 deletions(-) mode change 100644 => 100755 gradlew diff --git a/app/build.gradle b/app/build.gradle index 729c218..1ae1817 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -42,4 +42,7 @@ dependencies { exclude group: 'org.apache.logging.log4j', module: 'log4j-slf4j-impl' exclude group: 'com.lmax', module: 'disruptor' } + + implementation project(':librespot-android-sink') + implementation 'uk.uuid.slf4j:slf4j-android:1.7.30-0' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4cfd0d7..4f30c91 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -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"> diff --git a/app/src/main/java/xyz/gianlu/librespot/android/MainActivity.java b/app/src/main/java/xyz/gianlu/librespot/android/MainActivity.java index efc4166..69a8b78 100644 --- a/app/src/main/java/xyz/gianlu/librespot/android/MainActivity.java +++ b/app/src/main/java/xyz/gianlu/librespot/android/MainActivity.java @@ -1,32 +1,57 @@ 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.core.Session; import xyz.gianlu.librespot.mercury.MercuryClient; +import xyz.gianlu.librespot.player.Player; +import xyz.gianlu.librespot.player.PlayerConfiguration; public class MainActivity extends AppCompatActivity { + private static final String TAG = "Main"; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); new Thread(() -> { + Session session; try { - Session session = new Session.Builder() - .userPass("test123", "test123") - .create(); - - System.out.println("Logged in as: " + session.apWelcome().getCanonicalUsername()); - } catch (IOException | GeneralSecurityException | Session.SpotifyAuthenticationException | MercuryClient.MercuryException ex) { - ex.printStackTrace(); + 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(); + + 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); + player.load("spotify:album:5m4VYOPoIpkV0XgOiRKkWC", true, false); + }).start(); } } diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/librespot-android-sink/src/main/java/xyz/gianlu/librespot/android/sink/AndroidSinkOutput.java b/librespot-android-sink/src/main/java/xyz/gianlu/librespot/android/sink/AndroidSinkOutput.java index e71641c..d79db92 100644 --- a/librespot-android-sink/src/main/java/xyz/gianlu/librespot/android/sink/AndroidSinkOutput.java +++ b/librespot-android-sink/src/main/java/xyz/gianlu/librespot/android/sink/AndroidSinkOutput.java @@ -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 From 7cd36974363b9668af5109b2da52617d1485e5d2 Mon Sep 17 00:00:00 2001 From: Gianlu Date: Sun, 25 Apr 2021 10:42:17 +0200 Subject: [PATCH 2/2] Working with Tremolo decoder --- .../librespot/android/MainActivity.java | 8 +++ .../tremolo/OggDecodingInputStream.java | 49 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/app/src/main/java/xyz/gianlu/librespot/android/MainActivity.java b/app/src/main/java/xyz/gianlu/librespot/android/MainActivity.java index 61e00d8..b381349 100644 --- a/app/src/main/java/xyz/gianlu/librespot/android/MainActivity.java +++ b/app/src/main/java/xyz/gianlu/librespot/android/MainActivity.java @@ -51,12 +51,20 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { 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(); diff --git a/librespot-android-decoder-tremolo/src/main/java/xyz/gianlu/librespot/player/codecs/tremolo/OggDecodingInputStream.java b/librespot-android-decoder-tremolo/src/main/java/xyz/gianlu/librespot/player/codecs/tremolo/OggDecodingInputStream.java index 592b47a..aab36c2 100644 --- a/librespot-android-decoder-tremolo/src/main/java/xyz/gianlu/librespot/player/codecs/tremolo/OggDecodingInputStream.java +++ b/librespot-android-decoder-tremolo/src/main/java/xyz/gianlu/librespot/player/codecs/tremolo/OggDecodingInputStream.java @@ -1,5 +1,7 @@ package xyz.gianlu.librespot.player.codecs.tremolo; +import android.util.Log; + import org.jetbrains.annotations.NotNull; import java.io.IOException; @@ -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"); @@ -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();