aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorScott Jackson <daneren2005@gmail.com>2014-09-27 16:47:47 -0700
committerScott Jackson <daneren2005@gmail.com>2014-09-27 16:47:47 -0700
commit11810a9a0b0a041916faa8bfd07b0b7bd4533583 (patch)
treec6c77babf96ac0d09cec823555efe64b33cdd48e
parent5d5a0e607c753ffe32b4d0288d391ac3c56d9df8 (diff)
parentd3dd5cc574fe306e473199572dce4e86062476f9 (diff)
downloaddsub-11810a9a0b0a041916faa8bfd07b0b7bd4533583.tar.gz
dsub-11810a9a0b0a041916faa8bfd07b0b7bd4533583.tar.bz2
dsub-11810a9a0b0a041916faa8bfd07b0b7bd4533583.zip
Merge branch 'ReplayGain'
-rw-r--r--res/layout/seekbar_preference.xml18
-rw-r--r--res/values/strings.xml4
-rw-r--r--res/xml/settings.xml30
-rw-r--r--src/github/daneren2005/dsub/activity/SettingsActivity.java19
-rw-r--r--src/github/daneren2005/dsub/service/DownloadService.java87
-rw-r--r--src/github/daneren2005/dsub/util/Constants.java3
-rw-r--r--src/github/daneren2005/dsub/util/tags/Bastp.java85
-rw-r--r--src/github/daneren2005/dsub/util/tags/BastpUtil.java73
-rw-r--r--src/github/daneren2005/dsub/util/tags/Common.java111
-rw-r--r--src/github/daneren2005/dsub/util/tags/FlacFile.java85
-rw-r--r--src/github/daneren2005/dsub/util/tags/ID3v2File.java148
-rw-r--r--src/github/daneren2005/dsub/util/tags/LameHeader.java70
-rw-r--r--src/github/daneren2005/dsub/util/tags/OggFile.java114
-rw-r--r--src/github/daneren2005/dsub/view/SeekBarPreference.java137
14 files changed, 981 insertions, 3 deletions
diff --git a/res/layout/seekbar_preference.xml b/res/layout/seekbar_preference.xml
new file mode 100644
index 00000000..4d3bc877
--- /dev/null
+++ b/res/layout/seekbar_preference.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/value"
+ android:padding="5dip"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:gravity="right" />
+ <SeekBar
+ android:id="@+id/seek_bar"
+ android:padding="15dip"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content" />
+</LinearLayout>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 009e369b..b00f66bc 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -429,6 +429,10 @@
<string name="settings.large_album_art_summary">Display albums with large album art instead of in a list</string>
<string name="settings.admin_enabled">Admin Enabled</string>
<string name="settings.admin_enabled_summary">Whether or not to display the admin listing in the pull out drawer</string>
+ <string name="settings.replay_gain">Replay Gain</string>
+ <string name="settings.replay_gain_summary">Whether or not to scale playback volume by track and album replay gain tags</string>
+ <string name="settings.replay_gain_bump">Replay Gain Pre-amp</string>
+ <string name="settings.replay_gain_untagged">Songs without Replay Gain</string>
<string name="shuffle.title">Shuffle By</string>
<string name="shuffle.startYear">Start Year:</string>
diff --git a/res/xml/settings.xml b/res/xml/settings.xml
index bafb5f6c..553fe710 100644
--- a/res/xml/settings.xml
+++ b/res/xml/settings.xml
@@ -355,6 +355,30 @@
</PreferenceCategory>
<PreferenceCategory
+ android:title="@string/settings.replay_gain">
+
+ <CheckBoxPreference
+ android:title="@string/settings.replay_gain"
+ android:summary="@string/settings.replay_gain_summary"
+ android:key="replayGain"
+ android:defaultValue="false"/>
+
+ <github.daneren2005.dsub.view.SeekBarPreference
+ android:key="replayGainBump2"
+ android:negativeButtonText="@null"
+ android:dialogLayout="@layout/seekbar_preference"
+ android:title="@string/settings.replay_gain_bump"
+ android:defaultValue="75"/>
+
+ <github.daneren2005.dsub.view.SeekBarPreference
+ android:key="replayGainUntagged2"
+ android:negativeButtonText="@null"
+ android:dialogLayout="@layout/seekbar_preference"
+ android:title="@string/settings.replay_gain_untagged"
+ android:defaultValue="150"/>
+ </PreferenceCategory>
+
+ <PreferenceCategory
android:title="@string/settings.other_title">
<CheckBoxPreference
@@ -370,6 +394,12 @@
android:defaultValue="true"/>
<CheckBoxPreference
+ android:title="@string/settings.replay_gain"
+ android:summary="@string/settings.replay_gain_summary"
+ android:key="replayGain"
+ android:defaultValue="false"/>
+
+ <CheckBoxPreference
android:title="@string/settings.gapless_playback"
android:summary="@string/settings.gapless_playback_summary"
android:key="gaplessPlayback"
diff --git a/src/github/daneren2005/dsub/activity/SettingsActivity.java b/src/github/daneren2005/dsub/activity/SettingsActivity.java
index e40a62d2..5ca30f62 100644
--- a/src/github/daneren2005/dsub/activity/SettingsActivity.java
+++ b/src/github/daneren2005/dsub/activity/SettingsActivity.java
@@ -86,6 +86,9 @@ public class SettingsActivity extends PreferenceActivity implements SharedPrefer
private CheckBoxPreference syncNotification;
private CheckBoxPreference syncStarred;
private CheckBoxPreference syncMostRecent;
+ private CheckBoxPreference replayGain;
+ private Preference replayGainBump;
+ private Preference replayGainUntagged;
private String internalSSID;
private int serverCount = 3;
@@ -127,6 +130,9 @@ public class SettingsActivity extends PreferenceActivity implements SharedPrefer
syncNotification = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_SYNC_NOTIFICATION);
syncStarred = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_SYNC_STARRED);
syncMostRecent = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_SYNC_MOST_RECENT);
+ replayGain = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_REPLAY_GAIN);
+ replayGainBump = (Preference) findPreference(Constants.PREFERENCES_KEY_REPLAY_GAIN_BUMP);
+ replayGainUntagged = (Preference) findPreference(Constants.PREFERENCES_KEY_REPLAY_GAIN_UNTAGGED);
settings = Util.getPreferences(this);
serverCount = settings.getInt(Constants.PREFERENCES_KEY_SERVER_COUNT, 1);
@@ -267,6 +273,11 @@ public class SettingsActivity extends PreferenceActivity implements SharedPrefer
}
else if(Constants.PREFERENCES_KEY_SYNC_MOST_RECENT.equals(key)) {
SyncUtil.removeMostRecentSyncFiles(this);
+ } else if(Constants.PREFERENCES_KEY_REPLAY_GAIN.equals(key) || Constants.PREFERENCES_KEY_REPLAY_GAIN_BUMP.equals(key) || Constants.PREFERENCES_KEY_REPLAY_GAIN_UNTAGGED.equals(key)) {
+ DownloadService downloadService = DownloadService.getInstance();
+ if(downloadService != null) {
+ downloadService.reapplyVolume();
+ }
}
scheduleBackup();
@@ -326,6 +337,14 @@ public class SettingsActivity extends PreferenceActivity implements SharedPrefer
syncMostRecent.setEnabled(false);
}
}
+ if(replayGain.isChecked()) {
+ replayGainBump.setEnabled(true);
+ replayGainUntagged.setEnabled(true);
+ } else {
+ replayGainBump.setEnabled(false);
+ replayGainUntagged.setEnabled(false);
+ }
+
for (ServerSettings ss : serverSettings.values()) {
ss.update();
}
diff --git a/src/github/daneren2005/dsub/service/DownloadService.java b/src/github/daneren2005/dsub/service/DownloadService.java
index 798b4541..44d63ba3 100644
--- a/src/github/daneren2005/dsub/service/DownloadService.java
+++ b/src/github/daneren2005/dsub/service/DownloadService.java
@@ -48,10 +48,13 @@ import github.daneren2005.dsub.util.ShufflePlayBuffer;
import github.daneren2005.dsub.util.SimpleServiceBinder;
import github.daneren2005.dsub.util.Util;
import github.daneren2005.dsub.util.compat.RemoteControlClientHelper;
+import github.daneren2005.dsub.util.tags.BastpUtil;
import github.daneren2005.dsub.view.UpdateView;
import github.daneren2005.serverproxy.BufferProxy;
import java.io.File;
+import java.io.IOError;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
@@ -134,6 +137,9 @@ public class DownloadService extends Service {
private boolean keepScreenOn;
private int cachedPosition = 0;
private boolean downloadOngoing = false;
+ private float volume = 1.0f;
+ private boolean singleAlbum = false;
+ private String singleAlbumName;
private AudioEffectsController effectsController;
private RemoteControlState remoteState = RemoteControlState.LOCAL;
@@ -312,7 +318,7 @@ public class DownloadService extends Service {
for (MusicDirectory.Entry song : songs) {
if(song != null) {
DownloadFile downloadFile = new DownloadFile(this, song, save);
- downloadList.add(getCurrentPlayingIndex() + offset, downloadFile);
+ addToDownloadList(downloadFile, getCurrentPlayingIndex() + offset);
offset++;
}
}
@@ -323,7 +329,7 @@ public class DownloadService extends Service {
int index = getCurrentPlayingIndex();
for (MusicDirectory.Entry song : songs) {
DownloadFile downloadFile = new DownloadFile(this, song, save);
- downloadList.add(downloadFile);
+ addToDownloadList(downloadFile, -1);
}
if(!autoplay && (size - 1) == index) {
setNextPlaying();
@@ -350,6 +356,27 @@ public class DownloadService extends Service {
}
lifecycleSupport.serializeDownloadQueue();
}
+ private void addToDownloadList(DownloadFile file, int offset) {
+ if(offset == -1) {
+ downloadList.add(file);
+ } else {
+ downloadList.add(offset, file);
+ }
+
+ // Check if we are still dealing with a single album
+ // Don't bother with check if it is already false
+ if(singleAlbum) {
+ // If first download, set album to it
+ if(singleAlbumName == null) {
+ singleAlbumName = file.getSong().getAlbum();
+ } else {
+ // Otherwise, check again previous album name
+ if(!singleAlbumName.equals(file.getSong().getAlbum())) {
+ singleAlbum = false;
+ }
+ }
+ }
+ }
public synchronized void downloadBackground(List<MusicDirectory.Entry> songs, boolean save) {
for (MusicDirectory.Entry song : songs) {
DownloadFile downloadFile = new DownloadFile(this, song, save);
@@ -612,6 +639,8 @@ public class DownloadService extends Service {
suggestedPlaylistName = null;
suggestedPlaylistId = null;
+ singleAlbum = true;
+ singleAlbumName = null;
}
public synchronized void remove(int which) {
@@ -1417,6 +1446,8 @@ public class DownloadService extends Service {
}
cachedPosition = position;
+ applyReplayGain(mediaPlayer, downloadFile);
+
if (start || autoPlayStart) {
mediaPlayer.start();
setPlayerState(STARTED);
@@ -1475,6 +1506,8 @@ public class DownloadService extends Service {
mediaPlayer.setNextMediaPlayer(nextMediaPlayer);
nextSetup = true;
}
+
+ applyReplayGain(nextMediaPlayer, downloadFile);
} catch (Exception x) {
handleErrorNext(x);
}
@@ -1594,12 +1627,16 @@ public class DownloadService extends Service {
public void setVolume(float volume) {
if(mediaPlayer != null && (playerState == STARTED || playerState == PAUSED || playerState == STOPPED)) {
try {
- mediaPlayer.setVolume(volume, volume);
+ this.volume = volume;
+ reapplyVolume();
} catch(Exception e) {
Log.w(TAG, "Failed to set volume");
}
}
}
+ public void reapplyVolume() {
+ applyReplayGain(mediaPlayer, currentPlaying);
+ }
public synchronized void swap(boolean mainList, int from, int to) {
List<DownloadFile> list = mainList ? downloadList : backgroundDownloadList;
@@ -1784,6 +1821,7 @@ public class DownloadService extends Service {
}
}
currentPlayingIndex = downloadList.indexOf(currentPlaying);
+ singleAlbum = false;
if (revisionBefore != revision) {
updateJukeboxPlaylist();
@@ -1924,6 +1962,49 @@ public class DownloadService extends Service {
}
}
+ private void applyReplayGain(MediaPlayer mediaPlayer, DownloadFile downloadFile) {
+ if(currentPlaying == null) {
+ return;
+ }
+
+ SharedPreferences prefs = Util.getPreferences(this);
+ try {
+ float[] rg = BastpUtil.getReplayGainValues(downloadFile.getFile().getCanonicalPath()); /* track, album */
+ float adjust = 0f;
+ if (prefs.getBoolean(Constants.PREFERENCES_KEY_REPLAY_GAIN, false)) {
+ // If playing a single album or no track gain, use album gain
+ if((singleAlbum || rg[0] == 0) && rg[1] != 0) {
+ adjust = rg[1];
+ } else {
+ // Otherwise, give priority to track gain
+ adjust = rg[0];
+ }
+
+ if (adjust == 0) {
+ /* No RG value found: decrease volume for untagged song if requested by user */
+ int untagged = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_REPLAY_GAIN_UNTAGGED, "0"));
+ adjust = (untagged - 150) / 10f;
+ } else {
+ /* This song has some replay gain info, we are now going to apply the 'bump' value
+ ** The preferences stores the raw value of the seekbar, that's 0-150
+ ** But we want -15 <-> +15, so 75 shall be zero */
+ int bump = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_REPLAY_GAIN_BUMP, "0"));
+ adjust += 2 * (bump - 75) / 10f;
+ }
+ }
+
+ float rg_result = ((float) Math.pow(10, (adjust / 20))) * volume;
+ if (rg_result > 1.0f) {
+ rg_result = 1.0f; /* android would IGNORE the change if this is > 1 and we would end up with the wrong volume */
+ } else if (rg_result < 0.0f) {
+ rg_result = 0.0f;
+ }
+ mediaPlayer.setVolume(rg_result, rg_result);
+ } catch(IOException e) {
+ Log.w(TAG, "Failed to apply replay gain values", e);
+ }
+ }
+
private class BufferTask extends SilentBackgroundTask<Void> {
private final DownloadFile downloadFile;
private final int position;
diff --git a/src/github/daneren2005/dsub/util/Constants.java b/src/github/daneren2005/dsub/util/Constants.java
index a61102fe..30dc33b4 100644
--- a/src/github/daneren2005/dsub/util/Constants.java
+++ b/src/github/daneren2005/dsub/util/Constants.java
@@ -144,6 +144,9 @@ public final class Constants {
public static final String PREFERENCES_KEY_SERVER_SYNC = "serverSync";
public static final String PREFERENCES_KEY_RECENT_COUNT = "mostRecentCount";
public static final String PREFERENCES_KEY_MENU_RATING = "showRating";
+ public static final String PREFERENCES_KEY_REPLAY_GAIN = "replayGain";
+ public static final String PREFERENCES_KEY_REPLAY_GAIN_BUMP = "replayGainBump2";
+ public static final String PREFERENCES_KEY_REPLAY_GAIN_UNTAGGED = "replayGainUntagged2";
public static final String OFFLINE_SCROBBLE_COUNT = "scrobbleCount";
public static final String OFFLINE_SCROBBLE_ID = "scrobbleID";
diff --git a/src/github/daneren2005/dsub/util/tags/Bastp.java b/src/github/daneren2005/dsub/util/tags/Bastp.java
new file mode 100644
index 00000000..aa0a2e25
--- /dev/null
+++ b/src/github/daneren2005/dsub/util/tags/Bastp.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2013 Adrian Ulrich <adrian@blinkenlights.ch>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+package github.daneren2005.dsub.util.tags;
+
+import java.io.RandomAccessFile;
+import java.io.IOException;
+import java.util.HashMap;
+
+
+public class Bastp {
+
+ public Bastp() {
+ }
+
+ public HashMap getTags(String fname) {
+ HashMap tags = new HashMap();
+ try {
+ RandomAccessFile ra = new RandomAccessFile(fname, "r");
+ tags = getTags(ra);
+ ra.close();
+ }
+ catch(Exception e) {
+ /* we dont' care much: SOMETHING went wrong. d'oh! */
+ }
+
+ return tags;
+ }
+
+ public HashMap getTags(RandomAccessFile s) {
+ HashMap tags = new HashMap();
+ byte[] file_ff = new byte[4];
+
+ try {
+ s.read(file_ff);
+ String magic = new String(file_ff);
+ if(magic.equals("fLaC")) {
+ tags = (new FlacFile()).getTags(s);
+ }
+ else if(magic.equals("OggS")) {
+ tags = (new OggFile()).getTags(s);
+ }
+ else if(file_ff[0] == -1 && file_ff[1] == -5) { /* aka 0xfffb in real languages */
+ tags = (new LameHeader()).getTags(s);
+ }
+ else if(magic.substring(0,3).equals("ID3")) {
+ tags = (new ID3v2File()).getTags(s);
+ if(tags.containsKey("_hdrlen")) {
+ Long hlen = Long.parseLong( tags.get("_hdrlen").toString(), 10 );
+ HashMap lameInfo = (new LameHeader()).parseLameHeader(s, hlen);
+ /* add gain tags if not already present */
+ inheritTag("REPLAYGAIN_TRACK_GAIN", lameInfo, tags);
+ inheritTag("REPLAYGAIN_ALBUM_GAIN", lameInfo, tags);
+ }
+ }
+ tags.put("_magic", magic);
+ }
+ catch (IOException e) {
+ }
+ return tags;
+ }
+
+ private void inheritTag(String key, HashMap from, HashMap to) {
+ if(!to.containsKey(key) && from.containsKey(key)) {
+ to.put(key, from.get(key));
+ }
+ }
+
+}
+
diff --git a/src/github/daneren2005/dsub/util/tags/BastpUtil.java b/src/github/daneren2005/dsub/util/tags/BastpUtil.java
new file mode 100644
index 00000000..a738bbce
--- /dev/null
+++ b/src/github/daneren2005/dsub/util/tags/BastpUtil.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2013 Adrian Ulrich <adrian@blinkenlights.ch>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package github.daneren2005.dsub.util.tags;
+
+import android.util.LruCache;
+import java.util.HashMap;
+import java.util.Vector;
+
+public final class BastpUtil {
+ private static final RGLruCache rgCache = new RGLruCache(16);
+
+ /** Returns the ReplayGain values of 'path' as <track,album>
+ */
+ public static float[] getReplayGainValues(String path) {
+ float[] cached = rgCache.get(path);
+
+ if(cached == null) {
+ cached = getReplayGainValuesFromFile(path);
+ rgCache.put(path, cached);
+ }
+ return cached;
+ }
+
+
+
+ /** Parse given file and return track,album replay gain values
+ */
+ private static float[] getReplayGainValuesFromFile(String path) {
+ String[] keys = { "REPLAYGAIN_TRACK_GAIN", "REPLAYGAIN_ALBUM_GAIN" };
+ float[] adjust= { 0f , 0f };
+ HashMap tags = (new Bastp()).getTags(path);
+
+ for (int i=0; i<keys.length; i++) {
+ String curKey = keys[i];
+ if(tags.containsKey(curKey)) {
+ String rg_raw = (String)((Vector)tags.get(curKey)).get(0);
+ String rg_numonly = "";
+ float rg_float = 0f;
+ try {
+ String nums = rg_raw.replaceAll("[^0-9.-]","");
+ rg_float = Float.parseFloat(nums);
+ } catch(Exception e) {}
+ adjust[i] = rg_float;
+ }
+ }
+ return adjust;
+ }
+
+ /** LRU cache for ReplayGain values
+ */
+ private static class RGLruCache extends LruCache<String, float[]> {
+ public RGLruCache(int size) {
+ super(size);
+ }
+ }
+
+}
+
diff --git a/src/github/daneren2005/dsub/util/tags/Common.java b/src/github/daneren2005/dsub/util/tags/Common.java
new file mode 100644
index 00000000..51344d90
--- /dev/null
+++ b/src/github/daneren2005/dsub/util/tags/Common.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2013 Adrian Ulrich <adrian@blinkenlights.ch>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+package github.daneren2005.dsub.util.tags;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.HashMap;
+import java.util.Vector;
+
+public class Common {
+ private static final long MAX_PKT_SIZE = 524288;
+
+ public void xdie(String reason) throws IOException {
+ throw new IOException(reason);
+ }
+
+ /*
+ ** Returns a 32bit int from given byte offset in LE
+ */
+ public int b2le32(byte[] b, int off) {
+ int r = 0;
+ for(int i=0; i<4; i++) {
+ r |= ( b2u(b[off+i]) << (8*i) );
+ }
+ return r;
+ }
+
+ public int b2be32(byte[] b, int off) {
+ return swap32(b2le32(b, off));
+ }
+
+ public int swap32(int i) {
+ return((i&0xff)<<24)+((i&0xff00)<<8)+((i&0xff0000)>>8)+((i>>24)&0xff);
+ }
+
+ /*
+ ** convert 'byte' value into unsigned int
+ */
+ public int b2u(byte x) {
+ return (x & 0xFF);
+ }
+
+ /*
+ ** Printout debug message to STDOUT
+ */
+ public void debug(String s) {
+ System.out.println("DBUG "+s);
+ }
+
+ public HashMap parse_vorbis_comment(RandomAccessFile s, long offset, long payload_len) throws IOException {
+ HashMap tags = new HashMap();
+ int comments = 0; // number of found comments
+ int xoff = 0; // offset within 'scratch'
+ int can_read = (int)(payload_len > MAX_PKT_SIZE ? MAX_PKT_SIZE : payload_len);
+ byte[] scratch = new byte[can_read];
+
+ // seek to given position and slurp in the payload
+ s.seek(offset);
+ s.read(scratch);
+
+ // skip vendor string in format: [LEN][VENDOR_STRING]
+ xoff += 4 + b2le32(scratch, xoff); // 4 = LEN = 32bit int
+ comments = b2le32(scratch, xoff);
+ xoff += 4;
+
+ // debug("comments count = "+comments);
+ for(int i=0; i<comments; i++) {
+
+ int clen = (int)b2le32(scratch, xoff);
+ xoff += 4+clen;
+
+ if(xoff > scratch.length)
+ xdie("string out of bounds");
+
+ String tag_raw = new String(scratch, xoff-clen, clen);
+ String[] tag_vec = tag_raw.split("=",2);
+ String tag_key = tag_vec[0].toUpperCase();
+
+ addTagEntry(tags, tag_key, tag_vec[1]);
+ }
+ return tags;
+ }
+
+ public void addTagEntry(HashMap tags, String key, String value) {
+ if(tags.containsKey(key)) {
+ ((Vector)tags.get(key)).add(value); // just add to existing vector
+ }
+ else {
+ Vector vx = new Vector();
+ vx.add(value);
+ tags.put(key, vx);
+ }
+ }
+
+}
diff --git a/src/github/daneren2005/dsub/util/tags/FlacFile.java b/src/github/daneren2005/dsub/util/tags/FlacFile.java
new file mode 100644
index 00000000..de3584d1
--- /dev/null
+++ b/src/github/daneren2005/dsub/util/tags/FlacFile.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2013 Adrian Ulrich <adrian@blinkenlights.ch>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package github.daneren2005.dsub.util.tags;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.HashMap;
+import java.util.Enumeration;
+
+
+public class FlacFile extends Common {
+ private static final int FLAC_TYPE_COMMENT = 4; // ID of 'VorbisComment's
+
+ public FlacFile() {
+ }
+
+ public HashMap getTags(RandomAccessFile s) throws IOException {
+ int xoff = 4; // skip file magic
+ int retry = 64;
+ int r[];
+ HashMap tags = new HashMap();
+
+ for(; retry > 0; retry--) {
+ r = parse_metadata_block(s, xoff);
+
+ if(r[2] == FLAC_TYPE_COMMENT) {
+ tags = parse_vorbis_comment(s, xoff+r[0], r[1]);
+ break;
+ }
+
+ if(r[3] != 0)
+ break; // eof reached
+
+ // else: calculate next offset
+ xoff += r[0] + r[1];
+ }
+ return tags;
+ }
+
+ /* Parses the metadata block at 'offset' and returns
+ ** [header_size, payload_size, type, stop_after]
+ */
+ private int[] parse_metadata_block(RandomAccessFile s, long offset) throws IOException {
+ int[] result = new int[4];
+ byte[] mb_head = new byte[4];
+ int stop_after = 0;
+ int block_type = 0;
+ int block_size = 0;
+
+ s.seek(offset);
+
+ if( s.read(mb_head) != 4 )
+ xdie("failed to read metadata block header");
+
+ block_size = b2be32(mb_head,0); // read whole header as 32 big endian
+ block_type = (block_size >> 24) & 127; // BIT 1-7 are the type
+ stop_after = (((block_size >> 24) & 128) > 0 ? 1 : 0 ); // BIT 0 indicates the last-block flag
+ block_size = (block_size & 0x00FFFFFF); // byte 1-7 are the size
+
+ // debug("size="+block_size+", type="+block_type+", is_last="+stop_after);
+
+ result[0] = 4; // hardcoded - only returned to be consistent with OGG parser
+ result[1] = block_size;
+ result[2] = block_type;
+ result[3] = stop_after;
+
+ return result;
+ }
+
+}
diff --git a/src/github/daneren2005/dsub/util/tags/ID3v2File.java b/src/github/daneren2005/dsub/util/tags/ID3v2File.java
new file mode 100644
index 00000000..7710654e
--- /dev/null
+++ b/src/github/daneren2005/dsub/util/tags/ID3v2File.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2013 Adrian Ulrich <adrian@blinkenlights.ch>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package github.daneren2005.dsub.util.tags;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.HashMap;
+import java.util.Enumeration;
+
+
+
+public class ID3v2File extends Common {
+ private static int ID3_ENC_LATIN = 0x00;
+ private static int ID3_ENC_UTF16LE = 0x01;
+ private static int ID3_ENC_UTF16BE = 0x02;
+ private static int ID3_ENC_UTF8 = 0x03;
+
+ public ID3v2File() {
+ }
+
+ public HashMap getTags(RandomAccessFile s) throws IOException {
+ HashMap tags = new HashMap();
+
+ final int v2hdr_len = 10;
+ byte[] v2hdr = new byte[v2hdr_len];
+
+ // read the whole 10 byte header into memory
+ s.seek(0);
+ s.read(v2hdr);
+
+ int id3v = ((b2be32(v2hdr,0))) & 0xFF; // swapped ID3\04 -> ver. ist the first byte
+ int v3len = ((b2be32(v2hdr,6))); // total size EXCLUDING the this 10 byte header
+ v3len = ((v3len & 0x7f000000) >> 3) | // for some funky reason, this is encoded as 7*4 bits
+ ((v3len & 0x007f0000) >> 2) |
+ ((v3len & 0x00007f00) >> 1) |
+ ((v3len & 0x0000007f) >> 0) ;
+
+ // debug(">> tag version ID3v2."+id3v);
+ // debug(">> LEN= "+v3len+" // "+v3len);
+
+ // we should already be at the first frame
+ // so we can start the parsing right now
+ tags = parse_v3_frames(s, v3len);
+ tags.put("_hdrlen", v3len+v2hdr_len);
+ return tags;
+ }
+
+ /* Parses all ID3v2 frames at the current position up until payload_len
+ ** bytes were read
+ */
+ public HashMap parse_v3_frames(RandomAccessFile s, long payload_len) throws IOException {
+ HashMap tags = new HashMap();
+ byte[] frame = new byte[10]; // a frame header is always 10 bytes
+ long bread = 0; // total amount of read bytes
+
+ while(bread < payload_len) {
+ bread += s.read(frame);
+ String framename = new String(frame, 0, 4);
+ int slen = b2be32(frame, 4);
+
+ /* Abort on silly sizes */
+ if(slen < 1 || slen > 524288)
+ break;
+
+ byte[] xpl = new byte[slen];
+ bread += s.read(xpl);
+
+ if(framename.substring(0,1).equals("T")) {
+ String[] nmzInfo = normalizeTaginfo(framename, xpl);
+ String oggKey = nmzInfo[0];
+ String decPld = nmzInfo[1];
+
+ if(oggKey.length() > 0 && !tags.containsKey(oggKey)) {
+ addTagEntry(tags, oggKey, decPld);
+ }
+ }
+ else if(framename.equals("RVA2")) {
+ //
+ }
+
+ }
+ return tags;
+ }
+
+ /* Converts ID3v2 sillyframes to OggNames */
+ private String[] normalizeTaginfo(String k, byte[] v) {
+ String[] rv = new String[] {"",""};
+ HashMap lu = new HashMap<String, String>();
+ lu.put("TIT2", "TITLE");
+ lu.put("TALB", "ALBUM");
+ lu.put("TPE1", "ARTIST");
+
+ if(lu.containsKey(k)) {
+ /* A normal, known key: translate into Ogg-Frame name */
+ rv[0] = (String)lu.get(k);
+ rv[1] = getDecodedString(v);
+ }
+ else if(k.equals("TXXX")) {
+ /* A freestyle field, ieks! */
+ String txData[] = getDecodedString(v).split(Character.toString('\0'), 2);
+ /* Check if we got replaygain info in key\0value style */
+ if(txData.length == 2 && txData[0].matches("^(?i)REPLAYGAIN_(ALBUM|TRACK)_GAIN$")) {
+ rv[0] = txData[0].toUpperCase(); /* some tagwriters use lowercase for this */
+ rv[1] = txData[1];
+ }
+ }
+
+ return rv;
+ }
+
+ /* Converts a raw byte-stream text into a java String */
+ private String getDecodedString(byte[] raw) {
+ int encid = raw[0] & 0xFF;
+ int len = raw.length;
+ String v = "";
+ try {
+ if(encid == ID3_ENC_LATIN) {
+ v = new String(raw, 1, len-1, "ISO-8859-1");
+ }
+ else if (encid == ID3_ENC_UTF8) {
+ v = new String(raw, 1, len-1, "UTF-8");
+ }
+ else if (encid == ID3_ENC_UTF16LE) {
+ v = new String(raw, 3, len-3, "UTF-16LE");
+ }
+ else if (encid == ID3_ENC_UTF16BE) {
+ v = new String(raw, 3, len-3, "UTF-16BE");
+ }
+ } catch(Exception e) {}
+ return v;
+ }
+
+}
diff --git a/src/github/daneren2005/dsub/util/tags/LameHeader.java b/src/github/daneren2005/dsub/util/tags/LameHeader.java
new file mode 100644
index 00000000..720ee87f
--- /dev/null
+++ b/src/github/daneren2005/dsub/util/tags/LameHeader.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2013 Adrian Ulrich <adrian@blinkenlights.ch>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package github.daneren2005.dsub.util.tags;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.HashMap;
+import java.util.Enumeration;
+
+
+public class LameHeader extends Common {
+
+ public LameHeader() {
+ }
+
+ public HashMap getTags(RandomAccessFile s) throws IOException {
+ return parseLameHeader(s, 0);
+ }
+
+ public HashMap parseLameHeader(RandomAccessFile s, long offset) throws IOException {
+ HashMap tags = new HashMap();
+ byte[] chunk = new byte[4];
+
+ s.seek(offset + 0x24);
+ s.read(chunk);
+
+ String lameMark = new String(chunk, 0, chunk.length, "ISO-8859-1");
+
+ if(lameMark.equals("Info") || lameMark.equals("Xing")) {
+ s.seek(offset+0xAB);
+ s.read(chunk);
+
+ int raw = b2be32(chunk, 0);
+ int gtrk_raw = raw >> 16; /* first 16 bits are the raw track gain value */
+ int galb_raw = raw & 0xFFFF; /* the rest is for the album gain value */
+
+ float gtrk_val = (float)(gtrk_raw & 0x01FF)/10;
+ float galb_val = (float)(galb_raw & 0x01FF)/10;
+
+ gtrk_val = ((gtrk_raw&0x0200)!=0 ? -1*gtrk_val : gtrk_val);
+ galb_val = ((galb_raw&0x0200)!=0 ? -1*galb_val : galb_val);
+
+ if( (gtrk_raw&0xE000) == 0x2000 ) {
+ addTagEntry(tags, "REPLAYGAIN_TRACK_GAIN", gtrk_val+" dB");
+ }
+ if( (gtrk_raw&0xE000) == 0x4000 ) {
+ addTagEntry(tags, "REPLAYGAIN_ALBUM_GAIN", galb_val+" dB");
+ }
+
+ }
+
+ return tags;
+ }
+
+}
diff --git a/src/github/daneren2005/dsub/util/tags/OggFile.java b/src/github/daneren2005/dsub/util/tags/OggFile.java
new file mode 100644
index 00000000..d0b31671
--- /dev/null
+++ b/src/github/daneren2005/dsub/util/tags/OggFile.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2013 Adrian Ulrich <adrian@blinkenlights.ch>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package github.daneren2005.dsub.util.tags;
+
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.HashMap;
+
+
+public class OggFile extends Common {
+
+ private static final int OGG_PAGE_SIZE = 27; // Static size of an OGG Page
+ private static final int OGG_TYPE_COMMENT = 3; // ID of 'VorbisComment's
+
+ public OggFile() {
+ }
+
+ public HashMap getTags(RandomAccessFile s) throws IOException {
+ long offset = 0;
+ int retry = 64;
+ HashMap tags = new HashMap();
+
+ for( ; retry > 0 ; retry-- ) {
+ long res[] = parse_ogg_page(s, offset);
+ if(res[2] == OGG_TYPE_COMMENT) {
+ tags = parse_ogg_vorbis_comment(s, offset+res[0], res[1]);
+ break;
+ }
+ offset += res[0] + res[1];
+ }
+ return tags;
+ }
+
+
+ /* Parses the ogg page at offset 'offset' and returns
+ ** [header_size, payload_size, type]
+ */
+ private long[] parse_ogg_page(RandomAccessFile s, long offset) throws IOException {
+ long[] result = new long[3]; // [header_size, payload_size]
+ byte[] p_header = new byte[OGG_PAGE_SIZE]; // buffer for the page header
+ byte[] scratch;
+ int bread = 0; // number of bytes read
+ int psize = 0; // payload-size
+ int nsegs = 0; // Number of segments
+
+ s.seek(offset);
+ bread = s.read(p_header);
+ if(bread != OGG_PAGE_SIZE)
+ xdie("Unable to read() OGG_PAGE_HEADER");
+ if((new String(p_header, 0, 5)).equals("OggS\0") != true)
+ xdie("Invalid magic - not an ogg file?");
+
+ nsegs = b2u(p_header[26]);
+ // debug("> file seg: "+nsegs);
+ if(nsegs > 0) {
+ scratch = new byte[nsegs];
+ bread = s.read(scratch);
+ if(bread != nsegs)
+ xdie("Failed to read segtable");
+
+ for(int i=0; i<nsegs; i++) {
+ psize += b2u(scratch[i]);
+ }
+ }
+
+ // populate result array
+ result[0] = (s.getFilePointer() - offset);
+ result[1] = psize;
+ result[2] = -1;
+
+ /* next byte is most likely the type -> pre-read */
+ if(psize >= 1 && s.read(p_header, 0, 1) == 1) {
+ result[2] = b2u(p_header[0]);
+ }
+
+ return result;
+ }
+
+ /* In 'vorbiscomment' field is prefixed with \3vorbis in OGG files
+ ** we check that this marker is present and call the generic comment
+ ** parset with the correct offset (+7) */
+ private HashMap parse_ogg_vorbis_comment(RandomAccessFile s, long offset, long pl_len) throws IOException {
+ final int pfx_len = 7;
+ byte[] pfx = new byte[pfx_len];
+
+ if(pl_len < pfx_len)
+ xdie("ogg vorbis comment field is too short!");
+
+ s.seek(offset);
+ s.read(pfx);
+
+ if( (new String(pfx, 0, pfx_len)).equals("\3vorbis") == false )
+ xdie("Damaged packet found!");
+
+ return parse_vorbis_comment(s, offset+pfx_len, pl_len-pfx_len);
+ }
+
+};
diff --git a/src/github/daneren2005/dsub/view/SeekBarPreference.java b/src/github/daneren2005/dsub/view/SeekBarPreference.java
new file mode 100644
index 00000000..d0127857
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/SeekBarPreference.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2012 Christopher Eby <kreed@kreed.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.preference.DialogPreference;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.util.Constants;
+
+/**
+ * SeekBar preference to set the shake force threshold.
+ */
+public class SeekBarPreference extends DialogPreference implements SeekBar.OnSeekBarChangeListener {
+ /**
+ * The current value.
+ */
+ private String mValue;
+
+ /**
+ * Our context (needed for getResources())
+ */
+ private Context mContext;
+
+ /**
+ * TextView to display current threshold.
+ */
+ private TextView mValueText;
+
+ public SeekBarPreference(Context context, AttributeSet attrs)
+ {
+ super(context, attrs);
+ mContext = context;
+ }
+
+ @Override
+ public CharSequence getSummary()
+ {
+ return getSummary(mValue);
+ }
+
+ @Override
+ protected Object onGetDefaultValue(TypedArray a, int index)
+ {
+ return a.getString(index);
+ }
+
+ @Override
+ protected void onSetInitialValue(boolean restoreValue, Object defaultValue)
+ {
+ mValue = restoreValue ? getPersistedString((String) defaultValue) : (String)defaultValue;
+ }
+
+ /**
+ * Create the summary for the given value.
+ *
+ * @param value The force threshold.
+ * @return A string representation of the threshold.
+ */
+ private String getSummary(String value) {
+ int val = Integer.parseInt(value);
+ if(Constants.PREFERENCES_KEY_REPLAY_GAIN_UNTAGGED.equals(getKey())) {
+ return String.format("%+.1f dB", (val - 150) / 10f);
+ } else if(Constants.PREFERENCES_KEY_REPLAY_GAIN_BUMP.equals(getKey())) {
+ return String.format("%+.1f dB", 2 * (val - 75) / 10f);
+ } else {
+ return String.format("%+.1f", val);
+ }
+ }
+
+ @Override
+ protected View onCreateDialogView()
+ {
+ View view = super.onCreateDialogView();
+
+ mValueText = (TextView)view.findViewById(R.id.value);
+ mValueText.setText(getSummary(mValue));
+
+ SeekBar seekBar = (SeekBar)view.findViewById(R.id.seek_bar);
+ seekBar.setMax(150);
+ seekBar.setProgress(Integer.parseInt(mValue));
+ seekBar.setOnSeekBarChangeListener(this);
+
+ return view;
+ }
+
+ @Override
+ protected void onDialogClosed(boolean positiveResult)
+ {
+ persistString(mValue);
+ notifyChanged();
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)
+ {
+ if (fromUser) {
+ mValue = String.valueOf(progress);
+ mValueText.setText(getSummary(mValue));
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar)
+ {
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar)
+ {
+ }
+}