mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-25 18:24:31 +01:00
Lollipop Support ,Migrate to Painter , Bug Fixes
This commit is contained in:
parent
638fa31415
commit
ff95a77f35
@ -42,15 +42,15 @@ fun Activity.checkIfLatestVersion() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Activity.checkPermissions():Boolean{
|
fun Activity.checkPermissions():Boolean = ContextCompat
|
||||||
return (ContextCompat
|
|
||||||
.checkSelfPermission(this,
|
.checkSelfPermission(this,
|
||||||
Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
|
Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
|
||||||
&&
|
&&
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
|
||||||
ContextCompat.checkSelfPermission(this,
|
ContextCompat.checkSelfPermission(this,
|
||||||
Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) == PackageManager.PERMISSION_GRANTED)
|
Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) == PackageManager.PERMISSION_GRANTED
|
||||||
|
} else true
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("BatteryLife", "ObsoleteSdkInt")
|
@SuppressLint("BatteryLife", "ObsoleteSdkInt")
|
||||||
fun Activity.disableDozeMode(requestCode:Int) {
|
fun Activity.disableDozeMode(requestCode:Int) {
|
||||||
|
@ -45,7 +45,7 @@ object Versions {
|
|||||||
|
|
||||||
// Android
|
// Android
|
||||||
const val versionCode = 15
|
const val versionCode = 15
|
||||||
const val minSdkVersion = 24
|
const val minSdkVersion = 21
|
||||||
const val compileSdkVersion = 29
|
const val compileSdkVersion = 29
|
||||||
const val targetSdkVersion = 29
|
const val targetSdkVersion = 29
|
||||||
const val androidLifecycle = "2.3.0"
|
const val androidLifecycle = "2.3.0"
|
||||||
@ -139,7 +139,7 @@ object Extras {
|
|||||||
//const val youtubeDownloader = "com.github.sealedtx:java-youtube-downloader:2.5.1"
|
//const val youtubeDownloader = "com.github.sealedtx:java-youtube-downloader:2.5.1"
|
||||||
const val youtubeDownloader = "com.shabinder.downloader:youtube-api-dl:0.1" //Local Maven
|
const val youtubeDownloader = "com.shabinder.downloader:youtube-api-dl:0.1" //Local Maven
|
||||||
const val fuzzyWuzzy = "me.xdrop:fuzzywuzzy:1.3.1"
|
const val fuzzyWuzzy = "me.xdrop:fuzzywuzzy:1.3.1"
|
||||||
const val mp3agic = "com.mpatric:mp3agic:0.9.1"
|
const val mp3agic = "com.mpatric:mp3agic:0.9.0"
|
||||||
const val kermit = "co.touchlab:kermit:${Versions.kermit}"
|
const val kermit = "co.touchlab:kermit:${Versions.kermit}"
|
||||||
object Android {
|
object Android {
|
||||||
val razorpay = "com.razorpay:checkout:1.6.5"
|
val razorpay = "com.razorpay:checkout:1.6.5"
|
||||||
|
@ -18,7 +18,6 @@
|
|||||||
|
|
||||||
package com.shabinder.common.uikit
|
package com.shabinder.common.uikit
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@ -30,10 +29,8 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.vectorResource
|
|
||||||
import androidx.compose.ui.text.font.Font
|
import androidx.compose.ui.text.font.Font
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
@ -52,6 +49,7 @@ actual fun ImageLoad(
|
|||||||
// placeholder: ImageVector
|
// placeholder: ImageVector
|
||||||
) {
|
) {
|
||||||
var pic by remember(link) { mutableStateOf<ImageBitmap?>(null) }
|
var pic by remember(link) { mutableStateOf<ImageBitmap?>(null) }
|
||||||
|
|
||||||
LaunchedEffect(link) {
|
LaunchedEffect(link) {
|
||||||
withContext(dispatcherIO) {
|
withContext(dispatcherIO) {
|
||||||
pic = loader(link).image
|
pic = loader(link).image
|
||||||
@ -100,37 +98,34 @@ actual fun DownloadImageArrow(modifier: Modifier) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun DownloadAllImage() = vectorResource(R.drawable.ic_download_arrow)
|
actual fun DownloadAllImage() = painterResource(R.drawable.ic_download_arrow)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun ShareImage() = vectorResource(R.drawable.ic_share_open)
|
actual fun ShareImage() = painterResource(R.drawable.ic_share_open)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun PlaceHolderImage() = vectorResource(R.drawable.music)
|
actual fun PlaceHolderImage() = painterResource(R.drawable.music)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun SpotiFlyerLogo() = vectorResource(R.drawable.ic_spotiflyer_logo)
|
actual fun SpotiFlyerLogo() = painterResource(R.drawable.ic_spotiflyer_logo)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun HeartIcon() = vectorResource(R.drawable.ic_heart)
|
actual fun HeartIcon() = painterResource(R.drawable.ic_heart)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun SpotifyLogo() = vectorResource(R.drawable.ic_spotify_logo)
|
actual fun SpotifyLogo() = painterResource(R.drawable.ic_spotify_logo)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun GaanaLogo() = vectorResource(R.drawable.ic_gaana)
|
actual fun GaanaLogo() = painterResource(R.drawable.ic_gaana)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun YoutubeLogo() = vectorResource(R.drawable.ic_youtube)
|
actual fun YoutubeLogo() = painterResource(R.drawable.ic_youtube)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun YoutubeMusicLogo() = vectorResource(R.drawable.ic_youtube_music_logo)
|
actual fun YoutubeMusicLogo() = painterResource(R.drawable.ic_youtube_music_logo)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun GithubLogo() = vectorResource(R.drawable.ic_github)
|
actual fun GithubLogo() = painterResource(R.drawable.ic_github)
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun vectorResource(@DrawableRes id: Int) = ImageVector.Companion.vectorResource(id)
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun Toast(
|
actual fun Toast(
|
||||||
@ -140,6 +135,7 @@ actual fun Toast(
|
|||||||
) {
|
) {
|
||||||
// We Have Android's Implementation of Toast so its just Empty
|
// We Have Android's Implementation of Toast so its just Empty
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun showPopUpMessage(text: String) {
|
actual fun showPopUpMessage(text: String) {
|
||||||
android.widget.Toast.makeText(appContext, text, android.widget.Toast.LENGTH_SHORT).show()
|
android.widget.Toast.makeText(appContext, text, android.widget.Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ package com.shabinder.common.uikit
|
|||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import com.shabinder.common.di.Picture
|
import com.shabinder.common.di.Picture
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -28,41 +28,41 @@ expect fun ImageLoad(
|
|||||||
loader: suspend (String) -> Picture,
|
loader: suspend (String) -> Picture,
|
||||||
desc: String = "Album Art",
|
desc: String = "Album Art",
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
// placeholder:ImageVector = PlaceHolderImage()
|
// placeholder:Painter = PlaceHolderImage()
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun DownloadImageTick()
|
expect fun DownloadImageTick()
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun DownloadAllImage(): ImageVector
|
expect fun DownloadAllImage(): Painter
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun ShareImage(): ImageVector
|
expect fun ShareImage(): Painter
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun PlaceHolderImage(): ImageVector
|
expect fun PlaceHolderImage(): Painter
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun SpotiFlyerLogo(): ImageVector
|
expect fun SpotiFlyerLogo(): Painter
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun SpotifyLogo(): ImageVector
|
expect fun SpotifyLogo(): Painter
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun YoutubeLogo(): ImageVector
|
expect fun YoutubeLogo(): Painter
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun GaanaLogo(): ImageVector
|
expect fun GaanaLogo(): Painter
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun YoutubeMusicLogo(): ImageVector
|
expect fun YoutubeMusicLogo(): Painter
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun GithubLogo(): ImageVector
|
expect fun GithubLogo(): Painter
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun HeartIcon(): ImageVector
|
expect fun HeartIcon(): Painter
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun DownloadImageError()
|
expect fun DownloadImageError()
|
||||||
|
@ -186,7 +186,7 @@ fun DownloadAllButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
|
|||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
text = { Text("Download All") },
|
text = { Text("Download All") },
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
icon = { Icon(imageVector = DownloadAllImage(), "Download All Button", tint = Color(0xFF000000)) },
|
icon = { Icon(DownloadAllImage(), "Download All Button", tint = Color(0xFF000000)) },
|
||||||
backgroundColor = colorAccent,
|
backgroundColor = colorAccent,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
|
@ -219,7 +219,7 @@ fun SearchPanel(
|
|||||||
@Composable
|
@Composable
|
||||||
fun AboutColumn(modifier: Modifier = Modifier) {
|
fun AboutColumn(modifier: Modifier = Modifier) {
|
||||||
// TODO Make Scrollable
|
// TODO Make Scrollable
|
||||||
Column(modifier.fillMaxSize().verticalScroll(rememberScrollState())) {
|
Column(modifier.fillMaxSize().padding(8.dp).verticalScroll(rememberScrollState())) {
|
||||||
Card(
|
Card(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
border = BorderStroke(1.dp, Color.Gray)
|
border = BorderStroke(1.dp, Color.Gray)
|
||||||
@ -233,7 +233,7 @@ fun AboutColumn(modifier: Modifier = Modifier) {
|
|||||||
Spacer(modifier = Modifier.padding(top = 12.dp))
|
Spacer(modifier = Modifier.padding(top = 12.dp))
|
||||||
Row(horizontalArrangement = Arrangement.Center, modifier = modifier.fillMaxWidth()) {
|
Row(horizontalArrangement = Arrangement.Center, modifier = modifier.fillMaxWidth()) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = SpotifyLogo(),
|
SpotifyLogo(),
|
||||||
"Open Spotify",
|
"Open Spotify",
|
||||||
tint = Color.Unspecified,
|
tint = Color.Unspecified,
|
||||||
modifier = Modifier.clip(SpotiFlyerShapes.small).clickable(
|
modifier = Modifier.clip(SpotiFlyerShapes.small).clickable(
|
||||||
@ -242,7 +242,7 @@ fun AboutColumn(modifier: Modifier = Modifier) {
|
|||||||
)
|
)
|
||||||
Spacer(modifier = modifier.padding(start = 16.dp))
|
Spacer(modifier = modifier.padding(start = 16.dp))
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = GaanaLogo(),
|
GaanaLogo(),
|
||||||
"Open Gaana",
|
"Open Gaana",
|
||||||
tint = Color.Unspecified,
|
tint = Color.Unspecified,
|
||||||
modifier = Modifier.clip(SpotiFlyerShapes.small).clickable(
|
modifier = Modifier.clip(SpotiFlyerShapes.small).clickable(
|
||||||
@ -251,7 +251,7 @@ fun AboutColumn(modifier: Modifier = Modifier) {
|
|||||||
)
|
)
|
||||||
Spacer(modifier = modifier.padding(start = 16.dp))
|
Spacer(modifier = modifier.padding(start = 16.dp))
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = YoutubeLogo(),
|
YoutubeLogo(),
|
||||||
"Open Youtube",
|
"Open Youtube",
|
||||||
tint = Color.Unspecified,
|
tint = Color.Unspecified,
|
||||||
modifier = Modifier.clip(SpotiFlyerShapes.small).clickable(
|
modifier = Modifier.clip(SpotiFlyerShapes.small).clickable(
|
||||||
@ -260,7 +260,7 @@ fun AboutColumn(modifier: Modifier = Modifier) {
|
|||||||
)
|
)
|
||||||
Spacer(modifier = modifier.padding(start = 12.dp))
|
Spacer(modifier = modifier.padding(start = 12.dp))
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = YoutubeMusicLogo(),
|
YoutubeMusicLogo(),
|
||||||
"Open Youtube Music",
|
"Open Youtube Music",
|
||||||
tint = Color.Unspecified,
|
tint = Color.Unspecified,
|
||||||
modifier = Modifier.clip(SpotiFlyerShapes.small).clickable(
|
modifier = Modifier.clip(SpotiFlyerShapes.small).clickable(
|
||||||
@ -289,7 +289,7 @@ fun AboutColumn(modifier: Modifier = Modifier) {
|
|||||||
)
|
)
|
||||||
.padding(vertical = 6.dp)
|
.padding(vertical = 6.dp)
|
||||||
) {
|
) {
|
||||||
Icon(imageVector = GithubLogo(), "Open Project Repo", tint = Color(0xFFCCCCCC))
|
Icon(GithubLogo(), "Open Project Repo", tint = Color(0xFFCCCCCC))
|
||||||
Spacer(modifier = Modifier.padding(start = 16.dp))
|
Spacer(modifier = Modifier.padding(start = 16.dp))
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
@ -373,7 +373,7 @@ fun HistoryColumn(
|
|||||||
) {
|
) {
|
||||||
Crossfade(list) {
|
Crossfade(list) {
|
||||||
if (it.isEmpty()) {
|
if (it.isEmpty()) {
|
||||||
Column(Modifier.padding(bottom = 32.dp).fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(Modifier.padding(8.dp).fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Outlined.Info, "No History Available Yet", modifier = Modifier.size(80.dp),
|
Icons.Outlined.Info, "No History Available Yet", modifier = Modifier.size(80.dp),
|
||||||
colorOffWhite
|
colorOffWhite
|
||||||
@ -423,7 +423,7 @@ fun DownloadRecordItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Image(
|
Image(
|
||||||
imageVector = ShareImage(),
|
ShareImage(),
|
||||||
"Research",
|
"Research",
|
||||||
modifier = Modifier.clickable(
|
modifier = Modifier.clickable(
|
||||||
onClick = {
|
onClick = {
|
||||||
|
@ -139,7 +139,7 @@ fun AppBar(
|
|||||||
title = {
|
title = {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Image(
|
Image(
|
||||||
imageVector = SpotiFlyerLogo(),
|
SpotiFlyerLogo(),
|
||||||
"SpotiFlyer Logo",
|
"SpotiFlyer Logo",
|
||||||
Modifier.size(32.dp),
|
Modifier.size(32.dp),
|
||||||
)
|
)
|
||||||
|
@ -55,7 +55,7 @@ fun Splash(modifier: Modifier = Modifier, onTimeout: () -> Unit) {
|
|||||||
delay(SplashWaitTime)
|
delay(SplashWaitTime)
|
||||||
currentOnTimeout()
|
currentOnTimeout()
|
||||||
}
|
}
|
||||||
Image(imageVector = SpotiFlyerLogo(), "SpotiFlyer Logo")
|
Image(SpotiFlyerLogo(), "SpotiFlyer Logo")
|
||||||
MadeInIndia(Modifier.align(Alignment.BottomCenter))
|
MadeInIndia(Modifier.align(Alignment.BottomCenter))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,10 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.graphics.vector.VectorPainter
|
||||||
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.res.vectorXmlResource
|
import androidx.compose.ui.res.vectorXmlResource
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
@ -94,32 +97,32 @@ actual fun DownloadImageArrow(modifier: Modifier) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun DownloadAllImage(): ImageVector = vectorXmlResource("drawable/ic_download_arrow.xml")
|
actual fun DownloadAllImage() = rememberVectorPainter(vectorXmlResource("drawable/ic_download_arrow.xml")) as Painter
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun ShareImage(): ImageVector = vectorXmlResource("drawable/ic_share_open.xml")
|
actual fun ShareImage() = rememberVectorPainter(vectorXmlResource("drawable/ic_share_open.xml")) as Painter
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun PlaceHolderImage(): ImageVector = vectorXmlResource("drawable/music.xml")
|
actual fun PlaceHolderImage() = rememberVectorPainter(vectorXmlResource("drawable/music.xml"))
|
||||||
|
as Painter
|
||||||
|
@Composable
|
||||||
|
actual fun SpotiFlyerLogo() =
|
||||||
|
rememberVectorPainter(vectorXmlResource("drawable/ic_spotiflyer_logo.xml")) as Painter
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun SpotiFlyerLogo(): ImageVector =
|
actual fun HeartIcon() = rememberVectorPainter(vectorXmlResource("drawable/ic_heart.xml")) as Painter
|
||||||
vectorXmlResource("drawable/ic_spotiflyer_logo.xml")
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun HeartIcon(): ImageVector = vectorXmlResource("drawable/ic_heart.xml")
|
actual fun SpotifyLogo() = rememberVectorPainter(vectorXmlResource("drawable/ic_spotify_logo.xml")) as Painter
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun SpotifyLogo(): ImageVector = vectorXmlResource("drawable/ic_spotify_logo.xml")
|
actual fun YoutubeLogo() = rememberVectorPainter(vectorXmlResource("drawable/ic_youtube.xml")) as Painter
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun YoutubeLogo(): ImageVector = vectorXmlResource("drawable/ic_youtube.xml")
|
actual fun GaanaLogo() = rememberVectorPainter(vectorXmlResource("drawable/ic_gaana.xml")) as Painter
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun GaanaLogo(): ImageVector = vectorXmlResource("drawable/ic_gaana.xml")
|
actual fun YoutubeMusicLogo() = rememberVectorPainter(vectorXmlResource("drawable/ic_youtube_music_logo.xml")) as Painter
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun YoutubeMusicLogo(): ImageVector = vectorXmlResource("drawable/ic_youtube_music_logo.xml")
|
actual fun GithubLogo() = rememberVectorPainter(vectorXmlResource("drawable/ic_github.xml")) as Painter
|
||||||
|
|
||||||
@Composable
|
|
||||||
actual fun GithubLogo(): ImageVector = vectorXmlResource("drawable/ic_github.xml")
|
|
||||||
|
@ -52,6 +52,7 @@ kotlin {
|
|||||||
implementation(Extras.Android.fetch)
|
implementation(Extras.Android.fetch)
|
||||||
implementation(Extras.Android.razorpay)
|
implementation(Extras.Android.razorpay)
|
||||||
api(Extras.mp3agic)
|
api(Extras.mp3agic)
|
||||||
|
api(project(":jaudiotagger"))
|
||||||
// api(files("$rootDir/libs/mobile-ffmpeg.aar"))
|
// api(files("$rootDir/libs/mobile-ffmpeg.aar"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -61,6 +62,7 @@ kotlin {
|
|||||||
implementation(Ktor.clientApache)
|
implementation(Ktor.clientApache)
|
||||||
implementation(Ktor.slf4j)
|
implementation(Ktor.slf4j)
|
||||||
api(Extras.mp3agic)
|
api(Extras.mp3agic)
|
||||||
|
api(project(":jaudiotagger"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jsMain {
|
jsMain {
|
||||||
|
@ -16,15 +16,69 @@
|
|||||||
|
|
||||||
package com.shabinder.common.di
|
package com.shabinder.common.di
|
||||||
|
|
||||||
|
import com.shabinder.common.models.TrackDetails
|
||||||
|
import java.io.File
|
||||||
|
import com.mp3.jaudiotagger.tag.images.ArtworkFactory
|
||||||
import com.mpatric.mp3agic.ID3v1Tag
|
import com.mpatric.mp3agic.ID3v1Tag
|
||||||
import com.mpatric.mp3agic.ID3v24Tag
|
import com.mpatric.mp3agic.ID3v24Tag
|
||||||
import com.mpatric.mp3agic.Mp3File
|
import com.mpatric.mp3agic.Mp3File
|
||||||
import com.shabinder.common.models.DownloadResult
|
import com.shabinder.common.models.DownloadResult
|
||||||
import com.shabinder.common.models.TrackDetails
|
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
suspend fun MP3File.setAudioTags(track: TrackDetails) {
|
||||||
|
|
||||||
|
val id3v1Tag = this.iD3v1Tag ?: ID3v1Tag()
|
||||||
|
id3v1Tag.apply {
|
||||||
|
setField(FieldKey.ALBUM,track.albumName)
|
||||||
|
setField(FieldKey.ARTIST,track.artists.getOrNull(0) ?: "")
|
||||||
|
setField(FieldKey.ARTIST,track.artists.getOrNull(0) ?: "")
|
||||||
|
setField(FieldKey.TITLE,track.title)
|
||||||
|
setField(FieldKey.YEAR,track.year)
|
||||||
|
setField(FieldKey.COMMENT,track.comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
val id3v2Tag = this.iD3v2TagAsv24 ?: ID3v24Tag()
|
||||||
|
id3v2Tag.apply {
|
||||||
|
setField(FieldKey.ALBUM,track.albumName)
|
||||||
|
setField(FieldKey.ARTISTS,track.artists.joinToString(","))
|
||||||
|
setField(FieldKey.ARTIST,track.artists.getOrNull(0) ?: "")
|
||||||
|
setField(FieldKey.ARTIST,track.artists.getOrNull(0) ?: "")
|
||||||
|
setField(FieldKey.TITLE,track.title)
|
||||||
|
setField(FieldKey.YEAR,track.year)
|
||||||
|
setField(FieldKey.COMMENT,track.comment)
|
||||||
|
setField(FieldKey.LYRICS,"Gonna Implement Soon")
|
||||||
|
setField(FieldKey.URL_OFFICIAL_RELEASE_SITE,track.trackUrl)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val artwork = ArtworkFactory.createArtworkFromFile(File(track.albumArtPath))
|
||||||
|
createField(artwork)
|
||||||
|
setField(artwork)
|
||||||
|
} catch (e: java.io.FileNotFoundException) {
|
||||||
|
try {
|
||||||
|
// Image Still Not Downloaded!
|
||||||
|
// Lets Download Now and Write it into Album Art
|
||||||
|
downloadByteArray(track.albumArtURL)?.let {
|
||||||
|
setField(createArtworkField(it,"image/jpeg"))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
// log("Error", "Couldn't Write Mp3 Album Art, error: ${e.stackTrace}")
|
||||||
|
}
|
||||||
|
} catch (e:Exception) { e.printStackTrace() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write Tags to file
|
||||||
|
this.iD3v1Tag = id3v1Tag
|
||||||
|
this.iD3v2Tag = id3v2Tag
|
||||||
|
|
||||||
|
commit()
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
fun Mp3File.removeAllTags(): Mp3File {
|
fun Mp3File.removeAllTags(): Mp3File {
|
||||||
removeId3v1Tag()
|
removeId3v1Tag()
|
||||||
removeId3v2Tag()
|
removeId3v2Tag()
|
||||||
@ -32,9 +86,6 @@ fun Mp3File.removeAllTags(): Mp3File {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Modifying Mp3 with MetaData!
|
|
||||||
**/
|
|
||||||
fun Mp3File.setId3v1Tags(track: TrackDetails): Mp3File {
|
fun Mp3File.setId3v1Tags(track: TrackDetails): Mp3File {
|
||||||
val id3v1Tag = ID3v1Tag().apply {
|
val id3v1Tag = ID3v1Tag().apply {
|
||||||
artist = track.artists.joinToString(",")
|
artist = track.artists.joinToString(",")
|
||||||
@ -50,6 +101,7 @@ fun Mp3File.setId3v1Tags(track: TrackDetails): Mp3File {
|
|||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails) {
|
suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails) {
|
||||||
val id3v2Tag = ID3v24Tag().apply {
|
val id3v2Tag = ID3v24Tag().apply {
|
||||||
|
|
||||||
artist = track.artists.joinToString(",")
|
artist = track.artists.joinToString(",")
|
||||||
title = track.title
|
title = track.title
|
||||||
album = track.albumName
|
album = track.albumName
|
@ -22,10 +22,14 @@ import com.shabinder.common.di.utils.removeIllegalChars
|
|||||||
import com.shabinder.common.models.DownloadResult
|
import com.shabinder.common.models.DownloadResult
|
||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
import com.shabinder.database.Database
|
import com.shabinder.database.Database
|
||||||
import io.ktor.client.request.get
|
import com.shabinder.downloader.exceptions.YoutubeException
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.features.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
import io.ktor.client.statement.HttpStatement
|
import io.ktor.client.statement.HttpStatement
|
||||||
import io.ktor.http.contentLength
|
import io.ktor.http.contentLength
|
||||||
import io.ktor.http.isSuccess
|
import io.ktor.http.isSuccess
|
||||||
|
import io.ktor.utils.io.errors.*
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@ -67,6 +71,21 @@ suspend fun downloadFile(url: String): Flow<DownloadResult> {
|
|||||||
client.close()
|
client.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun downloadByteArray(
|
||||||
|
url: String,
|
||||||
|
httpBuilder: HttpRequestBuilder.()->Unit = {}
|
||||||
|
): ByteArray? {
|
||||||
|
val client = createHttpClient()
|
||||||
|
val response = try {
|
||||||
|
client.get<ByteArray>(url,httpBuilder)
|
||||||
|
} catch (e: Exception){
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
client.close()
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
fun getNameURL(url: String): String {
|
fun getNameURL(url: String): String {
|
||||||
return url.substring(url.lastIndexOf('/', url.lastIndexOf('/') - 1) + 1, url.length).replace('/', '_')
|
return url.substring(url.lastIndexOf('/', url.lastIndexOf('/') - 1) + 1, url.length).replace('/', '_')
|
||||||
}
|
}
|
||||||
|
1
jaudiotagger/.gitignore
vendored
Normal file
1
jaudiotagger/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
8
jaudiotagger/build.gradle.kts
Normal file
8
jaudiotagger/build.gradle.kts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
plugins {
|
||||||
|
id("java-library")
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_1_7
|
||||||
|
targetCompatibility = JavaVersion.VERSION_1_7
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* @author : Paul Taylor
|
||||||
|
*
|
||||||
|
* Version @version:$Id$
|
||||||
|
*
|
||||||
|
* Jaudiotagger Copyright (C)2004,2005
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser
|
||||||
|
* General Public License as published by the Free Software Foundation; either version 2.1 of the License,
|
||||||
|
* or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library 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 Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License along with this library; if not,
|
||||||
|
* you can get a copy from http://www.opensource.org/licenses/lgpl-license.php or write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*
|
||||||
|
* Description:
|
||||||
|
*/
|
||||||
|
package com.mp3.jaudiotagger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definitions of the bit used when reading file format from file
|
||||||
|
*/
|
||||||
|
public interface FileConstants
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* defined for convenience
|
||||||
|
*/
|
||||||
|
int BIT7 = 0x80;
|
||||||
|
/**
|
||||||
|
* defined for convenience
|
||||||
|
*/
|
||||||
|
int BIT6 = 0x40;
|
||||||
|
/**
|
||||||
|
* defined for convenience
|
||||||
|
*/
|
||||||
|
int BIT5 = 0x20;
|
||||||
|
/**
|
||||||
|
* defined for convenience
|
||||||
|
*/
|
||||||
|
int BIT4 = 0x10;
|
||||||
|
/**
|
||||||
|
* defined for convenience
|
||||||
|
*/
|
||||||
|
int BIT3 = 0x08;
|
||||||
|
/**
|
||||||
|
* defined for convenience
|
||||||
|
*/
|
||||||
|
int BIT2 = 0x04;
|
||||||
|
/**
|
||||||
|
* defined for convenience
|
||||||
|
*/
|
||||||
|
int BIT1 = 0x02;
|
||||||
|
/**
|
||||||
|
* defined for convenience
|
||||||
|
*/
|
||||||
|
int BIT0 = 0x01;
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved.
|
||||||
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||||
|
*
|
||||||
|
* This code is free software; you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU General Public License version 2 only, as
|
||||||
|
* published by the Free Software Foundation. Oracle designates this
|
||||||
|
* particular file as subject to the "Classpath" exception as provided
|
||||||
|
* by Oracle in the LICENSE file that accompanied this code.
|
||||||
|
*
|
||||||
|
* This code 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
|
||||||
|
* version 2 for more details (a copy is included in the LICENSE file that
|
||||||
|
* accompanied this code).
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License version
|
||||||
|
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||||
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
*
|
||||||
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||||
|
* or visit www.oracle.com if you need additional information or have any
|
||||||
|
* questions.
|
||||||
|
*/
|
||||||
|
package com.mp3.jaudiotagger;
|
||||||
|
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constant definitions for the standard {@link Charset Charsets}. These
|
||||||
|
* charsets are guaranteed to be available on every implementation of the Java
|
||||||
|
* platform.
|
||||||
|
*
|
||||||
|
* @see <a href="Charset#standard">Standard Charsets</a>
|
||||||
|
* @since 1.7
|
||||||
|
*/
|
||||||
|
public final class StandardCharsets {
|
||||||
|
|
||||||
|
private StandardCharsets() {
|
||||||
|
throw new AssertionError("No com.mp3.jaudiotagger.StandardCharsets instances for you!");
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Seven-bit ASCII, a.k.a. ISO646-US, a.k.a. the Basic Latin block of the
|
||||||
|
* Unicode character set
|
||||||
|
*/
|
||||||
|
public static final Charset US_ASCII = Charset.forName("US-ASCII");
|
||||||
|
/**
|
||||||
|
* ISO Latin Alphabet No. 1, a.k.a. ISO-LATIN-1
|
||||||
|
*/
|
||||||
|
public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
|
||||||
|
/**
|
||||||
|
* Eight-bit UCS Transformation Format
|
||||||
|
*/
|
||||||
|
public static final Charset UTF_8 = Charset.forName("UTF-8");
|
||||||
|
/**
|
||||||
|
* Sixteen-bit UCS Transformation Format, big-endian byte order
|
||||||
|
*/
|
||||||
|
public static final Charset UTF_16BE = Charset.forName("UTF-16BE");
|
||||||
|
/**
|
||||||
|
* Sixteen-bit UCS Transformation Format, little-endian byte order
|
||||||
|
*/
|
||||||
|
public static final Charset UTF_16LE = Charset.forName("UTF-16LE");
|
||||||
|
/**
|
||||||
|
* Sixteen-bit UCS Transformation Format, byte order identified by an
|
||||||
|
* optional byte-order mark
|
||||||
|
*/
|
||||||
|
public static final Charset UTF_16 = Charset.forName("UTF-16");
|
||||||
|
}
|
@ -0,0 +1,476 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.dsf.Dsf;
|
||||||
|
import com.mp3.jaudiotagger.audio.exceptions.*;
|
||||||
|
import com.mp3.jaudiotagger.audio.flac.metadatablock.MetadataBlockDataPicture;
|
||||||
|
import com.mp3.jaudiotagger.audio.real.RealTag;
|
||||||
|
import com.mp3.jaudiotagger.logging.ErrorMessage;
|
||||||
|
import com.mp3.jaudiotagger.tag.Tag;
|
||||||
|
import com.mp3.jaudiotagger.tag.TagOptionSingleton;
|
||||||
|
import com.mp3.jaudiotagger.tag.aiff.AiffTag;
|
||||||
|
import com.mp3.jaudiotagger.tag.asf.AsfTag;
|
||||||
|
import com.mp3.jaudiotagger.tag.flac.FlacTag;
|
||||||
|
import com.mp3.jaudiotagger.tag.id3.AbstractID3v2Tag;
|
||||||
|
import com.mp3.jaudiotagger.tag.id3.ID3v22Tag;
|
||||||
|
import com.mp3.jaudiotagger.tag.id3.ID3v23Tag;
|
||||||
|
import com.mp3.jaudiotagger.tag.id3.ID3v24Tag;
|
||||||
|
import com.mp3.jaudiotagger.tag.mp4.Mp4Tag;
|
||||||
|
import com.mp3.jaudiotagger.tag.reference.ID3V2Version;
|
||||||
|
import com.mp3.jaudiotagger.tag.vorbiscomment.VorbisCommentTag;
|
||||||
|
import com.mp3.jaudiotagger.tag.wav.WavTag;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.RandomAccessFile;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>This is the main object manipulated by the user representing an audiofile, its properties and its tag.
|
||||||
|
* <p>The preferred way to obtain an <code>AudioFile</code> is to use the <code>AudioFileIO.read(File)</code> method.
|
||||||
|
* <p>The <code>AudioHeader</code> contains every properties associated with the file itself (no meta-data), like the bitrate, the sampling rate, the encoding audioHeaders, etc.
|
||||||
|
* <p>To get the meta-data contained in this file you have to get the <code>Tag</code> of this <code>AudioFile</code>
|
||||||
|
*
|
||||||
|
* @author Raphael Slinckx
|
||||||
|
* @version $Id$
|
||||||
|
* @see AudioFileIO
|
||||||
|
* @see Tag
|
||||||
|
* @since v0.01
|
||||||
|
*/
|
||||||
|
public class AudioFile
|
||||||
|
{
|
||||||
|
//Logger
|
||||||
|
public static Logger logger = Logger.getLogger("com.mp3.jaudiotagger.audio");
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* The physical file that this instance represents.
|
||||||
|
*/
|
||||||
|
protected File file;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Audio header info
|
||||||
|
*/
|
||||||
|
protected AudioHeader audioHeader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tag
|
||||||
|
*/
|
||||||
|
protected Tag tag;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tag
|
||||||
|
*/
|
||||||
|
protected String extension;
|
||||||
|
|
||||||
|
public AudioFile()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>These constructors are used by the different readers, users should not use them, but use the <code>AudioFileIO.read(File)</code> method instead !.
|
||||||
|
* <p>Create the AudioFile representing file f, the encoding audio headers and containing the tag
|
||||||
|
*
|
||||||
|
* @param f The file of the audio file
|
||||||
|
* @param audioHeader the encoding audioHeaders over this file
|
||||||
|
* @param tag the tag contained in this file or null if no tag exists
|
||||||
|
*/
|
||||||
|
public AudioFile(File f, AudioHeader audioHeader, Tag tag)
|
||||||
|
{
|
||||||
|
this.file = f;
|
||||||
|
this.audioHeader = audioHeader;
|
||||||
|
this.tag = tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>These constructors are used by the different readers, users should not use them, but use the <code>AudioFileIO.read(File)</code> method instead !.
|
||||||
|
* <p>Create the AudioFile representing file denoted by pathnames, the encoding audio Headers and containing the tag
|
||||||
|
*
|
||||||
|
* @param s The pathname of the audio file
|
||||||
|
* @param audioHeader the encoding audioHeaders over this file
|
||||||
|
* @param tag the tag contained in this file
|
||||||
|
*/
|
||||||
|
public AudioFile(String s, AudioHeader audioHeader, Tag tag)
|
||||||
|
{
|
||||||
|
this.file = new File(s);
|
||||||
|
this.audioHeader = audioHeader;
|
||||||
|
this.tag = tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Write the tag contained in this AudioFile in the actual file on the disk, this is the same as calling the <code>AudioFileIO.write(this)</code> method.
|
||||||
|
*
|
||||||
|
* @throws NoWritePermissionsException if the file could not be written to due to file permissions
|
||||||
|
* @throws CannotWriteException If the file could not be written/accessed, the extension wasn't recognized, or other IO error occured.
|
||||||
|
* @see AudioFileIO
|
||||||
|
*/
|
||||||
|
public void commit() throws CannotWriteException
|
||||||
|
{
|
||||||
|
AudioFileIO.write(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Delete any tags that exist in the fie , this is the same as calling the <code>AudioFileIO.delete(this)</code> method.
|
||||||
|
*
|
||||||
|
* @throws CannotWriteException If the file could not be written/accessed, the extension wasn't recognized, or other IO error occured.
|
||||||
|
* @see AudioFileIO
|
||||||
|
*/
|
||||||
|
public void delete() throws CannotReadException, CannotWriteException
|
||||||
|
{
|
||||||
|
AudioFileIO.delete(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the file to store the info in
|
||||||
|
*
|
||||||
|
* @param file
|
||||||
|
*/
|
||||||
|
public void setFile(File file)
|
||||||
|
{
|
||||||
|
this.file = file;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the physical file
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public File getFile()
|
||||||
|
{
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the file extension
|
||||||
|
*
|
||||||
|
* @param ext
|
||||||
|
*/
|
||||||
|
public void setExt(String ext)
|
||||||
|
{
|
||||||
|
this.extension = ext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the file extension
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public String getExt()
|
||||||
|
{
|
||||||
|
return extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign a tag to this audio file
|
||||||
|
*
|
||||||
|
* @param tag Tag to be assigned
|
||||||
|
*/
|
||||||
|
public void setTag(Tag tag)
|
||||||
|
{
|
||||||
|
this.tag = tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return audio header information
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public AudioHeader getAudioHeader()
|
||||||
|
{
|
||||||
|
return audioHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Returns the tag contained in this AudioFile, the <code>Tag</code> contains any useful meta-data, like
|
||||||
|
* artist, album, title, etc. If the file does not contain any tag the null is returned. Some audio formats do
|
||||||
|
* not allow there to be no tag so in this case the reader would return an empty tag whereas for others such
|
||||||
|
* as mp3 it is purely optional.
|
||||||
|
*
|
||||||
|
* @return Returns the tag contained in this AudioFile, or null if no tag exists.
|
||||||
|
*/
|
||||||
|
public Tag getTag()
|
||||||
|
{
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Returns a multi-line string with the file path, the encoding audioHeader, and the tag contents.
|
||||||
|
*
|
||||||
|
* @return A multi-line string with the file path, the encoding audioHeader, and the tag contents.
|
||||||
|
* TODO Maybe this can be changed ?
|
||||||
|
*/
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "AudioFile " + getFile().getAbsolutePath()
|
||||||
|
+ " --------\n" + audioHeader.toString() + "\n" + ((tag == null) ? "" : tag.toString()) + "\n-------------------";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check does file exist
|
||||||
|
*
|
||||||
|
* @param file
|
||||||
|
* @throws FileNotFoundException if file not found
|
||||||
|
*/
|
||||||
|
public void checkFileExists(File file)throws FileNotFoundException
|
||||||
|
{
|
||||||
|
logger.config("Reading file:" + "path" + file.getPath() + ":abs:" + file.getAbsolutePath());
|
||||||
|
if (!file.exists())
|
||||||
|
{
|
||||||
|
logger.severe("Unable to find:" + file.getPath());
|
||||||
|
throw new FileNotFoundException(ErrorMessage.UNABLE_TO_FIND_FILE.getMsg(file.getPath()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the file is accessible with the correct permissions, otherwise exception occurs
|
||||||
|
*
|
||||||
|
* @param file
|
||||||
|
* @param readOnly
|
||||||
|
* @throws ReadOnlyFileException
|
||||||
|
* @throws FileNotFoundException
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
protected RandomAccessFile checkFilePermissions(File file, boolean readOnly) throws ReadOnlyFileException, FileNotFoundException, CannotReadException
|
||||||
|
{
|
||||||
|
RandomAccessFile newFile;
|
||||||
|
checkFileExists(file);
|
||||||
|
|
||||||
|
// Unless opened as readonly the file must be writable
|
||||||
|
if (readOnly)
|
||||||
|
{
|
||||||
|
//May not even be readable
|
||||||
|
if(!file.canRead())
|
||||||
|
{
|
||||||
|
logger.severe("Unable to read file:" + file.getPath());
|
||||||
|
throw new NoReadPermissionsException(ErrorMessage.GENERAL_READ_FAILED_DO_NOT_HAVE_PERMISSION_TO_READ_FILE.getMsg(file.getPath()));
|
||||||
|
}
|
||||||
|
newFile = new RandomAccessFile(file, "r");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (TagOptionSingleton.getInstance().isCheckIsWritable() && !file.canWrite())
|
||||||
|
{
|
||||||
|
throw new ReadOnlyFileException(ErrorMessage.NO_PERMISSIONS_TO_WRITE_TO_FILE.getMsg(file.getPath()));
|
||||||
|
}
|
||||||
|
newFile = new RandomAccessFile(file, "rw");
|
||||||
|
}
|
||||||
|
return newFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional debugging method. Must override to do anything interesting.
|
||||||
|
*
|
||||||
|
* @return Empty string.
|
||||||
|
*/
|
||||||
|
public String displayStructureAsXML()
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional debugging method. Must override to do anything interesting.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public String displayStructureAsPlainText()
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Create Default Tag
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public Tag createDefaultTag()
|
||||||
|
{
|
||||||
|
String extension = getExt();
|
||||||
|
if(extension == null)
|
||||||
|
{
|
||||||
|
String fileName = file.getName();
|
||||||
|
extension = fileName.substring(fileName.lastIndexOf('.') + 1);
|
||||||
|
setExt(extension);
|
||||||
|
}
|
||||||
|
if(SupportedFileFormat.FLAC.getFilesuffix().equals(extension))
|
||||||
|
{
|
||||||
|
return new FlacTag(VorbisCommentTag.createNewTag(), new ArrayList< MetadataBlockDataPicture >());
|
||||||
|
}
|
||||||
|
else if(SupportedFileFormat.OGG.getFilesuffix().equals(extension))
|
||||||
|
{
|
||||||
|
return VorbisCommentTag.createNewTag();
|
||||||
|
}
|
||||||
|
else if(SupportedFileFormat.MP4.getFilesuffix().equals(extension))
|
||||||
|
{
|
||||||
|
return new Mp4Tag();
|
||||||
|
}
|
||||||
|
else if(SupportedFileFormat.M4A.getFilesuffix().equals(extension))
|
||||||
|
{
|
||||||
|
return new Mp4Tag();
|
||||||
|
}
|
||||||
|
else if(SupportedFileFormat.M4P.getFilesuffix().equals(extension))
|
||||||
|
{
|
||||||
|
return new Mp4Tag();
|
||||||
|
}
|
||||||
|
else if(SupportedFileFormat.WMA.getFilesuffix().equals(extension))
|
||||||
|
{
|
||||||
|
return new AsfTag();
|
||||||
|
}
|
||||||
|
else if(SupportedFileFormat.WAV.getFilesuffix().equals(extension))
|
||||||
|
{
|
||||||
|
return new WavTag(TagOptionSingleton.getInstance().getWavOptions());
|
||||||
|
}
|
||||||
|
else if(SupportedFileFormat.RA.getFilesuffix().equals(extension))
|
||||||
|
{
|
||||||
|
return new RealTag();
|
||||||
|
}
|
||||||
|
else if(SupportedFileFormat.RM.getFilesuffix().equals(extension))
|
||||||
|
{
|
||||||
|
return new RealTag();
|
||||||
|
}
|
||||||
|
else if(SupportedFileFormat.AIF.getFilesuffix().equals(extension))
|
||||||
|
{
|
||||||
|
return new AiffTag();
|
||||||
|
}
|
||||||
|
else if(SupportedFileFormat.AIFC.getFilesuffix().equals(extension))
|
||||||
|
{
|
||||||
|
return new AiffTag();
|
||||||
|
}
|
||||||
|
else if(SupportedFileFormat.AIFF.getFilesuffix().equals(extension))
|
||||||
|
{
|
||||||
|
return new AiffTag();
|
||||||
|
}
|
||||||
|
else if(SupportedFileFormat.DSF.getFilesuffix().equals(extension))
|
||||||
|
{
|
||||||
|
return Dsf.createDefaultTag();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new RuntimeException("Unable to create default tag for this file format");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the tag or if the file doesn't have one at all, create a default tag and return
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public Tag getTagOrCreateDefault()
|
||||||
|
{
|
||||||
|
Tag tag = getTag();
|
||||||
|
if(tag==null)
|
||||||
|
{
|
||||||
|
return createDefaultTag();
|
||||||
|
}
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the tag or if the file doesn't have one at all, create a default tag and set it
|
||||||
|
* as the tag of this file
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public Tag getTagOrCreateAndSetDefault()
|
||||||
|
{
|
||||||
|
Tag tag = getTagOrCreateDefault();
|
||||||
|
setTag(tag);
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the tag and convert to the default tag version or if the file doesn't have one at all, create a default tag
|
||||||
|
* set as tag for this file
|
||||||
|
*
|
||||||
|
* Conversions are currently only necessary/available for formats that support ID3
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public Tag getTagAndConvertOrCreateAndSetDefault()
|
||||||
|
{
|
||||||
|
Tag tag = getTagOrCreateDefault();
|
||||||
|
|
||||||
|
/* TODO Currently only works for Dsf We need additional check here for Wav and Aif because they wrap the ID3 tag so never return
|
||||||
|
* null for getTag() and the wrapper stores the location of the existing tag, would that be broken if tag set to something else
|
||||||
|
*/
|
||||||
|
if(tag instanceof AbstractID3v2Tag)
|
||||||
|
{
|
||||||
|
Tag convertedTag = convertID3Tag((AbstractID3v2Tag)tag, TagOptionSingleton.getInstance().getID3V2Version());
|
||||||
|
if(convertedTag!=null)
|
||||||
|
{
|
||||||
|
setTag(convertedTag);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setTag(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setTag(tag);
|
||||||
|
}
|
||||||
|
return getTag();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param file
|
||||||
|
* @return filename with audioFormat separator stripped off.
|
||||||
|
*/
|
||||||
|
public static String getBaseFilename(File file)
|
||||||
|
{
|
||||||
|
int index=file.getName().toLowerCase().lastIndexOf(".");
|
||||||
|
if(index>0)
|
||||||
|
{
|
||||||
|
return file.getName().substring(0,index);
|
||||||
|
}
|
||||||
|
return file.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If using ID3 format convert tag from current version to another as specified by id3V2Version,
|
||||||
|
*
|
||||||
|
* @return null if no conversion necessary
|
||||||
|
*/
|
||||||
|
public AbstractID3v2Tag convertID3Tag(AbstractID3v2Tag tag, ID3V2Version id3V2Version)
|
||||||
|
{
|
||||||
|
if(tag instanceof ID3v24Tag)
|
||||||
|
{
|
||||||
|
switch(id3V2Version)
|
||||||
|
{
|
||||||
|
case ID3_V22:
|
||||||
|
return new ID3v22Tag((ID3v24Tag)tag);
|
||||||
|
case ID3_V23:
|
||||||
|
return new ID3v23Tag((ID3v24Tag)tag);
|
||||||
|
case ID3_V24:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(tag instanceof ID3v23Tag)
|
||||||
|
{
|
||||||
|
switch(id3V2Version)
|
||||||
|
{
|
||||||
|
case ID3_V22:
|
||||||
|
return new ID3v22Tag((ID3v23Tag)tag);
|
||||||
|
case ID3_V23:
|
||||||
|
return null;
|
||||||
|
case ID3_V24:
|
||||||
|
return new ID3v24Tag((ID3v23Tag)tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(tag instanceof ID3v22Tag)
|
||||||
|
{
|
||||||
|
switch(id3V2Version)
|
||||||
|
{
|
||||||
|
case ID3_V22:
|
||||||
|
return null;
|
||||||
|
case ID3_V23:
|
||||||
|
return new ID3v23Tag((ID3v22Tag)tag);
|
||||||
|
case ID3_V24:
|
||||||
|
return new ID3v24Tag((ID3v22Tag)tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* Entagged Audio Tag library
|
||||||
|
* Copyright (c) 2003-2005 Raphaël Slinckx <raphael@slinckx.net>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library 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
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package com.mp3.jaudiotagger.audio;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.generic.Utils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileFilter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>This is a simple FileFilter that will only allow the file supported by this library.
|
||||||
|
* <p>It will also accept directories. An additional condition is that file must be readable (read permission) and
|
||||||
|
* are not hidden (dot files, or hidden files)
|
||||||
|
*
|
||||||
|
* @author Raphael Slinckx
|
||||||
|
* @version $Id$
|
||||||
|
* @since v0.01
|
||||||
|
*/
|
||||||
|
public class AudioFileFilter implements FileFilter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* allows Directories
|
||||||
|
*/
|
||||||
|
private final boolean allowDirectories;
|
||||||
|
|
||||||
|
public AudioFileFilter( boolean allowDirectories)
|
||||||
|
{
|
||||||
|
this.allowDirectories=allowDirectories;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AudioFileFilter()
|
||||||
|
{
|
||||||
|
this(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Check whether the given file meet the required conditions (supported by the library OR directory).
|
||||||
|
* The File must also be readable and not hidden.
|
||||||
|
*
|
||||||
|
* @param f The file to test
|
||||||
|
* @return a boolean indicating if the file is accepted or not
|
||||||
|
*/
|
||||||
|
public boolean accept(File f)
|
||||||
|
{
|
||||||
|
if (f.isHidden() || !f.canRead())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (f.isDirectory())
|
||||||
|
{
|
||||||
|
return allowDirectories;
|
||||||
|
}
|
||||||
|
|
||||||
|
String ext = Utils.getExtension(f);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (SupportedFileFormat.valueOf(ext.toUpperCase()) != null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(IllegalArgumentException iae)
|
||||||
|
{
|
||||||
|
//Not known enum value
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,502 @@
|
|||||||
|
/*
|
||||||
|
* Entagged Audio Tag library
|
||||||
|
* Copyright (c) 2003-2005 Raphaël Slinckx <raphael@slinckx.net>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library 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
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package com.mp3.jaudiotagger.audio;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.aiff.AiffFileReader;
|
||||||
|
import com.mp3.jaudiotagger.audio.aiff.AiffFileWriter;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.AsfFileReader;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.AsfFileWriter;
|
||||||
|
import com.mp3.jaudiotagger.audio.dsf.DsfFileReader;
|
||||||
|
import com.mp3.jaudiotagger.audio.dsf.DsfFileWriter;
|
||||||
|
import com.mp3.jaudiotagger.audio.exceptions.*;
|
||||||
|
import com.mp3.jaudiotagger.audio.flac.FlacFileReader;
|
||||||
|
import com.mp3.jaudiotagger.audio.flac.FlacFileWriter;
|
||||||
|
import com.mp3.jaudiotagger.audio.generic.*;
|
||||||
|
import com.mp3.jaudiotagger.audio.mp3.MP3FileReader;
|
||||||
|
import com.mp3.jaudiotagger.audio.mp3.MP3FileWriter;
|
||||||
|
import com.mp3.jaudiotagger.audio.mp4.Mp4FileReader;
|
||||||
|
import com.mp3.jaudiotagger.audio.mp4.Mp4FileWriter;
|
||||||
|
import com.mp3.jaudiotagger.audio.ogg.OggFileReader;
|
||||||
|
import com.mp3.jaudiotagger.audio.ogg.OggFileWriter;
|
||||||
|
import com.mp3.jaudiotagger.audio.real.RealFileReader;
|
||||||
|
import com.mp3.jaudiotagger.audio.wav.WavFileReader;
|
||||||
|
import com.mp3.jaudiotagger.audio.wav.WavFileWriter;
|
||||||
|
import com.mp3.jaudiotagger.logging.ErrorMessage;
|
||||||
|
import com.mp3.jaudiotagger.tag.TagException;
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* The main entry point for the Tag Reading/Writing operations, this class will
|
||||||
|
* select the appropriate reader/writer for the given file.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* It selects the appropriate reader/writer based on the file extension (case
|
||||||
|
* ignored).
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Here is an simple example of use:
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* <code>
|
||||||
|
* AudioFile audioFile = AudioFileIO.read(new File("audiofile.mp3")); //Reads the given file.
|
||||||
|
* int bitrate = audioFile.getBitrate(); //Retreives the bitrate of the file.
|
||||||
|
* String artist = audioFile.getTag().getFirst(TagFieldKey.ARTIST); //Retreive the artist name.
|
||||||
|
* audioFile.getTag().setGenre("Progressive Rock"); //Sets the genre to Prog. Rock, note the file on disk is still unmodified.
|
||||||
|
* AudioFileIO.write(audioFile); //Write the modifications in the file on disk.
|
||||||
|
* </code>
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* You can also use the <code>commit()</code> method defined for
|
||||||
|
* <code>AudioFile</code>s to achieve the same goal as
|
||||||
|
* <code>AudioFileIO.write(File)</code>, like this:
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* <code>
|
||||||
|
* AudioFile audioFile = AudioFileIO.read(new File("audiofile.mp3"));
|
||||||
|
* audioFile.getTag().setGenre("Progressive Rock");
|
||||||
|
* audioFile.commit(); //Write the modifications in the file on disk.
|
||||||
|
* </code>
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @author Raphael Slinckx
|
||||||
|
* @version $Id$
|
||||||
|
* @see AudioFile
|
||||||
|
* @see com.mp3.jaudiotagger.tag.Tag
|
||||||
|
* @since v0.01
|
||||||
|
*/
|
||||||
|
public class AudioFileIO
|
||||||
|
{
|
||||||
|
|
||||||
|
//Logger
|
||||||
|
public static Logger logger = Logger.getLogger("com.mp3.jaudiotagger.audio");
|
||||||
|
|
||||||
|
// !! Do not forget to also add new supported extensions to AudioFileFilter
|
||||||
|
// !!
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This field contains the default instance for static use.
|
||||||
|
*/
|
||||||
|
private static AudioFileIO defaultInstance;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Delete the tag, if any, contained in the given file.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param f The file where the tag will be deleted
|
||||||
|
* @throws CannotWriteException If the file could not be written/accessed, the extension
|
||||||
|
* wasn't recognized, or other IO error occurred.
|
||||||
|
* @throws CannotReadException
|
||||||
|
*/
|
||||||
|
public static void delete(AudioFile f) throws CannotReadException, CannotWriteException
|
||||||
|
{
|
||||||
|
getDefaultAudioFileIO().deleteTag(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method returns the default instance for static use.<br>
|
||||||
|
*
|
||||||
|
* @return The default instance.
|
||||||
|
*/
|
||||||
|
public static AudioFileIO getDefaultAudioFileIO()
|
||||||
|
{
|
||||||
|
if (defaultInstance == null)
|
||||||
|
{
|
||||||
|
defaultInstance = new AudioFileIO();
|
||||||
|
}
|
||||||
|
return defaultInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Read the tag contained in the given file.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param f The file to read.
|
||||||
|
* @param ext The extension to be used.
|
||||||
|
* @return The AudioFile with the file tag and the file encoding info.
|
||||||
|
* @throws CannotReadException If the file could not be read, the extension wasn't
|
||||||
|
* recognized, or an IO error occurred during the read.
|
||||||
|
* @throws TagException
|
||||||
|
* @throws ReadOnlyFileException
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidAudioFrameException
|
||||||
|
*/
|
||||||
|
public static AudioFile readAs(File f,String ext)
|
||||||
|
throws CannotReadException, IOException, TagException, ReadOnlyFileException, InvalidAudioFrameException
|
||||||
|
{
|
||||||
|
return getDefaultAudioFileIO().readFileAs(f,ext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Read the tag contained in the given file.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param f The file to read.
|
||||||
|
* @return The AudioFile with the file tag and the file encoding info.
|
||||||
|
* @throws CannotReadException If the file could not be read, the extension wasn't
|
||||||
|
* recognized, or an IO error occurred during the read.
|
||||||
|
* @throws TagException
|
||||||
|
* @throws ReadOnlyFileException
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidAudioFrameException
|
||||||
|
*/
|
||||||
|
public static AudioFile readMagic(File f)
|
||||||
|
throws CannotReadException, IOException, TagException, ReadOnlyFileException, InvalidAudioFrameException
|
||||||
|
{
|
||||||
|
return getDefaultAudioFileIO().readFileMagic(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Read the tag contained in the given file.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param f The file to read.
|
||||||
|
* @return The AudioFile with the file tag and the file encoding info.
|
||||||
|
* @throws CannotReadException If the file could not be read, the extension wasn't
|
||||||
|
* recognized, or an IO error occurred during the read.
|
||||||
|
* @throws TagException
|
||||||
|
* @throws ReadOnlyFileException
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidAudioFrameException
|
||||||
|
*/
|
||||||
|
public static AudioFile read(File f)
|
||||||
|
throws CannotReadException, IOException, TagException, ReadOnlyFileException, InvalidAudioFrameException
|
||||||
|
{
|
||||||
|
return getDefaultAudioFileIO().readFile(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Write the tag contained in the audioFile in the actual file on the disk.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param f The AudioFile to be written
|
||||||
|
* @throws NoWritePermissionsException if the file could not be written to due to file permissions
|
||||||
|
* @throws CannotWriteException If the file could not be written/accessed, the extension
|
||||||
|
* wasn't recognized, or other IO error occurred.
|
||||||
|
*/
|
||||||
|
public static void write(AudioFile f) throws CannotWriteException
|
||||||
|
{
|
||||||
|
getDefaultAudioFileIO().writeFile(f,null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Write the tag contained in the audioFile in the actual file on the disk.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param f The AudioFile to be written
|
||||||
|
* @param targetPath The AudioFile path to which to be written without the extension. Cannot be null
|
||||||
|
* @throws NoWritePermissionsException if the file could not be written to due to file permissions
|
||||||
|
* @throws CannotWriteException If the file could not be written/accessed, the extension
|
||||||
|
* wasn't recognized, or other IO error occurred.
|
||||||
|
*/
|
||||||
|
public static void writeAs(AudioFile f, String targetPath) throws CannotWriteException
|
||||||
|
{
|
||||||
|
if (targetPath == null || targetPath.isEmpty()) {
|
||||||
|
throw new CannotWriteException("Not a valid target path: " + targetPath);
|
||||||
|
}
|
||||||
|
getDefaultAudioFileIO().writeFile(f,targetPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This member is used to broadcast modification events to registered
|
||||||
|
*/
|
||||||
|
private final ModificationHandler modificationHandler;
|
||||||
|
|
||||||
|
// These tables contains all the readers/writers associated with extension
|
||||||
|
// as a key
|
||||||
|
public Map<String, AudioFileReader> readers = new HashMap<String, AudioFileReader>();
|
||||||
|
public Map<String, AudioFileWriter> writers = new HashMap<String, AudioFileWriter>();
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*/
|
||||||
|
public AudioFileIO()
|
||||||
|
{
|
||||||
|
this.modificationHandler = new ModificationHandler();
|
||||||
|
prepareReadersAndWriters();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an listener for all file formats.
|
||||||
|
*
|
||||||
|
* @param listener listener
|
||||||
|
*/
|
||||||
|
public void addAudioFileModificationListener(
|
||||||
|
AudioFileModificationListener listener)
|
||||||
|
{
|
||||||
|
this.modificationHandler.addAudioFileModificationListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Delete the tag, if any, contained in the given file.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param f The file where the tag will be deleted
|
||||||
|
* @throws CannotWriteException If the file could not be written/accessed, the extension
|
||||||
|
* wasn't recognized, or other IO error occurred.
|
||||||
|
* @throws CannotReadException
|
||||||
|
*/
|
||||||
|
public void deleteTag(AudioFile f) throws CannotReadException, CannotWriteException
|
||||||
|
{
|
||||||
|
String ext = Utils.getExtension(f.getFile());
|
||||||
|
|
||||||
|
Object afw = writers.get(ext);
|
||||||
|
if (afw == null)
|
||||||
|
{
|
||||||
|
throw new CannotWriteException(ErrorMessage.NO_DELETER_FOR_THIS_FORMAT.getMsg(ext));
|
||||||
|
}
|
||||||
|
|
||||||
|
((AudioFileWriter) afw).delete(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the readers and writers.
|
||||||
|
*/
|
||||||
|
private void prepareReadersAndWriters()
|
||||||
|
{
|
||||||
|
|
||||||
|
// Tag Readers
|
||||||
|
readers.put(SupportedFileFormat.OGG.getFilesuffix(), new OggFileReader());
|
||||||
|
readers.put(SupportedFileFormat.FLAC.getFilesuffix(),new FlacFileReader());
|
||||||
|
readers.put(SupportedFileFormat.MP3.getFilesuffix(), new MP3FileReader());
|
||||||
|
readers.put(SupportedFileFormat.MP4.getFilesuffix(), new Mp4FileReader());
|
||||||
|
readers.put(SupportedFileFormat.M4A.getFilesuffix(), new Mp4FileReader());
|
||||||
|
readers.put(SupportedFileFormat.M4P.getFilesuffix(), new Mp4FileReader());
|
||||||
|
readers.put(SupportedFileFormat.M4B.getFilesuffix(), new Mp4FileReader());
|
||||||
|
readers.put(SupportedFileFormat.WAV.getFilesuffix(), new WavFileReader());
|
||||||
|
readers.put(SupportedFileFormat.WMA.getFilesuffix(), new AsfFileReader());
|
||||||
|
readers.put(SupportedFileFormat.AIF.getFilesuffix(), new AiffFileReader());
|
||||||
|
readers.put(SupportedFileFormat.AIFC.getFilesuffix(), new AiffFileReader());
|
||||||
|
readers.put(SupportedFileFormat.AIFF.getFilesuffix(), new AiffFileReader());
|
||||||
|
readers.put(SupportedFileFormat.DSF.getFilesuffix(), new DsfFileReader());
|
||||||
|
final RealFileReader realReader = new RealFileReader();
|
||||||
|
readers.put(SupportedFileFormat.RA.getFilesuffix(), realReader);
|
||||||
|
readers.put(SupportedFileFormat.RM.getFilesuffix(), realReader);
|
||||||
|
|
||||||
|
// Tag Writers
|
||||||
|
writers.put(SupportedFileFormat.OGG.getFilesuffix(), new OggFileWriter());
|
||||||
|
writers.put(SupportedFileFormat.FLAC.getFilesuffix(), new FlacFileWriter());
|
||||||
|
writers.put(SupportedFileFormat.MP3.getFilesuffix(), new MP3FileWriter());
|
||||||
|
writers.put(SupportedFileFormat.MP4.getFilesuffix(), new Mp4FileWriter());
|
||||||
|
writers.put(SupportedFileFormat.M4A.getFilesuffix(), new Mp4FileWriter());
|
||||||
|
writers.put(SupportedFileFormat.M4P.getFilesuffix(), new Mp4FileWriter());
|
||||||
|
writers.put(SupportedFileFormat.M4B.getFilesuffix(), new Mp4FileWriter());
|
||||||
|
writers.put(SupportedFileFormat.WAV.getFilesuffix(), new WavFileWriter());
|
||||||
|
writers.put(SupportedFileFormat.WMA.getFilesuffix(), new AsfFileWriter());
|
||||||
|
writers.put(SupportedFileFormat.AIF.getFilesuffix(), new AiffFileWriter());
|
||||||
|
writers.put(SupportedFileFormat.AIFC.getFilesuffix(), new AiffFileWriter());
|
||||||
|
writers.put(SupportedFileFormat.AIFF.getFilesuffix(), new AiffFileWriter());
|
||||||
|
writers.put(SupportedFileFormat.DSF.getFilesuffix(), new DsfFileWriter());
|
||||||
|
|
||||||
|
// Register modificationHandler
|
||||||
|
Iterator<AudioFileWriter> it = writers.values().iterator();
|
||||||
|
for (AudioFileWriter curr : writers.values())
|
||||||
|
{
|
||||||
|
curr.setAudioFileModificationListener(this.modificationHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Read the tag contained in the given file.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param f The file to read.
|
||||||
|
* @return The AudioFile with the file tag and the file encoding info.
|
||||||
|
* @throws CannotReadException If the file could not be read, the extension wasn't
|
||||||
|
* recognized, or an IO error occurred during the read.
|
||||||
|
* @throws TagException
|
||||||
|
* @throws ReadOnlyFileException
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidAudioFrameException
|
||||||
|
*/
|
||||||
|
public AudioFile readFile(File f)
|
||||||
|
throws CannotReadException, IOException, TagException, ReadOnlyFileException, InvalidAudioFrameException
|
||||||
|
{
|
||||||
|
checkFileExists(f);
|
||||||
|
String ext = Utils.getExtension(f);
|
||||||
|
|
||||||
|
AudioFileReader afr = readers.get(ext);
|
||||||
|
if (afr == null)
|
||||||
|
{
|
||||||
|
throw new CannotReadException(ErrorMessage.NO_READER_FOR_THIS_FORMAT.getMsg(ext));
|
||||||
|
}
|
||||||
|
AudioFile tempFile = afr.read(f);
|
||||||
|
tempFile.setExt(ext);
|
||||||
|
return tempFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Read the tag contained in the given file.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param f The file to read.
|
||||||
|
* @return The AudioFile with the file tag and the file encoding info.
|
||||||
|
* @throws CannotReadException If the file could not be read, the extension wasn't
|
||||||
|
* recognized, or an IO error occurred during the read.
|
||||||
|
* @throws TagException
|
||||||
|
* @throws ReadOnlyFileException
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidAudioFrameException
|
||||||
|
*/
|
||||||
|
public AudioFile readFileMagic(File f)
|
||||||
|
throws CannotReadException, IOException, TagException, ReadOnlyFileException, InvalidAudioFrameException
|
||||||
|
{
|
||||||
|
checkFileExists(f);
|
||||||
|
String ext = Utils.getMagicExtension(f);
|
||||||
|
|
||||||
|
AudioFileReader afr = readers.get(ext);
|
||||||
|
if (afr == null)
|
||||||
|
{
|
||||||
|
throw new CannotReadException(ErrorMessage.NO_READER_FOR_THIS_FORMAT.getMsg(ext));
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioFile tempFile = afr.read(f);
|
||||||
|
tempFile.setExt(ext);
|
||||||
|
return tempFile;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Read the tag contained in the given file.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param f The file to read.
|
||||||
|
* @param ext The extension to be used.
|
||||||
|
* @return The AudioFile with the file tag and the file encoding info.
|
||||||
|
* @throws CannotReadException If the file could not be read, the extension wasn't
|
||||||
|
* recognized, or an IO error occurred during the read.
|
||||||
|
* @throws TagException
|
||||||
|
* @throws ReadOnlyFileException
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidAudioFrameException
|
||||||
|
*/
|
||||||
|
public AudioFile readFileAs(File f,String ext)
|
||||||
|
throws CannotReadException, IOException, TagException, ReadOnlyFileException, InvalidAudioFrameException
|
||||||
|
{
|
||||||
|
checkFileExists(f);
|
||||||
|
// String ext = Utils.getExtension(f);
|
||||||
|
|
||||||
|
AudioFileReader afr = readers.get(ext);
|
||||||
|
if (afr == null)
|
||||||
|
{
|
||||||
|
throw new CannotReadException(ErrorMessage.NO_READER_FOR_THIS_FORMAT.getMsg(ext));
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioFile tempFile = afr.read(f);
|
||||||
|
tempFile.setExt(ext);
|
||||||
|
return tempFile;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check does file exist
|
||||||
|
*
|
||||||
|
* @param file
|
||||||
|
* @throws FileNotFoundException
|
||||||
|
*/
|
||||||
|
public void checkFileExists(File file)throws FileNotFoundException
|
||||||
|
{
|
||||||
|
logger.config("Reading file:" + "path" + file.getPath() + ":abs:" + file.getAbsolutePath());
|
||||||
|
if (!file.exists())
|
||||||
|
{
|
||||||
|
logger.severe("Unable to find:" + file.getPath());
|
||||||
|
throw new FileNotFoundException(ErrorMessage.UNABLE_TO_FIND_FILE.getMsg(file.getPath()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Removes a listener for all file formats.
|
||||||
|
*
|
||||||
|
* @param listener listener
|
||||||
|
*/
|
||||||
|
public void removeAudioFileModificationListener(
|
||||||
|
AudioFileModificationListener listener)
|
||||||
|
{
|
||||||
|
this.modificationHandler.removeAudioFileModificationListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Write the tag contained in the audioFile in the actual file on the disk.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param f The AudioFile to be written
|
||||||
|
* @param targetPath a file path, without an extension, which provides a "save as". If null, then normal "save" function
|
||||||
|
* @throws NoWritePermissionsException if the file could not be written to due to file permissions
|
||||||
|
* @throws CannotWriteException If the file could not be written/accessed, the extension
|
||||||
|
* wasn't recognized, or other IO error occurred.
|
||||||
|
*/
|
||||||
|
public void writeFile(AudioFile f, String targetPath) throws CannotWriteException
|
||||||
|
{
|
||||||
|
String ext = f.getExt();
|
||||||
|
|
||||||
|
if (targetPath != null && !targetPath.isEmpty())
|
||||||
|
{
|
||||||
|
final File destination = new File(targetPath + "." + ext);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Utils.copyThrowsOnException(f.getFile(), destination);
|
||||||
|
f.setFile(destination);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new CannotWriteException("Error While Copying" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioFileWriter afw = writers.get(ext);
|
||||||
|
if (afw == null)
|
||||||
|
{
|
||||||
|
throw new CannotWriteException(ErrorMessage.NO_WRITER_FOR_THIS_FORMAT.getMsg(ext));
|
||||||
|
}
|
||||||
|
|
||||||
|
afw.write(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes resource unconditionally such as input stream or output stream.
|
||||||
|
*/
|
||||||
|
public static void closeQuietly(Closeable... closeables) {
|
||||||
|
for (Closeable closeable : closeables) {
|
||||||
|
if (closeable != null) {
|
||||||
|
try {
|
||||||
|
closeable.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,113 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Representation of AudioHeader
|
||||||
|
*
|
||||||
|
* <p>Contains info about the Audio Header
|
||||||
|
*/
|
||||||
|
public interface AudioHeader
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return the audio file type
|
||||||
|
*/
|
||||||
|
public abstract String getEncodingType();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the ByteRate of the Audio, this is the total average amount of bytes of data sampled per second
|
||||||
|
*/
|
||||||
|
public Integer getByteRate();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the BitRate of the Audio, this is the amount of kilobits of data sampled per second
|
||||||
|
*/
|
||||||
|
public String getBitRate();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bitRate as a number, this is the amount of kilobits of data sampled per second
|
||||||
|
*/
|
||||||
|
public long getBitRateAsNumber();
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @return length of the audio data in bytes, exactly what this means depends on the audio format
|
||||||
|
*
|
||||||
|
* TODO currently only used by Wav/Aiff/Flac/Mp4
|
||||||
|
*/
|
||||||
|
public Long getAudioDataLength();
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @return the location in the file where the audio samples start
|
||||||
|
*
|
||||||
|
* TODO currently only used by Wav/Aiff/Flac/Mp4
|
||||||
|
*/
|
||||||
|
public Long getAudioDataStartPosition();
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @return the location in the file where the audio samples end
|
||||||
|
*
|
||||||
|
* TODO currently only used by Wav/Aiff/Flac/Mp4
|
||||||
|
*/
|
||||||
|
public Long getAudioDataEndPosition();
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the Sampling rate, the number of samples taken per second
|
||||||
|
*/
|
||||||
|
public String getSampleRate();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return he Sampling rate, the number of samples taken per second
|
||||||
|
*/
|
||||||
|
public int getSampleRateAsNumber();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the format
|
||||||
|
*/
|
||||||
|
public String getFormat();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the number of channels (i.e 1 = Mono, 2 = Stereo)
|
||||||
|
*/
|
||||||
|
public String getChannels();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return if the sampling bitRate is variable or constant
|
||||||
|
*/
|
||||||
|
public boolean isVariableBitRate();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return track length in seconds
|
||||||
|
*/
|
||||||
|
public int getTrackLength();
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @return track length as float
|
||||||
|
*/
|
||||||
|
public double getPreciseTrackLength();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the number of bits in each sample
|
||||||
|
*/
|
||||||
|
public int getBitsPerSample();
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @return if the audio codec is lossless or lossy
|
||||||
|
*/
|
||||||
|
public boolean isLossless();
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @return the total number of samples, this can usually be used in conjunction with the
|
||||||
|
* sample rate to determine the track duration
|
||||||
|
*/
|
||||||
|
public Long getNoOfSamples();
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Files formats currently supported by Library.
|
||||||
|
* Each enum value is associated with a file suffix (extension).
|
||||||
|
*/
|
||||||
|
public enum SupportedFileFormat
|
||||||
|
{
|
||||||
|
OGG("ogg"),
|
||||||
|
MP3("mp3"),
|
||||||
|
FLAC("flac"),
|
||||||
|
MP4("mp4"),
|
||||||
|
M4A("m4a"),
|
||||||
|
M4P("m4p"),
|
||||||
|
WMA("wma"),
|
||||||
|
WAV("wav"),
|
||||||
|
RA("ra"),
|
||||||
|
RM("rm"),
|
||||||
|
M4B("m4b"),
|
||||||
|
AIF("aif"),
|
||||||
|
AIFF("aiff"),
|
||||||
|
AIFC("aifc"),
|
||||||
|
DSF("dsf");
|
||||||
|
|
||||||
|
private String filesuffix;
|
||||||
|
|
||||||
|
/** Constructor for internal use by this enum.
|
||||||
|
*/
|
||||||
|
SupportedFileFormat(String filesuffix)
|
||||||
|
{
|
||||||
|
this.filesuffix = filesuffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the file suffix (lower case without initial .) associated with the format.
|
||||||
|
*/
|
||||||
|
public String getFilesuffix()
|
||||||
|
{
|
||||||
|
return filesuffix;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,233 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.aiff;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.generic.GenericAudioHeader;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Non-"tag" metadata from the AIFF file. In general, read-only.
|
||||||
|
*/
|
||||||
|
public class AiffAudioHeader extends GenericAudioHeader
|
||||||
|
{
|
||||||
|
|
||||||
|
public enum Endian
|
||||||
|
{
|
||||||
|
BIG_ENDIAN,
|
||||||
|
LITTLE_ENDIAN
|
||||||
|
}
|
||||||
|
|
||||||
|
private AiffType fileType;
|
||||||
|
private Date timestamp;
|
||||||
|
private Endian endian;
|
||||||
|
private String audioEncoding;
|
||||||
|
private String name;
|
||||||
|
private String author;
|
||||||
|
private String copyright;
|
||||||
|
|
||||||
|
private List<String> applicationIdentifiers;
|
||||||
|
private List<String> comments;
|
||||||
|
private List<String> annotations;
|
||||||
|
|
||||||
|
public AiffAudioHeader()
|
||||||
|
{
|
||||||
|
this.applicationIdentifiers = new ArrayList<String>();
|
||||||
|
this.comments = new ArrayList<String>();
|
||||||
|
this.annotations = new ArrayList<String>();
|
||||||
|
this.endian = Endian.BIG_ENDIAN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the timestamp of the file.
|
||||||
|
*/
|
||||||
|
public Date getTimestamp()
|
||||||
|
{
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the timestamp.
|
||||||
|
*/
|
||||||
|
public void setTimestamp(Date d)
|
||||||
|
{
|
||||||
|
timestamp = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the file type (AIFF or AIFC)
|
||||||
|
*/
|
||||||
|
public AiffType getFileType()
|
||||||
|
{
|
||||||
|
return fileType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the file type (AIFF or AIFC)
|
||||||
|
*/
|
||||||
|
public void setFileType(AiffType typ)
|
||||||
|
{
|
||||||
|
fileType = typ;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the author
|
||||||
|
*/
|
||||||
|
public String getAuthor()
|
||||||
|
{
|
||||||
|
return author;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the author
|
||||||
|
*/
|
||||||
|
public void setAuthor(String a)
|
||||||
|
{
|
||||||
|
author = a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the name. May be null.
|
||||||
|
*/
|
||||||
|
public String getName()
|
||||||
|
{
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the name
|
||||||
|
*/
|
||||||
|
public void setName(String n)
|
||||||
|
{
|
||||||
|
name = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the copyright. May be null.
|
||||||
|
*/
|
||||||
|
public String getCopyright()
|
||||||
|
{
|
||||||
|
return copyright;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the copyright
|
||||||
|
*/
|
||||||
|
public void setCopyright(String c)
|
||||||
|
{
|
||||||
|
copyright = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return endian status (big or little)
|
||||||
|
*/
|
||||||
|
public Endian getEndian()
|
||||||
|
{
|
||||||
|
return endian;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set endian status (big or little)
|
||||||
|
*/
|
||||||
|
public void setEndian(Endian e)
|
||||||
|
{
|
||||||
|
endian = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of all application identifiers
|
||||||
|
*/
|
||||||
|
public List<String> getApplicationIdentifiers()
|
||||||
|
{
|
||||||
|
return applicationIdentifiers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an application identifier. There can be any number of these.
|
||||||
|
*/
|
||||||
|
public void addApplicationIdentifier(String id)
|
||||||
|
{
|
||||||
|
applicationIdentifiers.add(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of all annotations
|
||||||
|
*/
|
||||||
|
public List<String> getAnnotations()
|
||||||
|
{
|
||||||
|
return annotations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an annotation. There can be any number of these.
|
||||||
|
*/
|
||||||
|
public void addAnnotation(String a)
|
||||||
|
{
|
||||||
|
annotations.add(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of all comments
|
||||||
|
*/
|
||||||
|
public List<String> getComments()
|
||||||
|
{
|
||||||
|
return comments;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a comment. There can be any number of these.
|
||||||
|
*/
|
||||||
|
public void addComment(String c)
|
||||||
|
{
|
||||||
|
comments.add(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder("\n");
|
||||||
|
|
||||||
|
if(name!=null && !name.isEmpty())
|
||||||
|
{
|
||||||
|
sb.append("\tName:"+name+"\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(author!=null && !author.isEmpty())
|
||||||
|
{
|
||||||
|
sb.append("\tAuthor:"+author+"\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(copyright!=null && !copyright.isEmpty())
|
||||||
|
{
|
||||||
|
sb.append("\tCopyright:"+copyright+"\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(comments.size()>0)
|
||||||
|
{
|
||||||
|
sb.append("Comments:\n");
|
||||||
|
for(String next:comments)
|
||||||
|
{
|
||||||
|
sb.append("\t"+next+"\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(applicationIdentifiers.size()>0)
|
||||||
|
{
|
||||||
|
sb.append("ApplicationIds:\n");
|
||||||
|
for(String next:applicationIdentifiers)
|
||||||
|
{
|
||||||
|
sb.append("\t"+next+"\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(annotations.size()>0)
|
||||||
|
{
|
||||||
|
sb.append("Annotations:\n");
|
||||||
|
for(String next:annotations)
|
||||||
|
{
|
||||||
|
sb.append("\t"+next+"\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.toString() + sb.toString();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,93 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.aiff;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.exceptions.CannotReadException;
|
||||||
|
import com.mp3.jaudiotagger.audio.generic.Utils;
|
||||||
|
import com.mp3.jaudiotagger.logging.Hex;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.FileChannel;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import static java.nio.ByteOrder.BIG_ENDIAN;
|
||||||
|
import static com.mp3.jaudiotagger.audio.aiff.AiffType.AIFC;
|
||||||
|
import static com.mp3.jaudiotagger.audio.aiff.AiffType.AIFF;
|
||||||
|
import static com.mp3.jaudiotagger.audio.iff.IffHeaderChunk.HEADER_LENGTH;
|
||||||
|
import static com.mp3.jaudiotagger.audio.iff.IffHeaderChunk.TYPE_LENGTH;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* Aiff File Header always consists of
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li>ckID - always FORM</li>
|
||||||
|
* <li>chSize - size in 4 bytes</li>
|
||||||
|
* <li>formType - currently either AIFF or AIFC, see {@link AiffType}</li>
|
||||||
|
* <li>chunks[] - an array of chunks</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public class AiffFileHeader
|
||||||
|
{
|
||||||
|
private static final String FORM = "FORM";
|
||||||
|
private static Logger logger = Logger.getLogger("com.mp3.jaudiotagger.audio.aiff.AudioFileHeader");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the file header and registers the data (file type) with the given header.
|
||||||
|
*
|
||||||
|
* @param fc random access file
|
||||||
|
* @param aiffAudioHeader the {@link com.mp3.jaudiotagger.audio.AudioHeader} we set the read data to
|
||||||
|
* @param fileName
|
||||||
|
* @return the number of bytes in the FORM chunk, i.e. the size of the payload
|
||||||
|
* @throws IOException
|
||||||
|
* @throws CannotReadException if the file is not a valid AIFF file
|
||||||
|
*/
|
||||||
|
public long readHeader(FileChannel fc, final AiffAudioHeader aiffAudioHeader, String fileName) throws IOException, CannotReadException
|
||||||
|
{
|
||||||
|
final ByteBuffer headerData = ByteBuffer.allocateDirect(HEADER_LENGTH);
|
||||||
|
headerData.order(BIG_ENDIAN);
|
||||||
|
final int bytesRead = fc.read(headerData);
|
||||||
|
headerData.position(0);
|
||||||
|
|
||||||
|
if (bytesRead < HEADER_LENGTH)
|
||||||
|
{
|
||||||
|
throw new IOException(fileName + " AIFF:Unable to read required number of databytes read:" + bytesRead + ":required:" + HEADER_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String signature = Utils.readFourBytesAsChars(headerData);
|
||||||
|
if(FORM.equals(signature))
|
||||||
|
{
|
||||||
|
// read chunk size
|
||||||
|
final long chunkSize = headerData.getInt();
|
||||||
|
logger.severe(fileName + " Reading AIFF header size:" + Hex.asDecAndHex(chunkSize));
|
||||||
|
|
||||||
|
readFileType(headerData, aiffAudioHeader);
|
||||||
|
// subtract the file type length from the chunk size to get remaining number of bytes
|
||||||
|
return chunkSize - TYPE_LENGTH;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new CannotReadException(fileName + "Not an AIFF file: incorrect signature " + signature);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the file type ({@link AiffType}).
|
||||||
|
*
|
||||||
|
* @throws CannotReadException if the file type is not supported
|
||||||
|
*/
|
||||||
|
private void readFileType(final ByteBuffer bytes, final AiffAudioHeader aiffAudioHeader) throws IOException, CannotReadException {
|
||||||
|
final String type = Utils.readFourBytesAsChars(bytes);
|
||||||
|
if (AIFF.getCode().equals(type))
|
||||||
|
{
|
||||||
|
aiffAudioHeader.setFileType(AIFF);
|
||||||
|
}
|
||||||
|
else if (AIFC.getCode().equals(type))
|
||||||
|
{
|
||||||
|
aiffAudioHeader.setFileType(AIFC);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new CannotReadException("Invalid AIFF file: Incorrect file type info " + type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.aiff;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.exceptions.CannotReadException;
|
||||||
|
import com.mp3.jaudiotagger.audio.generic.AudioFileReader2;
|
||||||
|
import com.mp3.jaudiotagger.audio.generic.GenericAudioHeader;
|
||||||
|
import com.mp3.jaudiotagger.tag.Tag;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads Audio and Metadata information contained in Aiff file.
|
||||||
|
*/
|
||||||
|
public class AiffFileReader extends AudioFileReader2 {
|
||||||
|
private AiffInfoReader ir = new AiffInfoReader();
|
||||||
|
private AiffTagReader im = new AiffTagReader();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected GenericAudioHeader getEncodingInfo(File file) throws CannotReadException, IOException {
|
||||||
|
return ir.read(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Tag getTag(File file) throws CannotReadException, IOException {
|
||||||
|
return im.read(file);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* Entagged Audio Tag library
|
||||||
|
* Copyright (c) 2003-2005 Raphaël Slinckx <raphael@slinckx.net>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library 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
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package com.mp3.jaudiotagger.audio.aiff;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.exceptions.CannotWriteException;
|
||||||
|
import com.mp3.jaudiotagger.audio.generic.AudioFileWriter2;
|
||||||
|
import com.mp3.jaudiotagger.tag.Tag;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write/delete tag info for Aiff file (Old Apple format)
|
||||||
|
*/
|
||||||
|
public class AiffFileWriter extends AudioFileWriter2
|
||||||
|
{
|
||||||
|
|
||||||
|
private AiffTagWriter tw = new AiffTagWriter();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void writeTag(Tag tag, File file) throws CannotWriteException
|
||||||
|
{
|
||||||
|
tw.write(tag, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void deleteTag(Tag tag, File file) throws CannotWriteException
|
||||||
|
{
|
||||||
|
tw.delete(tag, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,161 @@
|
|||||||
|
|
||||||
|
package com.mp3.jaudiotagger.audio.aiff;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.AudioFileIO;
|
||||||
|
import com.mp3.jaudiotagger.audio.aiff.chunk.*;
|
||||||
|
import com.mp3.jaudiotagger.audio.exceptions.CannotReadException;
|
||||||
|
import com.mp3.jaudiotagger.audio.generic.GenericAudioHeader;
|
||||||
|
import com.mp3.jaudiotagger.audio.generic.Utils;
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.Chunk;
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.ChunkHeader;
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.IffHeaderChunk;
|
||||||
|
import com.mp3.jaudiotagger.logging.Hex;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.RandomAccessFile;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.nio.channels.FileChannel;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read Aiff chunks, except the ID3 chunk.
|
||||||
|
*/
|
||||||
|
public class AiffInfoReader extends AiffChunkReader {
|
||||||
|
public static Logger logger = Logger.getLogger("com.mp3.jaudiotagger.audio.aiff");
|
||||||
|
|
||||||
|
|
||||||
|
protected GenericAudioHeader read(File file) throws CannotReadException, IOException {
|
||||||
|
RandomAccessFile raf = null;
|
||||||
|
try {
|
||||||
|
raf = new RandomAccessFile(file, "r");
|
||||||
|
FileChannel fc = raf.getChannel();
|
||||||
|
logger.config(file + " Reading AIFF file size:" + Hex.asDecAndHex(fc.size()));
|
||||||
|
AiffAudioHeader aiffAudioHeader = new AiffAudioHeader();
|
||||||
|
final AiffFileHeader fileHeader = new AiffFileHeader();
|
||||||
|
long noOfBytes = fileHeader.readHeader(fc, aiffAudioHeader, file.toString());
|
||||||
|
while (fc.position() < fc.size()) {
|
||||||
|
if (!readChunk(fc, aiffAudioHeader, file.toString())) {
|
||||||
|
logger.severe(file + " UnableToReadProcessChunk");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
calculateBitRate(aiffAudioHeader);
|
||||||
|
return aiffAudioHeader;
|
||||||
|
} finally {
|
||||||
|
AudioFileIO.closeQuietly(raf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate bitrate, done it here because requires data from multiple chunks
|
||||||
|
*
|
||||||
|
* @param info
|
||||||
|
* @throws CannotReadException
|
||||||
|
*/
|
||||||
|
private void calculateBitRate(GenericAudioHeader info) throws CannotReadException {
|
||||||
|
if (info.getAudioDataLength() != null) {
|
||||||
|
info.setBitRate((int) (Math.round(info.getAudioDataLength()
|
||||||
|
* Utils.BITS_IN_BYTE_MULTIPLIER / (info.getPreciseTrackLength() * Utils.KILOBYTE_MULTIPLIER))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads an AIFF Chunk.
|
||||||
|
*
|
||||||
|
* @return {@code false}, if we were not able to read a valid chunk id
|
||||||
|
*/
|
||||||
|
private boolean readChunk(FileChannel fc, AiffAudioHeader aiffAudioHeader, String fileName) throws IOException, CannotReadException {
|
||||||
|
logger.config(fileName + " Reading Info Chunk");
|
||||||
|
final Chunk chunk;
|
||||||
|
final ChunkHeader chunkHeader = new ChunkHeader(ByteOrder.BIG_ENDIAN);
|
||||||
|
if (!chunkHeader.readHeader(fc)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.config(fileName + "Reading Next Chunk:" + chunkHeader.getID() + ":starting at:" + chunkHeader.getStartLocationInFile() + ":sizeIncHeader:" + (chunkHeader.getSize() + ChunkHeader.CHUNK_HEADER_SIZE));
|
||||||
|
chunk = createChunk(fc, chunkHeader, aiffAudioHeader);
|
||||||
|
if (chunk != null) {
|
||||||
|
if (!chunk.readChunk()) {
|
||||||
|
logger.severe(fileName + "ChunkReadFail:" + chunkHeader.getID());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (chunkHeader.getSize() < 0) {
|
||||||
|
String msg = fileName + " Not a valid header, unable to read a sensible size:Header"
|
||||||
|
+ chunkHeader.getID() + "Size:" + chunkHeader.getSize();
|
||||||
|
logger.severe(msg);
|
||||||
|
throw new CannotReadException(msg);
|
||||||
|
}
|
||||||
|
fc.position(fc.position() + chunkHeader.getSize());
|
||||||
|
}
|
||||||
|
IffHeaderChunk.ensureOnEqualBoundary(fc, chunkHeader);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a chunk. May return {@code null}, if the chunk is not of a valid type.
|
||||||
|
*
|
||||||
|
* @param fc
|
||||||
|
* @param chunkHeader
|
||||||
|
* @param aiffAudioHeader
|
||||||
|
* @return
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
private Chunk createChunk(FileChannel fc, final ChunkHeader chunkHeader, AiffAudioHeader aiffAudioHeader)
|
||||||
|
throws IOException {
|
||||||
|
final AiffChunkType chunkType = AiffChunkType.get(chunkHeader.getID());
|
||||||
|
Chunk chunk;
|
||||||
|
if (chunkType != null) {
|
||||||
|
switch (chunkType) {
|
||||||
|
case FORMAT_VERSION:
|
||||||
|
chunk = new FormatVersionChunk(chunkHeader, readChunkDataIntoBuffer(fc, chunkHeader), aiffAudioHeader);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case APPLICATION:
|
||||||
|
chunk = new ApplicationChunk(chunkHeader, readChunkDataIntoBuffer(fc, chunkHeader), aiffAudioHeader);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case COMMON:
|
||||||
|
chunk = new CommonChunk(chunkHeader, readChunkDataIntoBuffer(fc, chunkHeader), aiffAudioHeader);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case COMMENTS:
|
||||||
|
chunk = new CommentsChunk(chunkHeader, readChunkDataIntoBuffer(fc, chunkHeader), aiffAudioHeader);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NAME:
|
||||||
|
chunk = new NameChunk(chunkHeader, readChunkDataIntoBuffer(fc, chunkHeader), aiffAudioHeader);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AUTHOR:
|
||||||
|
chunk = new AuthorChunk(chunkHeader, readChunkDataIntoBuffer(fc, chunkHeader), aiffAudioHeader);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case COPYRIGHT:
|
||||||
|
chunk = new CopyrightChunk(chunkHeader, readChunkDataIntoBuffer(fc, chunkHeader), aiffAudioHeader);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ANNOTATION:
|
||||||
|
chunk = new AnnotationChunk(chunkHeader, readChunkDataIntoBuffer(fc, chunkHeader), aiffAudioHeader);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SOUND:
|
||||||
|
//Dont need to read chunk itself just need size
|
||||||
|
aiffAudioHeader.setAudioDataLength(chunkHeader.getSize());
|
||||||
|
aiffAudioHeader.setAudioDataStartPosition(fc.position());
|
||||||
|
aiffAudioHeader.setAudioDataEndPosition(fc.position() + chunkHeader.getSize());
|
||||||
|
|
||||||
|
chunk = null;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
chunk = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
chunk = null;
|
||||||
|
}
|
||||||
|
return chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.aiff;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum for AIFF fields that don't have obvious matches in FieldKey
|
||||||
|
*/
|
||||||
|
public enum AiffTagFieldKey
|
||||||
|
{
|
||||||
|
TIMESTAMP("TIMESTAMP");
|
||||||
|
|
||||||
|
private String fieldName;
|
||||||
|
|
||||||
|
AiffTagFieldKey(String fieldName)
|
||||||
|
{
|
||||||
|
this.fieldName = fieldName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFieldName()
|
||||||
|
{
|
||||||
|
return fieldName;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,138 @@
|
|||||||
|
|
||||||
|
package com.mp3.jaudiotagger.audio.aiff;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.AudioFileIO;
|
||||||
|
import com.mp3.jaudiotagger.audio.aiff.chunk.AiffChunkReader;
|
||||||
|
import com.mp3.jaudiotagger.audio.aiff.chunk.AiffChunkType;
|
||||||
|
import com.mp3.jaudiotagger.audio.aiff.chunk.ID3Chunk;
|
||||||
|
import com.mp3.jaudiotagger.audio.exceptions.CannotReadException;
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.Chunk;
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.ChunkHeader;
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.ChunkSummary;
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.IffHeaderChunk;
|
||||||
|
import com.mp3.jaudiotagger.logging.Hex;
|
||||||
|
import com.mp3.jaudiotagger.tag.aiff.AiffTag;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.RandomAccessFile;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.nio.channels.FileChannel;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the AIff file chunks, until finds Aiff Common chunk and then generates AudioHeader from it
|
||||||
|
*/
|
||||||
|
public class AiffTagReader extends AiffChunkReader {
|
||||||
|
public static Logger logger = Logger.getLogger("com.mp3.jaudiotagger.audio.aiff");
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read editable Metadata
|
||||||
|
*
|
||||||
|
* @param file
|
||||||
|
* @return
|
||||||
|
* @throws CannotReadException
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public AiffTag read(File file) throws CannotReadException, IOException {
|
||||||
|
RandomAccessFile raf = null;
|
||||||
|
try {
|
||||||
|
raf = new RandomAccessFile(file, "r");
|
||||||
|
FileChannel fc = raf.getChannel();
|
||||||
|
AiffAudioHeader aiffAudioHeader = new AiffAudioHeader();
|
||||||
|
AiffTag aiffTag = new AiffTag();
|
||||||
|
|
||||||
|
final AiffFileHeader fileHeader = new AiffFileHeader();
|
||||||
|
fileHeader.readHeader(fc, aiffAudioHeader, file.toString());
|
||||||
|
while (fc.position() < fc.size()) {
|
||||||
|
if (!readChunk(fc, aiffTag, file.toString())) {
|
||||||
|
logger.severe(file + " UnableToReadProcessChunk");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aiffTag.getID3Tag() == null) {
|
||||||
|
aiffTag.setID3Tag(AiffTag.createDefaultID3Tag());
|
||||||
|
}
|
||||||
|
return aiffTag;
|
||||||
|
} finally {
|
||||||
|
AudioFileIO.closeQuietly(raf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads an AIFF ID3 Chunk.
|
||||||
|
*
|
||||||
|
* @return {@code false}, if we were not able to read a valid chunk id
|
||||||
|
*/
|
||||||
|
private boolean readChunk(FileChannel fc, AiffTag aiffTag, String fileName) throws IOException {
|
||||||
|
logger.config(fileName + " Reading Tag Chunk");
|
||||||
|
|
||||||
|
ChunkHeader chunkHeader = new ChunkHeader(ByteOrder.BIG_ENDIAN);
|
||||||
|
if (!chunkHeader.readHeader(fc)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
logger.config(fileName + " Reading Chunk:" + chunkHeader.getID() + ":starting at:"
|
||||||
|
+ Hex.asDecAndHex(chunkHeader.getStartLocationInFile())
|
||||||
|
+ ":sizeIncHeader:" + (chunkHeader.getSize() + ChunkHeader.CHUNK_HEADER_SIZE));
|
||||||
|
|
||||||
|
long startLocationOfId3TagInFile = fc.position();
|
||||||
|
AiffChunkType chunkType = AiffChunkType.get(chunkHeader.getID());
|
||||||
|
if (chunkType != null && chunkType == AiffChunkType.TAG && chunkHeader.getSize() > 0) {
|
||||||
|
ByteBuffer chunkData = readChunkDataIntoBuffer(fc, chunkHeader);
|
||||||
|
aiffTag.addChunkSummary(new ChunkSummary(chunkHeader.getID(), chunkHeader.getStartLocationInFile(), chunkHeader.getSize()));
|
||||||
|
|
||||||
|
//If we havent already for an ID3 Tag
|
||||||
|
if (aiffTag.getID3Tag() == null) {
|
||||||
|
Chunk chunk = new ID3Chunk(chunkHeader, chunkData, aiffTag);
|
||||||
|
chunk.readChunk();
|
||||||
|
aiffTag.setExistingId3Tag(true);
|
||||||
|
aiffTag.getID3Tag().setStartLocationInFile(startLocationOfId3TagInFile);
|
||||||
|
aiffTag.getID3Tag().setEndLocationInFile(fc.position());
|
||||||
|
}
|
||||||
|
//else otherwise we discard because the first one found is the one that will be used by other apps
|
||||||
|
{
|
||||||
|
logger.warning(fileName + " Ignoring ID3Tag because already have one:"
|
||||||
|
+ chunkHeader.getID() + ":"
|
||||||
|
+ chunkHeader.getStartLocationInFile()
|
||||||
|
+ Hex.asDecAndHex(chunkHeader.getStartLocationInFile() - 1)
|
||||||
|
+ ":sizeIncHeader:" + (chunkHeader.getSize() + ChunkHeader.CHUNK_HEADER_SIZE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Special handling to recognise ID3Tags written on odd boundary because original preceding chunk odd length but
|
||||||
|
//didn't write padding byte
|
||||||
|
else if (chunkType != null && chunkType == AiffChunkType.CORRUPT_TAG_LATE) {
|
||||||
|
logger.warning(fileName + "Found Corrupt ID3 Chunk, starting at Odd Location:" + chunkHeader.getID() + ":"
|
||||||
|
+ Hex.asDecAndHex(chunkHeader.getStartLocationInFile() - 1)
|
||||||
|
+ ":sizeIncHeader:" + (chunkHeader.getSize() + ChunkHeader.CHUNK_HEADER_SIZE));
|
||||||
|
|
||||||
|
//We only want to know if first metadata tag is misaligned
|
||||||
|
if (aiffTag.getID3Tag() == null) {
|
||||||
|
aiffTag.setIncorrectlyAlignedTag(true);
|
||||||
|
}
|
||||||
|
fc.position(fc.position() - (ChunkHeader.CHUNK_HEADER_SIZE + 1));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
//Other Special handling for ID3Tags
|
||||||
|
else if (chunkType != null && chunkType == AiffChunkType.CORRUPT_TAG_EARLY) {
|
||||||
|
logger.warning(fileName + " Found Corrupt ID3 Chunk, starting at Odd Location:" + chunkHeader.getID()
|
||||||
|
+ ":" + Hex.asDecAndHex(chunkHeader.getStartLocationInFile())
|
||||||
|
+ ":sizeIncHeader:" + (chunkHeader.getSize() + ChunkHeader.CHUNK_HEADER_SIZE));
|
||||||
|
|
||||||
|
//We only want to know if first metadata tag is misaligned
|
||||||
|
if (aiffTag.getID3Tag() == null) {
|
||||||
|
aiffTag.setIncorrectlyAlignedTag(true);
|
||||||
|
}
|
||||||
|
fc.position(fc.position() - (ChunkHeader.CHUNK_HEADER_SIZE - 1));
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
logger.config(fileName + "Skipping Chunk:" + chunkHeader.getID() + ":" + chunkHeader.getSize());
|
||||||
|
aiffTag.addChunkSummary(new ChunkSummary(chunkHeader.getID(), chunkHeader.getStartLocationInFile(), chunkHeader.getSize()));
|
||||||
|
fc.position(fc.position() + chunkHeader.getSize());
|
||||||
|
}
|
||||||
|
IffHeaderChunk.ensureOnEqualBoundary(fc, chunkHeader);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,403 @@
|
|||||||
|
/*
|
||||||
|
* Entagged Audio Tag library
|
||||||
|
* Copyright (c) 2003-2005 Raphaël Slinckx <raphael@slinckx.net>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library 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
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package com.mp3.jaudiotagger.audio.aiff;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.AudioFileIO;
|
||||||
|
import com.mp3.jaudiotagger.audio.aiff.chunk.AiffChunkSummary;
|
||||||
|
import com.mp3.jaudiotagger.audio.aiff.chunk.AiffChunkType;
|
||||||
|
import com.mp3.jaudiotagger.audio.exceptions.CannotReadException;
|
||||||
|
import com.mp3.jaudiotagger.audio.exceptions.CannotWriteException;
|
||||||
|
import com.mp3.jaudiotagger.audio.generic.Utils;
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.Chunk;
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.ChunkHeader;
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.ChunkSummary;
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.IffHeaderChunk;
|
||||||
|
import com.mp3.jaudiotagger.tag.Tag;
|
||||||
|
import com.mp3.jaudiotagger.tag.TagOptionSingleton;
|
||||||
|
import com.mp3.jaudiotagger.tag.aiff.AiffTag;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.nio.channels.FileChannel;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import static com.mp3.jaudiotagger.audio.iff.IffHeaderChunk.SIGNATURE_LENGTH;
|
||||||
|
import static com.mp3.jaudiotagger.audio.iff.IffHeaderChunk.SIZE_LENGTH;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write Aiff Tag.
|
||||||
|
*/
|
||||||
|
public class AiffTagWriter {
|
||||||
|
// Logger Object
|
||||||
|
public static Logger logger = Logger.getLogger("com.mp3.jaudiotagger.audio.aiff");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read existing metadata
|
||||||
|
*
|
||||||
|
* @param file
|
||||||
|
* @return tags within Tag wrapper
|
||||||
|
* @throws IOException
|
||||||
|
* @throws CannotWriteException
|
||||||
|
*/
|
||||||
|
private AiffTag getExistingMetadata(File file) throws IOException, CannotWriteException {
|
||||||
|
try {
|
||||||
|
//Find AiffTag (if any)
|
||||||
|
AiffTagReader im = new AiffTagReader();
|
||||||
|
return im.read(file);
|
||||||
|
} catch (CannotReadException ex) {
|
||||||
|
throw new CannotWriteException(file + " Failed to read file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seek in file to start of LIST Metadata chunk
|
||||||
|
*
|
||||||
|
* @param fc
|
||||||
|
* @param existingTag
|
||||||
|
* @throws IOException
|
||||||
|
* @throws CannotWriteException
|
||||||
|
*/
|
||||||
|
private ChunkHeader seekToStartOfMetadata(FileChannel fc, AiffTag existingTag, String fileName) throws IOException, CannotWriteException {
|
||||||
|
fc.position(existingTag.getStartLocationInFileOfId3Chunk());
|
||||||
|
final ChunkHeader chunkHeader = new ChunkHeader(ByteOrder.BIG_ENDIAN);
|
||||||
|
chunkHeader.readHeader(fc);
|
||||||
|
fc.position(fc.position() - ChunkHeader.CHUNK_HEADER_SIZE);
|
||||||
|
|
||||||
|
if (!AiffChunkType.TAG.getCode().equals(chunkHeader.getID())) {
|
||||||
|
throw new CannotWriteException(fileName + " Unable to find ID3 chunk at expected location:" + existingTag.getStartLocationInFileOfId3Chunk());
|
||||||
|
}
|
||||||
|
return chunkHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param existingTag
|
||||||
|
* @param fc
|
||||||
|
* @return true if at end of file (also take into account padding byte)
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
private boolean isAtEndOfFileAllowingForPaddingByte(AiffTag existingTag, FileChannel fc) throws IOException {
|
||||||
|
return (
|
||||||
|
(
|
||||||
|
existingTag.getID3Tag().getEndLocationInFile() == fc.size()
|
||||||
|
)
|
||||||
|
||
|
||||||
|
(
|
||||||
|
Utils.isOddLength(existingTag.getID3Tag().getEndLocationInFile())
|
||||||
|
&&
|
||||||
|
existingTag.getID3Tag().getEndLocationInFile() + 1 == fc.size()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete given {@link Tag} from file.
|
||||||
|
*
|
||||||
|
* @param tag tag, must be instance of {@link AiffTag}
|
||||||
|
* @param file
|
||||||
|
* @throws IOException
|
||||||
|
* @throws com.mp3.jaudiotagger.audio.exceptions.CannotWriteException
|
||||||
|
*/
|
||||||
|
public void delete(final Tag tag, File file) throws CannotWriteException {
|
||||||
|
RandomAccessFile raf = null;
|
||||||
|
try {
|
||||||
|
raf = new RandomAccessFile(file, "rw");
|
||||||
|
FileChannel fc = raf.getChannel();
|
||||||
|
logger.severe(file + " Deleting tag from file");
|
||||||
|
final AiffTag existingTag = getExistingMetadata(file);
|
||||||
|
|
||||||
|
if (existingTag.isExistingId3Tag() && existingTag.getID3Tag().getStartLocationInFile() != null) {
|
||||||
|
ChunkHeader chunkHeader = seekToStartOfMetadata(fc, existingTag, file.toString());
|
||||||
|
if (isAtEndOfFileAllowingForPaddingByte(existingTag, fc)) {
|
||||||
|
logger.severe(file + " Setting new length to:" + (existingTag.getStartLocationInFileOfId3Chunk()));
|
||||||
|
fc.truncate(existingTag.getStartLocationInFileOfId3Chunk());
|
||||||
|
} else {
|
||||||
|
logger.severe(file + " Deleting tag chunk");
|
||||||
|
deleteTagChunk(fc, existingTag, chunkHeader, file.toString());
|
||||||
|
}
|
||||||
|
rewriteRiffHeaderSize(fc);
|
||||||
|
}
|
||||||
|
logger.severe(file + " Deleted tag from file");
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new CannotWriteException(file + ":" + ioe.getMessage());
|
||||||
|
} finally {
|
||||||
|
AudioFileIO.closeQuietly(raf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Deletes the given ID3-{@link Tag}/{@link Chunk} from the file by moving all following chunks up.</p>
|
||||||
|
* <pre>
|
||||||
|
* [chunk][-id3-][chunk][chunk]
|
||||||
|
* [chunk] <<--- [chunk][chunk]
|
||||||
|
* [chunk][chunk][chunk]
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @param fc, filechannel
|
||||||
|
* @param existingTag existing tag
|
||||||
|
* @param tagChunkHeader existing chunk header for the tag
|
||||||
|
* @throws IOException if something goes wrong
|
||||||
|
*/
|
||||||
|
private void deleteTagChunk(FileChannel fc, final AiffTag existingTag, final ChunkHeader tagChunkHeader, String fileName) throws IOException {
|
||||||
|
int lengthTagChunk = (int) tagChunkHeader.getSize() + ChunkHeader.CHUNK_HEADER_SIZE;
|
||||||
|
if (Utils.isOddLength(lengthTagChunk)) {
|
||||||
|
if (existingTag.getStartLocationInFileOfId3Chunk() + lengthTagChunk < fc.size()) {
|
||||||
|
lengthTagChunk++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final long newLength = fc.size() - lengthTagChunk;
|
||||||
|
logger.severe(fileName + " Size of id3 chunk to delete is:" + lengthTagChunk + ":Location:" + existingTag.getStartLocationInFileOfId3Chunk());
|
||||||
|
|
||||||
|
// position for reading after the id3 tag
|
||||||
|
fc.position(existingTag.getStartLocationInFileOfId3Chunk() + lengthTagChunk);
|
||||||
|
|
||||||
|
deleteTagChunkUsingSmallByteBufferSegments(existingTag, fc, newLength, lengthTagChunk);
|
||||||
|
// truncate the file after the last chunk
|
||||||
|
logger.severe(fileName + " Setting new length to:" + newLength);
|
||||||
|
fc.truncate(newLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If Metadata tags are corrupted and no other tags later in the file then just truncate ID3 tags and start again
|
||||||
|
*
|
||||||
|
* @param fc
|
||||||
|
* @param existingTag
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
private void deleteRemainderOfFile(FileChannel fc, final AiffTag existingTag, String fileName) throws IOException {
|
||||||
|
ChunkSummary precedingChunk = AiffChunkSummary.getChunkBeforeStartingMetadataTag(existingTag);
|
||||||
|
if (!Utils.isOddLength(precedingChunk.getEndLocation())) {
|
||||||
|
logger.severe(fileName + " Truncating corrupted ID3 tags from:" + (existingTag.getStartLocationInFileOfId3Chunk() - 1));
|
||||||
|
fc.truncate(existingTag.getStartLocationInFileOfId3Chunk() - 1);
|
||||||
|
} else {
|
||||||
|
logger.severe(fileName + " Truncating corrupted ID3 tags from:" + (existingTag.getStartLocationInFileOfId3Chunk()));
|
||||||
|
fc.truncate(existingTag.getStartLocationInFileOfId3Chunk());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The following seems to work on Windows but hangs on OSX!
|
||||||
|
* Bug is filed <a href="https://bugs.openjdk.java.net/browse/JDK-8140241">here</a>.
|
||||||
|
*
|
||||||
|
* @param existingTag existing tag
|
||||||
|
* @param channel channel
|
||||||
|
* @param newLength new length
|
||||||
|
* @throws IOException if something goes wrong
|
||||||
|
*/
|
||||||
|
private void deleteTagChunkUsingChannelTransfer(final AiffTag existingTag, final FileChannel channel, final long newLength)
|
||||||
|
throws IOException {
|
||||||
|
long read;
|
||||||
|
//Read from just after the ID3Chunk into the channel at where the ID3 chunk started, should usually only require one transfer
|
||||||
|
//but put into loop in case multiple calls are required
|
||||||
|
for (long position = existingTag.getStartLocationInFileOfId3Chunk();
|
||||||
|
(read = channel.transferFrom(channel, position, newLength - position)) < newLength - position;
|
||||||
|
position += read)
|
||||||
|
;//is this problem if loop called more than once do we need to update position of channel to modify
|
||||||
|
//where write to ?
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use ByteBuffers to copy a 4mb chunk, write the chunk and repeat until the rest of the file after the ID3 tag
|
||||||
|
* is rewritten
|
||||||
|
*
|
||||||
|
* @param existingTag existing tag
|
||||||
|
* @param channel channel
|
||||||
|
* @param newLength new length
|
||||||
|
* @param lengthTagChunk length tag chunk
|
||||||
|
* @throws IOException if something goes wrong
|
||||||
|
*/
|
||||||
|
// TODO: arguments are not used, position is implicit
|
||||||
|
private void deleteTagChunkUsingSmallByteBufferSegments(final AiffTag existingTag, final FileChannel channel, final long newLength, final long lengthTagChunk)
|
||||||
|
throws IOException {
|
||||||
|
final ByteBuffer buffer = ByteBuffer.allocateDirect((int) TagOptionSingleton.getInstance().getWriteChunkSize());
|
||||||
|
while (channel.read(buffer) >= 0 || buffer.position() != 0) {
|
||||||
|
buffer.flip();
|
||||||
|
final long readPosition = channel.position();
|
||||||
|
channel.position(readPosition - lengthTagChunk - buffer.limit());
|
||||||
|
channel.write(buffer);
|
||||||
|
channel.position(readPosition);
|
||||||
|
buffer.compact();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param tag
|
||||||
|
* @param file
|
||||||
|
* @throws CannotWriteException
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void write(final Tag tag, File file) throws CannotWriteException {
|
||||||
|
logger.severe(file + " Writing Aiff tag to file");
|
||||||
|
AiffTag existingTag = null;
|
||||||
|
try {
|
||||||
|
existingTag = getExistingMetadata(file);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new CannotWriteException(file + ":" + ioe.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
RandomAccessFile raf = null;
|
||||||
|
try {
|
||||||
|
raf = new RandomAccessFile(file, "rw");
|
||||||
|
FileChannel fc = raf.getChannel();
|
||||||
|
long existingFileLength = fc.size();
|
||||||
|
|
||||||
|
final AiffTag aiffTag = (AiffTag) tag;
|
||||||
|
final ByteBuffer bb = convert(aiffTag, existingTag);
|
||||||
|
|
||||||
|
//Replacing ID3 tag
|
||||||
|
if (existingTag.isExistingId3Tag() && existingTag.getID3Tag().getStartLocationInFile() != null) {
|
||||||
|
//Usual case
|
||||||
|
if (!existingTag.isIncorrectlyAlignedTag()) {
|
||||||
|
final ChunkHeader chunkHeader = seekToStartOfMetadata(fc, existingTag, file.toString());
|
||||||
|
logger.info(file + "Current Space allocated:" + existingTag.getSizeOfID3TagOnly() + ":NewTagRequires:" + bb.limit());
|
||||||
|
|
||||||
|
//Usual case ID3 is last chunk
|
||||||
|
if (isAtEndOfFileAllowingForPaddingByte(existingTag, fc)) {
|
||||||
|
writeDataToFile(fc, bb);
|
||||||
|
}
|
||||||
|
//Unusual Case where ID3 is not last chunk
|
||||||
|
else {
|
||||||
|
deleteTagChunk(fc, existingTag, chunkHeader, file.toString());
|
||||||
|
fc.position(fc.size());
|
||||||
|
writeExtraByteIfChunkOddSize(fc, fc.size());
|
||||||
|
writeDataToFile(fc, bb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Existing ID3 tag is incorrectly aligned so if we can lets delete it and any subsequentially added
|
||||||
|
//ID3 tags as we only want one ID3 tag.
|
||||||
|
else if (AiffChunkSummary.isOnlyMetadataTagsAfterStartingMetadataTag(existingTag)) {
|
||||||
|
deleteRemainderOfFile(fc, existingTag, file.toString());
|
||||||
|
fc.position(fc.size());
|
||||||
|
writeExtraByteIfChunkOddSize(fc, fc.size());
|
||||||
|
writeDataToFile(fc, bb);
|
||||||
|
} else {
|
||||||
|
throw new CannotWriteException(file + " Metadata tags are corrupted and not at end of file so cannot be fixed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//New Tag
|
||||||
|
else {
|
||||||
|
fc.position(fc.size());
|
||||||
|
if (Utils.isOddLength(fc.size())) {
|
||||||
|
fc.write(ByteBuffer.allocateDirect(1));
|
||||||
|
}
|
||||||
|
writeDataToFile(fc, bb);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingFileLength != fc.size()) {
|
||||||
|
rewriteRiffHeaderSize(fc);
|
||||||
|
}
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new CannotWriteException(file + ":" + ioe.getMessage());
|
||||||
|
} finally {
|
||||||
|
AudioFileIO.closeQuietly(raf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rewrite RAF header to reflect new file length
|
||||||
|
*
|
||||||
|
* @param fc
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
private void rewriteRiffHeaderSize(FileChannel fc) throws IOException {
|
||||||
|
|
||||||
|
fc.position(IffHeaderChunk.SIGNATURE_LENGTH);
|
||||||
|
ByteBuffer bb = ByteBuffer.allocateDirect(IffHeaderChunk.SIZE_LENGTH);
|
||||||
|
bb.order(ByteOrder.BIG_ENDIAN);
|
||||||
|
int size = ((int) fc.size()) - SIGNATURE_LENGTH - SIZE_LENGTH;
|
||||||
|
bb.putInt(size);
|
||||||
|
bb.flip();
|
||||||
|
fc.write(bb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes data as a {@link AiffChunkType#TAG} chunk to the file.
|
||||||
|
*
|
||||||
|
* @param fc filechannel
|
||||||
|
* @param bb data to write
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
private void writeDataToFile(FileChannel fc, final ByteBuffer bb)
|
||||||
|
throws IOException {
|
||||||
|
final ChunkHeader ch = new ChunkHeader(ByteOrder.BIG_ENDIAN);
|
||||||
|
ch.setID(AiffChunkType.TAG.getCode());
|
||||||
|
ch.setSize(bb.limit());
|
||||||
|
fc.write(ch.writeHeader());
|
||||||
|
fc.write(bb);
|
||||||
|
writeExtraByteIfChunkOddSize(fc, bb.limit());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chunk must also start on an even byte so if our chunksize is odd we need
|
||||||
|
* to write another byte. This should never happen as ID3Tag is now amended
|
||||||
|
* to ensure always write padding byte if needed to stop it being odd sized
|
||||||
|
* but we keep check in just incase.
|
||||||
|
*
|
||||||
|
* @param fc
|
||||||
|
* @param size
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
private void writeExtraByteIfChunkOddSize(FileChannel fc, long size)
|
||||||
|
throws IOException {
|
||||||
|
if (Utils.isOddLength(size)) {
|
||||||
|
fc.write(ByteBuffer.allocateDirect(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts tag to {@link ByteBuffer}.
|
||||||
|
*
|
||||||
|
* @param tag tag
|
||||||
|
* @param existingTag
|
||||||
|
* @return byte buffer containing the tag data
|
||||||
|
* @throws UnsupportedEncodingException
|
||||||
|
*/
|
||||||
|
public ByteBuffer convert(final AiffTag tag, AiffTag existingTag) throws UnsupportedEncodingException {
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
long existingTagSize = existingTag.getSizeOfID3TagOnly();
|
||||||
|
|
||||||
|
//If existingTag is uneven size lets make it even
|
||||||
|
if (existingTagSize > 0) {
|
||||||
|
if ((existingTagSize & 1) != 0) {
|
||||||
|
existingTagSize++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Write Tag to buffer
|
||||||
|
tag.getID3Tag().write(baos, (int) existingTagSize);
|
||||||
|
|
||||||
|
//If the tag is now odd because we needed to increase size and the data made it odd sized
|
||||||
|
//we redo adding a padding byte to make it even
|
||||||
|
if ((baos.toByteArray().length & 1) != 0) {
|
||||||
|
int newSize = baos.toByteArray().length + 1;
|
||||||
|
baos = new ByteArrayOutputStream();
|
||||||
|
tag.getID3Tag().write(baos, newSize);
|
||||||
|
}
|
||||||
|
final ByteBuffer buf = ByteBuffer.wrap(baos.toByteArray());
|
||||||
|
buf.rewind();
|
||||||
|
return buf;
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
//Should never happen as not writing to file at this point
|
||||||
|
throw new RuntimeException(ioe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.aiff;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AIFF types, refers to BigEndian or LittleEndian
|
||||||
|
*/
|
||||||
|
public enum AiffType
|
||||||
|
{
|
||||||
|
AIFF("AIFF"), //Original non-compressed format on Mac pre-intel hardware
|
||||||
|
AIFC("AIFC"), //Originally Compressed AIFF but also used for Uncompressed in LE rather than BE order
|
||||||
|
;
|
||||||
|
|
||||||
|
String code;
|
||||||
|
|
||||||
|
AiffType(String code)
|
||||||
|
{
|
||||||
|
this.code=code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCode()
|
||||||
|
{
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.aiff;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility methods only of use for Aiff datatypes
|
||||||
|
*/
|
||||||
|
public class AiffUtil
|
||||||
|
{
|
||||||
|
|
||||||
|
private final static SimpleDateFormat dateFmt = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
|
||||||
|
|
||||||
|
|
||||||
|
public static double read80BitDouble(ByteBuffer chunkData) throws IOException
|
||||||
|
{
|
||||||
|
byte[] buf = new byte[10];
|
||||||
|
chunkData.get(buf);
|
||||||
|
ExtDouble xd = new ExtDouble(buf);
|
||||||
|
return xd.toDouble();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Macintosh-style timestamp (seconds since
|
||||||
|
* January 1, 1904) into a Java date. The timestamp is
|
||||||
|
* treated as a time in the default localization.
|
||||||
|
* Depending on that localization,
|
||||||
|
* there may be some variation in the exact hour of the date
|
||||||
|
* returned, e.g., due to daylight savings time.
|
||||||
|
*/
|
||||||
|
public static Date timestampToDate(long timestamp)
|
||||||
|
{
|
||||||
|
Calendar cal = Calendar.getInstance();
|
||||||
|
cal.set(1904, 0, 1, 0, 0, 0);
|
||||||
|
|
||||||
|
// If we add the seconds directly, we'll truncate the long
|
||||||
|
// value when converting to int. So convert to hours plus
|
||||||
|
// residual seconds.
|
||||||
|
int hours = (int) (timestamp / 3600);
|
||||||
|
int seconds = (int) (timestamp - (long) hours * 3600L);
|
||||||
|
cal.add(Calendar.HOUR_OF_DAY, hours);
|
||||||
|
cal.add(Calendar.SECOND, seconds);
|
||||||
|
Date dat = cal.getTime();
|
||||||
|
return dat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a date as text
|
||||||
|
*/
|
||||||
|
public static String formatDate(Date dat)
|
||||||
|
{
|
||||||
|
return dateFmt.format(dat);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.aiff;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Code to deal with the 80-bit floating point (extended double)
|
||||||
|
* numbers which occur in AIFF files. Should also be applicable
|
||||||
|
* in general.
|
||||||
|
* <p/>
|
||||||
|
* Java has no built-in support for IEEE 754 extended double numbers.
|
||||||
|
* Thus, we have to unpack the number and convert it to a double by
|
||||||
|
* hand. There is, of course, loss of precision.
|
||||||
|
* <p/>
|
||||||
|
* This isn't designed for high-precision work; as the standard
|
||||||
|
* disclaimer says, don't use it for life support systems or nuclear
|
||||||
|
* power plants.
|
||||||
|
* <p/>
|
||||||
|
* Lifted bodily from JHOVE.
|
||||||
|
*
|
||||||
|
* @author Gary McGath
|
||||||
|
*/
|
||||||
|
public class ExtDouble
|
||||||
|
{
|
||||||
|
|
||||||
|
byte[] _rawData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* @param rawData A 10-byte array representing the number
|
||||||
|
* in the sequence in which it was stored.
|
||||||
|
*/
|
||||||
|
public ExtDouble(byte[] rawData)
|
||||||
|
{
|
||||||
|
_rawData = rawData;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the value to a Java double. This results in
|
||||||
|
* loss of precision. If the number is out of range,
|
||||||
|
* results aren't guaranteed.
|
||||||
|
*/
|
||||||
|
public double toDouble()
|
||||||
|
{
|
||||||
|
int sign;
|
||||||
|
int exponent;
|
||||||
|
long mantissa = 0;
|
||||||
|
|
||||||
|
// Extract the sign bit.
|
||||||
|
sign = _rawData[0] >> 7;
|
||||||
|
|
||||||
|
// Extract the exponent. It's stored with a
|
||||||
|
// bias of 16383, so subtract that off.
|
||||||
|
// Also, the mantissa is between 1 and 2 (i.e.,
|
||||||
|
// all but 1 digits are to the right of the binary point, so
|
||||||
|
// we take 62 (not 63: see below) off the exponent for that.
|
||||||
|
exponent = (_rawData[0] << 8) | _rawData[1];
|
||||||
|
exponent &= 0X7FFF; // strip off sign bit
|
||||||
|
exponent -= (16383 + 62); // 1 is added to the "real" exponent
|
||||||
|
|
||||||
|
// Extract the mantissa. It's 64 bits of unsigned
|
||||||
|
// data, but a long is a signed number, so we have to
|
||||||
|
// discard the LSB. We'll lose more than that converting
|
||||||
|
// to double anyway. This division by 2 is the reason for
|
||||||
|
// adding an extra 1 to the exponent above.
|
||||||
|
int shifter = 55;
|
||||||
|
for (int i = 2; i < 9; i++)
|
||||||
|
{
|
||||||
|
mantissa |= ((long) _rawData[i] & 0XFFL) << shifter;
|
||||||
|
shifter -= 8;
|
||||||
|
}
|
||||||
|
mantissa |= _rawData[9] >>> 1;
|
||||||
|
|
||||||
|
// Now put it together in a floating point number.
|
||||||
|
double val = Math.pow(2, exponent);
|
||||||
|
val *= mantissa;
|
||||||
|
if (sign != 0)
|
||||||
|
{
|
||||||
|
val = -val;
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.aiff.chunk;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.ChunkHeader;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.nio.channels.FileChannel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class For reading Aiff Chunks used by both Audio and Tag Reader
|
||||||
|
*/
|
||||||
|
public abstract class AiffChunkReader
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Read the next chunk into ByteBuffer as specified by ChunkHeader and moves raf file pointer
|
||||||
|
* to start of next chunk/end of file.
|
||||||
|
*
|
||||||
|
* @param fc
|
||||||
|
* @param chunkHeader
|
||||||
|
* @return
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
protected ByteBuffer readChunkDataIntoBuffer(FileChannel fc, final ChunkHeader chunkHeader) throws IOException
|
||||||
|
{
|
||||||
|
final ByteBuffer chunkData = ByteBuffer.allocateDirect((int)chunkHeader.getSize());
|
||||||
|
chunkData.order(ByteOrder.BIG_ENDIAN);
|
||||||
|
fc.read(chunkData);
|
||||||
|
chunkData.position(0);
|
||||||
|
return chunkData;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.aiff.chunk;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.ChunkSummary;
|
||||||
|
import com.mp3.jaudiotagger.tag.aiff.AiffTag;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AIFF Specific methods for ChunkSummarys
|
||||||
|
*/
|
||||||
|
public class AiffChunkSummary
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Checks that there are only id3 tags after the currently selected id3tag because this means its safe to truncate
|
||||||
|
* the remainder of the file.
|
||||||
|
*
|
||||||
|
* @param tag
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static boolean isOnlyMetadataTagsAfterStartingMetadataTag(AiffTag tag)
|
||||||
|
{
|
||||||
|
boolean firstId3Tag = false;
|
||||||
|
for(ChunkSummary cs:tag.getChunkSummaryList())
|
||||||
|
{
|
||||||
|
if(firstId3Tag)
|
||||||
|
{
|
||||||
|
if(!cs.getChunkId().equals(AiffChunkType.TAG.getCode()))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (cs.getFileStartLocation() == tag.getStartLocationInFileOfId3Chunk())
|
||||||
|
{
|
||||||
|
//Found starting point
|
||||||
|
firstId3Tag = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Should always be true but this is to protect against something gone wrong
|
||||||
|
if(firstId3Tag==true)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get chunk before starting metadata tag
|
||||||
|
*
|
||||||
|
* @param tag
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static ChunkSummary getChunkBeforeStartingMetadataTag(AiffTag tag)
|
||||||
|
{
|
||||||
|
for(int i=0;i < tag.getChunkSummaryList().size(); i++)
|
||||||
|
{
|
||||||
|
ChunkSummary cs = tag.getChunkSummaryList().get(i);
|
||||||
|
if (cs.getFileStartLocation() == tag.getStartLocationInFileOfId3Chunk())
|
||||||
|
{
|
||||||
|
return tag.getChunkSummaryList().get(i - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.aiff.chunk;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chunk types mark each {@link com.mp3.jaudiotagger.audio.iff.ChunkHeader}. They are <em>always</em> 4 ASCII chars long.
|
||||||
|
*
|
||||||
|
* @see com.mp3.jaudiotagger.audio.iff.Chunk
|
||||||
|
*/
|
||||||
|
public enum AiffChunkType
|
||||||
|
{
|
||||||
|
FORMAT_VERSION("FVER"),
|
||||||
|
APPLICATION("APPL"),
|
||||||
|
SOUND("SSND"),
|
||||||
|
COMMON("COMM"),
|
||||||
|
COMMENTS("COMT"),
|
||||||
|
NAME("NAME"),
|
||||||
|
AUTHOR("AUTH"),
|
||||||
|
COPYRIGHT("(c) "),
|
||||||
|
ANNOTATION("ANNO"),
|
||||||
|
TAG("ID3 "),
|
||||||
|
CORRUPT_TAG_LATE("D3 \u0000"),
|
||||||
|
CORRUPT_TAG_EARLY("\u0000ID3");
|
||||||
|
|
||||||
|
private static final Map<String, AiffChunkType> CODE_TYPE_MAP = new HashMap<String, AiffChunkType>();
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param code 4 char string
|
||||||
|
*/
|
||||||
|
AiffChunkType(final String code)
|
||||||
|
{
|
||||||
|
this.code=code;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get {@link AiffChunkType} for code (e.g. "SSND").
|
||||||
|
*
|
||||||
|
* @param code chunk id
|
||||||
|
* @return chunk type or {@code null} if not registered
|
||||||
|
*/
|
||||||
|
public synchronized static AiffChunkType get(final String code) {
|
||||||
|
if (CODE_TYPE_MAP.isEmpty()) {
|
||||||
|
for (final AiffChunkType type : values()) {
|
||||||
|
CODE_TYPE_MAP.put(type.getCode(), type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CODE_TYPE_MAP.get(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 4 char type code.
|
||||||
|
*
|
||||||
|
* @return 4 char type code, e.g. "SSND" for the sound chunk.
|
||||||
|
*/
|
||||||
|
public String getCode()
|
||||||
|
{
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.aiff.chunk;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Known compression types that can be used with AIFF, taken from
|
||||||
|
* <a href="https://en.wikipedia.org/wiki/Audio_Interchange_File_Format">https://en.wikipedia.org/wiki/Audio_Interchange_File_Format</a>.
|
||||||
|
*
|
||||||
|
* Note SOWT is not a compression format but it uses AIFF-C to allow it store music data in Little-Endian order,
|
||||||
|
* it only affects audio data not the other chunks such as metadata.
|
||||||
|
*/
|
||||||
|
public enum AiffCompressionType
|
||||||
|
{
|
||||||
|
NONE("NONE", "not compressed","big-endian", "Apple", true),
|
||||||
|
RAW("raw ", "PCM 8-bit","offset-binary", "Apple", false),
|
||||||
|
TWOS("twos", "PCM 16-bit","twos-complement little-endian", "Apple", false),
|
||||||
|
SOWT("sowt", "not compressed","little-endian", "Apple", true),
|
||||||
|
fl32("fl32", "PCM 32-bit","floating point,", "Apple", false),
|
||||||
|
ll64("fl64", "PCM 64-bit","floating point", "Apple", false),
|
||||||
|
IN24("in24", "PCM 24-bit","integer", "Apple", false),
|
||||||
|
IN32("in32", "PCM 32-bit","integer", "Apple", false),
|
||||||
|
alaw("alaw","Alaw 2:1", "8-bit ITU-T G.711 A-law", "Apple", false),
|
||||||
|
ulaw("ulaw","µlaw 2:1","8-bit ITU-T G.711 µ-law","Apple", false),
|
||||||
|
MAC3("MAC3", "MACE 3-to-1","", "Apple", false),
|
||||||
|
MAC6("MAC6", "MACE 6-to-1","", "Apple", false),
|
||||||
|
ALAW("ALAW","CCITT G.711 A-law", "8-bit ITU-T G.711 A-law (64 kbit/s)", "SGI", false),
|
||||||
|
ULAW("ULAW","CCITT G.711 u-law","8-bit ITU-T G.711 A-law (64 kbit/s)","SGI", false),
|
||||||
|
FL32("FL32", "Float 32","IEEE 32-bit float", "SoundHack & Csound", false),
|
||||||
|
rt24("rt24", "RT24 50:1","", "Voxware", false),
|
||||||
|
rt29("rt29", "RT29 50:1","", "Voxware", false),
|
||||||
|
;
|
||||||
|
|
||||||
|
// Reverse-lookup map for getting a compression type from code
|
||||||
|
private static final Map<String, AiffCompressionType> lookup = new HashMap<String, AiffCompressionType>();
|
||||||
|
|
||||||
|
static
|
||||||
|
{
|
||||||
|
for (AiffCompressionType d : AiffCompressionType.values())
|
||||||
|
{
|
||||||
|
lookup.put(d.getCode(), d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
private final String compression;
|
||||||
|
private final String dataType;
|
||||||
|
private final String provider;
|
||||||
|
private final boolean isLossless;
|
||||||
|
|
||||||
|
AiffCompressionType(final String code, final String compression, final String dataType, final String provider, final boolean isLossless)
|
||||||
|
{
|
||||||
|
this.code = code;
|
||||||
|
this.compression = compression;
|
||||||
|
this.dataType = dataType;
|
||||||
|
this.provider=provider;
|
||||||
|
this.isLossless=isLossless;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCode()
|
||||||
|
{
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCompression()
|
||||||
|
{
|
||||||
|
return compression;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isLossless()
|
||||||
|
{
|
||||||
|
return isLossless;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDataType()
|
||||||
|
{
|
||||||
|
return dataType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProvider()
|
||||||
|
{
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AiffCompressionType getByCode(String code)
|
||||||
|
{
|
||||||
|
return lookup.get(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.aiff.chunk;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.aiff.AiffAudioHeader;
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.ChunkHeader;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains a comment. Use of this chunk is discouraged within FORM AIFF. The more powerful {@link CommentsChunk}
|
||||||
|
* should be used instead. The Annotation Chunk is optional. Many Annotation Chunks may exist within a FORM AIFF.
|
||||||
|
*
|
||||||
|
* @see CommentsChunk
|
||||||
|
*/
|
||||||
|
public class AnnotationChunk extends TextChunk
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param chunkHeader The header for this chunk
|
||||||
|
* @param chunkData The buffer from which the AIFF data are being read
|
||||||
|
* @param aiffAudioHeader The AiffAudioHeader into which information is stored
|
||||||
|
*/
|
||||||
|
public AnnotationChunk(final ChunkHeader chunkHeader, final ByteBuffer chunkData, final AiffAudioHeader aiffAudioHeader)
|
||||||
|
{
|
||||||
|
super(chunkHeader, chunkData, aiffAudioHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean readChunk() throws IOException
|
||||||
|
{
|
||||||
|
aiffAudioHeader.addAnnotation(readChunkText());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.aiff.chunk;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.aiff.AiffAudioHeader;
|
||||||
|
import com.mp3.jaudiotagger.audio.generic.Utils;
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.Chunk;
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.ChunkHeader;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Application Chunk can be used for any purposes whatsoever by developers and application authors. For
|
||||||
|
* example, an application that edits sounds might want to use this chunk to store editor state parameters such as
|
||||||
|
* magnification levels, last cursor position, etc.
|
||||||
|
*/
|
||||||
|
public class ApplicationChunk extends Chunk
|
||||||
|
{
|
||||||
|
private static final String SIGNATURE_PDOS = "pdos";
|
||||||
|
private static final String SIGNATURE_STOC = "stoc";
|
||||||
|
|
||||||
|
private AiffAudioHeader aiffHeader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* @param chunkHeader The header for this chunk
|
||||||
|
* @param chunkData The file from which the AIFF data are being read
|
||||||
|
* @param aiffAudioHeader audio header
|
||||||
|
*/
|
||||||
|
public ApplicationChunk(final ChunkHeader chunkHeader, final ByteBuffer chunkData, final AiffAudioHeader aiffAudioHeader)
|
||||||
|
{
|
||||||
|
super(chunkData, chunkHeader);
|
||||||
|
this.aiffHeader = aiffAudioHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a chunk and puts an Application property into
|
||||||
|
* the RepInfo object.
|
||||||
|
*
|
||||||
|
* @return <code>false</code> if the chunk is structurally
|
||||||
|
* invalid, otherwise <code>true</code>
|
||||||
|
*/
|
||||||
|
public boolean readChunk() throws IOException
|
||||||
|
{
|
||||||
|
final String applicationSignature = Utils.readFourBytesAsChars(chunkData);
|
||||||
|
String applicationName = null;
|
||||||
|
|
||||||
|
/* If the application signature is 'pdos' or 'stoc',
|
||||||
|
* then the beginning of the data area is a Pascal
|
||||||
|
* string naming the application. Otherwise, we
|
||||||
|
* ignore the data. ('pdos' is for Apple II
|
||||||
|
* applications, 'stoc' for the entire non-Apple world.)
|
||||||
|
*/
|
||||||
|
if (SIGNATURE_STOC.equals(applicationSignature) || SIGNATURE_PDOS.equals(applicationSignature))
|
||||||
|
{
|
||||||
|
applicationName = Utils.readPascalString(chunkData);
|
||||||
|
}
|
||||||
|
aiffHeader.addApplicationIdentifier(applicationSignature + ": " + applicationName);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.aiff.chunk;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.aiff.AiffAudioHeader;
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.ChunkHeader;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains one or more author names. An author in this case is the creator of a sampled sound.
|
||||||
|
* The Author Chunk is optional. No more than one Author Chunk may exist within a FORM AIFF.
|
||||||
|
*/
|
||||||
|
public class AuthorChunk extends TextChunk
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param chunkHeader The header for this chunk
|
||||||
|
* @param chunkData The buffer from which the AIFF data are being read
|
||||||
|
* @param aiffAudioHeader The AiffAudioHeader into which information is stored
|
||||||
|
*/
|
||||||
|
public AuthorChunk(final ChunkHeader chunkHeader, final ByteBuffer chunkData, final AiffAudioHeader aiffAudioHeader)
|
||||||
|
{
|
||||||
|
super(chunkHeader, chunkData, aiffAudioHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean readChunk() throws IOException
|
||||||
|
{
|
||||||
|
aiffAudioHeader.setAuthor(readChunkText());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,97 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.aiff.chunk;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.StandardCharsets;
|
||||||
|
import com.mp3.jaudiotagger.audio.aiff.AiffAudioHeader;
|
||||||
|
import com.mp3.jaudiotagger.audio.aiff.AiffUtil;
|
||||||
|
import com.mp3.jaudiotagger.audio.generic.Utils;
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.Chunk;
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.ChunkHeader;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* A comment consists of a time stamp, marker id, and a text count followed by text.
|
||||||
|
* </p>
|
||||||
|
* <pre>
|
||||||
|
* typedef struct {
|
||||||
|
* unsigned long timeStamp;
|
||||||
|
* MarkerID marker;
|
||||||
|
* unsigned short count;
|
||||||
|
* char text[];
|
||||||
|
* } Comment;
|
||||||
|
* </pre>
|
||||||
|
* <p>
|
||||||
|
* {@code timeStamp} indicates when the comment was created. Units are the number of seconds
|
||||||
|
* since January 1, 1904. (This time convention is the one used by the Macintosh. For procedures
|
||||||
|
* that manipulate the time stamp, see The Operating System Utilities chapter in Inside Macintosh,
|
||||||
|
* vol II). For a routine that will convert this to an Apple II GS/OS format time, please see
|
||||||
|
* Apple II File Type Note for filetype 0xD8, aux type 0x0000.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* A comment can be linked to a marker. This allows applications to store long descriptions of
|
||||||
|
* markers as a comment. If the comment is referring to a marker, then marker is the ID of that
|
||||||
|
* marker. Otherwise, marker is zero, indicating that this comment is not linked to a marker.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* {@code count} is the length of the text that makes up the comment. This is a 16 bit quantity,
|
||||||
|
* allowing much longer comments than would be available with a pstring.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* {@code text} contains the comment itself. This text must be padded with a byte at the end to
|
||||||
|
* insure that it is an even number of bytes in length. This pad byte, if present, is not
|
||||||
|
* included in count.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @see AnnotationChunk
|
||||||
|
*/
|
||||||
|
public class CommentsChunk extends Chunk
|
||||||
|
{
|
||||||
|
private static final int TIMESTAMP_LENGTH = 4;
|
||||||
|
private static final int MARKERID_LENGTH = 2;
|
||||||
|
private static final int COUNT_LENGTH = 2;
|
||||||
|
|
||||||
|
private AiffAudioHeader aiffHeader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param chunkHeader The header for this chunk
|
||||||
|
* @param chunkData The buffer from which the AIFF data are being read
|
||||||
|
* @param aiffAudioHeader audio header
|
||||||
|
*/
|
||||||
|
public CommentsChunk(final ChunkHeader chunkHeader, final ByteBuffer chunkData, final AiffAudioHeader aiffAudioHeader)
|
||||||
|
{
|
||||||
|
super(chunkData, chunkHeader);
|
||||||
|
this.aiffHeader = aiffAudioHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a chunk and extracts information.
|
||||||
|
*
|
||||||
|
* @return <code>false</code> if the chunk is structurally
|
||||||
|
* invalid, otherwise <code>true</code>
|
||||||
|
*/
|
||||||
|
public boolean readChunk() throws IOException
|
||||||
|
{
|
||||||
|
final int numComments = Utils.u(chunkData.getShort());
|
||||||
|
|
||||||
|
//For each comment
|
||||||
|
for (int i = 0; i < numComments; i++)
|
||||||
|
{
|
||||||
|
final long timestamp = Utils.u(chunkData.getInt());
|
||||||
|
final Date jTimestamp = AiffUtil.timestampToDate(timestamp);
|
||||||
|
final int marker = Utils.u(chunkData.getShort());
|
||||||
|
final int count = Utils.u(chunkData.getShort());
|
||||||
|
// Append a timestamp to the comment
|
||||||
|
final String text = Utils.getString(chunkData, 0, count, StandardCharsets.ISO_8859_1) + " " + AiffUtil.formatDate(jTimestamp);
|
||||||
|
if (count % 2 != 0) {
|
||||||
|
// if count is odd, text is padded with an extra byte that we need to consume
|
||||||
|
chunkData.get();
|
||||||
|
}
|
||||||
|
aiffHeader.addComment(text);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,107 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.aiff.chunk;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.aiff.AiffAudioHeader;
|
||||||
|
import com.mp3.jaudiotagger.audio.aiff.AiffType;
|
||||||
|
import com.mp3.jaudiotagger.audio.aiff.AiffUtil;
|
||||||
|
import com.mp3.jaudiotagger.audio.generic.Utils;
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.Chunk;
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.ChunkHeader;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
The Common Chunk describes fundamental parameters of the waveform data such as sample rate,
|
||||||
|
bit resolution, and how many channels of digital audio are stored in the FORM AIFF.
|
||||||
|
*/
|
||||||
|
public class CommonChunk extends Chunk
|
||||||
|
{
|
||||||
|
private AiffAudioHeader aiffHeader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param hdr
|
||||||
|
* @param chunkData
|
||||||
|
* @param aiffAudioHeader
|
||||||
|
*/
|
||||||
|
public CommonChunk(ChunkHeader hdr, ByteBuffer chunkData, AiffAudioHeader aiffAudioHeader)
|
||||||
|
{
|
||||||
|
super(chunkData, hdr);
|
||||||
|
aiffHeader = aiffAudioHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean readChunk() throws IOException
|
||||||
|
{
|
||||||
|
|
||||||
|
int numChannels = Utils.u(chunkData.getShort());
|
||||||
|
long numSamples = chunkData.getInt();
|
||||||
|
int bitsPerSample = Utils.u(chunkData.getShort());
|
||||||
|
double sampleRate = AiffUtil.read80BitDouble(chunkData);
|
||||||
|
//Compression format, but not necessarily compressed
|
||||||
|
String compressionType;
|
||||||
|
String compressionName;
|
||||||
|
if (aiffHeader.getFileType() == AiffType.AIFC)
|
||||||
|
{
|
||||||
|
// This is a rather special case, but testing did turn up
|
||||||
|
// a file that misbehaved in this way.
|
||||||
|
if (chunkData.remaining()==0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
compressionType = Utils.readFourBytesAsChars(chunkData);
|
||||||
|
if (compressionType.equals(AiffCompressionType.SOWT.getCode()))
|
||||||
|
{
|
||||||
|
aiffHeader.setEndian(AiffAudioHeader.Endian.LITTLE_ENDIAN);
|
||||||
|
}
|
||||||
|
compressionName = Utils.readPascalString(chunkData);
|
||||||
|
// Proper handling of compression type should depend
|
||||||
|
// on whether raw output is set
|
||||||
|
if (compressionType != null)
|
||||||
|
{
|
||||||
|
//Is it a known compression type
|
||||||
|
AiffCompressionType act = AiffCompressionType.getByCode(compressionType);
|
||||||
|
if (act != null)
|
||||||
|
{
|
||||||
|
compressionName = act.getCompression();
|
||||||
|
aiffHeader.setLossless(act.isLossless());
|
||||||
|
// we assume that the bitrate is not variable, if there is no compression
|
||||||
|
if (act == AiffCompressionType.NONE) {
|
||||||
|
aiffHeader.setVariableBitRate(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// We don't know compression type, so we have to assume lossy compression as we know we are using AIFC format
|
||||||
|
aiffHeader.setLossless(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (compressionName.isEmpty())
|
||||||
|
{
|
||||||
|
aiffHeader.setEncodingType(compressionType);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
aiffHeader.setEncodingType(compressionName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Must be lossless
|
||||||
|
else
|
||||||
|
{
|
||||||
|
aiffHeader.setLossless(true);
|
||||||
|
aiffHeader.setEncodingType(AiffCompressionType.NONE.getCompression());
|
||||||
|
// regular AIFF has no variable bit rate AFAIK
|
||||||
|
aiffHeader.setVariableBitRate(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
aiffHeader.setBitsPerSample(bitsPerSample);
|
||||||
|
aiffHeader.setSamplingRate((int) sampleRate);
|
||||||
|
aiffHeader.setChannelNumber(numChannels);
|
||||||
|
aiffHeader.setPreciseLength((numSamples / sampleRate));
|
||||||
|
aiffHeader.setNoOfSamples(numSamples);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.aiff.chunk;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.aiff.AiffAudioHeader;
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.ChunkHeader;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* The Copyright Chunk contains a copyright notice for the sound. text contains a date followed
|
||||||
|
* by the copyright owner. The chunk ID '(c) ' serves as the copyright characters '©'. For example,
|
||||||
|
* a Copyright Chunk containing the text "1988 Apple Computer, Inc." means "© 1988 Apple Computer, Inc."
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* The Copyright Chunk is optional. No more than one Copyright Chunk may exist within a FORM AIFF.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public class CopyrightChunk extends TextChunk
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param chunkHeader The header for this chunk
|
||||||
|
* @param chunkData The buffer from which the AIFF data are being read
|
||||||
|
* @param aiffAudioHeader The AiffAudioHeader into which information is stored
|
||||||
|
*/
|
||||||
|
public CopyrightChunk(final ChunkHeader chunkHeader, final ByteBuffer chunkData, final AiffAudioHeader aiffAudioHeader)
|
||||||
|
{
|
||||||
|
super(chunkHeader, chunkData, aiffAudioHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean readChunk() throws IOException
|
||||||
|
{
|
||||||
|
aiffAudioHeader.setCopyright(readChunkText());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.aiff.chunk;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.aiff.AiffAudioHeader;
|
||||||
|
import com.mp3.jaudiotagger.audio.aiff.AiffUtil;
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.Chunk;
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.ChunkHeader;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* The Format Version Chunk contains a date field to indicate the format rules for an
|
||||||
|
* AIFF-C specification. This will enable smoother future upgrades to this specification.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* ckID is always 'FVER'.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* {@code ckDataSize} is the size of the data portion of the chunk, in bytes. It does not
|
||||||
|
* include the 8 bytes used by ckID and ckDataSize. For this Chunk, ckDataSize has a value of 4.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* {@code timeStamp} indicates when the format version for the AIFF-C file was created.
|
||||||
|
* Units are the number of seconds since January 1, 1904. (This time convention is the one
|
||||||
|
* used by the Macintosh. For procedures that manipulate the time stamp, see The Operating
|
||||||
|
* System Utilities chapter in Inside Macintosh, vol II ). For a routine that will convert
|
||||||
|
* this to an Apple II GS/OS format time, please see Apple II File Type Note for filetype
|
||||||
|
* 0xD8, aux type 0x0000.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* The Format Version Chunk is required. One and only one Format Version Chunk must appear in a FORM AIFC.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public class FormatVersionChunk extends Chunk
|
||||||
|
{
|
||||||
|
private AiffAudioHeader aiffHeader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param chunkHeader The header for this chunk
|
||||||
|
* @param chunkData The buffer from which the AIFF data are being read
|
||||||
|
* @param aiffAudioHeader The AiffTag into which information is stored
|
||||||
|
*/
|
||||||
|
public FormatVersionChunk(final ChunkHeader chunkHeader, final ByteBuffer chunkData, final AiffAudioHeader aiffAudioHeader)
|
||||||
|
{
|
||||||
|
super(chunkData, chunkHeader);
|
||||||
|
this.aiffHeader = aiffAudioHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a chunk and extracts information.
|
||||||
|
*
|
||||||
|
* @return <code>false</code> if the chunk is structurally
|
||||||
|
* invalid, otherwise <code>true</code>
|
||||||
|
*/
|
||||||
|
public boolean readChunk() throws IOException
|
||||||
|
{
|
||||||
|
final long rawTimestamp = chunkData.getInt();
|
||||||
|
// The timestamp is in seconds since January 1, 1904.
|
||||||
|
// We must convert to Java time.
|
||||||
|
final Date timestamp = AiffUtil.timestampToDate(rawTimestamp);
|
||||||
|
aiffHeader.setTimestamp(timestamp);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,97 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.aiff.chunk;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.AudioFile;
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.Chunk;
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.ChunkHeader;
|
||||||
|
import com.mp3.jaudiotagger.tag.TagException;
|
||||||
|
import com.mp3.jaudiotagger.tag.aiff.AiffTag;
|
||||||
|
import com.mp3.jaudiotagger.tag.id3.AbstractID3v2Tag;
|
||||||
|
import com.mp3.jaudiotagger.tag.id3.ID3v22Tag;
|
||||||
|
import com.mp3.jaudiotagger.tag.id3.ID3v23Tag;
|
||||||
|
import com.mp3.jaudiotagger.tag.id3.ID3v24Tag;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains the ID3 tags.
|
||||||
|
*/
|
||||||
|
public class ID3Chunk extends Chunk
|
||||||
|
{
|
||||||
|
public static Logger logger = Logger.getLogger("com.mp3.jaudiotagger.audio.aiff.chunk");
|
||||||
|
private AiffTag aiffTag;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* @param chunkHeader The header for this chunk
|
||||||
|
* @param chunkData The content of this chunk
|
||||||
|
* @param tag The AiffTag into which information is stored
|
||||||
|
*/
|
||||||
|
public ID3Chunk(final ChunkHeader chunkHeader, final ByteBuffer chunkData, final AiffTag tag)
|
||||||
|
{
|
||||||
|
super(chunkData, chunkHeader);
|
||||||
|
aiffTag = tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean readChunk() throws IOException
|
||||||
|
{
|
||||||
|
AudioFile.logger.severe("Reading chunk");
|
||||||
|
if (!isId3v2Tag(chunkData))
|
||||||
|
{
|
||||||
|
logger.severe("Invalid ID3 header for ID3 chunk");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int version = chunkData.get();
|
||||||
|
final AbstractID3v2Tag id3Tag;
|
||||||
|
switch (version)
|
||||||
|
{
|
||||||
|
case ID3v22Tag.MAJOR_VERSION:
|
||||||
|
id3Tag = new ID3v22Tag();
|
||||||
|
AudioFile.logger.severe("Reading ID3V2.2 tag");
|
||||||
|
break;
|
||||||
|
case ID3v23Tag.MAJOR_VERSION:
|
||||||
|
id3Tag = new ID3v23Tag();
|
||||||
|
AudioFile.logger.severe("Reading ID3V2.3 tag");
|
||||||
|
break;
|
||||||
|
case ID3v24Tag.MAJOR_VERSION:
|
||||||
|
id3Tag = new ID3v24Tag();
|
||||||
|
AudioFile.logger.severe("Reading ID3V2.4 tag");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false; // bad or unknown version
|
||||||
|
}
|
||||||
|
|
||||||
|
aiffTag.setID3Tag(id3Tag);
|
||||||
|
chunkData.position(0);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
id3Tag.read(chunkData);
|
||||||
|
}
|
||||||
|
catch (TagException e)
|
||||||
|
{
|
||||||
|
AudioFile.logger.info("Exception reading ID3 tag: " + e.getClass().getName() + ": " + e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads 3 bytes to determine if the tag really looks like ID3 data.
|
||||||
|
*/
|
||||||
|
private boolean isId3v2Tag(final ByteBuffer headerData) throws IOException
|
||||||
|
{
|
||||||
|
for (int i = 0; i < AbstractID3v2Tag.FIELD_TAGID_LENGTH; i++)
|
||||||
|
{
|
||||||
|
if (headerData.get() != AbstractID3v2Tag.TAG_ID[i])
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.aiff.chunk;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.aiff.AiffAudioHeader;
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.ChunkHeader;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains the name of the sampled sound. The Name Chunk is optional.
|
||||||
|
* No more than one Name Chunk may exist within a FORM AIFF.
|
||||||
|
*/
|
||||||
|
public class NameChunk extends TextChunk
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param chunkHeader The header for this chunk
|
||||||
|
* @param chunkData The buffer from which the AIFF data are being read
|
||||||
|
* @param aiffAudioHeader The AiffAudioHeader into which information is stored
|
||||||
|
*/
|
||||||
|
public NameChunk(final ChunkHeader chunkHeader, final ByteBuffer chunkData, final AiffAudioHeader aiffAudioHeader)
|
||||||
|
{
|
||||||
|
super(chunkHeader, chunkData, aiffAudioHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean readChunk() throws IOException
|
||||||
|
{
|
||||||
|
aiffAudioHeader.setName(readChunkText());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.aiff.chunk;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.Chunk;
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.ChunkHeader;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sound chunk.
|
||||||
|
* Doesn't actually read the content, but skips it.
|
||||||
|
*/
|
||||||
|
public class SoundChunk extends Chunk
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param chunkHeader The header for this chunk
|
||||||
|
* @param chunkData The file from which the AIFF data are being read
|
||||||
|
*/
|
||||||
|
public SoundChunk(final ChunkHeader chunkHeader, final ByteBuffer chunkData)
|
||||||
|
{
|
||||||
|
super(chunkData, chunkHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a chunk and extracts information.
|
||||||
|
*
|
||||||
|
* @return <code>false</code> if the chunk is structurally
|
||||||
|
* invalid, otherwise <code>true</code>
|
||||||
|
*/
|
||||||
|
public boolean readChunk() throws IOException
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.aiff.chunk;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.StandardCharsets;
|
||||||
|
import com.mp3.jaudiotagger.audio.aiff.AiffAudioHeader;
|
||||||
|
import com.mp3.jaudiotagger.audio.generic.Utils;
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.Chunk;
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.ChunkHeader;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides common functionality for textual chunks like {@link NameChunk}, {@link AuthorChunk},
|
||||||
|
* {@link CopyrightChunk} and {@link AnnotationChunk}.
|
||||||
|
*/
|
||||||
|
public abstract class TextChunk extends Chunk
|
||||||
|
{
|
||||||
|
protected final AiffAudioHeader aiffAudioHeader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* @param chunkHeader The header for this chunk
|
||||||
|
* @param chunkData The buffer from which the AIFF data are being read
|
||||||
|
* @param aiffAudioHeader aiff header
|
||||||
|
*/
|
||||||
|
public TextChunk(final ChunkHeader chunkHeader, final ByteBuffer chunkData, final AiffAudioHeader aiffAudioHeader)
|
||||||
|
{
|
||||||
|
super(chunkData, chunkHeader);
|
||||||
|
this.aiffAudioHeader = aiffAudioHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the chunk and transforms it to a {@link String}.
|
||||||
|
*
|
||||||
|
* @return text string
|
||||||
|
* @throws IOException if the read fails
|
||||||
|
*/
|
||||||
|
protected String readChunkText() throws IOException {
|
||||||
|
// the spec actually only defines ASCII, not ISO_8859_1, but it probably does not hurt to be lenient
|
||||||
|
return Utils.getString(chunkData, 0, chunkData.remaining(), StandardCharsets.ISO_8859_1);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,278 @@
|
|||||||
|
/*
|
||||||
|
* Entagged Audio Tag library
|
||||||
|
* Copyright (c) 2004-2005 Christian Laireiter <liree@web.de>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library 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
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package com.mp3.jaudiotagger.audio.asf;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.AudioFile;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.AsfHeader;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.AudioStreamChunk;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.MetadataContainer;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.MetadataDescriptor;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.io.*;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.TagConverter;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
import com.mp3.jaudiotagger.audio.exceptions.CannotReadException;
|
||||||
|
import com.mp3.jaudiotagger.audio.exceptions.InvalidAudioFrameException;
|
||||||
|
import com.mp3.jaudiotagger.audio.exceptions.ReadOnlyFileException;
|
||||||
|
import com.mp3.jaudiotagger.audio.generic.AudioFileReader;
|
||||||
|
import com.mp3.jaudiotagger.audio.generic.GenericAudioHeader;
|
||||||
|
import com.mp3.jaudiotagger.logging.ErrorMessage;
|
||||||
|
import com.mp3.jaudiotagger.tag.TagException;
|
||||||
|
import com.mp3.jaudiotagger.tag.asf.AsfTag;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This reader can read ASF files containing any content (stream type). <br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public class AsfFileReader extends AudioFileReader
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logger instance
|
||||||
|
*/
|
||||||
|
private final static Logger LOGGER = Logger.getLogger("com.mp3.jaudiotagger.audio.asf");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This reader will be configured to read tag and audio header information.<br>
|
||||||
|
*/
|
||||||
|
private final static AsfHeaderReader HEADER_READER;
|
||||||
|
|
||||||
|
static
|
||||||
|
{
|
||||||
|
final List<Class<? extends ChunkReader>> readers = new ArrayList<Class<? extends ChunkReader>>();
|
||||||
|
readers.add(ContentDescriptionReader.class);
|
||||||
|
readers.add(ContentBrandingReader.class);
|
||||||
|
readers.add(MetadataReader.class);
|
||||||
|
readers.add(LanguageListReader.class);
|
||||||
|
|
||||||
|
// Create the header extension object reader with just content
|
||||||
|
// description reader as well
|
||||||
|
// as extended content description reader.
|
||||||
|
final AsfExtHeaderReader extReader = new AsfExtHeaderReader(readers, true);
|
||||||
|
readers.add(FileHeaderReader.class);
|
||||||
|
readers.add(StreamChunkReader.class);
|
||||||
|
HEADER_READER = new AsfHeaderReader(readers, true);
|
||||||
|
HEADER_READER.setExtendedHeaderReader(extReader);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the "isVbr" field is set in the extended content
|
||||||
|
* description.<br>
|
||||||
|
*
|
||||||
|
* @param header the header to look up.
|
||||||
|
* @return <code>true</code> if "isVbr" is present with a
|
||||||
|
* <code>true</code> value.
|
||||||
|
*/
|
||||||
|
private boolean determineVariableBitrate(final AsfHeader header)
|
||||||
|
{
|
||||||
|
assert header != null;
|
||||||
|
boolean result = false;
|
||||||
|
final MetadataContainer extDesc = header.findExtendedContentDescription();
|
||||||
|
if (extDesc != null)
|
||||||
|
{
|
||||||
|
final List<MetadataDescriptor> descriptors = extDesc.getDescriptorsByName("IsVBR");
|
||||||
|
if (descriptors != null && !descriptors.isEmpty())
|
||||||
|
{
|
||||||
|
result = Boolean.TRUE.toString().equals(descriptors.get(0).getString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a generic audio header instance with provided data from header.
|
||||||
|
*
|
||||||
|
* @param header ASF header which contains the information.
|
||||||
|
* @return generic audio header representation.
|
||||||
|
* @throws CannotReadException If header does not contain mandatory information. (Audio
|
||||||
|
* stream chunk and file header chunk)
|
||||||
|
*/
|
||||||
|
private GenericAudioHeader getAudioHeader(final AsfHeader header) throws CannotReadException
|
||||||
|
{
|
||||||
|
final GenericAudioHeader info = new GenericAudioHeader();
|
||||||
|
if (header.getFileHeader() == null)
|
||||||
|
{
|
||||||
|
throw new CannotReadException("Invalid ASF/WMA file. File header object not available.");
|
||||||
|
}
|
||||||
|
if (header.getAudioStreamChunk() == null)
|
||||||
|
{
|
||||||
|
throw new CannotReadException("Invalid ASF/WMA file. No audio stream contained.");
|
||||||
|
}
|
||||||
|
info.setBitRate(header.getAudioStreamChunk().getKbps());
|
||||||
|
info.setChannelNumber((int) header.getAudioStreamChunk().getChannelCount());
|
||||||
|
info.setEncodingType("ASF (audio): " + header.getAudioStreamChunk().getCodecDescription());
|
||||||
|
info.setLossless(header.getAudioStreamChunk().getCompressionFormat() == AudioStreamChunk.WMA_LOSSLESS);
|
||||||
|
info.setPreciseLength(header.getFileHeader().getPreciseDuration());
|
||||||
|
info.setSamplingRate((int) header.getAudioStreamChunk().getSamplingRate());
|
||||||
|
info.setVariableBitRate(determineVariableBitrate(header));
|
||||||
|
info.setBitsPerSample(header.getAudioStreamChunk().getBitsPerSample());
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (overridden)
|
||||||
|
*
|
||||||
|
* @see com.mp3.jaudiotagger.audio.generic.AudioFileReader#getEncodingInfo(RandomAccessFile)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected GenericAudioHeader getEncodingInfo(final RandomAccessFile raf) throws CannotReadException, IOException
|
||||||
|
{
|
||||||
|
raf.seek(0);
|
||||||
|
GenericAudioHeader info;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
final AsfHeader header = AsfHeaderReader.readInfoHeader(raf);
|
||||||
|
if (header == null)
|
||||||
|
{
|
||||||
|
throw new CannotReadException("Some values must have been " + "incorrect for interpretation as asf with wma content.");
|
||||||
|
}
|
||||||
|
info = getAudioHeader(header);
|
||||||
|
}
|
||||||
|
catch (final Exception e)
|
||||||
|
{
|
||||||
|
if (e instanceof IOException)
|
||||||
|
{
|
||||||
|
throw (IOException) e;
|
||||||
|
}
|
||||||
|
else if (e instanceof CannotReadException)
|
||||||
|
{
|
||||||
|
throw (CannotReadException) e;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new CannotReadException("Failed to read. Cause: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a tag instance with provided data from header.
|
||||||
|
*
|
||||||
|
* @param header ASF header which contains the information.
|
||||||
|
* @return generic audio header representation.
|
||||||
|
*/
|
||||||
|
private AsfTag getTag(final AsfHeader header)
|
||||||
|
{
|
||||||
|
return TagConverter.createTagOf(header);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (overridden)
|
||||||
|
*
|
||||||
|
* @see com.mp3.jaudiotagger.audio.generic.AudioFileReader#getTag(RandomAccessFile)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected AsfTag getTag(final RandomAccessFile raf) throws CannotReadException, IOException
|
||||||
|
{
|
||||||
|
raf.seek(0);
|
||||||
|
AsfTag tag;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
final AsfHeader header = AsfHeaderReader.readTagHeader(raf);
|
||||||
|
if (header == null)
|
||||||
|
{
|
||||||
|
throw new CannotReadException("Some values must have been " + "incorrect for interpretation as asf with wma content.");
|
||||||
|
}
|
||||||
|
|
||||||
|
tag = TagConverter.createTagOf(header);
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (final Exception e)
|
||||||
|
{
|
||||||
|
logger.severe(e.getMessage());
|
||||||
|
if (e instanceof IOException)
|
||||||
|
{
|
||||||
|
throw (IOException) e;
|
||||||
|
}
|
||||||
|
else if (e instanceof CannotReadException)
|
||||||
|
{
|
||||||
|
throw (CannotReadException) e;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new CannotReadException("Failed to read. Cause: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public AudioFile read(final File f) throws CannotReadException, IOException, TagException, ReadOnlyFileException, InvalidAudioFrameException
|
||||||
|
{
|
||||||
|
if (!f.canRead())
|
||||||
|
{
|
||||||
|
throw new CannotReadException(ErrorMessage.GENERAL_READ_FAILED_DO_NOT_HAVE_PERMISSION_TO_READ_FILE.getMsg(f.getAbsolutePath()));
|
||||||
|
}
|
||||||
|
InputStream stream = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
stream = new FullRequestInputStream(new BufferedInputStream(new FileInputStream(f)));
|
||||||
|
final AsfHeader header = HEADER_READER.read(Utils.readGUID(stream), stream, 0);
|
||||||
|
if (header == null)
|
||||||
|
{
|
||||||
|
throw new CannotReadException(ErrorMessage.ASF_HEADER_MISSING.getMsg(f.getAbsolutePath()));
|
||||||
|
}
|
||||||
|
if (header.getFileHeader() == null)
|
||||||
|
{
|
||||||
|
throw new CannotReadException(ErrorMessage.ASF_FILE_HEADER_MISSING.getMsg(f.getAbsolutePath()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just log a warning because file seems to play okay
|
||||||
|
if (header.getFileHeader().getFileSize().longValue() != f.length())
|
||||||
|
{
|
||||||
|
logger.warning(ErrorMessage.ASF_FILE_HEADER_SIZE_DOES_NOT_MATCH_FILE_SIZE.getMsg(f.getAbsolutePath(), header.getFileHeader().getFileSize().longValue(), f.length()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AudioFile(f, getAudioHeader(header), getTag(header));
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (final CannotReadException e)
|
||||||
|
{
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
catch (final Exception e)
|
||||||
|
{
|
||||||
|
throw new CannotReadException("\"" + f + "\" :" + e, e);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (stream != null)
|
||||||
|
{
|
||||||
|
stream.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (final Exception ex)
|
||||||
|
{
|
||||||
|
LOGGER.severe("\"" + f + "\" :" + ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,144 @@
|
|||||||
|
/*
|
||||||
|
* Entagged Audio Tag library
|
||||||
|
* Copyright (c) 2004-2005 Christian Laireiter <liree@web.de>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library 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
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package com.mp3.jaudiotagger.audio.asf;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.AudioFile;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.AsfHeader;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.ChunkContainer;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.MetadataContainer;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.io.*;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.TagConverter;
|
||||||
|
import com.mp3.jaudiotagger.audio.exceptions.CannotWriteException;
|
||||||
|
import com.mp3.jaudiotagger.audio.generic.AudioFileWriter;
|
||||||
|
import com.mp3.jaudiotagger.tag.Tag;
|
||||||
|
import com.mp3.jaudiotagger.tag.asf.AsfTag;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.RandomAccessFile;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class writes given tags to ASF files containing WMA content. <br>
|
||||||
|
* <br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public class AsfFileWriter extends AudioFileWriter
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void deleteTag(Tag tag, final RandomAccessFile raf, final RandomAccessFile tempRaf) throws CannotWriteException, IOException
|
||||||
|
{
|
||||||
|
writeTag(null, new AsfTag(true), raf, tempRaf);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean[] searchExistence(final ChunkContainer container, final MetadataContainer[] metaContainers)
|
||||||
|
{
|
||||||
|
assert container != null;
|
||||||
|
assert metaContainers != null;
|
||||||
|
final boolean[] result = new boolean[metaContainers.length];
|
||||||
|
for (int i = 0; i < result.length; i++)
|
||||||
|
{
|
||||||
|
result[i] = container.hasChunkByGUID(metaContainers[i].getContainerType().getContainerGUID());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void writeTag(AudioFile audioFile, final Tag tag, final RandomAccessFile raf, final RandomAccessFile rafTemp) throws CannotWriteException, IOException
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* Since this implementation should not change the structure of the ASF
|
||||||
|
* file (locations of content description chunks), we need to read the
|
||||||
|
* content description chunk and the extended content description chunk
|
||||||
|
* from the source file. In the second step we need to determine which
|
||||||
|
* modifier (asf header or asf extended header) gets the appropriate
|
||||||
|
* modifiers. The following policies are applied: if the source does not
|
||||||
|
* contain any descriptor, the necessary descriptors are appended to the
|
||||||
|
* header object.
|
||||||
|
*
|
||||||
|
* if the source contains only one descriptor in the header extension
|
||||||
|
* object, and the other type is needed as well, the other one will be
|
||||||
|
* put into the header extension object.
|
||||||
|
*
|
||||||
|
* for each descriptor type, if an object is found, an updater will be
|
||||||
|
* configured.
|
||||||
|
*/
|
||||||
|
final AsfHeader sourceHeader = AsfHeaderReader.readTagHeader(raf);
|
||||||
|
raf.seek(0); // Reset for the streamer
|
||||||
|
/*
|
||||||
|
* Now createField modifiers for metadata descriptor and extended content
|
||||||
|
* descriptor as implied by the given Tag.
|
||||||
|
*/
|
||||||
|
// TODO not convinced that we need to copy fields here
|
||||||
|
final AsfTag copy = new AsfTag(tag, true);
|
||||||
|
final MetadataContainer[] distribution = TagConverter.distributeMetadata(copy);
|
||||||
|
final boolean[] existHeader = searchExistence(sourceHeader, distribution);
|
||||||
|
final boolean[] existExtHeader = searchExistence(sourceHeader.getExtendedHeader(), distribution);
|
||||||
|
// Modifiers for the asf header object
|
||||||
|
final List<ChunkModifier> headerModifier = new ArrayList<ChunkModifier>();
|
||||||
|
// Modifiers for the asf header extension object
|
||||||
|
final List<ChunkModifier> extHeaderModifier = new ArrayList<ChunkModifier>();
|
||||||
|
for (int i = 0; i < distribution.length; i++)
|
||||||
|
{
|
||||||
|
final WriteableChunkModifer modifier = new WriteableChunkModifer(distribution[i]);
|
||||||
|
if (existHeader[i])
|
||||||
|
{
|
||||||
|
// Will remove or modify chunks in ASF header
|
||||||
|
headerModifier.add(modifier);
|
||||||
|
}
|
||||||
|
else if (existExtHeader[i])
|
||||||
|
{
|
||||||
|
// Will remove or modify chunks in extended header
|
||||||
|
extHeaderModifier.add(modifier);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Objects (chunks) will be added here.
|
||||||
|
if (i == 0 || i == 2 || i == 1)
|
||||||
|
{
|
||||||
|
// Add content description and extended content description
|
||||||
|
// at header for maximum compatibility
|
||||||
|
headerModifier.add(modifier);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// For now, the rest should be created at extended header
|
||||||
|
// since other positions aren't known.
|
||||||
|
extHeaderModifier.add(modifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// only addField an AsfExtHeaderModifier, if there is actually something to
|
||||||
|
// change (performance)
|
||||||
|
if (!extHeaderModifier.isEmpty())
|
||||||
|
{
|
||||||
|
headerModifier.add(new AsfExtHeaderModifier(extHeaderModifier));
|
||||||
|
}
|
||||||
|
new AsfStreamer().createModifiedCopy(new RandomAccessFileInputstream(raf), new RandomAccessFileOutputStream(rafTemp), headerModifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.asf.data;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class represents the ASF extended header object (chunk).<br>
|
||||||
|
* Like {@link AsfHeader} it contains multiple other ASF objects (chunks).<br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public final class AsfExtendedHeader extends ChunkContainer
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.<br>
|
||||||
|
*
|
||||||
|
* @param pos Position within the stream.<br>
|
||||||
|
* @param length the length of the extended header object.
|
||||||
|
*/
|
||||||
|
public AsfExtendedHeader(final long pos, final BigInteger length)
|
||||||
|
{
|
||||||
|
super(GUID.GUID_HEADER_EXTENSION, pos, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the contentDescription.
|
||||||
|
*/
|
||||||
|
public ContentDescription getContentDescription()
|
||||||
|
{
|
||||||
|
return (ContentDescription) getFirst(GUID.GUID_CONTENTDESCRIPTION, ContentDescription.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the tagHeader.
|
||||||
|
*/
|
||||||
|
public MetadataContainer getExtendedContentDescription()
|
||||||
|
{
|
||||||
|
return (MetadataContainer) getFirst(GUID.GUID_EXTENDED_CONTENT_DESCRIPTION, MetadataContainer.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a language list object if present.
|
||||||
|
*
|
||||||
|
* @return a language list object.
|
||||||
|
*/
|
||||||
|
public LanguageList getLanguageList()
|
||||||
|
{
|
||||||
|
return (LanguageList) getFirst(GUID.GUID_LANGUAGE_LIST, LanguageList.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a metadata library object if present.
|
||||||
|
*
|
||||||
|
* @return metadata library objet
|
||||||
|
*/
|
||||||
|
public MetadataContainer getMetadataLibraryObject()
|
||||||
|
{
|
||||||
|
return (MetadataContainer) getFirst(GUID.GUID_METADATA_LIBRARY, MetadataContainer.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a metadata object if present.
|
||||||
|
*
|
||||||
|
* @return metadata object
|
||||||
|
*/
|
||||||
|
public MetadataContainer getMetadataObject()
|
||||||
|
{
|
||||||
|
return (MetadataContainer) getFirst(GUID.GUID_METADATA, MetadataContainer.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,223 @@
|
|||||||
|
/*
|
||||||
|
* Entagged Audio Tag library
|
||||||
|
* Copyright (c) 2004-2005 Christian Laireiter <liree@web.de>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library 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
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package com.mp3.jaudiotagger.audio.asf.data;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Each ASF file starts with a so called header. <br>
|
||||||
|
* This header contains other chunks. Each chunk starts with a 16 byte GUID
|
||||||
|
* followed by the length (in bytes) of the chunk (including GUID). The length
|
||||||
|
* number takes 8 bytes and is unsigned. Finally the chunk's data appears. <br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public final class AsfHeader extends ChunkContainer
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The charset "UTF-16LE" is mandatory for ASF handling.
|
||||||
|
*/
|
||||||
|
public final static Charset ASF_CHARSET = Charset.forName("UTF-16LE"); //$NON-NLS-1$
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Byte sequence representing the zero term character.
|
||||||
|
*/
|
||||||
|
public final static byte[] ZERO_TERM = {0, 0};
|
||||||
|
|
||||||
|
static
|
||||||
|
{
|
||||||
|
Set<GUID> MULTI_CHUNKS = new HashSet<GUID>();
|
||||||
|
MULTI_CHUNKS.add(GUID.GUID_STREAM);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ASF header contains multiple chunks. <br>
|
||||||
|
* The count of those is stored here.
|
||||||
|
*/
|
||||||
|
private final long chunkCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*
|
||||||
|
* @param pos see {@link Chunk#position}
|
||||||
|
* @param chunkLen see {@link Chunk#chunkLength}
|
||||||
|
* @param chunkCnt
|
||||||
|
*/
|
||||||
|
public AsfHeader(final long pos, final BigInteger chunkLen, final long chunkCnt)
|
||||||
|
{
|
||||||
|
super(GUID.GUID_HEADER, pos, chunkLen);
|
||||||
|
this.chunkCount = chunkCnt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method looks for an content description object in this header
|
||||||
|
* instance, if not found there, it tries to get one from a contained ASF
|
||||||
|
* header extension object.
|
||||||
|
*
|
||||||
|
* @return content description if found, <code>null</code> otherwise.
|
||||||
|
*/
|
||||||
|
public ContentDescription findContentDescription()
|
||||||
|
{
|
||||||
|
ContentDescription result = getContentDescription();
|
||||||
|
if (result == null && getExtendedHeader() != null)
|
||||||
|
{
|
||||||
|
result = getExtendedHeader().getContentDescription();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method looks for an extended content description object in this
|
||||||
|
* header instance, if not found there, it tries to get one from a contained
|
||||||
|
* ASF header extension object.
|
||||||
|
*
|
||||||
|
* @return extended content description if found, <code>null</code>
|
||||||
|
* otherwise.
|
||||||
|
*/
|
||||||
|
public MetadataContainer findExtendedContentDescription()
|
||||||
|
{
|
||||||
|
MetadataContainer result = getExtendedContentDescription();
|
||||||
|
if (result == null && getExtendedHeader() != null)
|
||||||
|
{
|
||||||
|
result = getExtendedHeader().getExtendedContentDescription();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method searches for a metadata container of the given type.<br>
|
||||||
|
*
|
||||||
|
* @param type the type of the container to look up.
|
||||||
|
* @return a container of specified type, of <code>null</code> if not
|
||||||
|
* contained.
|
||||||
|
*/
|
||||||
|
public MetadataContainer findMetadataContainer(final ContainerType type)
|
||||||
|
{
|
||||||
|
MetadataContainer result = (MetadataContainer) getFirst(type.getContainerGUID(), MetadataContainer.class);
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
result = (MetadataContainer) getExtendedHeader().getFirst(type.getContainerGUID(), MetadataContainer.class);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method returns the first audio stream chunk found in the asf file or
|
||||||
|
* stream.
|
||||||
|
*
|
||||||
|
* @return Returns the audioStreamChunk.
|
||||||
|
*/
|
||||||
|
public AudioStreamChunk getAudioStreamChunk()
|
||||||
|
{
|
||||||
|
AudioStreamChunk result = null;
|
||||||
|
final List<Chunk> streamChunks = assertChunkList(GUID.GUID_STREAM);
|
||||||
|
for (int i = 0; i < streamChunks.size() && result == null; i++)
|
||||||
|
{
|
||||||
|
if (streamChunks.get(i) instanceof AudioStreamChunk)
|
||||||
|
{
|
||||||
|
result = (AudioStreamChunk) streamChunks.get(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the amount of chunks, when this instance was created.<br>
|
||||||
|
* If chunks have been added, this won't be reflected with this call.<br>
|
||||||
|
* For that use {@link #getChunks()}.
|
||||||
|
*
|
||||||
|
* @return Chunkcount at instance creation.
|
||||||
|
*/
|
||||||
|
public long getChunkCount()
|
||||||
|
{
|
||||||
|
return this.chunkCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the contentDescription.
|
||||||
|
*/
|
||||||
|
public ContentDescription getContentDescription()
|
||||||
|
{
|
||||||
|
return (ContentDescription) getFirst(GUID.GUID_CONTENTDESCRIPTION, ContentDescription.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the encodingChunk.
|
||||||
|
*/
|
||||||
|
public EncodingChunk getEncodingChunk()
|
||||||
|
{
|
||||||
|
return (EncodingChunk) getFirst(GUID.GUID_ENCODING, EncodingChunk.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the encodingChunk.
|
||||||
|
*/
|
||||||
|
public EncryptionChunk getEncryptionChunk()
|
||||||
|
{
|
||||||
|
return (EncryptionChunk) getFirst(GUID.GUID_CONTENT_ENCRYPTION, EncryptionChunk.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the tagHeader.
|
||||||
|
*/
|
||||||
|
public MetadataContainer getExtendedContentDescription()
|
||||||
|
{
|
||||||
|
return (MetadataContainer) getFirst(GUID.GUID_EXTENDED_CONTENT_DESCRIPTION, MetadataContainer.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the extended header.
|
||||||
|
*/
|
||||||
|
public AsfExtendedHeader getExtendedHeader()
|
||||||
|
{
|
||||||
|
return (AsfExtendedHeader) getFirst(GUID.GUID_HEADER_EXTENSION, AsfExtendedHeader.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the fileHeader.
|
||||||
|
*/
|
||||||
|
public FileHeader getFileHeader()
|
||||||
|
{
|
||||||
|
return (FileHeader) getFirst(GUID.GUID_FILE, FileHeader.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the streamBitratePropertiesChunk.
|
||||||
|
*/
|
||||||
|
public StreamBitratePropertiesChunk getStreamBitratePropertiesChunk()
|
||||||
|
{
|
||||||
|
return (StreamBitratePropertiesChunk) getFirst(GUID.GUID_STREAM_BITRATE_PROPERTIES, StreamBitratePropertiesChunk.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String prettyPrint(final String prefix)
|
||||||
|
{
|
||||||
|
final StringBuilder result = new StringBuilder(super.prettyPrint(prefix, prefix + " | : Contains: \"" + getChunkCount() + "\" chunks" + Utils.LINE_SEPARATOR));
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,318 @@
|
|||||||
|
/*
|
||||||
|
* Entagged Audio Tag library
|
||||||
|
* Copyright (c) 2004-2005 Christian Laireiter <liree@web.de>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library 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
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package com.mp3.jaudiotagger.audio.asf.data;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class represents the stream chunk describing an audio stream. <br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public final class AudioStreamChunk extends StreamChunk
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Stores the hex values of codec identifiers to their descriptions. <br>
|
||||||
|
*/
|
||||||
|
public final static String[][] CODEC_DESCRIPTIONS = {{"161", " (Windows Media Audio (ver 7,8,9))"}, {"162", " (Windows Media Audio 9 series (Professional))"}, {"163", "(Windows Media Audio 9 series (Lossless))"}, {"7A21", " (GSM-AMR (CBR))"}, {"7A22", " (GSM-AMR (VBR))"}};
|
||||||
|
/**
|
||||||
|
* Stores the audio codec number for WMA
|
||||||
|
*/
|
||||||
|
public final static long WMA = 0x161;
|
||||||
|
/**
|
||||||
|
* Stores the audio codec number for WMA (CBR)
|
||||||
|
*/
|
||||||
|
public final static long WMA_CBR = 0x7A21;
|
||||||
|
/**
|
||||||
|
* Stores the audio codec number for WMA_LOSSLESS
|
||||||
|
*/
|
||||||
|
public final static long WMA_LOSSLESS = 0x163;
|
||||||
|
/**
|
||||||
|
* Stores the audio codec number for WMA_PRO
|
||||||
|
*/
|
||||||
|
public final static long WMA_PRO = 0x162;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the audio codec number for WMA (VBR)
|
||||||
|
*/
|
||||||
|
public final static long WMA_VBR = 0x7A22;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the average amount of bytes used by audio stream. <br>
|
||||||
|
* This value is a field within type specific data of audio stream. Maybe it
|
||||||
|
* could be used to calculate the KBPs.
|
||||||
|
*/
|
||||||
|
private long averageBytesPerSec;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount of bits used per sample. <br>
|
||||||
|
*/
|
||||||
|
private int bitsPerSample;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The block alignment of the audio data.
|
||||||
|
*/
|
||||||
|
private long blockAlignment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of channels.
|
||||||
|
*/
|
||||||
|
private long channelCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some data which needs to be interpreted if the codec is handled.
|
||||||
|
*/
|
||||||
|
private byte[] codecData = new byte[0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The audio compression format code.
|
||||||
|
*/
|
||||||
|
private long compressionFormat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* this field stores the error concealment type.
|
||||||
|
*/
|
||||||
|
private GUID errorConcealment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sampling rate of audio stream.
|
||||||
|
*/
|
||||||
|
private long samplingRate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*
|
||||||
|
* @param chunkLen Length of the entire chunk (including guid and size)
|
||||||
|
*/
|
||||||
|
public AudioStreamChunk(final BigInteger chunkLen)
|
||||||
|
{
|
||||||
|
super(GUID.GUID_AUDIOSTREAM, chunkLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the averageBytesPerSec.
|
||||||
|
*/
|
||||||
|
public long getAverageBytesPerSec()
|
||||||
|
{
|
||||||
|
return this.averageBytesPerSec;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the bitsPerSample.
|
||||||
|
*/
|
||||||
|
public int getBitsPerSample()
|
||||||
|
{
|
||||||
|
return this.bitsPerSample;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the blockAlignment.
|
||||||
|
*/
|
||||||
|
public long getBlockAlignment()
|
||||||
|
{
|
||||||
|
return this.blockAlignment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the channelCount.
|
||||||
|
*/
|
||||||
|
public long getChannelCount()
|
||||||
|
{
|
||||||
|
return this.channelCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the codecData.
|
||||||
|
*/
|
||||||
|
public byte[] getCodecData()
|
||||||
|
{
|
||||||
|
return this.codecData.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method will take a look at {@link #compressionFormat}and returns a
|
||||||
|
* String with its hex value and if known a textual note on what coded it
|
||||||
|
* represents. <br>
|
||||||
|
*
|
||||||
|
* @return A description for the used codec.
|
||||||
|
*/
|
||||||
|
public String getCodecDescription()
|
||||||
|
{
|
||||||
|
final StringBuilder result = new StringBuilder(Long.toHexString(getCompressionFormat()));
|
||||||
|
String furtherDesc = " (Unknown)";
|
||||||
|
for (final String[] aCODEC_DESCRIPTIONS : CODEC_DESCRIPTIONS)
|
||||||
|
{
|
||||||
|
if (aCODEC_DESCRIPTIONS[0].equalsIgnoreCase(result.toString()))
|
||||||
|
{
|
||||||
|
furtherDesc = aCODEC_DESCRIPTIONS[1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result.length() % 2 == 0)
|
||||||
|
{
|
||||||
|
result.insert(0, "0x");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result.insert(0, "0x0");
|
||||||
|
}
|
||||||
|
result.append(furtherDesc);
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the compressionFormat.
|
||||||
|
*/
|
||||||
|
public long getCompressionFormat()
|
||||||
|
{
|
||||||
|
return this.compressionFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the errorConcealment.
|
||||||
|
*/
|
||||||
|
public GUID getErrorConcealment()
|
||||||
|
{
|
||||||
|
return this.errorConcealment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method takes the value of {@link #getAverageBytesPerSec()}and
|
||||||
|
* calculates the kbps out of it, by simply multiplying by 8 and dividing by
|
||||||
|
* 1000. <br>
|
||||||
|
*
|
||||||
|
* @return amount of bits per second in kilo bits.
|
||||||
|
*/
|
||||||
|
public int getKbps()
|
||||||
|
{
|
||||||
|
return (int) getAverageBytesPerSec() * 8 / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the samplingRate.
|
||||||
|
*/
|
||||||
|
public long getSamplingRate()
|
||||||
|
{
|
||||||
|
return this.samplingRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This mehtod returns whether the audio stream data is error concealed. <br>
|
||||||
|
* For now only interleaved concealment is known. <br>
|
||||||
|
*
|
||||||
|
* @return <code>true</code> if error concealment is used.
|
||||||
|
*/
|
||||||
|
public boolean isErrorConcealed()
|
||||||
|
{
|
||||||
|
return getErrorConcealment().equals(GUID.GUID_AUDIO_ERROR_CONCEALEMENT_INTERLEAVED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String prettyPrint(final String prefix)
|
||||||
|
{
|
||||||
|
final StringBuilder result = new StringBuilder(super.prettyPrint(prefix));
|
||||||
|
result.append(prefix).append(" |-> Audio info:").append(Utils.LINE_SEPARATOR);
|
||||||
|
result.append(prefix).append(" | : Bitrate : ").append(getKbps()).append(Utils.LINE_SEPARATOR);
|
||||||
|
result.append(prefix).append(" | : Channels : ").append(getChannelCount()).append(" at ").append(getSamplingRate()).append(" Hz").append(Utils.LINE_SEPARATOR);
|
||||||
|
result.append(prefix).append(" | : Bits per Sample: ").append(getBitsPerSample()).append(Utils.LINE_SEPARATOR);
|
||||||
|
result.append(prefix).append(" | : Formatcode: ").append(getCodecDescription()).append(Utils.LINE_SEPARATOR);
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param avgeBytesPerSec The averageBytesPerSec to set.
|
||||||
|
*/
|
||||||
|
public void setAverageBytesPerSec(final long avgeBytesPerSec)
|
||||||
|
{
|
||||||
|
this.averageBytesPerSec = avgeBytesPerSec;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the bitsPerSample
|
||||||
|
*
|
||||||
|
* @param bps
|
||||||
|
*/
|
||||||
|
public void setBitsPerSample(final int bps)
|
||||||
|
{
|
||||||
|
this.bitsPerSample = bps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the blockAlignment.
|
||||||
|
*
|
||||||
|
* @param align
|
||||||
|
*/
|
||||||
|
public void setBlockAlignment(final long align)
|
||||||
|
{
|
||||||
|
this.blockAlignment = align;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param channels The channelCount to set.
|
||||||
|
*/
|
||||||
|
public void setChannelCount(final long channels)
|
||||||
|
{
|
||||||
|
this.channelCount = channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the codecData
|
||||||
|
*
|
||||||
|
* @param codecSpecificData
|
||||||
|
*/
|
||||||
|
public void setCodecData(final byte[] codecSpecificData)
|
||||||
|
{
|
||||||
|
if (codecSpecificData == null)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
this.codecData = codecSpecificData.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param cFormatCode The compressionFormat to set.
|
||||||
|
*/
|
||||||
|
public void setCompressionFormat(final long cFormatCode)
|
||||||
|
{
|
||||||
|
this.compressionFormat = cFormatCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method sets the error concealment type which is given by two GUIDs. <br>
|
||||||
|
*
|
||||||
|
* @param errConc the type of error concealment the audio stream is stored as.
|
||||||
|
*/
|
||||||
|
public void setErrorConcealment(final GUID errConc)
|
||||||
|
{
|
||||||
|
this.errorConcealment = errConc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param sampRate The samplingRate to set.
|
||||||
|
*/
|
||||||
|
public void setSamplingRate(final long sampRate)
|
||||||
|
{
|
||||||
|
this.samplingRate = sampRate;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,183 @@
|
|||||||
|
/*
|
||||||
|
* Entagged Audio Tag library
|
||||||
|
* Copyright (c) 2004-2005 Christian Laireiter <liree@web.de>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library 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
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package com.mp3.jaudiotagger.audio.asf.data;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class represents a chunk within ASF streams. <br>
|
||||||
|
* Each chunk starts with a 16byte {@linkplain GUID GUID} identifying the type.
|
||||||
|
* After that a number (represented by 8 bytes) follows which shows the size in
|
||||||
|
* bytes of the chunk. Finally there is the data of the chunk.
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public class Chunk
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The length of current chunk. <br>
|
||||||
|
*/
|
||||||
|
protected final BigInteger chunkLength;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GUID of represented chunk header.
|
||||||
|
*/
|
||||||
|
protected final GUID guid;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The position of current header object within file or stream.
|
||||||
|
*/
|
||||||
|
protected long position;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance
|
||||||
|
*
|
||||||
|
* @param headerGuid The GUID of header object.
|
||||||
|
* @param chunkLen Length of current chunk.
|
||||||
|
*/
|
||||||
|
public Chunk(final GUID headerGuid, final BigInteger chunkLen)
|
||||||
|
{
|
||||||
|
if (headerGuid == null)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("GUID must not be null.");
|
||||||
|
}
|
||||||
|
if (chunkLen == null || chunkLen.compareTo(BigInteger.ZERO) < 0)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("chunkLen must not be null nor negative.");
|
||||||
|
}
|
||||||
|
this.guid = headerGuid;
|
||||||
|
this.chunkLength = chunkLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance
|
||||||
|
*
|
||||||
|
* @param headerGuid The GUID of header object.
|
||||||
|
* @param pos Position of header object within stream or file.
|
||||||
|
* @param chunkLen Length of current chunk.
|
||||||
|
*/
|
||||||
|
public Chunk(final GUID headerGuid, final long pos, final BigInteger chunkLen)
|
||||||
|
{
|
||||||
|
if (headerGuid == null)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("GUID must not be null");
|
||||||
|
}
|
||||||
|
if (pos < 0)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("Position of header can't be negative.");
|
||||||
|
}
|
||||||
|
if (chunkLen == null || chunkLen.compareTo(BigInteger.ZERO) < 0)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("chunkLen must not be null nor negative.");
|
||||||
|
}
|
||||||
|
this.guid = headerGuid;
|
||||||
|
this.position = pos;
|
||||||
|
this.chunkLength = chunkLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method returns the End of the current chunk introduced by current
|
||||||
|
* header object.
|
||||||
|
*
|
||||||
|
* @return Position after current chunk.
|
||||||
|
* @deprecated typo, use {@link #getChunkEnd()} instead.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public long getChunckEnd()
|
||||||
|
{
|
||||||
|
return this.position + this.chunkLength.longValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method returns the End of the current chunk introduced by current
|
||||||
|
* header object.
|
||||||
|
*
|
||||||
|
* @return Position after current chunk.
|
||||||
|
*/
|
||||||
|
public long getChunkEnd()
|
||||||
|
{
|
||||||
|
return this.position + this.chunkLength.longValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the chunkLength.
|
||||||
|
*/
|
||||||
|
public BigInteger getChunkLength()
|
||||||
|
{
|
||||||
|
return this.chunkLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the guid.
|
||||||
|
*/
|
||||||
|
public GUID getGuid()
|
||||||
|
{
|
||||||
|
return this.guid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the position.
|
||||||
|
*/
|
||||||
|
public long getPosition()
|
||||||
|
{
|
||||||
|
return this.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method creates a String containing useful information prepared to be
|
||||||
|
* printed on STD-OUT. <br>
|
||||||
|
* This method is intended to be overwritten by inheriting classes.
|
||||||
|
*
|
||||||
|
* @param prefix each line gets this string prepended.
|
||||||
|
* @return Information of current Chunk Object.
|
||||||
|
*/
|
||||||
|
public String prettyPrint(final String prefix)
|
||||||
|
{
|
||||||
|
final StringBuilder result = new StringBuilder();
|
||||||
|
result.append(prefix).append("-> GUID: ").append(GUID.getGuidDescription(this.guid)).append(Utils.LINE_SEPARATOR);
|
||||||
|
result.append(prefix).append(" | : Starts at position: ").append(getPosition()).append(Utils.LINE_SEPARATOR);
|
||||||
|
result.append(prefix).append(" | : Last byte at: ").append(getChunkEnd() - 1).append(Utils.LINE_SEPARATOR);
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the position.
|
||||||
|
*
|
||||||
|
* @param pos position to set.
|
||||||
|
*/
|
||||||
|
public void setPosition(final long pos)
|
||||||
|
{
|
||||||
|
this.position = pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (overridden)
|
||||||
|
*
|
||||||
|
* @see Object#toString()
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return prettyPrint("");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,191 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.asf.data;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.ChunkPositionComparator;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores multiple ASF objects (chunks) in form of {@link Chunk} objects, and is
|
||||||
|
* itself an ASF object (chunk).<br>
|
||||||
|
* <br>
|
||||||
|
* Because current implementation is solely used for ASF metadata, all chunks
|
||||||
|
* (except for {@link StreamChunk}) may only be {@linkplain #addChunk(Chunk)
|
||||||
|
* inserted} once.
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public class ChunkContainer extends Chunk
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the {@link GUID} instances, which are allowed multiple times
|
||||||
|
* within an ASF header.
|
||||||
|
*/
|
||||||
|
private final static Set<GUID> MULTI_CHUNKS;
|
||||||
|
|
||||||
|
static
|
||||||
|
{
|
||||||
|
MULTI_CHUNKS = new HashSet<GUID>();
|
||||||
|
MULTI_CHUNKS.add(GUID.GUID_STREAM);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests whether all stored chunks have a unique starting position among
|
||||||
|
* their brothers.
|
||||||
|
*
|
||||||
|
* @param container the container to test.
|
||||||
|
* @return <code>true</code> if all chunks are located at an unique
|
||||||
|
* position. However, no intersection is tested.
|
||||||
|
*/
|
||||||
|
protected static boolean chunkstartsUnique(final ChunkContainer container)
|
||||||
|
{
|
||||||
|
boolean result = true;
|
||||||
|
final Set<Long> chunkStarts = new HashSet<Long>();
|
||||||
|
final Collection<Chunk> chunks = container.getChunks();
|
||||||
|
for (final Chunk curr : chunks)
|
||||||
|
{
|
||||||
|
result &= chunkStarts.add(curr.getPosition());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the {@link Chunk} objects to their {@link GUID}.
|
||||||
|
*/
|
||||||
|
private final Map<GUID, List<Chunk>> chunkTable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*
|
||||||
|
* @param chunkGUID the GUID which identifies the chunk.
|
||||||
|
* @param pos the position of the chunk within the stream.
|
||||||
|
* @param length the length of the chunk.
|
||||||
|
*/
|
||||||
|
public ChunkContainer(final GUID chunkGUID, final long pos, final BigInteger length)
|
||||||
|
{
|
||||||
|
super(chunkGUID, pos, length);
|
||||||
|
this.chunkTable = new Hashtable<GUID, List<Chunk>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a chunk to the container.<br>
|
||||||
|
*
|
||||||
|
* @param toAdd The chunk which is to be added.
|
||||||
|
* @throws IllegalArgumentException If a chunk of same type is already added, except for
|
||||||
|
* {@link StreamChunk}.
|
||||||
|
*/
|
||||||
|
public void addChunk(final Chunk toAdd)
|
||||||
|
{
|
||||||
|
final List<Chunk> list = assertChunkList(toAdd.getGuid());
|
||||||
|
if (!list.isEmpty() && !MULTI_CHUNKS.contains(toAdd.getGuid()))
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("The GUID of the given chunk indicates, that there is no more instance allowed."); //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
list.add(toAdd);
|
||||||
|
assert chunkstartsUnique(this) : "Chunk has equal start position like an already inserted one."; //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method asserts that a {@link List} exists for the given {@link GUID}
|
||||||
|
* , in {@link #chunkTable}.<br>
|
||||||
|
*
|
||||||
|
* @param lookFor The GUID to get list for.
|
||||||
|
* @return an already existing, or newly created list.
|
||||||
|
*/
|
||||||
|
protected List<Chunk> assertChunkList(final GUID lookFor)
|
||||||
|
{
|
||||||
|
List<Chunk> result = this.chunkTable.get(lookFor);
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
result = new ArrayList<Chunk>();
|
||||||
|
this.chunkTable.put(lookFor, result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a collection of all contained chunks.<br>
|
||||||
|
*
|
||||||
|
* @return all contained chunks
|
||||||
|
*/
|
||||||
|
public Collection<Chunk> getChunks()
|
||||||
|
{
|
||||||
|
final List<Chunk> result = new ArrayList<Chunk>();
|
||||||
|
for (final List<Chunk> curr : this.chunkTable.values())
|
||||||
|
{
|
||||||
|
result.addAll(curr);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks for the first stored chunk which has the given GUID.
|
||||||
|
*
|
||||||
|
* @param lookFor GUID to look up.
|
||||||
|
* @param instanceOf The class which must additionally be matched.
|
||||||
|
* @return <code>null</code> if no chunk was found, or the stored instance
|
||||||
|
* doesn't match.
|
||||||
|
*/
|
||||||
|
protected Chunk getFirst(final GUID lookFor, final Class<? extends Chunk> instanceOf)
|
||||||
|
{
|
||||||
|
Chunk result = null;
|
||||||
|
final List<Chunk> list = this.chunkTable.get(lookFor);
|
||||||
|
if (list != null && !list.isEmpty())
|
||||||
|
{
|
||||||
|
final Chunk chunk = list.get(0);
|
||||||
|
if (instanceOf.isAssignableFrom(chunk.getClass()))
|
||||||
|
{
|
||||||
|
result = chunk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method checks if a chunk has been {@linkplain #addChunk(Chunk)
|
||||||
|
* added} with specified {@linkplain Chunk#getGuid() GUID}.<br>
|
||||||
|
*
|
||||||
|
* @param lookFor GUID to look up.
|
||||||
|
* @return <code>true</code> if chunk with specified GUID has been added.
|
||||||
|
*/
|
||||||
|
public boolean hasChunkByGUID(final GUID lookFor)
|
||||||
|
{
|
||||||
|
return this.chunkTable.containsKey(lookFor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String prettyPrint(final String prefix)
|
||||||
|
{
|
||||||
|
return prettyPrint(prefix, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nearly the same as {@link #prettyPrint(String)} however, additional
|
||||||
|
* information can be injected below the {@link Chunk#prettyPrint(String)}
|
||||||
|
* output and the listing of the contained chunks.<br>
|
||||||
|
*
|
||||||
|
* @param prefix The prefix to prepend.
|
||||||
|
* @param containerInfo Information to inject.
|
||||||
|
* @return Information of current Chunk Object.
|
||||||
|
*/
|
||||||
|
public String prettyPrint(final String prefix, final String containerInfo)
|
||||||
|
{
|
||||||
|
final StringBuilder result = new StringBuilder(super.prettyPrint(prefix));
|
||||||
|
result.append(containerInfo);
|
||||||
|
result.append(prefix).append(" |").append(Utils.LINE_SEPARATOR);
|
||||||
|
final ArrayList<Chunk> list = new ArrayList<Chunk>(getChunks());
|
||||||
|
Collections.sort(list, new ChunkPositionComparator());
|
||||||
|
|
||||||
|
for (Chunk curr : list)
|
||||||
|
{
|
||||||
|
result.append(curr.prettyPrint(prefix + " |"));
|
||||||
|
result.append(prefix).append(" |").append(Utils.LINE_SEPARATOR);
|
||||||
|
}
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,284 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.asf.data;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
import com.mp3.jaudiotagger.logging.ErrorMessage;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enumerates capabilities, respectively uses, of metadata descriptors.<br>
|
||||||
|
* <br>
|
||||||
|
* The {@link #METADATA_LIBRARY_OBJECT} allows the most variations of data, as
|
||||||
|
* well as no size limitation (if it can be stored within a DWORD amount of
|
||||||
|
* bytes).<br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public enum ContainerType
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The descriptor is used in the content branding object (chunk)
|
||||||
|
*/
|
||||||
|
CONTENT_BRANDING(GUID.GUID_CONTENT_BRANDING, 32, false, false, false, false),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The descriptor is used in the content description object (chunk), so
|
||||||
|
* {@linkplain MetadataDescriptor#DWORD_MAXVALUE maximum data length}
|
||||||
|
* applies, no language index and stream number are allowed, as well as no
|
||||||
|
* multiple values.
|
||||||
|
*/
|
||||||
|
CONTENT_DESCRIPTION(GUID.GUID_CONTENTDESCRIPTION, 16, false, false, false, false),
|
||||||
|
/**
|
||||||
|
* The descriptor is used in an extended content description object, so the
|
||||||
|
* {@linkplain MetadataDescriptor#DWORD_MAXVALUE maximum data size} applies,
|
||||||
|
* and no language index and stream number other than "0" is
|
||||||
|
* allowed. Additionally no multiple values are permitted.
|
||||||
|
*/
|
||||||
|
EXTENDED_CONTENT(GUID.GUID_EXTENDED_CONTENT_DESCRIPTION, 16, false, false, false, false),
|
||||||
|
/**
|
||||||
|
* The descriptor is used in a metadata library object. No real size limit
|
||||||
|
* (except DWORD range) applies. Stream numbers and language indexes can be
|
||||||
|
* specified.
|
||||||
|
*/
|
||||||
|
METADATA_LIBRARY_OBJECT(GUID.GUID_METADATA_LIBRARY, 32, true, true, true, true),
|
||||||
|
/**
|
||||||
|
* The descriptor is used in a metadata object. The
|
||||||
|
* {@linkplain MetadataDescriptor#DWORD_MAXVALUE maximum data size} applies.
|
||||||
|
* Stream numbers can be specified. But no language index (always
|
||||||
|
* "0").
|
||||||
|
*/
|
||||||
|
METADATA_OBJECT(GUID.GUID_METADATA, 16, false, true, false, true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if low has index as high, in respect to
|
||||||
|
* {@link #getOrdered()}
|
||||||
|
*
|
||||||
|
* @param low
|
||||||
|
* @param high
|
||||||
|
* @return <code>true</code> if in correct order.
|
||||||
|
*/
|
||||||
|
public static boolean areInCorrectOrder(final ContainerType low, final ContainerType high)
|
||||||
|
{
|
||||||
|
final List<ContainerType> asList = Arrays.asList(getOrdered());
|
||||||
|
return asList.indexOf(low) <= asList.indexOf(high);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the elements in an order, that indicates more capabilities
|
||||||
|
* (ascending).<br>
|
||||||
|
*
|
||||||
|
* @return capability ordered types
|
||||||
|
*/
|
||||||
|
public static ContainerType[] getOrdered()
|
||||||
|
{
|
||||||
|
return new ContainerType[]{CONTENT_DESCRIPTION, CONTENT_BRANDING, EXTENDED_CONTENT, METADATA_OBJECT, METADATA_LIBRARY_OBJECT};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the guid that identifies ASF chunks which store metadata of the
|
||||||
|
* current type.
|
||||||
|
*/
|
||||||
|
private final GUID containerGUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <code>true</code> if the descriptor field can store {@link GUID} values.
|
||||||
|
*/
|
||||||
|
private final boolean guidEnabled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <code>true</code> if descriptor field can refer to a language.
|
||||||
|
*/
|
||||||
|
private final boolean languageEnabled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum amount of bytes the descriptor data may consume.<br>
|
||||||
|
*/
|
||||||
|
private final BigInteger maximumDataLength;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <code>true</code> if the container may store multiple values of the same
|
||||||
|
* metadata descriptor specification (equality on name, language, and
|
||||||
|
* stream).<br>
|
||||||
|
* WindowsMedia players advanced tag editor for example stores the
|
||||||
|
* WM/Picture attribute once in the extended content description, and all
|
||||||
|
* others in the metadata library object.
|
||||||
|
*/
|
||||||
|
private final boolean multiValued;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* if <code>-1</code> a size value has to be compared against
|
||||||
|
* {@link #maximumDataLength} because {@link Long#MAX_VALUE} is exceeded.<br>
|
||||||
|
* Otherwise this is the {@link BigInteger#longValue()} representation.
|
||||||
|
*/
|
||||||
|
private final long perfMaxDataLen;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <code>true</code> if descriptor field can refer to specific streams.
|
||||||
|
*/
|
||||||
|
private final boolean streamEnabled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance
|
||||||
|
*
|
||||||
|
* @param guid see {@link #containerGUID}
|
||||||
|
* @param maxDataLenBits The amount of bits that is used to represent an unsigned value
|
||||||
|
* for the containers size descriptors. Will create a maximum
|
||||||
|
* value for {@link #maximumDataLength}. (2 ^ maxDataLenBits -1)
|
||||||
|
* @param guidAllowed see {@link #guidEnabled}
|
||||||
|
* @param stream see {@link #streamEnabled}
|
||||||
|
* @param language see {@link #languageEnabled}
|
||||||
|
* @param multiValue see {@link #multiValued}
|
||||||
|
*/
|
||||||
|
private ContainerType(final GUID guid, final int maxDataLenBits, final boolean guidAllowed, final boolean stream, final boolean language, final boolean multiValue)
|
||||||
|
{
|
||||||
|
this.containerGUID = guid;
|
||||||
|
this.maximumDataLength = BigInteger.valueOf(2).pow(maxDataLenBits).subtract(BigInteger.ONE);
|
||||||
|
if (this.maximumDataLength.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) <= 0)
|
||||||
|
{
|
||||||
|
this.perfMaxDataLen = this.maximumDataLength.longValue();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.perfMaxDataLen = -1;
|
||||||
|
}
|
||||||
|
this.guidEnabled = guidAllowed;
|
||||||
|
this.streamEnabled = stream;
|
||||||
|
this.languageEnabled = language;
|
||||||
|
this.multiValued = multiValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls {@link #checkConstraints(String, byte[], int, int, int)} and
|
||||||
|
* actually throws the exception if there is one.
|
||||||
|
*
|
||||||
|
* @param name name of the descriptor
|
||||||
|
* @param data content
|
||||||
|
* @param type data type
|
||||||
|
* @param stream stream number
|
||||||
|
* @param language language index
|
||||||
|
*/
|
||||||
|
public void assertConstraints(final String name, final byte[] data, final int type, final int stream, final int language)
|
||||||
|
{
|
||||||
|
final RuntimeException result = checkConstraints(name, data, type, stream, language);
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
throw result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the values for a {@linkplain MetadataDescriptor content
|
||||||
|
* descriptor} match the contraints of the container type, and returns a
|
||||||
|
* {@link RuntimeException} if the requirements aren't met.
|
||||||
|
*
|
||||||
|
* @param name name of the descriptor
|
||||||
|
* @param data content
|
||||||
|
* @param type data type
|
||||||
|
* @param stream stream number
|
||||||
|
* @param language language index
|
||||||
|
* @return <code>null</code> if everything is fine.
|
||||||
|
*/
|
||||||
|
public RuntimeException checkConstraints(final String name, final byte[] data, final int type, final int stream, final int language)
|
||||||
|
{
|
||||||
|
RuntimeException result = null;
|
||||||
|
// TODO generate tests
|
||||||
|
if (name == null || data == null)
|
||||||
|
{
|
||||||
|
result = new IllegalArgumentException("Arguments must not be null.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!Utils.isStringLengthValidNullSafe(name))
|
||||||
|
{
|
||||||
|
result = new IllegalArgumentException(ErrorMessage.WMA_LENGTH_OF_STRING_IS_TOO_LARGE.getMsg(name.length()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result == null && !isWithinValueRange(data.length))
|
||||||
|
{
|
||||||
|
result = new IllegalArgumentException(ErrorMessage.WMA_LENGTH_OF_DATA_IS_TOO_LARGE.getMsg(data.length, getMaximumDataLength(), getContainerGUID().getDescription()));
|
||||||
|
}
|
||||||
|
if (result == null && (stream < 0 || stream > MetadataDescriptor.MAX_STREAM_NUMBER || (!isStreamNumberEnabled() && stream != 0)))
|
||||||
|
{
|
||||||
|
final String streamAllowed = isStreamNumberEnabled() ? "0 to 127" : "0";
|
||||||
|
result = new IllegalArgumentException(ErrorMessage.WMA_INVALID_STREAM_REFERNCE.getMsg(stream, streamAllowed, getContainerGUID().getDescription()));
|
||||||
|
}
|
||||||
|
if (result == null && type == MetadataDescriptor.TYPE_GUID && !isGuidEnabled())
|
||||||
|
{
|
||||||
|
result = new IllegalArgumentException(ErrorMessage.WMA_INVALID_GUID_USE.getMsg(getContainerGUID().getDescription()));
|
||||||
|
}
|
||||||
|
if (result == null && ((language != 0 && !isLanguageEnabled()) || (language < 0 || language >= MetadataDescriptor.MAX_LANG_INDEX)))
|
||||||
|
{
|
||||||
|
final String langAllowed = isStreamNumberEnabled() ? "0 to 126" : "0";
|
||||||
|
result = new IllegalArgumentException(ErrorMessage.WMA_INVALID_LANGUAGE_USE.getMsg(language, getContainerGUID().getDescription(), langAllowed));
|
||||||
|
}
|
||||||
|
if (result == null && this == CONTENT_DESCRIPTION && type != MetadataDescriptor.TYPE_STRING)
|
||||||
|
{
|
||||||
|
result = new IllegalArgumentException(ErrorMessage.WMA_ONLY_STRING_IN_CD.getMsg());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the containerGUID
|
||||||
|
*/
|
||||||
|
public GUID getContainerGUID()
|
||||||
|
{
|
||||||
|
return this.containerGUID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the maximumDataLength
|
||||||
|
*/
|
||||||
|
public BigInteger getMaximumDataLength()
|
||||||
|
{
|
||||||
|
return this.maximumDataLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the guidEnabled
|
||||||
|
*/
|
||||||
|
public boolean isGuidEnabled()
|
||||||
|
{
|
||||||
|
return this.guidEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the languageEnabled
|
||||||
|
*/
|
||||||
|
public boolean isLanguageEnabled()
|
||||||
|
{
|
||||||
|
return this.languageEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests if the given value is less than or equal to
|
||||||
|
* {@link #getMaximumDataLength()}, and greater or equal to zero.<br>
|
||||||
|
*
|
||||||
|
* @param value The value to test
|
||||||
|
* @return <code>true</code> if size restrictions for binary data are met
|
||||||
|
* with this container type.
|
||||||
|
*/
|
||||||
|
public boolean isWithinValueRange(final long value)
|
||||||
|
{
|
||||||
|
return (this.perfMaxDataLen == -1 || this.perfMaxDataLen >= value) && value >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the multiValued
|
||||||
|
*/
|
||||||
|
public boolean isMultiValued()
|
||||||
|
{
|
||||||
|
return this.multiValued;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the streamEnabled
|
||||||
|
*/
|
||||||
|
public boolean isStreamNumberEnabled()
|
||||||
|
{
|
||||||
|
return this.streamEnabled;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,224 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.asf.data;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.StandardCharsets;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This structure represents the value of the content branding object, which
|
||||||
|
* stores the banner image, the banner image URL and the copyright URL.<br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public final class ContentBranding extends MetadataContainer
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the allowed {@linkplain MetadataDescriptor#getName() descriptor
|
||||||
|
* keys}.
|
||||||
|
*/
|
||||||
|
public final static Set<String> ALLOWED;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Descriptor key representing the banner image.
|
||||||
|
*/
|
||||||
|
public final static String KEY_BANNER_IMAGE = "BANNER_IMAGE";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Descriptor key representing the banner image type.<br>
|
||||||
|
* <br>
|
||||||
|
* <b>Known/valid values are:</b>
|
||||||
|
* <ol>
|
||||||
|
* <li>0: there is no image present</li>
|
||||||
|
* <li>1: there is a BMP image</li>
|
||||||
|
* <li>2: there is a JPEG image</li>
|
||||||
|
* <li>3: there is a GIF image</li>
|
||||||
|
* </ol>
|
||||||
|
*/
|
||||||
|
public final static String KEY_BANNER_TYPE = "BANNER_IMAGE_TYPE";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Descriptor key representing the banner image URL.
|
||||||
|
*/
|
||||||
|
public final static String KEY_BANNER_URL = "BANNER_IMAGE_URL";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Descriptor key representing the copyright URL.
|
||||||
|
*/
|
||||||
|
public final static String KEY_COPYRIGHT_URL = "COPYRIGHT_URL";
|
||||||
|
|
||||||
|
static
|
||||||
|
{
|
||||||
|
ALLOWED = new HashSet<String>();
|
||||||
|
ALLOWED.add(KEY_BANNER_IMAGE);
|
||||||
|
ALLOWED.add(KEY_BANNER_TYPE);
|
||||||
|
ALLOWED.add(KEY_BANNER_URL);
|
||||||
|
ALLOWED.add(KEY_COPYRIGHT_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*/
|
||||||
|
public ContentBranding()
|
||||||
|
{
|
||||||
|
this(0, BigInteger.ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*
|
||||||
|
* @param pos Position of content description within file or stream
|
||||||
|
* @param size Length of content description.
|
||||||
|
*/
|
||||||
|
public ContentBranding(final long pos, final BigInteger size)
|
||||||
|
{
|
||||||
|
super(ContainerType.CONTENT_BRANDING, pos, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the banner image URL.
|
||||||
|
*
|
||||||
|
* @return the banner image URL.
|
||||||
|
*/
|
||||||
|
public String getBannerImageURL()
|
||||||
|
{
|
||||||
|
return getValueFor(KEY_BANNER_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the copyright URL.
|
||||||
|
*
|
||||||
|
* @return the banner image URL.
|
||||||
|
*/
|
||||||
|
public String getCopyRightURL()
|
||||||
|
{
|
||||||
|
return getValueFor(KEY_COPYRIGHT_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public long getCurrentAsfChunkSize()
|
||||||
|
{
|
||||||
|
// GUID, size, image type, image data size, image url data size,
|
||||||
|
// copyright data size
|
||||||
|
long result = 40;
|
||||||
|
result += assertDescriptor(KEY_BANNER_IMAGE, MetadataDescriptor.TYPE_BINARY).getRawDataSize();
|
||||||
|
result += getBannerImageURL().length();
|
||||||
|
result += getCopyRightURL().length();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the binary image data.
|
||||||
|
*
|
||||||
|
* @return binary image data.
|
||||||
|
*/
|
||||||
|
public byte[] getImageData()
|
||||||
|
{
|
||||||
|
return assertDescriptor(KEY_BANNER_IMAGE, MetadataDescriptor.TYPE_BINARY).getRawData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the image type.<br>
|
||||||
|
*
|
||||||
|
* @return image type
|
||||||
|
* @see #KEY_BANNER_TYPE for known/valid values.
|
||||||
|
*/
|
||||||
|
public long getImageType()
|
||||||
|
{
|
||||||
|
if (!hasDescriptor(KEY_BANNER_TYPE))
|
||||||
|
{
|
||||||
|
final MetadataDescriptor descriptor = new MetadataDescriptor(ContainerType.CONTENT_BRANDING, KEY_BANNER_TYPE, MetadataDescriptor.TYPE_DWORD);
|
||||||
|
descriptor.setDWordValue(0);
|
||||||
|
addDescriptor(descriptor);
|
||||||
|
}
|
||||||
|
return assertDescriptor(KEY_BANNER_TYPE).getNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean isAddSupported(final MetadataDescriptor descriptor)
|
||||||
|
{
|
||||||
|
return ALLOWED.contains(descriptor.getName()) && super.isAddSupported(descriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method sets the banner image URL, if <code>imageURL</code> is not
|
||||||
|
* blank.<br>
|
||||||
|
*
|
||||||
|
* @param imageURL image URL to set.
|
||||||
|
*/
|
||||||
|
public void setBannerImageURL(final String imageURL)
|
||||||
|
{
|
||||||
|
if (Utils.isBlank(imageURL))
|
||||||
|
{
|
||||||
|
removeDescriptorsByName(KEY_BANNER_URL);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
assertDescriptor(KEY_BANNER_URL).setStringValue(imageURL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method sets the copyright URL, if <code>copyRight</code> is not
|
||||||
|
* blank.<br>
|
||||||
|
*
|
||||||
|
* @param copyRight copyright URL to set.
|
||||||
|
*/
|
||||||
|
public void setCopyRightURL(final String copyRight)
|
||||||
|
{
|
||||||
|
if (Utils.isBlank(copyRight))
|
||||||
|
{
|
||||||
|
removeDescriptorsByName(KEY_COPYRIGHT_URL);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
assertDescriptor(KEY_COPYRIGHT_URL).setStringValue(copyRight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param imageType
|
||||||
|
* @param imageData
|
||||||
|
*/
|
||||||
|
public void setImage(final long imageType, final byte[] imageData)
|
||||||
|
{
|
||||||
|
assert imageType >= 0 && imageType <= 3;
|
||||||
|
assert imageType > 0 || imageData.length == 0;
|
||||||
|
assertDescriptor(KEY_BANNER_TYPE, MetadataDescriptor.TYPE_DWORD).setDWordValue(imageType);
|
||||||
|
assertDescriptor(KEY_BANNER_IMAGE, MetadataDescriptor.TYPE_BINARY).setBinaryValue(imageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public long writeInto(final OutputStream out) throws IOException
|
||||||
|
{
|
||||||
|
final long chunkSize = getCurrentAsfChunkSize();
|
||||||
|
out.write(getGuid().getBytes());
|
||||||
|
Utils.writeUINT64(chunkSize, out);
|
||||||
|
Utils.writeUINT32(getImageType(), out);
|
||||||
|
assert getImageType() >= 0 && getImageType() <= 3;
|
||||||
|
final byte[] imageData = getImageData();
|
||||||
|
assert getImageType() > 0 || imageData.length == 0;
|
||||||
|
Utils.writeUINT32(imageData.length, out);
|
||||||
|
out.write(imageData);
|
||||||
|
Utils.writeUINT32(getBannerImageURL().length(), out);
|
||||||
|
out.write(getBannerImageURL().getBytes(StandardCharsets.US_ASCII));
|
||||||
|
Utils.writeUINT32(getCopyRightURL().length(), out);
|
||||||
|
out.write(getCopyRightURL().getBytes(StandardCharsets.US_ASCII));
|
||||||
|
return chunkSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,254 @@
|
|||||||
|
/*
|
||||||
|
* Entagged Audio Tag library
|
||||||
|
* Copyright (c) 2004-2005 Christian Laireiter <liree@web.de>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library 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
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package com.mp3.jaudiotagger.audio.asf.data;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class represents the data of a chunk which contains title, author,
|
||||||
|
* copyright, description and the rating of the file. <br>
|
||||||
|
* It is optional within ASF files. But if, exists only once.
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public final class ContentDescription extends MetadataContainer
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Stores the only allowed keys of this metadata container.
|
||||||
|
*/
|
||||||
|
public final static Set<String> ALLOWED;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Field key for author.
|
||||||
|
*/
|
||||||
|
public final static String KEY_AUTHOR = "AUTHOR";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Field key for copyright.
|
||||||
|
*/
|
||||||
|
public final static String KEY_COPYRIGHT = "COPYRIGHT";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Field key for description.
|
||||||
|
*/
|
||||||
|
public final static String KEY_DESCRIPTION = "DESCRIPTION";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Field key for rating.
|
||||||
|
*/
|
||||||
|
public final static String KEY_RATING = "RATING";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Field key for title.
|
||||||
|
*/
|
||||||
|
public final static String KEY_TITLE = "TITLE";
|
||||||
|
|
||||||
|
static
|
||||||
|
{
|
||||||
|
ALLOWED = new HashSet<String>(Arrays.asList(KEY_AUTHOR, KEY_COPYRIGHT, KEY_DESCRIPTION, KEY_RATING, KEY_TITLE));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance. <br>
|
||||||
|
*/
|
||||||
|
public ContentDescription()
|
||||||
|
{
|
||||||
|
this(0, BigInteger.ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*
|
||||||
|
* @param pos Position of content description within file or stream
|
||||||
|
* @param chunkLen Length of content description.
|
||||||
|
*/
|
||||||
|
public ContentDescription(final long pos, final BigInteger chunkLen)
|
||||||
|
{
|
||||||
|
super(ContainerType.CONTENT_DESCRIPTION, pos, chunkLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the author.
|
||||||
|
*/
|
||||||
|
public String getAuthor()
|
||||||
|
{
|
||||||
|
return getValueFor(KEY_AUTHOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the comment.
|
||||||
|
*/
|
||||||
|
public String getComment()
|
||||||
|
{
|
||||||
|
return getValueFor(KEY_DESCRIPTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the copyRight.
|
||||||
|
*/
|
||||||
|
public String getCopyRight()
|
||||||
|
{
|
||||||
|
return getValueFor(KEY_COPYRIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public long getCurrentAsfChunkSize()
|
||||||
|
{
|
||||||
|
long result = 44; // GUID + UINT64 for size + 5 times string length
|
||||||
|
// (each
|
||||||
|
// 2 bytes) + 5 times zero term char (2 bytes each).
|
||||||
|
result += getAuthor().length() * 2; // UTF-16LE
|
||||||
|
result += getComment().length() * 2;
|
||||||
|
result += getRating().length() * 2;
|
||||||
|
result += getTitle().length() * 2;
|
||||||
|
result += getCopyRight().length() * 2;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return returns the rating.
|
||||||
|
*/
|
||||||
|
public String getRating()
|
||||||
|
{
|
||||||
|
return getValueFor(KEY_RATING);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the title.
|
||||||
|
*/
|
||||||
|
public String getTitle()
|
||||||
|
{
|
||||||
|
return getValueFor(KEY_TITLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean isAddSupported(final MetadataDescriptor descriptor)
|
||||||
|
{
|
||||||
|
return ALLOWED.contains(descriptor.getName()) && super.isAddSupported(descriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String prettyPrint(final String prefix)
|
||||||
|
{
|
||||||
|
final StringBuilder result = new StringBuilder(super.prettyPrint(prefix));
|
||||||
|
result.append(prefix).append(" |->Title : ").append(getTitle()).append(Utils.LINE_SEPARATOR);
|
||||||
|
result.append(prefix).append(" |->Author : ").append(getAuthor()).append(Utils.LINE_SEPARATOR);
|
||||||
|
result.append(prefix).append(" |->Copyright : ").append(getCopyRight()).append(Utils.LINE_SEPARATOR);
|
||||||
|
result.append(prefix).append(" |->Description: ").append(getComment()).append(Utils.LINE_SEPARATOR);
|
||||||
|
result.append(prefix).append(" |->Rating :").append(getRating()).append(Utils.LINE_SEPARATOR);
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param fileAuthor The author to set.
|
||||||
|
* @throws IllegalArgumentException If "UTF-16LE"-byte-representation would take more than 65535
|
||||||
|
* bytes.
|
||||||
|
*/
|
||||||
|
public void setAuthor(final String fileAuthor) throws IllegalArgumentException
|
||||||
|
{
|
||||||
|
setStringValue(KEY_AUTHOR, fileAuthor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param tagComment The comment to set.
|
||||||
|
* @throws IllegalArgumentException If "UTF-16LE"-byte-representation would take more than 65535
|
||||||
|
* bytes.
|
||||||
|
*/
|
||||||
|
public void setComment(final String tagComment) throws IllegalArgumentException
|
||||||
|
{
|
||||||
|
setStringValue(KEY_DESCRIPTION, tagComment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param cpright The copyRight to set.
|
||||||
|
* @throws IllegalArgumentException If "UTF-16LE"-byte-representation would take more than 65535
|
||||||
|
* bytes.
|
||||||
|
*/
|
||||||
|
public void setCopyright(final String cpright) throws IllegalArgumentException
|
||||||
|
{
|
||||||
|
setStringValue(KEY_COPYRIGHT, cpright);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ratingText The rating to be set.
|
||||||
|
* @throws IllegalArgumentException If "UTF-16LE"-byte-representation would take more than 65535
|
||||||
|
* bytes.
|
||||||
|
*/
|
||||||
|
public void setRating(final String ratingText) throws IllegalArgumentException
|
||||||
|
{
|
||||||
|
setStringValue(KEY_RATING, ratingText);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param songTitle The title to set.
|
||||||
|
* @throws IllegalArgumentException If "UTF-16LE"-byte-representation would take more than 65535
|
||||||
|
* bytes.
|
||||||
|
*/
|
||||||
|
public void setTitle(final String songTitle) throws IllegalArgumentException
|
||||||
|
{
|
||||||
|
setStringValue(KEY_TITLE, songTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public long writeInto(final OutputStream out) throws IOException
|
||||||
|
{
|
||||||
|
final long chunkSize = getCurrentAsfChunkSize();
|
||||||
|
|
||||||
|
out.write(this.getGuid().getBytes());
|
||||||
|
Utils.writeUINT64(getCurrentAsfChunkSize(), out);
|
||||||
|
// write the sizes of the string representations plus 2 bytes zero term
|
||||||
|
// character
|
||||||
|
Utils.writeUINT16(getTitle().length() * 2 + 2, out);
|
||||||
|
Utils.writeUINT16(getAuthor().length() * 2 + 2, out);
|
||||||
|
Utils.writeUINT16(getCopyRight().length() * 2 + 2, out);
|
||||||
|
Utils.writeUINT16(getComment().length() * 2 + 2, out);
|
||||||
|
Utils.writeUINT16(getRating().length() * 2 + 2, out);
|
||||||
|
// write the Strings
|
||||||
|
out.write(Utils.getBytes(getTitle(), AsfHeader.ASF_CHARSET));
|
||||||
|
out.write(AsfHeader.ZERO_TERM);
|
||||||
|
out.write(Utils.getBytes(getAuthor(), AsfHeader.ASF_CHARSET));
|
||||||
|
out.write(AsfHeader.ZERO_TERM);
|
||||||
|
out.write(Utils.getBytes(getCopyRight(), AsfHeader.ASF_CHARSET));
|
||||||
|
out.write(AsfHeader.ZERO_TERM);
|
||||||
|
out.write(Utils.getBytes(getComment(), AsfHeader.ASF_CHARSET));
|
||||||
|
out.write(AsfHeader.ZERO_TERM);
|
||||||
|
out.write(Utils.getBytes(getRating(), AsfHeader.ASF_CHARSET));
|
||||||
|
out.write(AsfHeader.ZERO_TERM);
|
||||||
|
return chunkSize;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* Entagged Audio Tag library
|
||||||
|
* Copyright (c) 2004-2005 Christian Laireiter <liree@web.de>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library 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
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package com.mp3.jaudiotagger.audio.asf.data;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class was intended to store the data of a chunk which contained the
|
||||||
|
* encoding parameters in textual form. <br>
|
||||||
|
* Since the needed parameters were found in other chunks the implementation of
|
||||||
|
* this class was paused. <br>
|
||||||
|
* TODO complete analysis.
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public class EncodingChunk extends Chunk
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The read strings.
|
||||||
|
*/
|
||||||
|
private final List<String> strings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*
|
||||||
|
* @param chunkLen Length of current chunk.
|
||||||
|
*/
|
||||||
|
public EncodingChunk(final BigInteger chunkLen)
|
||||||
|
{
|
||||||
|
super(GUID.GUID_ENCODING, chunkLen);
|
||||||
|
this.strings = new ArrayList<String>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method appends a String.
|
||||||
|
*
|
||||||
|
* @param toAdd String to add.
|
||||||
|
*/
|
||||||
|
public void addString(final String toAdd)
|
||||||
|
{
|
||||||
|
this.strings.add(toAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method returns a collection of all {@linkplain String Strings} which
|
||||||
|
* were added due {@link #addString(String)}.
|
||||||
|
*
|
||||||
|
* @return Inserted Strings.
|
||||||
|
*/
|
||||||
|
public Collection<String> getStrings()
|
||||||
|
{
|
||||||
|
return new ArrayList<String>(this.strings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String prettyPrint(final String prefix)
|
||||||
|
{
|
||||||
|
final StringBuilder result = new StringBuilder(super.prettyPrint(prefix));
|
||||||
|
this.strings.iterator();
|
||||||
|
for (final String string : this.strings)
|
||||||
|
{
|
||||||
|
result.append(prefix).append(" | : ").append(string).append(Utils.LINE_SEPARATOR);
|
||||||
|
}
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,159 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.asf.data;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author eric
|
||||||
|
*/
|
||||||
|
public class EncryptionChunk extends Chunk
|
||||||
|
{
|
||||||
|
private String keyID;
|
||||||
|
|
||||||
|
private String licenseURL;
|
||||||
|
private String protectionType;
|
||||||
|
private String secretData;
|
||||||
|
/**
|
||||||
|
* The read strings.
|
||||||
|
*/
|
||||||
|
private final ArrayList<String> strings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*
|
||||||
|
* @param chunkLen Length of current chunk.
|
||||||
|
*/
|
||||||
|
public EncryptionChunk(final BigInteger chunkLen)
|
||||||
|
{
|
||||||
|
super(GUID.GUID_CONTENT_ENCRYPTION, chunkLen);
|
||||||
|
this.strings = new ArrayList<String>();
|
||||||
|
this.secretData = "";
|
||||||
|
this.protectionType = "";
|
||||||
|
this.keyID = "";
|
||||||
|
this.licenseURL = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method appends a String.
|
||||||
|
*
|
||||||
|
* @param toAdd String to add.
|
||||||
|
*/
|
||||||
|
public void addString(final String toAdd)
|
||||||
|
{
|
||||||
|
this.strings.add(toAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method gets the keyID.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public String getKeyID()
|
||||||
|
{
|
||||||
|
return this.keyID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method gets the license URL.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public String getLicenseURL()
|
||||||
|
{
|
||||||
|
return this.licenseURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method gets the secret data.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public String getProtectionType()
|
||||||
|
{
|
||||||
|
return this.protectionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method gets the secret data.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public String getSecretData()
|
||||||
|
{
|
||||||
|
return this.secretData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method returns a collection of all {@link String}s which were addid
|
||||||
|
* due {@link #addString(String)}.
|
||||||
|
*
|
||||||
|
* @return Inserted Strings.
|
||||||
|
*/
|
||||||
|
public Collection<String> getStrings()
|
||||||
|
{
|
||||||
|
return new ArrayList<String>(this.strings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String prettyPrint(final String prefix)
|
||||||
|
{
|
||||||
|
final StringBuilder result = new StringBuilder(super.prettyPrint(prefix));
|
||||||
|
result.insert(0, Utils.LINE_SEPARATOR + prefix + " Encryption:" + Utils.LINE_SEPARATOR);
|
||||||
|
result.append(prefix).append(" |->keyID ").append(this.keyID).append(Utils.LINE_SEPARATOR);
|
||||||
|
result.append(prefix).append(" |->secretData ").append(this.secretData).append(Utils.LINE_SEPARATOR);
|
||||||
|
result.append(prefix).append(" |->protectionType ").append(this.protectionType).append(Utils.LINE_SEPARATOR);
|
||||||
|
result.append(prefix).append(" |->licenseURL ").append(this.licenseURL).append(Utils.LINE_SEPARATOR);
|
||||||
|
this.strings.iterator();
|
||||||
|
for (final String string : this.strings)
|
||||||
|
{
|
||||||
|
result.append(prefix).append(" |->").append(string).append(Utils.LINE_SEPARATOR);
|
||||||
|
}
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method appends a String.
|
||||||
|
*
|
||||||
|
* @param toAdd String to add.
|
||||||
|
*/
|
||||||
|
public void setKeyID(final String toAdd)
|
||||||
|
{
|
||||||
|
this.keyID = toAdd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method appends a String.
|
||||||
|
*
|
||||||
|
* @param toAdd String to add.
|
||||||
|
*/
|
||||||
|
public void setLicenseURL(final String toAdd)
|
||||||
|
{
|
||||||
|
this.licenseURL = toAdd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method appends a String.
|
||||||
|
*
|
||||||
|
* @param toAdd String to add.
|
||||||
|
*/
|
||||||
|
public void setProtectionType(final String toAdd)
|
||||||
|
{
|
||||||
|
this.protectionType = toAdd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method adds the secret data.
|
||||||
|
*
|
||||||
|
* @param toAdd String to add.
|
||||||
|
*/
|
||||||
|
public void setSecretData(final String toAdd)
|
||||||
|
{
|
||||||
|
this.secretData = toAdd;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,238 @@
|
|||||||
|
/*
|
||||||
|
* Entagged Audio Tag library
|
||||||
|
* Copyright (c) 2004-2005 Christian Laireiter <liree@web.de>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library 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
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package com.mp3.jaudiotagger.audio.asf.data;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class stores the information about the file, which is contained within a
|
||||||
|
* special chunk of ASF files.<br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public class FileHeader extends Chunk
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duration of the media content in 100ns steps.
|
||||||
|
*/
|
||||||
|
private final BigInteger duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time the file was created.
|
||||||
|
*/
|
||||||
|
private final Date fileCreationTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Size of the file or stream.
|
||||||
|
*/
|
||||||
|
private final BigInteger fileSize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Usually contains value of 2.
|
||||||
|
*/
|
||||||
|
private final long flags;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum size of stream packages. <br>
|
||||||
|
* <b>Warning: </b> must be same size as {@link #minPackageSize}. Its not
|
||||||
|
* known how to handle deviating values.
|
||||||
|
*/
|
||||||
|
private final long maxPackageSize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimun size of stream packages. <br>
|
||||||
|
* <b>Warning: </b> must be same size as {@link #maxPackageSize}. Its not
|
||||||
|
* known how to handle deviating values.
|
||||||
|
*/
|
||||||
|
private final long minPackageSize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of stream packages within the File.
|
||||||
|
*/
|
||||||
|
private final BigInteger packageCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No Idea of the Meaning, but stored anyway. <br>
|
||||||
|
* Source documentation says it is: "Timestamp of end position"
|
||||||
|
*/
|
||||||
|
private final BigInteger timeEndPos;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like {@link #timeEndPos}no Idea.
|
||||||
|
*/
|
||||||
|
private final BigInteger timeStartPos;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Size of an uncompressed video frame.
|
||||||
|
*/
|
||||||
|
private final long uncompressedFrameSize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*
|
||||||
|
* @param chunckLen Length of the file header (chunk)
|
||||||
|
* @param size Size of file or stream
|
||||||
|
* @param fileTime Time file or stream was created. Time is calculated since 1st
|
||||||
|
* january of 1601 in 100ns steps.
|
||||||
|
* @param pkgCount Number of stream packages.
|
||||||
|
* @param dur Duration of media clip in 100ns steps
|
||||||
|
* @param timestampStart Timestamp of start {@link #timeStartPos}
|
||||||
|
* @param timestampEnd Timestamp of end {@link #timeEndPos}
|
||||||
|
* @param headerFlags some stream related flags.
|
||||||
|
* @param minPkgSize minimum size of packages
|
||||||
|
* @param maxPkgSize maximum size of packages
|
||||||
|
* @param uncmpVideoFrameSize Size of an uncompressed Video Frame.
|
||||||
|
*/
|
||||||
|
public FileHeader(final BigInteger chunckLen, final BigInteger size, final BigInteger fileTime, final BigInteger pkgCount, final BigInteger dur, final BigInteger timestampStart, final BigInteger timestampEnd, final long headerFlags, final long minPkgSize, final long maxPkgSize, final long uncmpVideoFrameSize)
|
||||||
|
{
|
||||||
|
super(GUID.GUID_FILE, chunckLen);
|
||||||
|
this.fileSize = size;
|
||||||
|
this.packageCount = pkgCount;
|
||||||
|
this.duration = dur;
|
||||||
|
this.timeStartPos = timestampStart;
|
||||||
|
this.timeEndPos = timestampEnd;
|
||||||
|
this.flags = headerFlags;
|
||||||
|
this.minPackageSize = minPkgSize;
|
||||||
|
this.maxPackageSize = maxPkgSize;
|
||||||
|
this.uncompressedFrameSize = uncmpVideoFrameSize;
|
||||||
|
this.fileCreationTime = Utils.getDateOf(fileTime).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the duration.
|
||||||
|
*/
|
||||||
|
public BigInteger getDuration()
|
||||||
|
{
|
||||||
|
return this.duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method converts {@link #getDuration()}from 100ns steps to normal
|
||||||
|
* seconds.
|
||||||
|
*
|
||||||
|
* @return Duration of the media in seconds.
|
||||||
|
*/
|
||||||
|
public int getDurationInSeconds()
|
||||||
|
{
|
||||||
|
return this.duration.divide(new BigInteger("10000000")).intValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the fileCreationTime.
|
||||||
|
*/
|
||||||
|
public Date getFileCreationTime()
|
||||||
|
{
|
||||||
|
return new Date(this.fileCreationTime.getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the fileSize.
|
||||||
|
*/
|
||||||
|
public BigInteger getFileSize()
|
||||||
|
{
|
||||||
|
return this.fileSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the flags.
|
||||||
|
*/
|
||||||
|
public long getFlags()
|
||||||
|
{
|
||||||
|
return this.flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the maxPackageSize.
|
||||||
|
*/
|
||||||
|
public long getMaxPackageSize()
|
||||||
|
{
|
||||||
|
return this.maxPackageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the minPackageSize.
|
||||||
|
*/
|
||||||
|
public long getMinPackageSize()
|
||||||
|
{
|
||||||
|
return this.minPackageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the packageCount.
|
||||||
|
*/
|
||||||
|
public BigInteger getPackageCount()
|
||||||
|
{
|
||||||
|
return this.packageCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method converts {@link #getDuration()} from 100ns steps to normal
|
||||||
|
* seconds with a fractional part taking milliseconds.<br>
|
||||||
|
*
|
||||||
|
* @return The duration of the media in seconds (with a precision of
|
||||||
|
* milliseconds)
|
||||||
|
*/
|
||||||
|
public float getPreciseDuration()
|
||||||
|
{
|
||||||
|
return (float) (getDuration().doubleValue() / 10000000d);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the timeEndPos.
|
||||||
|
*/
|
||||||
|
public BigInteger getTimeEndPos()
|
||||||
|
{
|
||||||
|
return this.timeEndPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the timeStartPos.
|
||||||
|
*/
|
||||||
|
public BigInteger getTimeStartPos()
|
||||||
|
{
|
||||||
|
return this.timeStartPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the uncompressedFrameSize.
|
||||||
|
*/
|
||||||
|
public long getUncompressedFrameSize()
|
||||||
|
{
|
||||||
|
return this.uncompressedFrameSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (overridden)
|
||||||
|
*
|
||||||
|
* @see Chunk#prettyPrint(String)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String prettyPrint(final String prefix)
|
||||||
|
{
|
||||||
|
final StringBuilder result = new StringBuilder(super.prettyPrint(prefix));
|
||||||
|
result.append(prefix).append(" |-> Filesize = ").append(getFileSize().toString()).append(" Bytes").append(Utils.LINE_SEPARATOR);
|
||||||
|
result.append(prefix).append(" |-> Media duration= ").append(getDuration().divide(new BigInteger("10000")).toString()).append(" ms").append(Utils.LINE_SEPARATOR);
|
||||||
|
result.append(prefix).append(" |-> Created at = ").append(getFileCreationTime()).append(Utils.LINE_SEPARATOR);
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,520 @@
|
|||||||
|
/*
|
||||||
|
* Entagged Audio Tag library
|
||||||
|
* Copyright (c) 2004-2005 Christian Laireiter <liree@web.de>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library 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
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package com.mp3.jaudiotagger.audio.asf.data;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is used for representation of GUIDs and as a reference list of all
|
||||||
|
* Known GUIDs. <br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public final class GUID
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This constant defines the GUID for stream chunks describing audio
|
||||||
|
* streams, indicating the the audio stream has no error concealment. <br>
|
||||||
|
*/
|
||||||
|
public final static GUID GUID_AUDIO_ERROR_CONCEALEMENT_ABSENT = new GUID(new int[]{0x40, 0xA4, 0xF1, 0x49, 0xCE, 0x4E, 0xD0, 0x11, 0xA3, 0xAC, 0x00, 0xA0, 0xC9, 0x03, 0x48, 0xF6}, "Audio error concealment absent.");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This constant defines the GUID for stream chunks describing audio
|
||||||
|
* streams, indicating the the audio stream has interleaved error
|
||||||
|
* concealment. <br>
|
||||||
|
*/
|
||||||
|
public final static GUID GUID_AUDIO_ERROR_CONCEALEMENT_INTERLEAVED = new GUID(new int[]{0x40, 0xA4, 0xF1, 0x49, 0xCE, 0x4E, 0xD0, 0x11, 0xA3, 0xAC, 0x00, 0xA0, 0xC9, 0x03, 0x48, 0xF6}, "Interleaved audio error concealment.");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This constant stores the GUID indicating that stream type is audio.
|
||||||
|
*/
|
||||||
|
public final static GUID GUID_AUDIOSTREAM = new GUID(new int[]{0x40, 0x9E, 0x69, 0xF8, 0x4D, 0x5B, 0xCF, 0x11, 0xA8, 0xFD, 0x00, 0x80, 0x5F, 0x5C, 0x44, 0x2B}, " Audio stream");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This constant stores the GUID indicating a content branding object.
|
||||||
|
*/
|
||||||
|
public final static GUID GUID_CONTENT_BRANDING = new GUID(new int[]{0xFA, 0xB3, 0x11, 0x22, 0x23, 0xBD, 0xD2, 0x11, 0xB4, 0xB7, 0x00, 0xA0, 0xC9, 0x55, 0xFC, 0x6E}, "Content Branding");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is for the Content Encryption Object
|
||||||
|
* 2211B3FB-BD23-11D2-B4B7-00A0C955FC6E, needs to be little-endian.
|
||||||
|
*/
|
||||||
|
public final static GUID GUID_CONTENT_ENCRYPTION = new GUID(new int[]{0xfb, 0xb3, 0x11, 0x22, 0x23, 0xbd, 0xd2, 0x11, 0xb4, 0xb7, 0x00, 0xa0, 0xc9, 0x55, 0xfc, 0x6e}, "Content Encryption Object");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This constant represents the guidData for a chunk which contains Title,
|
||||||
|
* author, copyright, description and rating.
|
||||||
|
*/
|
||||||
|
public final static GUID GUID_CONTENTDESCRIPTION = new GUID(new int[]{0x33, 0x26, 0xB2, 0x75, 0x8E, 0x66, 0xCF, 0x11, 0xA6, 0xD9, 0x00, 0xAA, 0x00, 0x62, 0xCE, 0x6C}, "Content Description");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This constant stores the GUID for Encoding-Info chunks.
|
||||||
|
*/
|
||||||
|
public final static GUID GUID_ENCODING = new GUID(new int[]{0x40, 0x52, 0xD1, 0x86, 0x1D, 0x31, 0xD0, 0x11, 0xA3, 0xA4, 0x00, 0xA0, 0xC9, 0x03, 0x48, 0xF6}, "Encoding description");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This constant defines the GUID for a WMA "Extended Content Description"
|
||||||
|
* chunk. <br>
|
||||||
|
*/
|
||||||
|
public final static GUID GUID_EXTENDED_CONTENT_DESCRIPTION = new GUID(new int[]{0x40, 0xA4, 0xD0, 0xD2, 0x07, 0xE3, 0xD2, 0x11, 0x97, 0xF0, 0x00, 0xA0, 0xC9, 0x5E, 0xA8, 0x50}, "Extended Content Description");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GUID of ASF file header.
|
||||||
|
*/
|
||||||
|
public final static GUID GUID_FILE = new GUID(new int[]{0xA1, 0xDC, 0xAB, 0x8C, 0x47, 0xA9, 0xCF, 0x11, 0x8E, 0xE4, 0x00, 0xC0, 0x0C, 0x20, 0x53, 0x65}, "File header");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This constant defines the GUID of a asf header chunk.
|
||||||
|
*/
|
||||||
|
public final static GUID GUID_HEADER = new GUID(new int[]{0x30, 0x26, 0xb2, 0x75, 0x8e, 0x66, 0xcf, 0x11, 0xa6, 0xd9, 0x00, 0xaa, 0x00, 0x62, 0xce, 0x6c}, "Asf header");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This constant stores a GUID whose functionality is unknown.
|
||||||
|
*/
|
||||||
|
public final static GUID GUID_HEADER_EXTENSION = new GUID(new int[]{0xB5, 0x03, 0xBF, 0x5F, 0x2E, 0xA9, 0xCF, 0x11, 0x8E, 0xE3, 0x00, 0xC0, 0x0C, 0x20, 0x53, 0x65}, "Header Extension");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This constant stores the GUID indicating the asf language list object.<br>
|
||||||
|
*/
|
||||||
|
public final static GUID GUID_LANGUAGE_LIST = new GUID(new int[]{0xa9, 0x46, 0x43, 0x7c, 0xe0, 0xef, 0xfc, 0x4b, 0xb2, 0x29, 0x39, 0x3e, 0xde, 0x41, 0x5c, 0x85}, "Language List");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This constant stores the length of GUIDs used with ASF streams. <br>
|
||||||
|
*/
|
||||||
|
public final static int GUID_LENGTH = 16;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This constant stores the GUID indicating the asf metadata object.<br>
|
||||||
|
*/
|
||||||
|
public final static GUID GUID_METADATA = new GUID(new int[]{0xea, 0xcb, 0xf8, 0xc5, 0xaf, 0x5b, 0x77, 0x48, 0x84, 0x67, 0xaa, 0x8c, 0x44, 0xfa, 0x4c, 0xca}, "Metadata");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This constant stores the GUID indicating the asf metadata library object.<br>
|
||||||
|
*/
|
||||||
|
public final static GUID GUID_METADATA_LIBRARY = new GUID(new int[]{0x94, 0x1c, 0x23, 0x44, 0x98, 0x94, 0xd1, 0x49, 0xa1, 0x41, 0x1d, 0x13, 0x4e, 0x45, 0x70, 0x54}, "Metadata Library");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GUID String values format.<br>
|
||||||
|
*/
|
||||||
|
private final static Pattern GUID_PATTERN = Pattern.compile("[a-f0-9]{8}\\-[a-f0-9]{4}\\-[a-f0-9]{4}\\-[a-f0-9]{4}\\-[a-f0-9]{12}", Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This constant stores the GUID indicating a stream object.
|
||||||
|
*/
|
||||||
|
public final static GUID GUID_STREAM = new GUID(new int[]{0x91, 0x07, 0xDC, 0xB7, 0xB7, 0xA9, 0xCF, 0x11, 0x8E, 0xE6, 0x00, 0xC0, 0x0C, 0x20, 0x53, 0x65}, "Stream");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This constant stores a GUID indicating a "stream bitrate properties"
|
||||||
|
* chunk.
|
||||||
|
*/
|
||||||
|
public final static GUID GUID_STREAM_BITRATE_PROPERTIES = new GUID(new int[]{0xCE, 0x75, 0xF8, 0x7B, 0x8D, 0x46, 0xD1, 0x11, 0x8D, 0x82, 0x00, 0x60, 0x97, 0xC9, 0xA2, 0xB2}, "Stream bitrate properties");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This map is used, to get the description of a GUID instance, which has
|
||||||
|
* been created by reading.<br>
|
||||||
|
* The map comparison is done against the {@link GUID#guidData} field. But
|
||||||
|
* only the {@link #KNOWN_GUIDS} have a description set.
|
||||||
|
*/
|
||||||
|
private final static Map<GUID, GUID> GUID_TO_CONFIGURED;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This constant represents a GUID implementation which can be used for
|
||||||
|
* generic implementations, which have to provide a GUID, but do not really
|
||||||
|
* require a specific GUID to work.
|
||||||
|
*/
|
||||||
|
public final static GUID GUID_UNSPECIFIED = new GUID(new int[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, "Unspecified");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This constant stores the GUID indicating that stream type is video.
|
||||||
|
*/
|
||||||
|
public final static GUID GUID_VIDEOSTREAM = new GUID(new int[]{0xC0, 0xEF, 0x19, 0xBC, 0x4D, 0x5B, 0xCF, 0x11, 0xA8, 0xFD, 0x00, 0x80, 0x5F, 0x5C, 0x44, 0x2B}, "Video stream");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This field stores all known GUIDs.
|
||||||
|
*/
|
||||||
|
public final static GUID[] KNOWN_GUIDS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This constant stores the GUID for a "script command object".<br>
|
||||||
|
*/
|
||||||
|
public final static GUID SCRIPT_COMMAND_OBJECT = new GUID(new int[]{0x30, 0x1a, 0xfb, 0x1e, 0x62, 0x0b, 0xd0, 0x11, 0xa3, 0x9b, 0x00, 0xa0, 0xc9, 0x03, 0x48, 0xf6}, "Script Command Object");
|
||||||
|
|
||||||
|
static
|
||||||
|
{
|
||||||
|
KNOWN_GUIDS = new GUID[]{GUID_AUDIO_ERROR_CONCEALEMENT_ABSENT, GUID_CONTENTDESCRIPTION, GUID_AUDIOSTREAM, GUID_ENCODING, GUID_FILE, GUID_HEADER, GUID_STREAM, GUID_EXTENDED_CONTENT_DESCRIPTION, GUID_VIDEOSTREAM, GUID_HEADER_EXTENSION, GUID_STREAM_BITRATE_PROPERTIES, SCRIPT_COMMAND_OBJECT, GUID_CONTENT_ENCRYPTION, GUID_CONTENT_BRANDING, GUID_UNSPECIFIED, GUID_METADATA_LIBRARY, GUID_METADATA, GUID_LANGUAGE_LIST};
|
||||||
|
GUID_TO_CONFIGURED = new HashMap<GUID, GUID>(KNOWN_GUIDS.length);
|
||||||
|
for (final GUID curr : KNOWN_GUIDS)
|
||||||
|
{
|
||||||
|
assert !GUID_TO_CONFIGURED.containsKey(curr) : "Double definition: \"" + GUID_TO_CONFIGURED.get(curr).getDescription() + "\" <-> \"" + curr.getDescription() + "\"";
|
||||||
|
GUID_TO_CONFIGURED.put(curr, curr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method checks if the given <code>value</code> is matching the GUID
|
||||||
|
* specification of ASF streams. <br>
|
||||||
|
*
|
||||||
|
* @param value possible GUID.
|
||||||
|
* @return <code>true</code> if <code>value</code> matches the specification
|
||||||
|
* of a GUID.
|
||||||
|
*/
|
||||||
|
public static boolean assertGUID(final int[] value)
|
||||||
|
{
|
||||||
|
return value != null && value.length == GUID.GUID_LENGTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method looks up a GUID instance from {@link #KNOWN_GUIDS} which
|
||||||
|
* matches the value of the given GUID.
|
||||||
|
*
|
||||||
|
* @param orig GUID to look up.
|
||||||
|
* @return a GUID instance from {@link #KNOWN_GUIDS} if available.
|
||||||
|
* <code>null</code> else.
|
||||||
|
*/
|
||||||
|
public static GUID getConfigured(final GUID orig)
|
||||||
|
{
|
||||||
|
// safe against null
|
||||||
|
return GUID_TO_CONFIGURED.get(orig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method searches a GUID in {@link #KNOWN_GUIDS}which is equal to the
|
||||||
|
* given <code>guidData</code> and returns its description. <br>
|
||||||
|
* This method is useful if a GUID was read out of a file and no
|
||||||
|
* identification has been done yet.
|
||||||
|
*
|
||||||
|
* @param guid GUID, which description is needed.
|
||||||
|
* @return description of the GUID if found. Else <code>null</code>
|
||||||
|
*/
|
||||||
|
public static String getGuidDescription(final GUID guid)
|
||||||
|
{
|
||||||
|
String result = null;
|
||||||
|
if (guid == null)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("Argument must not be null.");
|
||||||
|
}
|
||||||
|
if (getConfigured(guid) != null)
|
||||||
|
{
|
||||||
|
result = getConfigured(guid).getDescription();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method parses a String as GUID.<br>
|
||||||
|
* The format is like the one in the ASF specification.<br>
|
||||||
|
* An Example: <code>C5F8CBEA-5BAF-4877-8467-AA8C44FA4CCA</code><br>
|
||||||
|
*
|
||||||
|
* @param guid the string to parse.
|
||||||
|
* @return the GUID.
|
||||||
|
* @throws GUIDFormatException If the GUID has an invalid format.
|
||||||
|
*/
|
||||||
|
public static GUID parseGUID(final String guid) throws GUIDFormatException
|
||||||
|
{
|
||||||
|
if (guid == null)
|
||||||
|
{
|
||||||
|
throw new GUIDFormatException("null");
|
||||||
|
}
|
||||||
|
if (!GUID_PATTERN.matcher(guid).matches())
|
||||||
|
{
|
||||||
|
throw new GUIDFormatException("Invalid guidData format.");
|
||||||
|
}
|
||||||
|
final int[] bytes = new int[GUID_LENGTH];
|
||||||
|
/*
|
||||||
|
* Don't laugh, but did not really come up with a nicer solution today
|
||||||
|
*/
|
||||||
|
final int[] arrayIndices = {3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15};
|
||||||
|
int arrayPointer = 0;
|
||||||
|
for (int i = 0; i < guid.length(); i++)
|
||||||
|
{
|
||||||
|
if (guid.charAt(i) == '-')
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
bytes[arrayIndices[arrayPointer++]] = Integer.parseInt(guid.substring(i, i + 2), 16);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return new GUID(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores an optionally description of the GUID.
|
||||||
|
*/
|
||||||
|
private String description = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An instance of this class stores the value of the wrapped GUID in this
|
||||||
|
* field. <br>
|
||||||
|
*/
|
||||||
|
private int[] guidData = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the hash code of the object.<br>
|
||||||
|
* <code>"-1"</code> if not determined yet.
|
||||||
|
*/
|
||||||
|
private int hash;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance and assigns given <code>value</code>.<br>
|
||||||
|
*
|
||||||
|
* @param value GUID, which should be assigned. (will be converted to int[])
|
||||||
|
*/
|
||||||
|
public GUID(final byte[] value)
|
||||||
|
{
|
||||||
|
assert value != null;
|
||||||
|
final int[] tmp = new int[value.length];
|
||||||
|
for (int i = 0; i < value.length; i++)
|
||||||
|
{
|
||||||
|
tmp[i] = (0xFF & value[i]);
|
||||||
|
}
|
||||||
|
setGUID(tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance and assigns given <code>value</code>.<br>
|
||||||
|
*
|
||||||
|
* @param value GUID, which should be assigned.
|
||||||
|
*/
|
||||||
|
public GUID(final int[] value)
|
||||||
|
{
|
||||||
|
setGUID(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance like {@link #GUID(int[])}and sets the optional
|
||||||
|
* description. <br>
|
||||||
|
*
|
||||||
|
* @param value GUID, which should be assigned.
|
||||||
|
* @param desc Description for the GUID.
|
||||||
|
*/
|
||||||
|
public GUID(final int[] value, final String desc)
|
||||||
|
{
|
||||||
|
this(value);
|
||||||
|
if (desc == null)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("Argument must not be null.");
|
||||||
|
}
|
||||||
|
this.description = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance like {@link #GUID(int[])} and sets the optional
|
||||||
|
* description. (the int[] is obtained by {@link GUID#parseGUID(String)}) <br>
|
||||||
|
*
|
||||||
|
* @param guidString GUID, which should be assigned.
|
||||||
|
* @param desc Description for the GUID.
|
||||||
|
*/
|
||||||
|
public GUID(final String guidString, final String desc)
|
||||||
|
{
|
||||||
|
this(parseGUID(guidString).getGUID());
|
||||||
|
if (desc == null)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("Argument must not be null.");
|
||||||
|
}
|
||||||
|
this.description = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method compares two objects. If the given Object is a {@link GUID},
|
||||||
|
* the stored GUID values are compared. <br>
|
||||||
|
*
|
||||||
|
* @see Object#equals(Object)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean equals(final Object obj)
|
||||||
|
{
|
||||||
|
boolean result = false;
|
||||||
|
if (obj instanceof GUID)
|
||||||
|
{
|
||||||
|
final GUID other = (GUID) obj;
|
||||||
|
result = Arrays.equals(this.getGUID(), other.getGUID());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method returns the GUID as an array of bytes. <br>
|
||||||
|
*
|
||||||
|
* @return The GUID as a byte array.
|
||||||
|
* @see #getGUID()
|
||||||
|
*/
|
||||||
|
public byte[] getBytes()
|
||||||
|
{
|
||||||
|
final byte[] result = new byte[this.guidData.length];
|
||||||
|
for (int i = 0; i < result.length; i++)
|
||||||
|
{
|
||||||
|
result[i] = (byte) (this.guidData[i] & 0xFF);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the description.
|
||||||
|
*/
|
||||||
|
public String getDescription()
|
||||||
|
{
|
||||||
|
return this.description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method returns the GUID of this object. <br>
|
||||||
|
*
|
||||||
|
* @return stored GUID.
|
||||||
|
*/
|
||||||
|
public int[] getGUID()
|
||||||
|
{
|
||||||
|
final int[] copy = new int[this.guidData.length];
|
||||||
|
System.arraycopy(this.guidData, 0, copy, 0, this.guidData.length);
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method to get 2digit hex values of each byte.
|
||||||
|
*
|
||||||
|
* @param bytes bytes to convert.
|
||||||
|
* @return each byte as 2 digit hex.
|
||||||
|
*/
|
||||||
|
private String[] getHex(final byte[] bytes)
|
||||||
|
{
|
||||||
|
final String[] result = new String[bytes.length];
|
||||||
|
final StringBuilder tmp = new StringBuilder();
|
||||||
|
for (int i = 0; i < bytes.length; i++)
|
||||||
|
{
|
||||||
|
tmp.delete(0, tmp.length());
|
||||||
|
tmp.append(Integer.toHexString(0xFF & bytes[i]));
|
||||||
|
if (tmp.length() == 1)
|
||||||
|
{
|
||||||
|
tmp.insert(0, "0");
|
||||||
|
}
|
||||||
|
result[i] = tmp.toString();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int hashCode()
|
||||||
|
{
|
||||||
|
if (this.hash == -1)
|
||||||
|
{
|
||||||
|
int tmp = 0;
|
||||||
|
for (final int curr : getGUID())
|
||||||
|
{
|
||||||
|
tmp = tmp * 31 + curr;
|
||||||
|
}
|
||||||
|
this.hash = tmp;
|
||||||
|
}
|
||||||
|
return this.hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method checks if the currently stored GUID ({@link #guidData}) is
|
||||||
|
* correctly filled. <br>
|
||||||
|
*
|
||||||
|
* @return <code>true</code> if it is.
|
||||||
|
*/
|
||||||
|
public boolean isValid()
|
||||||
|
{
|
||||||
|
return assertGUID(getGUID());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method gives a hex formatted representation of {@link #getGUID()}
|
||||||
|
*
|
||||||
|
* @return hex formatted representation.
|
||||||
|
*/
|
||||||
|
public String prettyPrint()
|
||||||
|
{
|
||||||
|
final StringBuilder result = new StringBuilder();
|
||||||
|
String descr = getDescription();
|
||||||
|
if (Utils.isBlank(descr))
|
||||||
|
{
|
||||||
|
descr = getGuidDescription(this);
|
||||||
|
}
|
||||||
|
if (!Utils.isBlank(descr))
|
||||||
|
{
|
||||||
|
result.append("Description: ").append(descr).append(Utils.LINE_SEPARATOR).append(" ");
|
||||||
|
}
|
||||||
|
result.append(this.toString());
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method saves a copy of the given <code>value</code> as the
|
||||||
|
* represented value of this object. <br>
|
||||||
|
* The given value is checked with {@link #assertGUID(int[])}.<br>
|
||||||
|
*
|
||||||
|
* @param value GUID to assign.
|
||||||
|
*/
|
||||||
|
private void setGUID(final int[] value)
|
||||||
|
{
|
||||||
|
if (assertGUID(value))
|
||||||
|
{
|
||||||
|
this.guidData = new int[GUID_LENGTH];
|
||||||
|
System.arraycopy(value, 0, this.guidData, 0, GUID_LENGTH);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("The given guidData doesn't match the GUID specification.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
// C5F8CBEA-5BAF-4877-8467-AA8C44FA4CCA
|
||||||
|
// 0xea, 0xcb,0xf8, 0xc5, 0xaf, 0x5b, 0x77, 0x48, 0x84, 0x67, 0xaa,
|
||||||
|
// 0x8c, 0x44,0xfa, 0x4c, 0xca
|
||||||
|
final StringBuilder result = new StringBuilder();
|
||||||
|
final String[] bytes = getHex(getBytes());
|
||||||
|
result.append(bytes[3]);
|
||||||
|
result.append(bytes[2]);
|
||||||
|
result.append(bytes[1]);
|
||||||
|
result.append(bytes[0]);
|
||||||
|
result.append('-');
|
||||||
|
result.append(bytes[5]);
|
||||||
|
result.append(bytes[4]);
|
||||||
|
result.append('-');
|
||||||
|
result.append(bytes[7]);
|
||||||
|
result.append(bytes[6]);
|
||||||
|
result.append('-');
|
||||||
|
result.append(bytes[8]);
|
||||||
|
result.append(bytes[9]);
|
||||||
|
result.append('-');
|
||||||
|
result.append(bytes[10]);
|
||||||
|
result.append(bytes[11]);
|
||||||
|
result.append(bytes[12]);
|
||||||
|
result.append(bytes[13]);
|
||||||
|
result.append(bytes[14]);
|
||||||
|
result.append(bytes[15]);
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.asf.data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This exception is used when a string was about to be interpreted as a GUID,
|
||||||
|
* but did not match the format.<br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public class GUIDFormatException extends IllegalArgumentException
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private static final long serialVersionUID = 6035645678612384953L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*
|
||||||
|
* @param detail detail message.
|
||||||
|
*/
|
||||||
|
public GUIDFormatException(final String detail)
|
||||||
|
{
|
||||||
|
super(detail);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,125 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.asf.data;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
import com.mp3.jaudiotagger.logging.ErrorMessage;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This structure represents the data of the ASF language object.<br>
|
||||||
|
* The language list is simply a listing of language codes which should comply
|
||||||
|
* to RFC-1766.<br>
|
||||||
|
* <b>Consider:</b> the index of a language is used by other entries in the ASF
|
||||||
|
* metadata.
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public class LanguageList extends Chunk
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of language codes, complying RFC-1766
|
||||||
|
*/
|
||||||
|
private final List<String> languages = new ArrayList<String>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance.<br>
|
||||||
|
*/
|
||||||
|
public LanguageList()
|
||||||
|
{
|
||||||
|
super(GUID.GUID_LANGUAGE_LIST, 0, BigInteger.ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*
|
||||||
|
* @param pos position within the ASF file.
|
||||||
|
* @param size size of the chunk
|
||||||
|
*/
|
||||||
|
public LanguageList(final long pos, final BigInteger size)
|
||||||
|
{
|
||||||
|
super(GUID.GUID_LANGUAGE_LIST, pos, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method adds a language.<br>
|
||||||
|
*
|
||||||
|
* @param language language code
|
||||||
|
*/
|
||||||
|
public void addLanguage(final String language)
|
||||||
|
{
|
||||||
|
if (language.length() < MetadataDescriptor.MAX_LANG_INDEX)
|
||||||
|
{
|
||||||
|
if (!this.languages.contains(language))
|
||||||
|
{
|
||||||
|
this.languages.add(language);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException(ErrorMessage.WMA_LENGTH_OF_LANGUAGE_IS_TOO_LARGE.getMsg(language.length() * 2 + 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the language code at the specified index.
|
||||||
|
*
|
||||||
|
* @param index the index of the language code to get.
|
||||||
|
* @return the language code at given index.
|
||||||
|
*/
|
||||||
|
public String getLanguage(final int index)
|
||||||
|
{
|
||||||
|
return this.languages.get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the amount of stored language codes.
|
||||||
|
*
|
||||||
|
* @return number of stored language codes.
|
||||||
|
*/
|
||||||
|
public int getLanguageCount()
|
||||||
|
{
|
||||||
|
return this.languages.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all language codes in list.
|
||||||
|
*
|
||||||
|
* @return list of language codes.
|
||||||
|
*/
|
||||||
|
public List<String> getLanguages()
|
||||||
|
{
|
||||||
|
return new ArrayList<String>(this.languages);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String prettyPrint(final String prefix)
|
||||||
|
{
|
||||||
|
final StringBuilder result = new StringBuilder(super.prettyPrint(prefix));
|
||||||
|
for (int i = 0; i < getLanguageCount(); i++)
|
||||||
|
{
|
||||||
|
result.append(prefix);
|
||||||
|
result.append(" |-> ");
|
||||||
|
result.append(i);
|
||||||
|
result.append(" : ");
|
||||||
|
result.append(getLanguage(i));
|
||||||
|
result.append(Utils.LINE_SEPARATOR);
|
||||||
|
}
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the language entry at specified index.
|
||||||
|
*
|
||||||
|
* @param index index of language to remove.
|
||||||
|
*/
|
||||||
|
public void removeLanguage(final int index)
|
||||||
|
{
|
||||||
|
this.languages.remove(index);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,477 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.asf.data;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.io.WriteableChunk;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This structure represents the "Metadata Object","Metadata
|
||||||
|
* Library Object" and "Extended Content Description".<br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public class MetadataContainer extends Chunk implements WriteableChunk
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is used to uniquely identify an enclosed descriptor by its
|
||||||
|
* name, language index and stream number.<br>
|
||||||
|
* The type of the descriptor is ignored, since it just specifies the data
|
||||||
|
* content.
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
private final static class DescriptorPointer
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The represented descriptor.
|
||||||
|
*/
|
||||||
|
private MetadataDescriptor desc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*
|
||||||
|
* @param descriptor the metadata descriptor to identify.
|
||||||
|
*/
|
||||||
|
public DescriptorPointer(final MetadataDescriptor descriptor)
|
||||||
|
{
|
||||||
|
setDescriptor(descriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean equals(final Object obj)
|
||||||
|
{
|
||||||
|
boolean result = obj == this;
|
||||||
|
if (obj instanceof DescriptorPointer && !result)
|
||||||
|
{
|
||||||
|
final MetadataDescriptor other = ((DescriptorPointer) obj).desc;
|
||||||
|
result = this.desc.getName().equals(other.getName());
|
||||||
|
result &= this.desc.getLanguageIndex() == other.getLanguageIndex();
|
||||||
|
result &= this.desc.getStreamNumber() == other.getStreamNumber();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int hashCode()
|
||||||
|
{
|
||||||
|
int hashCode;
|
||||||
|
hashCode = this.desc.getName().hashCode();
|
||||||
|
hashCode = hashCode * 31 + this.desc.getLanguageIndex();
|
||||||
|
hashCode = hashCode * 31 + this.desc.getStreamNumber();
|
||||||
|
return hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the descriptor to identify.
|
||||||
|
*
|
||||||
|
* @param descriptor the descriptor to identify.
|
||||||
|
* @return this instance.
|
||||||
|
*/
|
||||||
|
protected DescriptorPointer setDescriptor(final MetadataDescriptor descriptor)
|
||||||
|
{
|
||||||
|
assert descriptor != null;
|
||||||
|
this.desc = descriptor;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up all {@linkplain ContainerType#getContainerGUID() guids} and
|
||||||
|
* returns the matching type.
|
||||||
|
*
|
||||||
|
* @param guid GUID to look up
|
||||||
|
* @return matching container type.
|
||||||
|
* @throws IllegalArgumentException if no container type matches
|
||||||
|
*/
|
||||||
|
private static ContainerType determineType(final GUID guid) throws IllegalArgumentException
|
||||||
|
{
|
||||||
|
assert guid != null;
|
||||||
|
ContainerType result = null;
|
||||||
|
for (final ContainerType curr : ContainerType.values())
|
||||||
|
{
|
||||||
|
if (curr.getContainerGUID().equals(guid))
|
||||||
|
{
|
||||||
|
result = curr;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("Unknown metadata container specified by GUID (" + guid.toString() + ")");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* stores the represented container type.<br>
|
||||||
|
*/
|
||||||
|
private final ContainerType containerType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the descriptors.
|
||||||
|
*/
|
||||||
|
private final Map<DescriptorPointer, List<MetadataDescriptor>> descriptors = new Hashtable<DescriptorPointer, List<MetadataDescriptor>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* for performance reasons this instance is used to look up existing
|
||||||
|
* descriptors in {@link #descriptors}.<br>
|
||||||
|
*/
|
||||||
|
private final DescriptorPointer perfPoint = new DescriptorPointer(new MetadataDescriptor(""));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*
|
||||||
|
* @param type determines the type of the container
|
||||||
|
*/
|
||||||
|
public MetadataContainer(final ContainerType type)
|
||||||
|
{
|
||||||
|
this(type, 0, BigInteger.ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*
|
||||||
|
* @param type determines the type of the container
|
||||||
|
* @param pos location in the ASF file
|
||||||
|
* @param size size of the chunk.
|
||||||
|
*/
|
||||||
|
public MetadataContainer(final ContainerType type, final long pos, final BigInteger size)
|
||||||
|
{
|
||||||
|
super(type.getContainerGUID(), pos, size);
|
||||||
|
this.containerType = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*
|
||||||
|
* @param containerGUID the containers GUID
|
||||||
|
* @param pos location in the ASF file
|
||||||
|
* @param size size of the chunk.
|
||||||
|
*/
|
||||||
|
public MetadataContainer(final GUID containerGUID, final long pos, final BigInteger size)
|
||||||
|
{
|
||||||
|
this(determineType(containerGUID), pos, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a metadata descriptor.
|
||||||
|
*
|
||||||
|
* @param toAdd the descriptor to add.
|
||||||
|
* @throws IllegalArgumentException if descriptor does not meet container requirements, or
|
||||||
|
* already exist.
|
||||||
|
*/
|
||||||
|
public final void addDescriptor(final MetadataDescriptor toAdd) throws IllegalArgumentException
|
||||||
|
{
|
||||||
|
// check with throwing exceptions
|
||||||
|
this.containerType.assertConstraints(toAdd.getName(), toAdd.getRawData(), toAdd.getType(), toAdd.getStreamNumber(), toAdd.getLanguageIndex());
|
||||||
|
// validate containers capabilities
|
||||||
|
if (!isAddSupported(toAdd))
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("Descriptor cannot be added, see isAddSupported(...)");
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* Check for containers types capabilities.
|
||||||
|
*/
|
||||||
|
// Search for descriptor list by name, language and stream.
|
||||||
|
List<MetadataDescriptor> list;
|
||||||
|
synchronized (this.perfPoint)
|
||||||
|
{
|
||||||
|
list = this.descriptors.get(this.perfPoint.setDescriptor(toAdd));
|
||||||
|
}
|
||||||
|
if (list == null)
|
||||||
|
{
|
||||||
|
list = new ArrayList<MetadataDescriptor>();
|
||||||
|
this.descriptors.put(new DescriptorPointer(toAdd), list);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!list.isEmpty() && !this.containerType.isMultiValued())
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("Container does not allow multiple values of descriptors with same name, language index and stream number");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list.add(toAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method asserts that this container has a descriptor with the
|
||||||
|
* specified key, means returns an existing or creates a new descriptor.
|
||||||
|
*
|
||||||
|
* @param key the descriptor name to look up (or create)
|
||||||
|
* @return the/a descriptor with the specified name (and initial type of
|
||||||
|
* {@link MetadataDescriptor#TYPE_STRING}.
|
||||||
|
*/
|
||||||
|
protected final MetadataDescriptor assertDescriptor(final String key)
|
||||||
|
{
|
||||||
|
return assertDescriptor(key, MetadataDescriptor.TYPE_STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method asserts that this container has a descriptor with the
|
||||||
|
* specified key, means returns an existing or creates a new descriptor.
|
||||||
|
*
|
||||||
|
* @param key the descriptor name to look up (or create)
|
||||||
|
* @param type if the descriptor is created, this data type is applied.
|
||||||
|
* @return the/a descriptor with the specified name.
|
||||||
|
*/
|
||||||
|
protected final MetadataDescriptor assertDescriptor(final String key, final int type)
|
||||||
|
{
|
||||||
|
MetadataDescriptor desc;
|
||||||
|
final List<MetadataDescriptor> descriptorsByName = getDescriptorsByName(key);
|
||||||
|
if (descriptorsByName == null || descriptorsByName.isEmpty())
|
||||||
|
{
|
||||||
|
desc = new MetadataDescriptor(getContainerType(), key, type);
|
||||||
|
addDescriptor(desc);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
desc = descriptorsByName.get(0);
|
||||||
|
}
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether a descriptor already exists.<br>
|
||||||
|
* Name, stream number and language index are compared. Data and data type
|
||||||
|
* are ignored.
|
||||||
|
*
|
||||||
|
* @param lookup descriptor to look up.
|
||||||
|
* @return <code>true</code> if such a descriptor already exists.
|
||||||
|
*/
|
||||||
|
public final boolean containsDescriptor(final MetadataDescriptor lookup)
|
||||||
|
{
|
||||||
|
assert lookup != null;
|
||||||
|
return this.descriptors.containsKey(this.perfPoint.setDescriptor(lookup));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the type of container this instance represents.<br>
|
||||||
|
*
|
||||||
|
* @return represented container type.
|
||||||
|
*/
|
||||||
|
public final ContainerType getContainerType()
|
||||||
|
{
|
||||||
|
return this.containerType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public long getCurrentAsfChunkSize()
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* 16 bytes GUID, 8 bytes chunk size, 2 bytes descriptor count
|
||||||
|
*/
|
||||||
|
long result = 26;
|
||||||
|
for (final MetadataDescriptor curr : getDescriptors())
|
||||||
|
{
|
||||||
|
result += curr.getCurrentAsfSize(this.containerType);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of contained descriptors.
|
||||||
|
*
|
||||||
|
* @return number of descriptors.
|
||||||
|
*/
|
||||||
|
public final int getDescriptorCount()
|
||||||
|
{
|
||||||
|
return this.getDescriptors().size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all stored descriptors.
|
||||||
|
*
|
||||||
|
* @return stored descriptors.
|
||||||
|
*/
|
||||||
|
public final List<MetadataDescriptor> getDescriptors()
|
||||||
|
{
|
||||||
|
final List<MetadataDescriptor> result = new ArrayList<MetadataDescriptor>();
|
||||||
|
for (final List<MetadataDescriptor> curr : this.descriptors.values())
|
||||||
|
{
|
||||||
|
result.addAll(curr);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of descriptors with the given
|
||||||
|
* {@linkplain MetadataDescriptor#getName() name}.<br>
|
||||||
|
*
|
||||||
|
* @param name name of the descriptors to return
|
||||||
|
* @return list of descriptors with given name.
|
||||||
|
*/
|
||||||
|
public final List<MetadataDescriptor> getDescriptorsByName(final String name)
|
||||||
|
{
|
||||||
|
assert name != null;
|
||||||
|
final List<MetadataDescriptor> result = new ArrayList<MetadataDescriptor>();
|
||||||
|
final Collection<List<MetadataDescriptor>> values = this.descriptors.values();
|
||||||
|
for (final List<MetadataDescriptor> currList : values)
|
||||||
|
{
|
||||||
|
if (!currList.isEmpty() && currList.get(0).getName().equals(name))
|
||||||
|
{
|
||||||
|
result.addAll(currList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method looks up a descriptor with given name and returns its value
|
||||||
|
* as string.<br>
|
||||||
|
*
|
||||||
|
* @param name the name of the descriptor to look up.
|
||||||
|
* @return the string representation of a found descriptors value. Even an
|
||||||
|
* empty string if no descriptor has been found.
|
||||||
|
*/
|
||||||
|
protected final String getValueFor(final String name)
|
||||||
|
{
|
||||||
|
String result = "";
|
||||||
|
final List<MetadataDescriptor> descs = getDescriptorsByName(name);
|
||||||
|
if (descs != null)
|
||||||
|
{
|
||||||
|
assert descs.size() <= 1;
|
||||||
|
if (!descs.isEmpty())
|
||||||
|
{
|
||||||
|
result = descs.get(0).getString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if this container contains a descriptor with given
|
||||||
|
* {@linkplain MetadataDescriptor#getName() name}.<br>
|
||||||
|
*
|
||||||
|
* @param name Name of the descriptor to look for.
|
||||||
|
* @return <code>true</code> if descriptor has been found.
|
||||||
|
*/
|
||||||
|
public final boolean hasDescriptor(final String name)
|
||||||
|
{
|
||||||
|
return !getDescriptorsByName(name).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines/checks if the given descriptor may be added to the container.<br>
|
||||||
|
* This implies a check for the capabilities of the container specified by
|
||||||
|
* its {@linkplain #getContainerType() container type}.<br>
|
||||||
|
*
|
||||||
|
* @param descriptor the descriptor to test.
|
||||||
|
* @return <code>true</code> if {@link #addDescriptor(MetadataDescriptor)}
|
||||||
|
* can be called with given descriptor.
|
||||||
|
*/
|
||||||
|
public boolean isAddSupported(final MetadataDescriptor descriptor)
|
||||||
|
{
|
||||||
|
boolean result = getContainerType().checkConstraints(descriptor.getName(), descriptor.getRawData(), descriptor.getType(), descriptor.getStreamNumber(), descriptor.getLanguageIndex()) == null;
|
||||||
|
// Now check if there is already a value contained.
|
||||||
|
if (result && !getContainerType().isMultiValued())
|
||||||
|
{
|
||||||
|
synchronized (this.perfPoint)
|
||||||
|
{
|
||||||
|
final List<MetadataDescriptor> list = this.descriptors.get(this.perfPoint.setDescriptor(descriptor));
|
||||||
|
if (list != null)
|
||||||
|
{
|
||||||
|
result = list.isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public final boolean isEmpty()
|
||||||
|
{
|
||||||
|
boolean result = true;
|
||||||
|
if (getDescriptorCount() != 0)
|
||||||
|
{
|
||||||
|
final Iterator<MetadataDescriptor> iterator = getDescriptors().iterator();
|
||||||
|
while (result && iterator.hasNext())
|
||||||
|
{
|
||||||
|
result &= iterator.next().isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String prettyPrint(final String prefix)
|
||||||
|
{
|
||||||
|
final StringBuilder result = new StringBuilder(super.prettyPrint(prefix));
|
||||||
|
for (final MetadataDescriptor curr : getDescriptors())
|
||||||
|
{
|
||||||
|
result.append(prefix).append(" |-> ");
|
||||||
|
result.append(curr);
|
||||||
|
result.append(Utils.LINE_SEPARATOR);
|
||||||
|
}
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all stored descriptors with the given
|
||||||
|
* {@linkplain MetadataDescriptor#getName() name}.<br>
|
||||||
|
*
|
||||||
|
* @param name the name to remove.
|
||||||
|
*/
|
||||||
|
public final void removeDescriptorsByName(final String name)
|
||||||
|
{
|
||||||
|
assert name != null;
|
||||||
|
final Iterator<List<MetadataDescriptor>> iterator = this.descriptors.values().iterator();
|
||||||
|
while (iterator.hasNext())
|
||||||
|
{
|
||||||
|
final List<MetadataDescriptor> curr = iterator.next();
|
||||||
|
if (!curr.isEmpty() && curr.get(0).getName().equals(name))
|
||||||
|
{
|
||||||
|
iterator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@linkplain #assertDescriptor(String) asserts} the existence of a
|
||||||
|
* descriptor with given <code>name</code> and
|
||||||
|
* {@linkplain MetadataDescriptor#setStringValue(String) assings} the string
|
||||||
|
* value.
|
||||||
|
*
|
||||||
|
* @param name the name of the descriptor to set the value for.
|
||||||
|
* @param value the string value.
|
||||||
|
*/
|
||||||
|
protected final void setStringValue(final String name, final String value)
|
||||||
|
{
|
||||||
|
assertDescriptor(name).setStringValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public long writeInto(final OutputStream out) throws IOException
|
||||||
|
{
|
||||||
|
final long chunkSize = getCurrentAsfChunkSize();
|
||||||
|
final List<MetadataDescriptor> descriptorList = getDescriptors();
|
||||||
|
out.write(getGuid().getBytes());
|
||||||
|
Utils.writeUINT64(chunkSize, out);
|
||||||
|
Utils.writeUINT16(descriptorList.size(), out);
|
||||||
|
for (final MetadataDescriptor curr : descriptorList)
|
||||||
|
{
|
||||||
|
curr.writeInto(out, this.containerType);
|
||||||
|
}
|
||||||
|
return chunkSize;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,97 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.asf.data;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A factory for creating appropriate {@link MetadataContainer} objects upon
|
||||||
|
* specified {@linkplain ContainerType container types}.<br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public final class MetadataContainerFactory
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory instance.
|
||||||
|
*/
|
||||||
|
private final static MetadataContainerFactory INSTANCE = new MetadataContainerFactory();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an instance.
|
||||||
|
*
|
||||||
|
* @return an instance.
|
||||||
|
*/
|
||||||
|
public static MetadataContainerFactory getInstance()
|
||||||
|
{
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hidden utility class constructor.
|
||||||
|
*/
|
||||||
|
private MetadataContainerFactory()
|
||||||
|
{
|
||||||
|
// Hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an appropriate {@linkplain MetadataContainer container
|
||||||
|
* implementation} for the given container type.
|
||||||
|
*
|
||||||
|
* @param type the type of container to get a container instance for.
|
||||||
|
* @return appropriate container implementation.
|
||||||
|
*/
|
||||||
|
public MetadataContainer createContainer(final ContainerType type)
|
||||||
|
{
|
||||||
|
return createContainer(type, 0, BigInteger.ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience Method for I/O. Same as
|
||||||
|
* {@link #createContainer(ContainerType)}, but additionally assigns
|
||||||
|
* position and size. (since a {@link MetadataContainer} is actually a
|
||||||
|
* {@link Chunk}).
|
||||||
|
*
|
||||||
|
* @param type The containers type.
|
||||||
|
* @param pos the position within the stream.
|
||||||
|
* @param chunkSize the size of the container.
|
||||||
|
* @return an appropriate container implementation with assigned size and
|
||||||
|
* position.
|
||||||
|
*/
|
||||||
|
public MetadataContainer createContainer(final ContainerType type, final long pos, final BigInteger chunkSize)
|
||||||
|
{
|
||||||
|
MetadataContainer result;
|
||||||
|
if (type == ContainerType.CONTENT_DESCRIPTION)
|
||||||
|
{
|
||||||
|
result = new ContentDescription(pos, chunkSize);
|
||||||
|
}
|
||||||
|
else if (type == ContainerType.CONTENT_BRANDING)
|
||||||
|
{
|
||||||
|
result = new ContentBranding(pos, chunkSize);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = new MetadataContainer(type, pos, chunkSize);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method which calls {@link #createContainer(ContainerType)}
|
||||||
|
* for each given container type.
|
||||||
|
*
|
||||||
|
* @param types types of the container which are to be created.
|
||||||
|
* @return appropriate container implementations.
|
||||||
|
*/
|
||||||
|
public MetadataContainer[] createContainers(final ContainerType[] types)
|
||||||
|
{
|
||||||
|
assert types != null;
|
||||||
|
final MetadataContainer[] result = new MetadataContainer[types.length];
|
||||||
|
for (int i = 0; i < result.length; i++)
|
||||||
|
{
|
||||||
|
result[i] = createContainer(types[i]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,940 @@
|
|||||||
|
/*
|
||||||
|
* Entagged Audio Tag library
|
||||||
|
* Copyright (c) 2004-2005 Christian Laireiter <liree@web.de>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library 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
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package com.mp3.jaudiotagger.audio.asf.data;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
import com.mp3.jaudiotagger.logging.ErrorMessage;
|
||||||
|
import com.mp3.jaudiotagger.tag.TagOptionSingleton;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This structure represents metadata objects in ASF {@link MetadataContainer}.<br>
|
||||||
|
* The values are
|
||||||
|
* {@linkplain ContainerType#assertConstraints(String, byte[], int, int, int)
|
||||||
|
* checked} against the capability introduced by the given
|
||||||
|
* {@link ContainerType} at construction.<br>
|
||||||
|
* <br>
|
||||||
|
* <b>Limitation</b>: Even though some container types do not restrict the data
|
||||||
|
* size to {@link Integer#MAX_VALUE}, this implementation does it (due to java
|
||||||
|
* nature).<br>
|
||||||
|
* 2 GiB of data should suffice, and even be to large for normal java heap.
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public class MetadataDescriptor implements Comparable<MetadataDescriptor>, Cloneable
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum value for WORD.
|
||||||
|
*/
|
||||||
|
public static final long DWORD_MAXVALUE = new BigInteger("FFFFFFFF", 16).longValue();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logger instance.
|
||||||
|
*/
|
||||||
|
private static final Logger LOGGER = Logger.getLogger("com.mp3.jaudiotagger.audio.asf.data");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum language index allowed. (exclusive)
|
||||||
|
*/
|
||||||
|
public static final int MAX_LANG_INDEX = 127;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum stream number. (inclusive)
|
||||||
|
*/
|
||||||
|
public static final int MAX_STREAM_NUMBER = 127;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum value for a QWORD value (64 bit unsigned).<br>
|
||||||
|
*/
|
||||||
|
public static final BigInteger QWORD_MAXVALUE = new BigInteger("FFFFFFFFFFFFFFFF", 16);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constant for the metadata descriptor-type for binary data.
|
||||||
|
*/
|
||||||
|
public final static int TYPE_BINARY = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constant for the metadata descriptor-type for booleans.
|
||||||
|
*/
|
||||||
|
public final static int TYPE_BOOLEAN = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constant for the metadata descriptor-type for DWORD (32-bit unsigned). <br>
|
||||||
|
*/
|
||||||
|
public final static int TYPE_DWORD = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constant for the metadata descriptor-type for GUIDs (128-bit).<br>
|
||||||
|
*/
|
||||||
|
public final static int TYPE_GUID = 6;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constant for the metadata descriptor-type for QWORD (64-bit unsinged). <br>
|
||||||
|
*/
|
||||||
|
public final static int TYPE_QWORD = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constant for the metadata descriptor-type for Strings.
|
||||||
|
*/
|
||||||
|
public final static int TYPE_STRING = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constant for the metadata descriptor-type for WORD (16-bit unsigned). <br>
|
||||||
|
*/
|
||||||
|
public final static int TYPE_WORD = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum value for WORD.
|
||||||
|
*/
|
||||||
|
public static final int WORD_MAXVALUE = 65535;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the containerType of the descriptor.
|
||||||
|
*/
|
||||||
|
private final ContainerType containerType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The binary representation of the value.
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
* Note: The maximum data length could be up to a 64-Bit number (unsigned),
|
||||||
|
* but java for now handles just int sized byte[]. Since this class stores
|
||||||
|
* all data in primitive byte[] this size restriction is cascaded to all
|
||||||
|
* dependent implementations.
|
||||||
|
*/
|
||||||
|
private byte[] content = new byte[0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This field shows the type of the metadata descriptor. <br>
|
||||||
|
*
|
||||||
|
* @see #TYPE_BINARY
|
||||||
|
* @see #TYPE_BOOLEAN
|
||||||
|
* @see #TYPE_DWORD
|
||||||
|
* @see #TYPE_GUID
|
||||||
|
* @see #TYPE_QWORD
|
||||||
|
* @see #TYPE_STRING
|
||||||
|
* @see #TYPE_WORD
|
||||||
|
*/
|
||||||
|
private int descriptorType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the index of the language in the {@linkplain LanguageList language list}
|
||||||
|
* this descriptor applies to.<br>
|
||||||
|
*/
|
||||||
|
private int languageIndex = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the metadata descriptor.
|
||||||
|
*/
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of the stream, this descriptor applies to.<br>
|
||||||
|
*/
|
||||||
|
private int streamNumber = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an Instance.<br>
|
||||||
|
*
|
||||||
|
* @param type the container type, this descriptor is resctricted to.
|
||||||
|
* @param propName Name of the MetadataDescriptor.
|
||||||
|
* @param propType Type of the metadata descriptor. See {@link #descriptorType}
|
||||||
|
*/
|
||||||
|
public MetadataDescriptor(final ContainerType type, final String propName, final int propType)
|
||||||
|
{
|
||||||
|
this(type, propName, propType, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an Instance.
|
||||||
|
*
|
||||||
|
* @param type The container type the values (the whole descriptor) is
|
||||||
|
* restricted to.
|
||||||
|
* @param propName Name of the MetadataDescriptor.
|
||||||
|
* @param propType Type of the metadata descriptor. See {@link #descriptorType}
|
||||||
|
* @param stream the number of the stream the descriptor refers to.
|
||||||
|
* @param language the index of the language entry in a {@link LanguageList} this
|
||||||
|
* descriptor refers to.<br>
|
||||||
|
* <b>Consider</b>: No checks performed if language entry exists.
|
||||||
|
*/
|
||||||
|
public MetadataDescriptor(final ContainerType type, final String propName, final int propType, final int stream, final int language)
|
||||||
|
{
|
||||||
|
assert type != null;
|
||||||
|
type.assertConstraints(propName, new byte[0], propType, stream, language);
|
||||||
|
this.containerType = type;
|
||||||
|
this.name = propName;
|
||||||
|
this.descriptorType = propType;
|
||||||
|
this.streamNumber = stream;
|
||||||
|
this.languageIndex = language;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.<br>
|
||||||
|
* Capabilities are set to {@link ContainerType#METADATA_LIBRARY_OBJECT}.<br>
|
||||||
|
*
|
||||||
|
* @param propName name of the metadata descriptor.
|
||||||
|
*/
|
||||||
|
public MetadataDescriptor(final String propName)
|
||||||
|
{
|
||||||
|
this(propName, TYPE_STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an Instance.<br>
|
||||||
|
* Capabilities are set to {@link ContainerType#METADATA_LIBRARY_OBJECT}.<br>
|
||||||
|
*
|
||||||
|
* @param propName Name of the MetadataDescriptor.
|
||||||
|
* @param propType Type of the metadata descriptor. See {@link #descriptorType}
|
||||||
|
*/
|
||||||
|
public MetadataDescriptor(final String propName, final int propType)
|
||||||
|
{
|
||||||
|
this(ContainerType.METADATA_LIBRARY_OBJECT, propName, propType, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the descriptors value into a number if possible.<br>
|
||||||
|
* A boolean will be converted to "1" if <code>true</code>,
|
||||||
|
* otherwise "0".<br>
|
||||||
|
* String will be interpreted as number with radix "10".<br>
|
||||||
|
* Binary data will be interpreted as the default WORD,DWORD or QWORD binary
|
||||||
|
* representation, but only if the data does not exceed 8 bytes. This
|
||||||
|
* precaution is done to prevent creating a number of a multi kilobyte
|
||||||
|
* image.<br>
|
||||||
|
* A GUID cannot be converted in any case.
|
||||||
|
*
|
||||||
|
* @return number representation.
|
||||||
|
* @throws NumberFormatException If no conversion is supported.
|
||||||
|
*/
|
||||||
|
public BigInteger asNumber()
|
||||||
|
{
|
||||||
|
BigInteger result = null;
|
||||||
|
switch (this.descriptorType)
|
||||||
|
{
|
||||||
|
case TYPE_BOOLEAN:
|
||||||
|
case TYPE_WORD:
|
||||||
|
case TYPE_DWORD:
|
||||||
|
case TYPE_QWORD:
|
||||||
|
case TYPE_BINARY:
|
||||||
|
if (this.content.length > 8)
|
||||||
|
{
|
||||||
|
throw new NumberFormatException("Binary data would exceed QWORD");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case TYPE_GUID:
|
||||||
|
throw new NumberFormatException("GUID cannot be converted to a number.");
|
||||||
|
case TYPE_STRING:
|
||||||
|
result = new BigInteger(getString(), 10);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
final byte[] copy = new byte[this.content.length];
|
||||||
|
for (int i = 0; i < copy.length; i++)
|
||||||
|
{
|
||||||
|
copy[i] = this.content[this.content.length - (i + 1)];
|
||||||
|
}
|
||||||
|
result = new BigInteger(1, copy);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (overridden)
|
||||||
|
*
|
||||||
|
* @see Object#clone()
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Object clone() throws CloneNotSupportedException
|
||||||
|
{
|
||||||
|
return super.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public int compareTo(final MetadataDescriptor other)
|
||||||
|
{
|
||||||
|
return getName().compareTo(other.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method creates a copy of the current object. <br>
|
||||||
|
* All data will be copied, too. <br>
|
||||||
|
*
|
||||||
|
* @return A new metadata descriptor containing the same values as the
|
||||||
|
* current one.
|
||||||
|
*/
|
||||||
|
public MetadataDescriptor createCopy()
|
||||||
|
{
|
||||||
|
final MetadataDescriptor result = new MetadataDescriptor(this.containerType, this.name, this.descriptorType, this.streamNumber, this.languageIndex);
|
||||||
|
result.content = getRawData();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (overridden)
|
||||||
|
*
|
||||||
|
* @see Object#equals(Object)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean equals(final Object obj)
|
||||||
|
{
|
||||||
|
boolean result = false;
|
||||||
|
if (obj instanceof MetadataDescriptor)
|
||||||
|
{
|
||||||
|
if (obj == this)
|
||||||
|
{
|
||||||
|
result = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
final MetadataDescriptor other = (MetadataDescriptor) obj;
|
||||||
|
result = other.getName().equals(getName()) && other.descriptorType == this.descriptorType && other.languageIndex == this.languageIndex && other.streamNumber == this.streamNumber && Arrays.equals(this.content, other.content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value of the MetadataDescriptor as a Boolean. <br>
|
||||||
|
* If no Conversion is Possible false is returned. <br>
|
||||||
|
* <code>true</code> if first byte of {@link #content}is not zero.
|
||||||
|
*
|
||||||
|
* @return boolean representation of the current value.
|
||||||
|
*/
|
||||||
|
public boolean getBoolean()
|
||||||
|
{
|
||||||
|
return this.content.length > 0 && this.content[0] != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method will return a byte array, which can directly be written into
|
||||||
|
* an "Extended Content Description"-chunk. <br>
|
||||||
|
*
|
||||||
|
* @return byte[] with the data, that occurs in ASF files.
|
||||||
|
* @deprecated {@link #writeInto(OutputStream, ContainerType)} is used
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public byte[] getBytes()
|
||||||
|
{
|
||||||
|
final ByteArrayOutputStream result = new ByteArrayOutputStream();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
writeInto(result, this.containerType);
|
||||||
|
}
|
||||||
|
catch (final IOException e)
|
||||||
|
{
|
||||||
|
LOGGER.warning(e.getMessage());
|
||||||
|
}
|
||||||
|
return result.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the container type this descriptor ist restricted to.
|
||||||
|
*
|
||||||
|
* @return the container type
|
||||||
|
*/
|
||||||
|
public ContainerType getContainerType()
|
||||||
|
{
|
||||||
|
return this.containerType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the size (in bytes) this descriptor will take when written to an
|
||||||
|
* ASF file.<br>
|
||||||
|
*
|
||||||
|
* @param type the container type for which the size is calculated.
|
||||||
|
* @return size of the descriptor in an ASF file.
|
||||||
|
*/
|
||||||
|
public int getCurrentAsfSize(final ContainerType type)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* 2 bytes name length, 2 bytes name zero term, 2 bytes type, 2 bytes
|
||||||
|
* content length
|
||||||
|
*/
|
||||||
|
int result = 8;
|
||||||
|
|
||||||
|
if (type != ContainerType.EXTENDED_CONTENT)
|
||||||
|
{
|
||||||
|
// Stream number and language index (respectively reserved field).
|
||||||
|
// And +2 bytes, because data type is 32 bit, not 16
|
||||||
|
result += 6;
|
||||||
|
}
|
||||||
|
result += getName().length() * 2;
|
||||||
|
|
||||||
|
if (this.getType() == TYPE_BOOLEAN)
|
||||||
|
{
|
||||||
|
result += 2;
|
||||||
|
if (type == ContainerType.EXTENDED_CONTENT)
|
||||||
|
{
|
||||||
|
// Extended content description boolean values are stored with
|
||||||
|
// 32-bit
|
||||||
|
result += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
|
||||||
|
result += this.content.length;
|
||||||
|
if (TYPE_STRING == this.getType())
|
||||||
|
{
|
||||||
|
result += 2; // zero term of content string.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the GUID value, if content could represent one.
|
||||||
|
*
|
||||||
|
* @return GUID value
|
||||||
|
*/
|
||||||
|
public GUID getGuid()
|
||||||
|
{
|
||||||
|
GUID result = null;
|
||||||
|
if (getType() == TYPE_GUID && this.content.length == GUID.GUID_LENGTH)
|
||||||
|
{
|
||||||
|
result = new GUID(this.content);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the index of the language that is referred (see
|
||||||
|
* {@link LanguageList}):
|
||||||
|
*
|
||||||
|
* @return the language index
|
||||||
|
*/
|
||||||
|
public int getLanguageIndex()
|
||||||
|
{
|
||||||
|
return this.languageIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method returns the name of the metadata descriptor.
|
||||||
|
*
|
||||||
|
* @return Name.
|
||||||
|
*/
|
||||||
|
public String getName()
|
||||||
|
{
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method returns the value of the metadata descriptor as a long. <br>
|
||||||
|
* Converts the needed amount of byte out of {@link #content}to a number. <br>
|
||||||
|
* Only possible if {@link #getType()}equals on of the following: <br>
|
||||||
|
*
|
||||||
|
* @return integer value.
|
||||||
|
* @see #TYPE_BOOLEAN
|
||||||
|
* @see #TYPE_DWORD
|
||||||
|
* @see #TYPE_QWORD
|
||||||
|
* @see #TYPE_WORD
|
||||||
|
*/
|
||||||
|
public long getNumber()
|
||||||
|
{
|
||||||
|
int bytesNeeded;
|
||||||
|
switch (getType())
|
||||||
|
{
|
||||||
|
case TYPE_BOOLEAN:
|
||||||
|
bytesNeeded = 1;
|
||||||
|
break;
|
||||||
|
case TYPE_DWORD:
|
||||||
|
bytesNeeded = 4;
|
||||||
|
break;
|
||||||
|
case TYPE_QWORD:
|
||||||
|
bytesNeeded = 8;
|
||||||
|
break;
|
||||||
|
case TYPE_WORD:
|
||||||
|
bytesNeeded = 2;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new UnsupportedOperationException("The current type doesn't allow an interpretation as a number. (" + getType() + ")");
|
||||||
|
}
|
||||||
|
if (bytesNeeded > this.content.length)
|
||||||
|
{
|
||||||
|
throw new IllegalStateException("The stored data cannot represent the type of current object.");
|
||||||
|
}
|
||||||
|
long result = 0;
|
||||||
|
for (int i = 0; i < bytesNeeded; i++)
|
||||||
|
{
|
||||||
|
result |= (((long) this.content[i] & 0xFF) << (i * 8));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method returns a copy of the content of the descriptor. <br>
|
||||||
|
*
|
||||||
|
* @return The content in binary representation, as it would be written to
|
||||||
|
* asf file. <br>
|
||||||
|
*/
|
||||||
|
public byte[] getRawData()
|
||||||
|
{
|
||||||
|
final byte[] copy = new byte[this.content.length];
|
||||||
|
System.arraycopy(this.content, 0, copy, 0, this.content.length);
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the size (in bytes) the binary representation of the content
|
||||||
|
* uses. (length of {@link #getRawData()})<br>
|
||||||
|
*
|
||||||
|
* @return size of binary representation of the content.
|
||||||
|
*/
|
||||||
|
public int getRawDataSize()
|
||||||
|
{
|
||||||
|
return this.content.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the stream number this descriptor applies to.<br>
|
||||||
|
*
|
||||||
|
* @return the stream number.
|
||||||
|
*/
|
||||||
|
public int getStreamNumber()
|
||||||
|
{
|
||||||
|
return this.streamNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value of the MetadataDescriptor as a String. <br>
|
||||||
|
*
|
||||||
|
* @return String - Representation Value
|
||||||
|
*/
|
||||||
|
public String getString()
|
||||||
|
{
|
||||||
|
String result = null;
|
||||||
|
switch (getType())
|
||||||
|
{
|
||||||
|
case TYPE_BINARY:
|
||||||
|
result = "binary data";
|
||||||
|
break;
|
||||||
|
case TYPE_BOOLEAN:
|
||||||
|
result = String.valueOf(getBoolean());
|
||||||
|
break;
|
||||||
|
case TYPE_GUID:
|
||||||
|
result = getGuid() == null ? "Invalid GUID" : getGuid().toString();
|
||||||
|
break;
|
||||||
|
case TYPE_QWORD:
|
||||||
|
case TYPE_DWORD:
|
||||||
|
case TYPE_WORD:
|
||||||
|
result = String.valueOf(getNumber());
|
||||||
|
break;
|
||||||
|
case TYPE_STRING:
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result = new String(this.content, "UTF-16LE");
|
||||||
|
}
|
||||||
|
catch (final UnsupportedEncodingException e)
|
||||||
|
{
|
||||||
|
LOGGER.warning(e.getMessage());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalStateException("Current type is not known.");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the type of the metadata descriptor. <br>
|
||||||
|
*
|
||||||
|
* @return the value of {@link #descriptorType}
|
||||||
|
* @see #TYPE_BINARY
|
||||||
|
* @see #TYPE_BOOLEAN
|
||||||
|
* @see #TYPE_DWORD
|
||||||
|
* @see #TYPE_GUID
|
||||||
|
* @see #TYPE_QWORD
|
||||||
|
* @see #TYPE_STRING
|
||||||
|
* @see #TYPE_WORD
|
||||||
|
*/
|
||||||
|
public int getType()
|
||||||
|
{
|
||||||
|
return this.descriptorType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int hashCode()
|
||||||
|
{
|
||||||
|
return this.name.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method checks if the binary data is empty. <br>
|
||||||
|
* Disregarding the type of the descriptor its content is stored as a byte
|
||||||
|
* array.
|
||||||
|
*
|
||||||
|
* @return <code>true</code> if no value is set.
|
||||||
|
*/
|
||||||
|
public boolean isEmpty()
|
||||||
|
{
|
||||||
|
return this.content.length == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the Value of the current metadata descriptor. <br>
|
||||||
|
* Using this method will change {@link #descriptorType}to
|
||||||
|
* {@link #TYPE_BINARY}.<br>
|
||||||
|
*
|
||||||
|
* @param data Value to set.
|
||||||
|
* @throws IllegalArgumentException if data is invalid for {@linkplain #getContainerType()
|
||||||
|
* container}.
|
||||||
|
*/
|
||||||
|
public void setBinaryValue(final byte[] data) throws IllegalArgumentException
|
||||||
|
{
|
||||||
|
this.containerType.assertConstraints(this.name, data, this.descriptorType, this.streamNumber, this.languageIndex);
|
||||||
|
this.content = data.clone();
|
||||||
|
this.descriptorType = TYPE_BINARY;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the Value of the current metadata descriptor. <br>
|
||||||
|
* Using this method will change {@link #descriptorType}to
|
||||||
|
* {@link #TYPE_BOOLEAN}.<br>
|
||||||
|
*
|
||||||
|
* @param value Value to set.
|
||||||
|
*/
|
||||||
|
public void setBooleanValue(final boolean value)
|
||||||
|
{
|
||||||
|
this.content = new byte[]{value ? (byte) 1 : 0};
|
||||||
|
this.descriptorType = TYPE_BOOLEAN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the Value of the current metadata descriptor. <br>
|
||||||
|
* Using this method will change {@link #descriptorType}to
|
||||||
|
* {@link #TYPE_DWORD}.
|
||||||
|
*
|
||||||
|
* @param value Value to set.
|
||||||
|
*/
|
||||||
|
public void setDWordValue(final long value)
|
||||||
|
{
|
||||||
|
if (value < 0 || value > DWORD_MAXVALUE)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("value out of range (0-" + DWORD_MAXVALUE + ")");
|
||||||
|
}
|
||||||
|
this.content = Utils.getBytes(value, 4);
|
||||||
|
this.descriptorType = TYPE_DWORD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the value of the metadata descriptor.<br>
|
||||||
|
* Using this method will change {@link #descriptorType} to
|
||||||
|
* {@link #TYPE_GUID}
|
||||||
|
*
|
||||||
|
* @param value value to set.
|
||||||
|
*/
|
||||||
|
public void setGUIDValue(final GUID value)
|
||||||
|
{
|
||||||
|
this.containerType.assertConstraints(this.name, value.getBytes(), TYPE_GUID, this.streamNumber, this.languageIndex);
|
||||||
|
this.content = value.getBytes();
|
||||||
|
this.descriptorType = TYPE_GUID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the index of the referred language (see {@link LanguageList}).<br>
|
||||||
|
* <b>Consider</b>: The {@linkplain #containerType requirements} must be
|
||||||
|
* held.
|
||||||
|
*
|
||||||
|
* @param language the language index to set
|
||||||
|
*/
|
||||||
|
public void setLanguageIndex(final int language)
|
||||||
|
{
|
||||||
|
this.containerType.assertConstraints(this.name, this.content, this.descriptorType, this.streamNumber, language);
|
||||||
|
this.languageIndex = language;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the Value of the current metadata descriptor. <br>
|
||||||
|
* Using this method will change {@link #descriptorType}to
|
||||||
|
* {@link #TYPE_QWORD}
|
||||||
|
*
|
||||||
|
* @param value Value to set.
|
||||||
|
* @throws NumberFormatException on <code>null</code> values.
|
||||||
|
* @throws IllegalArgumentException on illegal values or values exceeding range.
|
||||||
|
*/
|
||||||
|
public void setQWordValue(final BigInteger value) throws IllegalArgumentException
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
throw new NumberFormatException("null");
|
||||||
|
}
|
||||||
|
if (BigInteger.ZERO.compareTo(value) > 0)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("Only unsigned values allowed (no negative)");
|
||||||
|
}
|
||||||
|
if (MetadataDescriptor.QWORD_MAXVALUE.compareTo(value) < 0)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("Value exceeds QWORD (64 bit unsigned)");
|
||||||
|
}
|
||||||
|
this.content = new byte[8];
|
||||||
|
final byte[] valuesBytes = value.toByteArray();
|
||||||
|
if (valuesBytes.length <= 8)
|
||||||
|
{
|
||||||
|
for (int i = valuesBytes.length - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
this.content[valuesBytes.length - (i + 1)] = valuesBytes[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* In case of 64-Bit set
|
||||||
|
*/
|
||||||
|
Arrays.fill(this.content, (byte) 0xFF);
|
||||||
|
}
|
||||||
|
this.descriptorType = TYPE_QWORD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the Value of the current metadata descriptor. <br>
|
||||||
|
* Using this method will change {@link #descriptorType}to
|
||||||
|
* {@link #TYPE_QWORD}
|
||||||
|
*
|
||||||
|
* @param value Value to set.
|
||||||
|
*/
|
||||||
|
public void setQWordValue(final long value)
|
||||||
|
{
|
||||||
|
if (value < 0)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("value out of range (0-" + MetadataDescriptor.QWORD_MAXVALUE.toString() + ")");
|
||||||
|
}
|
||||||
|
this.content = Utils.getBytes(value, 8);
|
||||||
|
this.descriptorType = TYPE_QWORD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the stream number the descriptor applies to.<br>
|
||||||
|
* <b>Consider</b>: The {@linkplain #containerType requirements} must be
|
||||||
|
* held.
|
||||||
|
*
|
||||||
|
* @param stream the stream number to set
|
||||||
|
*/
|
||||||
|
public void setStreamNumber(final int stream)
|
||||||
|
{
|
||||||
|
this.containerType.assertConstraints(this.name, this.content, this.descriptorType, stream, this.languageIndex);
|
||||||
|
this.streamNumber = stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method converts the given string value into the current
|
||||||
|
* {@linkplain #getType() data type}.
|
||||||
|
*
|
||||||
|
* @param value value to set.
|
||||||
|
* @throws IllegalArgumentException If conversion was impossible.
|
||||||
|
*/
|
||||||
|
public void setString(final String value) throws IllegalArgumentException
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
switch (getType())
|
||||||
|
{
|
||||||
|
case TYPE_BINARY:
|
||||||
|
throw new IllegalArgumentException("Cannot interpret binary as string.");
|
||||||
|
case TYPE_BOOLEAN:
|
||||||
|
setBooleanValue(Boolean.parseBoolean(value));
|
||||||
|
break;
|
||||||
|
case TYPE_DWORD:
|
||||||
|
setDWordValue(Long.parseLong(value));
|
||||||
|
break;
|
||||||
|
case TYPE_QWORD:
|
||||||
|
setQWordValue(new BigInteger(value, 10));
|
||||||
|
break;
|
||||||
|
case TYPE_WORD:
|
||||||
|
setWordValue(Integer.parseInt(value));
|
||||||
|
break;
|
||||||
|
case TYPE_GUID:
|
||||||
|
setGUIDValue(GUID.parseGUID(value));
|
||||||
|
break;
|
||||||
|
case TYPE_STRING:
|
||||||
|
setStringValue(value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// new Type added but not handled.
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (final NumberFormatException nfe)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("Value cannot be parsed as Number or is out of range (\"" + value + "\")", nfe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the Value of the current metadata descriptor. <br>
|
||||||
|
* Using this method will change {@link #descriptorType}to
|
||||||
|
* {@link #TYPE_STRING}.
|
||||||
|
*
|
||||||
|
* @param value Value to set.
|
||||||
|
* @throws IllegalArgumentException If byte representation would take more than 65535 Bytes.
|
||||||
|
*/
|
||||||
|
// TODO Test
|
||||||
|
public void setStringValue(final String value) throws IllegalArgumentException
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
this.content = new byte[0];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
final byte[] tmp = Utils.getBytes(value, AsfHeader.ASF_CHARSET);
|
||||||
|
if (getContainerType().isWithinValueRange(tmp.length))
|
||||||
|
{
|
||||||
|
// Everything is fine here, data can be stored.
|
||||||
|
this.content = tmp;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Normally a size violation, check if JAudiotagger my truncate
|
||||||
|
// the string
|
||||||
|
if (TagOptionSingleton.getInstance().isTruncateTextWithoutErrors())
|
||||||
|
{
|
||||||
|
// truncate the string
|
||||||
|
final int copyBytes = (int) getContainerType().getMaximumDataLength().longValue();
|
||||||
|
this.content = new byte[copyBytes % 2 == 0 ? copyBytes : copyBytes - 1];
|
||||||
|
System.arraycopy(tmp, 0, this.content, 0, this.content.length);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// We may not truncate, so its an error
|
||||||
|
throw new IllegalArgumentException(ErrorMessage.WMA_LENGTH_OF_DATA_IS_TOO_LARGE.getMsg(tmp.length, getContainerType().getMaximumDataLength(), getContainerType().getContainerGUID().getDescription())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.descriptorType = TYPE_STRING;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the Value of the current metadata descriptor. <br>
|
||||||
|
* Using this method will change {@link #descriptorType}to
|
||||||
|
* {@link #TYPE_WORD}
|
||||||
|
*
|
||||||
|
* @param value Value to set.
|
||||||
|
* @throws IllegalArgumentException on negative values. ASF just supports unsigned values.
|
||||||
|
*/
|
||||||
|
public void setWordValue(final int value) throws IllegalArgumentException
|
||||||
|
{
|
||||||
|
if (value < 0 || value > WORD_MAXVALUE)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("value out of range (0-" + WORD_MAXVALUE + ")");
|
||||||
|
}
|
||||||
|
this.content = Utils.getBytes(value, 2);
|
||||||
|
this.descriptorType = TYPE_WORD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (overridden)
|
||||||
|
*
|
||||||
|
* @see Object#toString()
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return getName() + " : " + new String[]{"String: ", "Binary: ", "Boolean: ", "DWORD: ", "QWORD:", "WORD:", "GUID:"}[this.descriptorType] + getString() + " (language: " + this.languageIndex + " / stream: " + this.streamNumber + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes this descriptor into the specified output stream.<br>
|
||||||
|
*
|
||||||
|
* @param out stream to write into.
|
||||||
|
* @param contType the container type this descriptor is written to.
|
||||||
|
* @return amount of bytes written.
|
||||||
|
* @throws IOException on I/O Errors
|
||||||
|
*/
|
||||||
|
public int writeInto(final OutputStream out, final ContainerType contType) throws IOException
|
||||||
|
{
|
||||||
|
final int size = getCurrentAsfSize(contType);
|
||||||
|
/*
|
||||||
|
* Booleans are stored as one byte, if a boolean is written, the data
|
||||||
|
* must be converted according to the container type.
|
||||||
|
*/
|
||||||
|
byte[] binaryData;
|
||||||
|
if (this.descriptorType == TYPE_BOOLEAN)
|
||||||
|
{
|
||||||
|
binaryData = new byte[contType == ContainerType.EXTENDED_CONTENT ? 4 : 2];
|
||||||
|
binaryData[0] = (byte) (getBoolean() ? 1 : 0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
binaryData = this.content;
|
||||||
|
}
|
||||||
|
// for Metadata objects the stream number and language index
|
||||||
|
if (contType != ContainerType.EXTENDED_CONTENT)
|
||||||
|
{
|
||||||
|
Utils.writeUINT16(getLanguageIndex(), out);
|
||||||
|
Utils.writeUINT16(getStreamNumber(), out);
|
||||||
|
}
|
||||||
|
Utils.writeUINT16(getName().length() * 2 + 2, out);
|
||||||
|
|
||||||
|
// The name for the metadata objects come later
|
||||||
|
if (contType == ContainerType.EXTENDED_CONTENT)
|
||||||
|
{
|
||||||
|
out.write(Utils.getBytes(getName(), AsfHeader.ASF_CHARSET));
|
||||||
|
out.write(AsfHeader.ZERO_TERM);
|
||||||
|
}
|
||||||
|
|
||||||
|
// type and content len follow up are identical
|
||||||
|
final int type = getType();
|
||||||
|
Utils.writeUINT16(type, out);
|
||||||
|
int contentLen = binaryData.length;
|
||||||
|
if (TYPE_STRING == type)
|
||||||
|
{
|
||||||
|
contentLen += 2; // Zero Term
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contType == ContainerType.EXTENDED_CONTENT)
|
||||||
|
{
|
||||||
|
Utils.writeUINT16(contentLen, out);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Utils.writeUINT32(contentLen, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata objects now write their descriptor name
|
||||||
|
if (contType != ContainerType.EXTENDED_CONTENT)
|
||||||
|
{
|
||||||
|
out.write(Utils.getBytes(getName(), AsfHeader.ASF_CHARSET));
|
||||||
|
out.write(AsfHeader.ZERO_TERM);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The content.
|
||||||
|
out.write(binaryData);
|
||||||
|
if (TYPE_STRING == type)
|
||||||
|
{
|
||||||
|
out.write(AsfHeader.ZERO_TERM);
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,112 @@
|
|||||||
|
/*
|
||||||
|
* Entagged Audio Tag library
|
||||||
|
* Copyright (c) 2004-2005 Christian Laireiter <liree@web.de>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library 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
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package com.mp3.jaudiotagger.audio.asf.data;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class represents the "Stream Bitrate Properties" chunk of an ASF media
|
||||||
|
* file. <br>
|
||||||
|
* It is optional, but contains useful information about the streams bitrate.<br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public class StreamBitratePropertiesChunk extends Chunk
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For each call of {@link #addBitrateRecord(int, long)} an {@link Long}
|
||||||
|
* object is appended, which represents the average bitrate.
|
||||||
|
*/
|
||||||
|
private final List<Long> bitRates;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For each call of {@link #addBitrateRecord(int, long)} an {@link Integer}
|
||||||
|
* object is appended, which represents the stream-number.
|
||||||
|
*/
|
||||||
|
private final List<Integer> streamNumbers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*
|
||||||
|
* @param chunkLen Length of current chunk.
|
||||||
|
*/
|
||||||
|
public StreamBitratePropertiesChunk(final BigInteger chunkLen)
|
||||||
|
{
|
||||||
|
super(GUID.GUID_STREAM_BITRATE_PROPERTIES, chunkLen);
|
||||||
|
this.bitRates = new ArrayList<Long>();
|
||||||
|
this.streamNumbers = new ArrayList<Integer>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the public values of a stream-record.
|
||||||
|
*
|
||||||
|
* @param streamNum The number of the referred stream.
|
||||||
|
* @param averageBitrate Its average bitrate.
|
||||||
|
*/
|
||||||
|
public void addBitrateRecord(final int streamNum, final long averageBitrate)
|
||||||
|
{
|
||||||
|
this.streamNumbers.add(streamNum);
|
||||||
|
this.bitRates.add(averageBitrate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the average bitrate of the given stream.<br>
|
||||||
|
*
|
||||||
|
* @param streamNumber Number of the stream whose bitrate to determine.
|
||||||
|
* @return The average bitrate of the numbered stream. <code>-1</code> if no
|
||||||
|
* information was given.
|
||||||
|
*/
|
||||||
|
public long getAvgBitrate(final int streamNumber)
|
||||||
|
{
|
||||||
|
final Integer seach = streamNumber;
|
||||||
|
final int index = this.streamNumbers.indexOf(seach);
|
||||||
|
long result;
|
||||||
|
if (index == -1)
|
||||||
|
{
|
||||||
|
result = -1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = this.bitRates.get(index);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (overridden)
|
||||||
|
*
|
||||||
|
* @see Chunk#prettyPrint(String)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String prettyPrint(final String prefix)
|
||||||
|
{
|
||||||
|
final StringBuilder result = new StringBuilder(super.prettyPrint(prefix));
|
||||||
|
for (int i = 0; i < this.bitRates.size(); i++)
|
||||||
|
{
|
||||||
|
result.append(prefix).append(" |-> Stream no. \"").append(this.streamNumbers.get(i)).append("\" has an average bitrate of \"").append(this.bitRates.get(i)).append('"').append(Utils.LINE_SEPARATOR);
|
||||||
|
}
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,192 @@
|
|||||||
|
/*
|
||||||
|
* Entagged Audio Tag library
|
||||||
|
* Copyright (c) 2004-2005 Christian Laireiter <liree@web.de>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library 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
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package com.mp3.jaudiotagger.audio.asf.data;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is the base for all handled stream contents. <br>
|
||||||
|
* A Stream chunk delivers information about a audio or video stream. Because of
|
||||||
|
* this the stream chunk identifies in one field what type of stream it is
|
||||||
|
* describing and so other data is provided. However some information is common
|
||||||
|
* to all stream chunks which are stored in this hierarchy of the class tree.
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public abstract class StreamChunk extends Chunk
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If <code>true</code>, the stream data is encrypted.
|
||||||
|
*/
|
||||||
|
private boolean contentEncrypted;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This field stores the number of the current stream. <br>
|
||||||
|
*/
|
||||||
|
private int streamNumber;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see #typeSpecificDataSize
|
||||||
|
*/
|
||||||
|
private long streamSpecificDataSize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Something technical. <br>
|
||||||
|
* Format time in 100-ns steps.
|
||||||
|
*/
|
||||||
|
private long timeOffset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the stream type.<br>
|
||||||
|
*
|
||||||
|
* @see GUID#GUID_AUDIOSTREAM
|
||||||
|
* @see GUID#GUID_VIDEOSTREAM
|
||||||
|
*/
|
||||||
|
private final GUID type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the size of type specific data structure within chunk.
|
||||||
|
*/
|
||||||
|
private long typeSpecificDataSize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance
|
||||||
|
*
|
||||||
|
* @param streamType The GUID which tells the stream type represented (
|
||||||
|
* {@link GUID#GUID_AUDIOSTREAM} or {@link GUID#GUID_VIDEOSTREAM}
|
||||||
|
* ):
|
||||||
|
* @param chunkLen length of chunk
|
||||||
|
*/
|
||||||
|
public StreamChunk(final GUID streamType, final BigInteger chunkLen)
|
||||||
|
{
|
||||||
|
super(GUID.GUID_STREAM, chunkLen);
|
||||||
|
assert GUID.GUID_AUDIOSTREAM.equals(streamType) || GUID.GUID_VIDEOSTREAM.equals(streamType);
|
||||||
|
this.type = streamType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the streamNumber.
|
||||||
|
*/
|
||||||
|
public int getStreamNumber()
|
||||||
|
{
|
||||||
|
return this.streamNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the streamSpecificDataSize.
|
||||||
|
*/
|
||||||
|
public long getStreamSpecificDataSize()
|
||||||
|
{
|
||||||
|
return this.streamSpecificDataSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the stream type of the stream chunk.<br>
|
||||||
|
*
|
||||||
|
* @return {@link GUID#GUID_AUDIOSTREAM} or {@link GUID#GUID_VIDEOSTREAM}.
|
||||||
|
*/
|
||||||
|
public GUID getStreamType()
|
||||||
|
{
|
||||||
|
return this.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the timeOffset.
|
||||||
|
*/
|
||||||
|
public long getTimeOffset()
|
||||||
|
{
|
||||||
|
return this.timeOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the typeSpecificDataSize.
|
||||||
|
*/
|
||||||
|
public long getTypeSpecificDataSize()
|
||||||
|
{
|
||||||
|
return this.typeSpecificDataSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the contentEncrypted.
|
||||||
|
*/
|
||||||
|
public boolean isContentEncrypted()
|
||||||
|
{
|
||||||
|
return this.contentEncrypted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (overridden)
|
||||||
|
*
|
||||||
|
* @see Chunk#prettyPrint(String)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String prettyPrint(final String prefix)
|
||||||
|
{
|
||||||
|
final StringBuilder result = new StringBuilder(super.prettyPrint(prefix));
|
||||||
|
result.append(prefix).append(" |-> Stream number: ").append(getStreamNumber()).append(Utils.LINE_SEPARATOR);
|
||||||
|
result.append(prefix).append(" |-> Type specific data size : ").append(getTypeSpecificDataSize()).append(Utils.LINE_SEPARATOR);
|
||||||
|
result.append(prefix).append(" |-> Stream specific data size: ").append(getStreamSpecificDataSize()).append(Utils.LINE_SEPARATOR);
|
||||||
|
result.append(prefix).append(" |-> Time Offset : ").append(getTimeOffset()).append(Utils.LINE_SEPARATOR);
|
||||||
|
result.append(prefix).append(" |-> Content Encryption : ").append(isContentEncrypted()).append(Utils.LINE_SEPARATOR);
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param cntEnc The contentEncrypted to set.
|
||||||
|
*/
|
||||||
|
public void setContentEncrypted(final boolean cntEnc)
|
||||||
|
{
|
||||||
|
this.contentEncrypted = cntEnc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param streamNum The streamNumber to set.
|
||||||
|
*/
|
||||||
|
public void setStreamNumber(final int streamNum)
|
||||||
|
{
|
||||||
|
this.streamNumber = streamNum;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param strSpecDataSize The streamSpecificDataSize to set.
|
||||||
|
*/
|
||||||
|
public void setStreamSpecificDataSize(final long strSpecDataSize)
|
||||||
|
{
|
||||||
|
this.streamSpecificDataSize = strSpecDataSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param timeOffs sets the time offset
|
||||||
|
*/
|
||||||
|
public void setTimeOffset(final long timeOffs)
|
||||||
|
{
|
||||||
|
this.timeOffset = timeOffs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param typeSpecDataSize The typeSpecificDataSize to set.
|
||||||
|
*/
|
||||||
|
public void setTypeSpecificDataSize(final long typeSpecDataSize)
|
||||||
|
{
|
||||||
|
this.typeSpecificDataSize = typeSpecDataSize;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,140 @@
|
|||||||
|
/*
|
||||||
|
* Entagged Audio Tag library
|
||||||
|
* Copyright (c) 2004-2005 Christian Laireiter <liree@web.de>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library 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
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package com.mp3.jaudiotagger.audio.asf.data;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public class VideoStreamChunk extends StreamChunk
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the codecs id. Normally the Four-CC (4-Bytes).
|
||||||
|
*/
|
||||||
|
private byte[] codecId = new byte[0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This field stores the height of the video stream.
|
||||||
|
*/
|
||||||
|
private long pictureHeight;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This field stores the width of the video stream.
|
||||||
|
*/
|
||||||
|
private long pictureWidth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*
|
||||||
|
* @param chunkLen Length of the entire chunk (including guid and size)
|
||||||
|
*/
|
||||||
|
public VideoStreamChunk(final BigInteger chunkLen)
|
||||||
|
{
|
||||||
|
super(GUID.GUID_VIDEOSTREAM, chunkLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the codecId.
|
||||||
|
*/
|
||||||
|
public byte[] getCodecId()
|
||||||
|
{
|
||||||
|
return this.codecId.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link #getCodecId()}, as a String, where each byte has been
|
||||||
|
* converted to a <code>char</code>.
|
||||||
|
*
|
||||||
|
* @return Codec Id as String.
|
||||||
|
*/
|
||||||
|
public String getCodecIdAsString()
|
||||||
|
{
|
||||||
|
String result;
|
||||||
|
if (this.codecId == null)
|
||||||
|
{
|
||||||
|
result = "Unknown";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = new String(getCodecId());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the pictureHeight.
|
||||||
|
*/
|
||||||
|
public long getPictureHeight()
|
||||||
|
{
|
||||||
|
return this.pictureHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns the pictureWidth.
|
||||||
|
*/
|
||||||
|
public long getPictureWidth()
|
||||||
|
{
|
||||||
|
return this.pictureWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (overridden)
|
||||||
|
*
|
||||||
|
* @see StreamChunk#prettyPrint(String)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String prettyPrint(final String prefix)
|
||||||
|
{
|
||||||
|
final StringBuilder result = new StringBuilder(super.prettyPrint(prefix));
|
||||||
|
result.insert(0, Utils.LINE_SEPARATOR + prefix + "|->VideoStream");
|
||||||
|
result.append(prefix).append("Video info:").append(Utils.LINE_SEPARATOR);
|
||||||
|
result.append(prefix).append(" |->Width : ").append(getPictureWidth()).append(Utils.LINE_SEPARATOR);
|
||||||
|
result.append(prefix).append(" |->Heigth : ").append(getPictureHeight()).append(Utils.LINE_SEPARATOR);
|
||||||
|
result.append(prefix).append(" |->Codec : ").append(getCodecIdAsString()).append(Utils.LINE_SEPARATOR);
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param codecIdentifier The codecId to set.
|
||||||
|
*/
|
||||||
|
public void setCodecId(final byte[] codecIdentifier)
|
||||||
|
{
|
||||||
|
this.codecId = codecIdentifier.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param picHeight
|
||||||
|
*/
|
||||||
|
public void setPictureHeight(final long picHeight)
|
||||||
|
{
|
||||||
|
this.pictureHeight = picHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param picWidth
|
||||||
|
*/
|
||||||
|
public void setPictureWidth(final long picWidth)
|
||||||
|
{
|
||||||
|
this.pictureWidth = picWidth;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,140 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.asf.io;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.GUID;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This modifier manipulates an ASF header extension object.
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public class AsfExtHeaderModifier implements ChunkModifier
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of modifiers which are to be applied to contained chunks.
|
||||||
|
*/
|
||||||
|
private final List<ChunkModifier> modifierList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.<br>
|
||||||
|
*
|
||||||
|
* @param modifiers modifiers to apply.
|
||||||
|
*/
|
||||||
|
public AsfExtHeaderModifier(final List<ChunkModifier> modifiers)
|
||||||
|
{
|
||||||
|
assert modifiers != null;
|
||||||
|
this.modifierList = new ArrayList<ChunkModifier>(modifiers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simply copies a chunk from <code>source</code> to
|
||||||
|
* <code>destination</code>.<br>
|
||||||
|
* The method assumes, that the GUID has already been read and will write
|
||||||
|
* the provided one to the destination.<br>
|
||||||
|
* The chunk length however will be read and used to determine the amount of
|
||||||
|
* bytes to copy.
|
||||||
|
*
|
||||||
|
* @param guid GUID of the current CHUNK.
|
||||||
|
* @param source source of an ASF chunk, which is to be located at the chunk
|
||||||
|
* length field.
|
||||||
|
* @param destination the destination to copy the chunk to.
|
||||||
|
* @throws IOException on I/O errors.
|
||||||
|
*/
|
||||||
|
private void copyChunk(final GUID guid, final InputStream source, final OutputStream destination) throws IOException
|
||||||
|
{
|
||||||
|
final long chunkSize = Utils.readUINT64(source);
|
||||||
|
destination.write(guid.getBytes());
|
||||||
|
Utils.writeUINT64(chunkSize, destination);
|
||||||
|
Utils.copy(source, destination, chunkSize - 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public boolean isApplicable(final GUID guid)
|
||||||
|
{
|
||||||
|
return GUID.GUID_HEADER_EXTENSION.equals(guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public ModificationResult modify(final GUID guid, final InputStream source, final OutputStream destination) throws IOException
|
||||||
|
{
|
||||||
|
assert GUID.GUID_HEADER_EXTENSION.equals(guid);
|
||||||
|
|
||||||
|
long difference = 0;
|
||||||
|
final List<ChunkModifier> modders = new ArrayList<ChunkModifier>(this.modifierList);
|
||||||
|
final Set<GUID> occuredGuids = new HashSet<GUID>();
|
||||||
|
occuredGuids.add(guid);
|
||||||
|
|
||||||
|
final BigInteger chunkLen = Utils.readBig64(source);
|
||||||
|
final GUID reserved1 = Utils.readGUID(source);
|
||||||
|
final int reserved2 = Utils.readUINT16(source);
|
||||||
|
final long dataSize = Utils.readUINT32(source);
|
||||||
|
|
||||||
|
assert dataSize == 0 || dataSize >= 24;
|
||||||
|
assert chunkLen.subtract(BigInteger.valueOf(46)).longValue() == dataSize;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Stream buffer for the chunk list
|
||||||
|
*/
|
||||||
|
final ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
/*
|
||||||
|
* Stream which counts read bytes. Dirty but quick way of implementing
|
||||||
|
* this.
|
||||||
|
*/
|
||||||
|
final CountingInputStream cis = new CountingInputStream(source);
|
||||||
|
|
||||||
|
while (cis.getReadCount() < dataSize)
|
||||||
|
{
|
||||||
|
// read GUID
|
||||||
|
final GUID curr = Utils.readGUID(cis);
|
||||||
|
boolean handled = false;
|
||||||
|
for (int i = 0; i < modders.size() && !handled; i++)
|
||||||
|
{
|
||||||
|
if (modders.get(i).isApplicable(curr))
|
||||||
|
{
|
||||||
|
final ModificationResult modRes = modders.get(i).modify(curr, cis, bos);
|
||||||
|
difference += modRes.getByteDifference();
|
||||||
|
occuredGuids.addAll(modRes.getOccuredGUIDs());
|
||||||
|
modders.remove(i);
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!handled)
|
||||||
|
{
|
||||||
|
occuredGuids.add(curr);
|
||||||
|
copyChunk(curr, cis, bos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Now apply the left modifiers.
|
||||||
|
for (final ChunkModifier curr : modders)
|
||||||
|
{
|
||||||
|
// chunks, which were not in the source file, will be added to the
|
||||||
|
// destination
|
||||||
|
final ModificationResult result = curr.modify(null, null, bos);
|
||||||
|
difference += result.getByteDifference();
|
||||||
|
occuredGuids.addAll(result.getOccuredGUIDs());
|
||||||
|
}
|
||||||
|
destination.write(GUID.GUID_HEADER_EXTENSION.getBytes());
|
||||||
|
Utils.writeUINT64(chunkLen.add(BigInteger.valueOf(difference)).longValue(), destination);
|
||||||
|
destination.write(reserved1.getBytes());
|
||||||
|
Utils.writeUINT16(reserved2, destination);
|
||||||
|
Utils.writeUINT32(dataSize + difference, destination);
|
||||||
|
destination.write(bos.toByteArray());
|
||||||
|
return new ModificationResult(0, difference, occuredGuids);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.asf.io;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.AsfExtendedHeader;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.GUID;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This reader reads an ASF header extension object from an {@link InputStream}
|
||||||
|
* and creates an {@link AsfExtendedHeader} object.<br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public class AsfExtHeaderReader extends ChunkContainerReader<AsfExtendedHeader>
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GUID this reader {@linkplain #getApplyingIds() applies to}
|
||||||
|
*/
|
||||||
|
private final static GUID[] APPLYING = {GUID.GUID_HEADER_EXTENSION};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a reader instance, which only utilizes the given list of chunk
|
||||||
|
* readers.<br>
|
||||||
|
*
|
||||||
|
* @param toRegister List of {@link ChunkReader} class instances, which are to be
|
||||||
|
* utilized by the instance.
|
||||||
|
* @param readChunkOnce if <code>true</code>, each chunk type (identified by chunk
|
||||||
|
* GUID) will handled only once, if a reader is available, other
|
||||||
|
* chunks will be discarded.
|
||||||
|
*/
|
||||||
|
public AsfExtHeaderReader(final List<Class<? extends ChunkReader>> toRegister, final boolean readChunkOnce)
|
||||||
|
{
|
||||||
|
super(toRegister, readChunkOnce);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public boolean canFail()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected AsfExtendedHeader createContainer(final long streamPosition, final BigInteger chunkLength, final InputStream stream) throws IOException
|
||||||
|
{
|
||||||
|
Utils.readGUID(stream); // First reserved field (should be a specific
|
||||||
|
// GUID.
|
||||||
|
Utils.readUINT16(stream); // Second reserved field (should always be 6)
|
||||||
|
final long extensionSize = Utils.readUINT32(stream);
|
||||||
|
assert extensionSize == 0 || extensionSize >= 24;
|
||||||
|
assert chunkLength.subtract(BigInteger.valueOf(46)).longValue() == extensionSize;
|
||||||
|
return new AsfExtendedHeader(streamPosition, chunkLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public GUID[] getApplyingIds()
|
||||||
|
{
|
||||||
|
return APPLYING.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,236 @@
|
|||||||
|
/*
|
||||||
|
* Entagged Audio Tag library
|
||||||
|
* Copyright (c) 2004-2005 Christian Laireiter <liree@web.de>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library 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
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package com.mp3.jaudiotagger.audio.asf.io;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.AsfHeader;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.GUID;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This <i>class </i> reads an ASF header out of an input stream an creates an
|
||||||
|
* {@link com.mp3.jaudiotagger.audio.asf.data.AsfHeader} object if successful. <br>
|
||||||
|
* For now only ASF ver 1.0 is supported, because ver 2.0 seems not to be used
|
||||||
|
* anywhere. <br>
|
||||||
|
* ASF headers contains other chunks. As of this other readers of current
|
||||||
|
* <b>package </b> are called from within.
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public class AsfHeaderReader extends ChunkContainerReader<AsfHeader>
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The GUID this reader {@linkplain #getApplyingIds() applies to}
|
||||||
|
*/
|
||||||
|
private final static GUID[] APPLYING = {GUID.GUID_HEADER};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ASF reader configured to extract all information.
|
||||||
|
*/
|
||||||
|
private final static AsfHeaderReader FULL_READER;
|
||||||
|
/**
|
||||||
|
* ASF reader configured to just extract information about audio streams.<br>
|
||||||
|
* If the ASF file only contains one audio stream it works fine.<br>
|
||||||
|
*/
|
||||||
|
private final static AsfHeaderReader INFO_READER;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ASF reader configured to just extract metadata information.<br>
|
||||||
|
*/
|
||||||
|
private final static AsfHeaderReader TAG_READER;
|
||||||
|
|
||||||
|
static
|
||||||
|
{
|
||||||
|
final List<Class<? extends ChunkReader>> readers = new ArrayList<Class<? extends ChunkReader>>();
|
||||||
|
readers.add(FileHeaderReader.class);
|
||||||
|
readers.add(StreamChunkReader.class);
|
||||||
|
INFO_READER = new AsfHeaderReader(readers, true);
|
||||||
|
readers.clear();
|
||||||
|
readers.add(ContentDescriptionReader.class);
|
||||||
|
readers.add(ContentBrandingReader.class);
|
||||||
|
readers.add(LanguageListReader.class);
|
||||||
|
readers.add(MetadataReader.class);
|
||||||
|
/*
|
||||||
|
* Create the header extension object readers with just content
|
||||||
|
* description reader, extended content description reader, language
|
||||||
|
* list reader and both metadata object readers.
|
||||||
|
*/
|
||||||
|
final AsfExtHeaderReader extReader = new AsfExtHeaderReader(readers, true);
|
||||||
|
final AsfExtHeaderReader extReader2 = new AsfExtHeaderReader(readers, true);
|
||||||
|
TAG_READER = new AsfHeaderReader(readers, true);
|
||||||
|
TAG_READER.setExtendedHeaderReader(extReader);
|
||||||
|
readers.add(FileHeaderReader.class);
|
||||||
|
readers.add(StreamChunkReader.class);
|
||||||
|
readers.add(EncodingChunkReader.class);
|
||||||
|
readers.add(EncryptionChunkReader.class);
|
||||||
|
readers.add(StreamBitratePropertiesReader.class);
|
||||||
|
FULL_READER = new AsfHeaderReader(readers, false);
|
||||||
|
FULL_READER.setExtendedHeaderReader(extReader2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Stream that will read from the specified
|
||||||
|
* {@link RandomAccessFile};<br>
|
||||||
|
*
|
||||||
|
* @param raf data source to read from.
|
||||||
|
* @return a stream which accesses the source.
|
||||||
|
*/
|
||||||
|
private static InputStream createStream(final RandomAccessFile raf)
|
||||||
|
{
|
||||||
|
return new FullRequestInputStream(new BufferedInputStream(new RandomAccessFileInputstream(raf)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method extracts the full ASF-Header from the given file.<br>
|
||||||
|
* If no header could be extracted <code>null</code> is returned. <br>
|
||||||
|
*
|
||||||
|
* @param file the ASF file to read.<br>
|
||||||
|
* @return AsfHeader-Wrapper, or <code>null</code> if no supported ASF
|
||||||
|
* header was found.
|
||||||
|
* @throws IOException on I/O Errors.
|
||||||
|
*/
|
||||||
|
public static AsfHeader readHeader(final File file) throws IOException
|
||||||
|
{
|
||||||
|
final InputStream stream = new FileInputStream(file);
|
||||||
|
final AsfHeader result = FULL_READER.read(Utils.readGUID(stream), stream, 0);
|
||||||
|
stream.close();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method tries to extract a full ASF-header out of the given stream. <br>
|
||||||
|
* If no header could be extracted <code>null</code> is returned. <br>
|
||||||
|
*
|
||||||
|
* @param file File which contains the ASF header.
|
||||||
|
* @return AsfHeader-Wrapper, or <code>null</code> if no supported ASF
|
||||||
|
* header was found.
|
||||||
|
* @throws IOException Read errors
|
||||||
|
*/
|
||||||
|
public static AsfHeader readHeader(final RandomAccessFile file) throws IOException
|
||||||
|
{
|
||||||
|
final InputStream stream = createStream(file);
|
||||||
|
return FULL_READER.read(Utils.readGUID(stream), stream, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method tries to extract an ASF-header out of the given stream, which
|
||||||
|
* only contains information about the audio stream.<br>
|
||||||
|
* If no header could be extracted <code>null</code> is returned. <br>
|
||||||
|
*
|
||||||
|
* @param file File which contains the ASF header.
|
||||||
|
* @return AsfHeader-Wrapper, or <code>null</code> if no supported ASF
|
||||||
|
* header was found.
|
||||||
|
* @throws IOException Read errors
|
||||||
|
*/
|
||||||
|
public static AsfHeader readInfoHeader(final RandomAccessFile file) throws IOException
|
||||||
|
{
|
||||||
|
final InputStream stream = createStream(file);
|
||||||
|
return INFO_READER.read(Utils.readGUID(stream), stream, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method tries to extract an ASF-header out of the given stream, which
|
||||||
|
* only contains metadata.<br>
|
||||||
|
* If no header could be extracted <code>null</code> is returned. <br>
|
||||||
|
*
|
||||||
|
* @param file File which contains the ASF header.
|
||||||
|
* @return AsfHeader-Wrapper, or <code>null</code> if no supported ASF
|
||||||
|
* header was found.
|
||||||
|
* @throws IOException Read errors
|
||||||
|
*/
|
||||||
|
public static AsfHeader readTagHeader(final RandomAccessFile file) throws IOException
|
||||||
|
{
|
||||||
|
final InputStream stream = createStream(file);
|
||||||
|
return TAG_READER.read(Utils.readGUID(stream), stream, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance of this reader.
|
||||||
|
*
|
||||||
|
* @param toRegister The chunk readers to utilize.
|
||||||
|
* @param readChunkOnce if <code>true</code>, each chunk type (identified by chunk
|
||||||
|
* GUID) will handled only once, if a reader is available, other
|
||||||
|
* chunks will be discarded.
|
||||||
|
*/
|
||||||
|
public AsfHeaderReader(final List<Class<? extends ChunkReader>> toRegister, final boolean readChunkOnce)
|
||||||
|
{
|
||||||
|
super(toRegister, readChunkOnce);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public boolean canFail()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected AsfHeader createContainer(final long streamPosition, final BigInteger chunkLength, final InputStream stream) throws IOException
|
||||||
|
{
|
||||||
|
final long chunkCount = Utils.readUINT32(stream);
|
||||||
|
/*
|
||||||
|
* 2 reserved bytes. first should be equal to 0x01 and second 0x02. ASF
|
||||||
|
* specification suggests to not read the content if second byte is not
|
||||||
|
* 0x02.
|
||||||
|
*/
|
||||||
|
if (stream.read() != 1)
|
||||||
|
{
|
||||||
|
throw new IOException("No ASF"); //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
if (stream.read() != 2)
|
||||||
|
{
|
||||||
|
throw new IOException("No ASF"); //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* Creating the resulting object
|
||||||
|
*/
|
||||||
|
return new AsfHeader(streamPosition, chunkLength, chunkCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public GUID[] getApplyingIds()
|
||||||
|
{
|
||||||
|
return APPLYING.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link AsfExtHeaderReader}, which is to be used, when an header
|
||||||
|
* extension object is found.
|
||||||
|
*
|
||||||
|
* @param extReader header extension object reader.
|
||||||
|
*/
|
||||||
|
public void setExtendedHeaderReader(final AsfExtHeaderReader extReader)
|
||||||
|
{
|
||||||
|
for (final GUID curr : extReader.getApplyingIds())
|
||||||
|
{
|
||||||
|
this.readerMap.put(curr, extReader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,185 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.asf.io;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.GUID;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class creates a modified copy of an ASF file.<br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public class AsfStreamer
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simply copies a chunk from <code>source</code> to
|
||||||
|
* <code>destination</code>.<br>
|
||||||
|
* The method assumes, that the GUID has already been read and will write
|
||||||
|
* the provided one to the destination.<br>
|
||||||
|
* The chunk length however will be read and used to determine the amount of
|
||||||
|
* bytes to copy.
|
||||||
|
*
|
||||||
|
* @param guid GUID of the current chunk.
|
||||||
|
* @param source source of an ASF chunk, which is to be located at the chunk
|
||||||
|
* length field.
|
||||||
|
* @param destination the destination to copy the chunk to.
|
||||||
|
* @throws IOException on I/O errors.
|
||||||
|
*/
|
||||||
|
private void copyChunk(final GUID guid, final InputStream source, final OutputStream destination) throws IOException
|
||||||
|
{
|
||||||
|
final long chunkSize = Utils.readUINT64(source);
|
||||||
|
destination.write(guid.getBytes());
|
||||||
|
Utils.writeUINT64(chunkSize, destination);
|
||||||
|
Utils.copy(source, destination, chunkSize - 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the <code>source</code> and applies the modifications provided by
|
||||||
|
* the given <code>modifiers</code>, and puts it to <code>dest</code>.<br>
|
||||||
|
* Each {@linkplain ChunkModifier modifier} is used only once, so if one
|
||||||
|
* should be used multiple times, it should be added multiple times into the
|
||||||
|
* list.<br>
|
||||||
|
*
|
||||||
|
* @param source the source ASF file
|
||||||
|
* @param dest the destination to write the modified version to.
|
||||||
|
* @param modifiers list of chunk modifiers to apply.
|
||||||
|
* @throws IOException on I/O errors.
|
||||||
|
*/
|
||||||
|
public void createModifiedCopy(final InputStream source, final OutputStream dest, final List<ChunkModifier> modifiers) throws IOException
|
||||||
|
{
|
||||||
|
final List<ChunkModifier> modders = new ArrayList<ChunkModifier>();
|
||||||
|
if (modifiers != null)
|
||||||
|
{
|
||||||
|
modders.addAll(modifiers);
|
||||||
|
}
|
||||||
|
// Read and check ASF GUID
|
||||||
|
final GUID readGUID = Utils.readGUID(source);
|
||||||
|
if (GUID.GUID_HEADER.equals(readGUID))
|
||||||
|
{
|
||||||
|
// used to calculate differences
|
||||||
|
long totalDiff = 0;
|
||||||
|
long chunkDiff = 0;
|
||||||
|
|
||||||
|
// read header information
|
||||||
|
final long headerSize = Utils.readUINT64(source);
|
||||||
|
final long chunkCount = Utils.readUINT32(source);
|
||||||
|
final byte[] reserved = new byte[2];
|
||||||
|
reserved[0] = (byte) (source.read() & 0xFF);
|
||||||
|
reserved[1] = (byte) (source.read() & 0xFF);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* bos will get all unmodified and modified header chunks. This is
|
||||||
|
* necessary, because the header chunk (and file properties chunk)
|
||||||
|
* need to be adjusted but are written in front of the others.
|
||||||
|
*/
|
||||||
|
final ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
// fileHeader will get the binary representation of the file
|
||||||
|
// properties chunk, without GUID
|
||||||
|
byte[] fileHeader = null;
|
||||||
|
|
||||||
|
// Iterate through all chunks
|
||||||
|
for (long i = 0; i < chunkCount; i++)
|
||||||
|
{
|
||||||
|
// Read GUID
|
||||||
|
final GUID curr = Utils.readGUID(source);
|
||||||
|
// special case for file properties chunk
|
||||||
|
if (GUID.GUID_FILE.equals(curr))
|
||||||
|
{
|
||||||
|
final ByteArrayOutputStream tmp = new ByteArrayOutputStream();
|
||||||
|
final long size = Utils.readUINT64(source);
|
||||||
|
Utils.writeUINT64(size, tmp);
|
||||||
|
Utils.copy(source, tmp, size - 24);
|
||||||
|
fileHeader = tmp.toByteArray();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* Now look for ChunkModifier objects which modify the
|
||||||
|
* current chunk
|
||||||
|
*/
|
||||||
|
boolean handled = false;
|
||||||
|
for (int j = 0; j < modders.size() && !handled; j++)
|
||||||
|
{
|
||||||
|
if (modders.get(j).isApplicable(curr))
|
||||||
|
{
|
||||||
|
// alter current chunk
|
||||||
|
final ModificationResult result = modders.get(j).modify(curr, source, bos);
|
||||||
|
// remember size differences.
|
||||||
|
chunkDiff += result.getChunkCountDifference();
|
||||||
|
totalDiff += result.getByteDifference();
|
||||||
|
// remove current modifier from index.
|
||||||
|
modders.remove(j);
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!handled)
|
||||||
|
{
|
||||||
|
// copy chunks which are not modified.
|
||||||
|
copyChunk(curr, source, bos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Now apply the left modifiers.
|
||||||
|
for (final ChunkModifier curr : modders)
|
||||||
|
{
|
||||||
|
// chunks, which were not in the source file, will be added to
|
||||||
|
// the destination
|
||||||
|
final ModificationResult result = curr.modify(null, null, bos);
|
||||||
|
chunkDiff += result.getChunkCountDifference();
|
||||||
|
totalDiff += result.getByteDifference();
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* Now all header objects have been read or manipulated and stored
|
||||||
|
* in the internal buffer (bos).
|
||||||
|
*/
|
||||||
|
// write ASF GUID
|
||||||
|
dest.write(readGUID.getBytes());
|
||||||
|
// write altered header object size
|
||||||
|
Utils.writeUINT64(headerSize + totalDiff, dest);
|
||||||
|
// write altered number of chunks
|
||||||
|
Utils.writeUINT32(chunkCount + chunkDiff, dest);
|
||||||
|
// write the reserved 2 bytes (0x01,0x02).
|
||||||
|
dest.write(reserved);
|
||||||
|
// write the new file header
|
||||||
|
modifyFileHeader(new ByteArrayInputStream(fileHeader), dest, totalDiff);
|
||||||
|
// write the header objects (chunks)
|
||||||
|
dest.write(bos.toByteArray());
|
||||||
|
// copy the rest of the file (data and index)
|
||||||
|
Utils.flush(source, dest);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("No ASF header object.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a slight variation of
|
||||||
|
* {@link #copyChunk(GUID, InputStream, OutputStream)}, it only handles file
|
||||||
|
* property chunks correctly.<br>
|
||||||
|
* The copied chunk will have the file size field modified by the given
|
||||||
|
* <code>fileSizeDiff</code> value.
|
||||||
|
*
|
||||||
|
* @param source source of file properties chunk, located at its chunk length
|
||||||
|
* field.
|
||||||
|
* @param destination the destination to copy the chunk to.
|
||||||
|
* @param fileSizeDiff the difference which should be applied. (negative values would
|
||||||
|
* subtract the stored file size)
|
||||||
|
* @throws IOException on I/O errors.
|
||||||
|
*/
|
||||||
|
private void modifyFileHeader(final InputStream source, final OutputStream destination, final long fileSizeDiff) throws IOException
|
||||||
|
{
|
||||||
|
destination.write(GUID.GUID_FILE.getBytes());
|
||||||
|
final long chunkSize = Utils.readUINT64(source);
|
||||||
|
Utils.writeUINT64(chunkSize, destination);
|
||||||
|
destination.write(Utils.readGUID(source).getBytes());
|
||||||
|
final long fileSize = Utils.readUINT64(source);
|
||||||
|
Utils.writeUINT64(fileSize + fileSizeDiff, destination);
|
||||||
|
Utils.copy(source, destination, chunkSize - 48);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,231 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.asf.io;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.Chunk;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.ChunkContainer;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.GUID;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class represents a reader implementation, which is able to read ASF
|
||||||
|
* objects (chunks) which store other objects (chunks) within them.<br>
|
||||||
|
*
|
||||||
|
* @param <ChunkType> The {@link ChunkContainer} instance, the implementation will
|
||||||
|
* create.
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
abstract class ChunkContainerReader<ChunkType extends ChunkContainer> implements ChunkReader
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logger
|
||||||
|
*/
|
||||||
|
protected static final Logger LOGGER = Logger.getLogger("org.jaudiotabgger.audio"); //$NON-NLS-1$
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Within this range, a {@link ChunkReader} should be aware if it fails.
|
||||||
|
*/
|
||||||
|
public final static int READ_LIMIT = 8192;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If <code>true</code> each chunk type will only be read once.<br>
|
||||||
|
*/
|
||||||
|
protected final boolean eachChunkOnce;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If <code>true</code> due to a {@linkplain #register(Class) registered}
|
||||||
|
* chunk reader, all {@link InputStream} objects passed to
|
||||||
|
* {@link #read(GUID, InputStream, long)} must support mark/reset.
|
||||||
|
*/
|
||||||
|
protected boolean hasFailingReaders = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers GUIDs to their reader classes.<br>
|
||||||
|
*/
|
||||||
|
protected final Map<GUID, ChunkReader> readerMap = new HashMap<GUID, ChunkReader>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a reader instance, which only utilizes the given list of chunk
|
||||||
|
* readers.<br>
|
||||||
|
*
|
||||||
|
* @param toRegister List of {@link ChunkReader} class instances, which are to be
|
||||||
|
* utilized by the instance.
|
||||||
|
* @param readChunkOnce if <code>true</code>, each chunk type (identified by chunk
|
||||||
|
* GUID) will handled only once, if a reader is available, other
|
||||||
|
* chunks will be discarded.
|
||||||
|
*/
|
||||||
|
protected ChunkContainerReader(final List<Class<? extends ChunkReader>> toRegister, final boolean readChunkOnce)
|
||||||
|
{
|
||||||
|
this.eachChunkOnce = readChunkOnce;
|
||||||
|
for (final Class<? extends ChunkReader> curr : toRegister)
|
||||||
|
{
|
||||||
|
register(curr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks for the constraints of this class.
|
||||||
|
*
|
||||||
|
* @param stream stream to test.
|
||||||
|
* @throws IllegalArgumentException If stream does not meet the requirements.
|
||||||
|
*/
|
||||||
|
protected void checkStream(final InputStream stream) throws IllegalArgumentException
|
||||||
|
{
|
||||||
|
if (this.hasFailingReaders && !stream.markSupported())
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("Stream has to support mark/reset.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called by {@link #read(GUID, InputStream, long)} in order
|
||||||
|
* to create the resulting object. Implementations of this class should now
|
||||||
|
* return a new instance of their implementation specific result <b>AND</b>
|
||||||
|
* all data should be read, until the list of chunks starts. (The
|
||||||
|
* {@link ChunkContainer#getChunkEnd()} must return a sane result, too)<br>
|
||||||
|
*
|
||||||
|
* @param streamPosition position of the stream, the chunk starts.
|
||||||
|
* @param chunkLength the length of the chunk (from chunk header)
|
||||||
|
* @param stream to read the implementation specific information.
|
||||||
|
* @return instance of the implementations result.
|
||||||
|
* @throws IOException On I/O Errors and Invalid data.
|
||||||
|
*/
|
||||||
|
abstract protected ChunkType createContainer(long streamPosition, BigInteger chunkLength, InputStream stream) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a configured {@linkplain ChunkReader reader} instance for ASF
|
||||||
|
* objects (chunks) with the specified <code>guid</code>.
|
||||||
|
*
|
||||||
|
* @param guid GUID which identifies the chunk to be read.
|
||||||
|
* @return an appropriate reader implementation, <code>null</code> if not
|
||||||
|
* {@linkplain #register(Class) registered}.
|
||||||
|
*/
|
||||||
|
protected ChunkReader getReader(final GUID guid)
|
||||||
|
{
|
||||||
|
return this.readerMap.get(guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests whether {@link #getReader(GUID)} won't return <code>null</code>.<br>
|
||||||
|
*
|
||||||
|
* @param guid GUID which identifies the chunk to be read.
|
||||||
|
* @return <code>true</code> if a reader is available.
|
||||||
|
*/
|
||||||
|
protected boolean isReaderAvailable(final GUID guid)
|
||||||
|
{
|
||||||
|
return this.readerMap.containsKey(guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This Method implements the reading of a chunk container.<br>
|
||||||
|
*
|
||||||
|
* @param guid GUID of the currently read container.
|
||||||
|
* @param stream Stream which contains the chunk container.
|
||||||
|
* @param chunkStart The start of the chunk container from stream start.<br>
|
||||||
|
* For direct file streams one can assume <code>0</code> here.
|
||||||
|
* @return <code>null</code> if no valid data found, else a Wrapper
|
||||||
|
* containing all supported data.
|
||||||
|
* @throws IOException Read errors.
|
||||||
|
* @throws IllegalArgumentException If one used {@link ChunkReader} could
|
||||||
|
* {@linkplain ChunkReader#canFail() fail} and the stream source
|
||||||
|
* doesn't support mark/reset.
|
||||||
|
*/
|
||||||
|
public ChunkType read(final GUID guid, final InputStream stream, final long chunkStart) throws IOException, IllegalArgumentException
|
||||||
|
{
|
||||||
|
checkStream(stream);
|
||||||
|
final CountingInputStream cis = new CountingInputStream(stream);
|
||||||
|
if (!Arrays.asList(getApplyingIds()).contains(guid))
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("provided GUID is not supported by this reader.");
|
||||||
|
}
|
||||||
|
// For Know the file pointer pointed to an ASF header chunk.
|
||||||
|
final BigInteger chunkLen = Utils.readBig64(cis);
|
||||||
|
/*
|
||||||
|
* now read implementation specific information until the chunk
|
||||||
|
* collection starts and create the resulting object.
|
||||||
|
*/
|
||||||
|
final ChunkType result = createContainer(chunkStart, chunkLen, cis);
|
||||||
|
// 16 bytes have already been for providing the GUID
|
||||||
|
long currentPosition = chunkStart + cis.getReadCount() + 16;
|
||||||
|
|
||||||
|
final HashSet<GUID> alreadyRead = new HashSet<GUID>();
|
||||||
|
/*
|
||||||
|
* Now reading header of chuncks.
|
||||||
|
*/
|
||||||
|
while (currentPosition < result.getChunkEnd())
|
||||||
|
{
|
||||||
|
final GUID currentGUID = Utils.readGUID(cis);
|
||||||
|
final boolean skip = this.eachChunkOnce && (!isReaderAvailable(currentGUID) || !alreadyRead.add(currentGUID));
|
||||||
|
Chunk chunk;
|
||||||
|
/*
|
||||||
|
* If one reader tells it could fail (new method), then check the
|
||||||
|
* input stream for mark/reset. And use it if failed.
|
||||||
|
*/
|
||||||
|
if (!skip && isReaderAvailable(currentGUID))
|
||||||
|
{
|
||||||
|
final ChunkReader reader = getReader(currentGUID);
|
||||||
|
if (reader.canFail())
|
||||||
|
{
|
||||||
|
cis.mark(READ_LIMIT);
|
||||||
|
}
|
||||||
|
chunk = getReader(currentGUID).read(currentGUID, cis, currentPosition);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
chunk = ChunkHeaderReader.getInstance().read(currentGUID, cis, currentPosition);
|
||||||
|
}
|
||||||
|
if (chunk == null)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* Reader failed
|
||||||
|
*/
|
||||||
|
cis.reset();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!skip)
|
||||||
|
{
|
||||||
|
result.addChunk(chunk);
|
||||||
|
}
|
||||||
|
currentPosition = chunk.getChunkEnd();
|
||||||
|
// Always take into account, that 16 bytes have been read prior
|
||||||
|
// to calling this method
|
||||||
|
assert cis.getReadCount() + chunkStart + 16 == currentPosition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the given reader.<br>
|
||||||
|
*
|
||||||
|
* @param <T> The actual reader implementation.
|
||||||
|
* @param toRegister chunk reader which is to be registered.
|
||||||
|
*/
|
||||||
|
private <T extends ChunkReader> void register(final Class<T> toRegister)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
final T reader = toRegister.newInstance();
|
||||||
|
for (final GUID curr : reader.getApplyingIds())
|
||||||
|
{
|
||||||
|
this.readerMap.put(curr, reader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (InstantiationException e)
|
||||||
|
{
|
||||||
|
LOGGER.severe(e.getMessage());
|
||||||
|
}
|
||||||
|
catch (IllegalAccessException e)
|
||||||
|
{
|
||||||
|
LOGGER.severe(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
* Entagged Audio Tag library
|
||||||
|
* Copyright (c) 2004-2005 Christian Laireiter <liree@web.de>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library 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
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package com.mp3.jaudiotagger.audio.asf.io;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.Chunk;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.GUID;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default reader, Reads GUID and size out of an input stream and creates a
|
||||||
|
* {@link com.mp3.jaudiotagger.audio.asf.data.Chunk}object, finally skips the
|
||||||
|
* remaining chunk bytes.
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
final class ChunkHeaderReader implements ChunkReader
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GUID this reader {@linkplain #getApplyingIds() applies to}
|
||||||
|
*/
|
||||||
|
private final static GUID[] APPLYING = {GUID.GUID_UNSPECIFIED};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default instance.
|
||||||
|
*/
|
||||||
|
private static final ChunkHeaderReader INSTANCE = new ChunkHeaderReader();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an instance of the reader.
|
||||||
|
*
|
||||||
|
* @return instance.
|
||||||
|
*/
|
||||||
|
public static ChunkHeaderReader getInstance()
|
||||||
|
{
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hidden Utility class constructor.
|
||||||
|
*/
|
||||||
|
private ChunkHeaderReader()
|
||||||
|
{
|
||||||
|
// Hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public boolean canFail()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public GUID[] getApplyingIds()
|
||||||
|
{
|
||||||
|
return APPLYING.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public Chunk read(final GUID guid, final InputStream stream, final long chunkStart) throws IOException
|
||||||
|
{
|
||||||
|
final BigInteger chunkLen = Utils.readBig64(stream);
|
||||||
|
stream.skip(chunkLen.longValue() - 24);
|
||||||
|
return new Chunk(guid, chunkStart, chunkLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.asf.io;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.GUID;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads an ASF chunk and writes a modified copy.<br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public interface ChunkModifier
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines, whether the modifier handles chunks identified by given
|
||||||
|
* <code>guid</code>.
|
||||||
|
*
|
||||||
|
* @param guid GUID to test.
|
||||||
|
* @return <code>true</code>, if this modifier can be used to modify the
|
||||||
|
* chunk.
|
||||||
|
*/
|
||||||
|
boolean isApplicable(GUID guid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a modified copy of the chunk into the <code>destination.</code>.<br>
|
||||||
|
*
|
||||||
|
* @param guid GUID of the chunk to modify.
|
||||||
|
* @param source a stream providing the chunk, starting at the chunks length
|
||||||
|
* field.
|
||||||
|
* @param destination destination for the modified chunk.
|
||||||
|
* @return the differences between source and destination.
|
||||||
|
* @throws IOException on I/O errors.
|
||||||
|
*/
|
||||||
|
ModificationResult modify(GUID guid, InputStream source, OutputStream destination) throws IOException;
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.asf.io;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.Chunk;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.GUID;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A ChunkReader provides methods for reading an ASF chunk.<br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public interface ChunkReader
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells whether the reader can fail to return a valid chunk.<br>
|
||||||
|
* The current Use would be a modified version of {@link StreamChunkReader},
|
||||||
|
* which is configured to only manage audio streams. However, the primary
|
||||||
|
* GUID for audio and video streams is the same. So if a stream shows itself
|
||||||
|
* to be a video stream, the reader would return <code>null</code>.<br>
|
||||||
|
*
|
||||||
|
* @return <code>true</code>, if further analysis of the chunk can show,
|
||||||
|
* that the reader is not applicable, despite the header GUID
|
||||||
|
* {@linkplain #getApplyingIds() identification} told it can handle
|
||||||
|
* the chunk.
|
||||||
|
*/
|
||||||
|
boolean canFail();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the GUIDs identifying the types of chunk, this reader will parse.<br>
|
||||||
|
*
|
||||||
|
* @return the GUIDs identifying the types of chunk, this reader will parse.<br>
|
||||||
|
*/
|
||||||
|
GUID[] getApplyingIds();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the chunk.
|
||||||
|
*
|
||||||
|
* @param guid the GUID of the chunks header, which is about to be read.
|
||||||
|
* @param stream source to read chunk from.<br>
|
||||||
|
* No {@link GUID} is expected at the currents stream position.
|
||||||
|
* The length of the chunk is about to follow.
|
||||||
|
* @param streamPosition the position in stream, the chunk starts.<br>
|
||||||
|
* @return the read chunk. (Mostly a subclass of {@link Chunk}).<br>
|
||||||
|
* @throws IOException On I/O Errors.
|
||||||
|
*/
|
||||||
|
Chunk read(GUID guid, InputStream stream, long streamPosition) throws IOException;
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.asf.io;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.GUID;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This {@link ChunkModifier} implementation is meant to remove selected chunks.<br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"ManualArrayToCollectionCopy"})
|
||||||
|
public class ChunkRemover implements ChunkModifier
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the GUIDs, which are about to be removed by this modifier.<br>
|
||||||
|
*/
|
||||||
|
private final Set<GUID> toRemove;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance, for removing selected chunks.<br>
|
||||||
|
*
|
||||||
|
* @param guids the GUIDs which are about to be removed by this modifier.
|
||||||
|
*/
|
||||||
|
public ChunkRemover(final GUID... guids)
|
||||||
|
{
|
||||||
|
this.toRemove = new HashSet<GUID>();
|
||||||
|
for (final GUID current : guids)
|
||||||
|
{
|
||||||
|
this.toRemove.add(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public boolean isApplicable(final GUID guid)
|
||||||
|
{
|
||||||
|
return this.toRemove.contains(guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public ModificationResult modify(final GUID guid, final InputStream source, final OutputStream destination) throws IOException
|
||||||
|
{
|
||||||
|
ModificationResult result;
|
||||||
|
if (guid == null)
|
||||||
|
{
|
||||||
|
// Now a chunk should be added, however, this implementation is for
|
||||||
|
// removal.
|
||||||
|
result = new ModificationResult(0, 0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
assert isApplicable(guid);
|
||||||
|
// skip the chunk length minus 24 bytes for the already read length
|
||||||
|
// and the guid.
|
||||||
|
final long chunkLen = Utils.readUINT64(source);
|
||||||
|
source.skip(chunkLen - 24);
|
||||||
|
result = new ModificationResult(-1, -1 * chunkLen, guid);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,74 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.asf.io;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.Chunk;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.ContentBranding;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.GUID;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This reader is used to read the content branding object of ASF streams.<br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
* @see com.mp3.jaudiotagger.audio.asf.data.ContainerType#CONTENT_BRANDING
|
||||||
|
* @see ContentBranding
|
||||||
|
*/
|
||||||
|
public class ContentBrandingReader implements ChunkReader
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The GUID this reader {@linkplain #getApplyingIds() applies to}
|
||||||
|
*/
|
||||||
|
private final static GUID[] APPLYING = {GUID.GUID_CONTENT_BRANDING};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should not be used for now.
|
||||||
|
*/
|
||||||
|
protected ContentBrandingReader()
|
||||||
|
{
|
||||||
|
// NOTHING toDo
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public boolean canFail()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public GUID[] getApplyingIds()
|
||||||
|
{
|
||||||
|
return APPLYING.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public Chunk read(final GUID guid, final InputStream stream, final long streamPosition) throws IOException
|
||||||
|
{
|
||||||
|
assert GUID.GUID_CONTENT_BRANDING.equals(guid);
|
||||||
|
final BigInteger chunkSize = Utils.readBig64(stream);
|
||||||
|
final long imageType = Utils.readUINT32(stream);
|
||||||
|
assert imageType >= 0 && imageType <= 3 : imageType;
|
||||||
|
final long imageDataSize = Utils.readUINT32(stream);
|
||||||
|
assert imageType > 0 || imageDataSize == 0 : imageDataSize;
|
||||||
|
assert imageDataSize < Integer.MAX_VALUE;
|
||||||
|
final byte[] imageData = Utils.readBinary(stream, imageDataSize);
|
||||||
|
final long copyRightUrlLen = Utils.readUINT32(stream);
|
||||||
|
final String copyRight = new String(Utils.readBinary(stream, copyRightUrlLen));
|
||||||
|
final long imageUrlLen = Utils.readUINT32(stream);
|
||||||
|
final String imageUrl = new String(Utils.readBinary(stream, imageUrlLen));
|
||||||
|
final ContentBranding result = new ContentBranding(streamPosition, chunkSize);
|
||||||
|
result.setImage(imageType, imageData);
|
||||||
|
result.setCopyRightURL(copyRight);
|
||||||
|
result.setBannerImageURL(imageUrl);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,134 @@
|
|||||||
|
/*
|
||||||
|
* Entagged Audio Tag library
|
||||||
|
* Copyright (c) 2004-2005 Christian Laireiter <liree@web.de>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library 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
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package com.mp3.jaudiotagger.audio.asf.io;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.Chunk;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.ContentDescription;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.GUID;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads and interprets the data of a ASF chunk containing title, author... <br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
* @see com.mp3.jaudiotagger.audio.asf.data.ContentDescription
|
||||||
|
*/
|
||||||
|
public class ContentDescriptionReader implements ChunkReader
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GUID this reader {@linkplain #getApplyingIds() applies to}
|
||||||
|
*/
|
||||||
|
private final static GUID[] APPLYING = {GUID.GUID_CONTENTDESCRIPTION};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should not be used for now.
|
||||||
|
*/
|
||||||
|
protected ContentDescriptionReader()
|
||||||
|
{
|
||||||
|
// NOTHING toDo
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public boolean canFail()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public GUID[] getApplyingIds()
|
||||||
|
{
|
||||||
|
return APPLYING.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next 5 UINT16 values as an array.<br>
|
||||||
|
*
|
||||||
|
* @param stream stream to read from
|
||||||
|
* @return 5 int values read from stream.
|
||||||
|
* @throws IOException on I/O Errors.
|
||||||
|
*/
|
||||||
|
private int[] getStringSizes(final InputStream stream) throws IOException
|
||||||
|
{
|
||||||
|
final int[] result = new int[5];
|
||||||
|
for (int i = 0; i < result.length; i++)
|
||||||
|
{
|
||||||
|
result[i] = Utils.readUINT16(stream);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public Chunk read(final GUID guid, final InputStream stream, final long chunkStart) throws IOException
|
||||||
|
{
|
||||||
|
final BigInteger chunkSize = Utils.readBig64(stream);
|
||||||
|
/*
|
||||||
|
* Now comes 16-Bit values representing the length of the Strings which
|
||||||
|
* follows.
|
||||||
|
*/
|
||||||
|
final int[] stringSizes = getStringSizes(stream);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Now we know the String length of each occuring String.
|
||||||
|
*/
|
||||||
|
final String[] strings = new String[stringSizes.length];
|
||||||
|
for (int i = 0; i < strings.length; i++)
|
||||||
|
{
|
||||||
|
if (stringSizes[i] > 0)
|
||||||
|
{
|
||||||
|
strings[i] = Utils.readFixedSizeUTF16Str(stream, stringSizes[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* Now create the result
|
||||||
|
*/
|
||||||
|
final ContentDescription result = new ContentDescription(chunkStart, chunkSize);
|
||||||
|
if (stringSizes[0] > 0)
|
||||||
|
{
|
||||||
|
result.setTitle(strings[0]);
|
||||||
|
}
|
||||||
|
if (stringSizes[1] > 0)
|
||||||
|
{
|
||||||
|
result.setAuthor(strings[1]);
|
||||||
|
}
|
||||||
|
if (stringSizes[2] > 0)
|
||||||
|
{
|
||||||
|
result.setCopyright(strings[2]);
|
||||||
|
}
|
||||||
|
if (stringSizes[3] > 0)
|
||||||
|
{
|
||||||
|
result.setComment(strings[3]);
|
||||||
|
}
|
||||||
|
if (stringSizes[4] > 0)
|
||||||
|
{
|
||||||
|
result.setRating(strings[4]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,118 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.asf.io;
|
||||||
|
|
||||||
|
import java.io.FilterInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This implementation of {@link FilterInputStream} counts each read byte.<br>
|
||||||
|
* So at each time, with {@link #getReadCount()} one can determine how many
|
||||||
|
* bytes have been read, by this classes read and skip methods (mark and reset
|
||||||
|
* are also taken into account).<br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
class CountingInputStream extends FilterInputStream
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If {@link #mark(int)} has been called, the current value of
|
||||||
|
* {@link #readCount} is stored, in order to reset it upon {@link #reset()}.
|
||||||
|
*/
|
||||||
|
private long markPos;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The amount of read or skipped bytes.
|
||||||
|
*/
|
||||||
|
private long readCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance, which delegates the commands to the given stream.
|
||||||
|
*
|
||||||
|
* @param stream stream to actually work with.
|
||||||
|
*/
|
||||||
|
public CountingInputStream(final InputStream stream)
|
||||||
|
{
|
||||||
|
super(stream);
|
||||||
|
this.markPos = 0;
|
||||||
|
this.readCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts the given amount of bytes.
|
||||||
|
*
|
||||||
|
* @param amountRead number of bytes to increase.
|
||||||
|
*/
|
||||||
|
private synchronized void bytesRead(final long amountRead)
|
||||||
|
{
|
||||||
|
if (amountRead >= 0)
|
||||||
|
{
|
||||||
|
this.readCount += amountRead;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the readCount
|
||||||
|
*/
|
||||||
|
public synchronized long getReadCount()
|
||||||
|
{
|
||||||
|
return this.readCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public synchronized void mark(final int readlimit)
|
||||||
|
{
|
||||||
|
super.mark(readlimit);
|
||||||
|
this.markPos = this.readCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int read() throws IOException
|
||||||
|
{
|
||||||
|
final int result = super.read();
|
||||||
|
bytesRead(1);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int read(final byte[] destination, final int off, final int len) throws IOException
|
||||||
|
{
|
||||||
|
final int result = super.read(destination, off, len);
|
||||||
|
bytesRead(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public synchronized void reset() throws IOException
|
||||||
|
{
|
||||||
|
super.reset();
|
||||||
|
synchronized (this)
|
||||||
|
{
|
||||||
|
this.readCount = this.markPos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public long skip(final long amount) throws IOException
|
||||||
|
{
|
||||||
|
final long skipped = super.skip(amount);
|
||||||
|
bytesRead(skipped);
|
||||||
|
return skipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,96 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.asf.io;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This output stream wraps around another {@link OutputStream} and delegates
|
||||||
|
* the write calls.<br>
|
||||||
|
* Additionally all written bytes are counted and available by
|
||||||
|
* {@link #getCount()}.
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public class CountingOutputstream extends OutputStream
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the amount of bytes written.
|
||||||
|
*/
|
||||||
|
private long count = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The stream to forward the write calls.
|
||||||
|
*/
|
||||||
|
private final OutputStream wrapped;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance which will delegate the write calls to the given
|
||||||
|
* output stream.
|
||||||
|
*
|
||||||
|
* @param outputStream stream to wrap.
|
||||||
|
*/
|
||||||
|
public CountingOutputstream(final OutputStream outputStream)
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
assert outputStream != null;
|
||||||
|
this.wrapped = outputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException
|
||||||
|
{
|
||||||
|
this.wrapped.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void flush() throws IOException
|
||||||
|
{
|
||||||
|
this.wrapped.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the count
|
||||||
|
*/
|
||||||
|
public long getCount()
|
||||||
|
{
|
||||||
|
return this.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void write(final byte[] bytes) throws IOException
|
||||||
|
{
|
||||||
|
this.wrapped.write(bytes);
|
||||||
|
this.count += bytes.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void write(final byte[] bytes, final int off, final int len) throws IOException
|
||||||
|
{
|
||||||
|
this.wrapped.write(bytes, off, len);
|
||||||
|
this.count += len;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void write(final int toWrite) throws IOException
|
||||||
|
{
|
||||||
|
this.wrapped.write(toWrite);
|
||||||
|
this.count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,105 @@
|
|||||||
|
/*
|
||||||
|
* Entagged Audio Tag library
|
||||||
|
* Copyright (c) 2004-2005 Christian Laireiter <liree@web.de>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library 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
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package com.mp3.jaudiotagger.audio.asf.io;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.Chunk;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.EncodingChunk;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.GUID;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class reads the chunk containing encoding data <br>
|
||||||
|
* <b>Warning:<b><br>
|
||||||
|
* Implementation is not completed. More analysis of this chunk is needed.
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
class EncodingChunkReader implements ChunkReader
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The GUID this reader {@linkplain #getApplyingIds() applies to}
|
||||||
|
*/
|
||||||
|
private final static GUID[] APPLYING = {GUID.GUID_ENCODING};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should not be used for now.
|
||||||
|
*/
|
||||||
|
protected EncodingChunkReader()
|
||||||
|
{
|
||||||
|
// NOTHING toDo
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public boolean canFail()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public GUID[] getApplyingIds()
|
||||||
|
{
|
||||||
|
return APPLYING.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public Chunk read(final GUID guid, final InputStream stream, final long chunkStart) throws IOException
|
||||||
|
{
|
||||||
|
final BigInteger chunkLen = Utils.readBig64(stream);
|
||||||
|
final EncodingChunk result = new EncodingChunk(chunkLen);
|
||||||
|
int readBytes = 24;
|
||||||
|
// Can't be interpreted
|
||||||
|
/*
|
||||||
|
* What do I think of this data, well it seems to be another GUID. Then
|
||||||
|
* followed by a UINT16 indicating a length of data following (by half).
|
||||||
|
* My test files just had the length of one and a two bytes zero.
|
||||||
|
*/
|
||||||
|
stream.skip(20);
|
||||||
|
readBytes += 20;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Read the number of strings which will follow
|
||||||
|
*/
|
||||||
|
final int stringCount = Utils.readUINT16(stream);
|
||||||
|
readBytes += 2;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Now reading the specified amount of strings.
|
||||||
|
*/
|
||||||
|
for (int i = 0; i < stringCount; i++)
|
||||||
|
{
|
||||||
|
final String curr = Utils.readCharacterSizedString(stream);
|
||||||
|
result.addString(curr);
|
||||||
|
readBytes += 4 + 2 * curr.length();
|
||||||
|
}
|
||||||
|
stream.skip(chunkLen.longValue() - readBytes);
|
||||||
|
result.setPosition(chunkStart);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,132 @@
|
|||||||
|
/*
|
||||||
|
* Entagged Audio Tag library
|
||||||
|
* Copyright (c) 2004-2005 Christian Laireiter <liree@web.de>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library 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
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package com.mp3.jaudiotagger.audio.asf.io;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.Chunk;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.EncryptionChunk;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.GUID;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class reads the chunk containing encoding data <br>
|
||||||
|
* <b>Warning:<b><br>
|
||||||
|
* Implementation is not completed. More analysis of this chunk is needed.
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
class EncryptionChunkReader implements ChunkReader
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GUID this reader {@linkplain #getApplyingIds() applies to}
|
||||||
|
*/
|
||||||
|
private final static GUID[] APPLYING = {GUID.GUID_CONTENT_ENCRYPTION};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should not be used for now.
|
||||||
|
*/
|
||||||
|
protected EncryptionChunkReader()
|
||||||
|
{
|
||||||
|
// NOTHING toDo
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public boolean canFail()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public GUID[] getApplyingIds()
|
||||||
|
{
|
||||||
|
return APPLYING.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public Chunk read(final GUID guid, final InputStream stream, final long chunkStart) throws IOException
|
||||||
|
{
|
||||||
|
EncryptionChunk result;
|
||||||
|
final BigInteger chunkLen = Utils.readBig64(stream);
|
||||||
|
result = new EncryptionChunk(chunkLen);
|
||||||
|
|
||||||
|
// Can't be interpreted
|
||||||
|
/*
|
||||||
|
* Object ID GUID 128 Object Size QWORD 64 Secret Data Length DWORD 32
|
||||||
|
* Secret Data INTEGER varies Protection Type Length DWORD 32 Protection
|
||||||
|
* Type char varies Key ID Length DWORD 32 Key ID char varies License
|
||||||
|
* URL Length DWORD 32 License URL char varies * Read the
|
||||||
|
*/
|
||||||
|
byte[] secretData;
|
||||||
|
byte[] protectionType;
|
||||||
|
byte[] keyID;
|
||||||
|
byte[] licenseURL;
|
||||||
|
|
||||||
|
// Secret Data length
|
||||||
|
int fieldLength;
|
||||||
|
fieldLength = (int) Utils.readUINT32(stream);
|
||||||
|
// Secret Data
|
||||||
|
secretData = new byte[fieldLength + 1];
|
||||||
|
stream.read(secretData, 0, fieldLength);
|
||||||
|
secretData[fieldLength] = 0;
|
||||||
|
|
||||||
|
// Protection type Length
|
||||||
|
fieldLength = 0;
|
||||||
|
fieldLength = (int) Utils.readUINT32(stream);
|
||||||
|
// Protection Data Length
|
||||||
|
protectionType = new byte[fieldLength + 1];
|
||||||
|
stream.read(protectionType, 0, fieldLength);
|
||||||
|
protectionType[fieldLength] = 0;
|
||||||
|
|
||||||
|
// Key ID length
|
||||||
|
fieldLength = 0;
|
||||||
|
fieldLength = (int) Utils.readUINT32(stream);
|
||||||
|
// Key ID
|
||||||
|
keyID = new byte[fieldLength + 1];
|
||||||
|
stream.read(keyID, 0, fieldLength);
|
||||||
|
keyID[fieldLength] = 0;
|
||||||
|
|
||||||
|
// License URL length
|
||||||
|
fieldLength = 0;
|
||||||
|
fieldLength = (int) Utils.readUINT32(stream);
|
||||||
|
// License URL
|
||||||
|
licenseURL = new byte[fieldLength + 1];
|
||||||
|
stream.read(licenseURL, 0, fieldLength);
|
||||||
|
licenseURL[fieldLength] = 0;
|
||||||
|
|
||||||
|
result.setSecretData(new String(secretData));
|
||||||
|
result.setProtectionType(new String(protectionType));
|
||||||
|
result.setKeyID(new String(keyID));
|
||||||
|
result.setLicenseURL(new String(licenseURL));
|
||||||
|
|
||||||
|
result.setPosition(chunkStart);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
* Entagged Audio Tag library
|
||||||
|
* Copyright (c) 2004-2005 Christian Laireiter <liree@web.de>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library 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
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package com.mp3.jaudiotagger.audio.asf.io;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.Chunk;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.FileHeader;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.GUID;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads and interprets the data of the file header. <br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public class FileHeaderReader implements ChunkReader
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GUID this reader {@linkplain #getApplyingIds() applies to}
|
||||||
|
*/
|
||||||
|
private final static GUID[] APPLYING = {GUID.GUID_FILE};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should not be used for now.
|
||||||
|
*/
|
||||||
|
protected FileHeaderReader()
|
||||||
|
{
|
||||||
|
// NOTHING toDo
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public boolean canFail()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public GUID[] getApplyingIds()
|
||||||
|
{
|
||||||
|
return APPLYING.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public Chunk read(final GUID guid, final InputStream stream, final long chunkStart) throws IOException
|
||||||
|
{
|
||||||
|
final BigInteger chunkLen = Utils.readBig64(stream);
|
||||||
|
// Skip client GUID.
|
||||||
|
stream.skip(16);
|
||||||
|
final BigInteger fileSize = Utils.readBig64(stream);
|
||||||
|
// fileTime in 100 ns since midnight of 1st january 1601 GMT
|
||||||
|
final BigInteger fileTime = Utils.readBig64(stream);
|
||||||
|
|
||||||
|
final BigInteger packageCount = Utils.readBig64(stream);
|
||||||
|
|
||||||
|
final BigInteger timeEndPos = Utils.readBig64(stream);
|
||||||
|
final BigInteger duration = Utils.readBig64(stream);
|
||||||
|
final BigInteger timeStartPos = Utils.readBig64(stream);
|
||||||
|
|
||||||
|
final long flags = Utils.readUINT32(stream);
|
||||||
|
|
||||||
|
final long minPkgSize = Utils.readUINT32(stream);
|
||||||
|
final long maxPkgSize = Utils.readUINT32(stream);
|
||||||
|
final long uncompressedFrameSize = Utils.readUINT32(stream);
|
||||||
|
|
||||||
|
final FileHeader result = new FileHeader(chunkLen, fileSize, fileTime, packageCount, duration, timeStartPos, timeEndPos, flags, minPkgSize, maxPkgSize, uncompressedFrameSize);
|
||||||
|
result.setPosition(chunkStart);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.asf.io;
|
||||||
|
|
||||||
|
import java.io.FilterInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This implementation repeatedly reads from the wrapped input stream until the
|
||||||
|
* requested amount of bytes are read.<br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public class FullRequestInputStream extends FilterInputStream
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*
|
||||||
|
* @param source stream to read from.
|
||||||
|
*/
|
||||||
|
public FullRequestInputStream(final InputStream source)
|
||||||
|
{
|
||||||
|
super(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int read(final byte[] buffer) throws IOException
|
||||||
|
{
|
||||||
|
return read(buffer, 0, buffer.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int read(final byte[] buffer, final int off, final int len) throws IOException
|
||||||
|
{
|
||||||
|
int totalRead = 0;
|
||||||
|
int read;
|
||||||
|
while (totalRead < len)
|
||||||
|
{
|
||||||
|
read = super.read(buffer, off + totalRead, len - totalRead);
|
||||||
|
if (read >= 0)
|
||||||
|
{
|
||||||
|
totalRead += read;
|
||||||
|
}
|
||||||
|
if (read == -1)
|
||||||
|
{
|
||||||
|
throw new IOException((len - totalRead) + " more bytes expected.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return totalRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public long skip(final long amount) throws IOException
|
||||||
|
{
|
||||||
|
long skipped = 0;
|
||||||
|
int zeroSkipCnt = 0;
|
||||||
|
long currSkipped;
|
||||||
|
while (skipped < amount)
|
||||||
|
{
|
||||||
|
currSkipped = super.skip(amount - skipped);
|
||||||
|
if (currSkipped == 0)
|
||||||
|
{
|
||||||
|
zeroSkipCnt++;
|
||||||
|
if (zeroSkipCnt == 2)
|
||||||
|
{
|
||||||
|
// If the skip value exceeds streams size, this and the
|
||||||
|
// number is extremely large, this can lead to a very long
|
||||||
|
// running loop.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
skipped += currSkipped;
|
||||||
|
}
|
||||||
|
return skipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.asf.io;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.Chunk;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.GUID;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.LanguageList;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads and interprets the "Language List Object" of ASF files.<br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public class LanguageListReader implements ChunkReader
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GUID this reader {@linkplain #getApplyingIds() applies to}
|
||||||
|
*/
|
||||||
|
private final static GUID[] APPLYING = {GUID.GUID_LANGUAGE_LIST};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public boolean canFail()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public GUID[] getApplyingIds()
|
||||||
|
{
|
||||||
|
return APPLYING.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public Chunk read(final GUID guid, final InputStream stream, final long streamPosition) throws IOException
|
||||||
|
{
|
||||||
|
assert GUID.GUID_LANGUAGE_LIST.equals(guid);
|
||||||
|
final BigInteger chunkLen = Utils.readBig64(stream);
|
||||||
|
|
||||||
|
final int readUINT16 = Utils.readUINT16(stream);
|
||||||
|
|
||||||
|
final LanguageList result = new LanguageList(streamPosition, chunkLen);
|
||||||
|
for (int i = 0; i < readUINT16; i++)
|
||||||
|
{
|
||||||
|
final int langIdLen = (stream.read() & 0xFF);
|
||||||
|
final String langId = Utils.readFixedSizeUTF16Str(stream, langIdLen);
|
||||||
|
// langIdLen = 2 bytes for each char and optionally one zero
|
||||||
|
// termination character
|
||||||
|
assert langId.length() == langIdLen / 2 - 1 || langId.length() == langIdLen / 2;
|
||||||
|
result.addLanguage(langId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,154 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.asf.io;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.*;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads an interprets "Metadata Object", "Metadata Library
|
||||||
|
* Object" and "Extended Content Description" of ASF files.<br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public class MetadataReader implements ChunkReader
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GUID this reader {@linkplain #getApplyingIds() applies to}
|
||||||
|
*/
|
||||||
|
private final static GUID[] APPLYING = {ContainerType.EXTENDED_CONTENT.getContainerGUID(), ContainerType.METADATA_OBJECT.getContainerGUID(), ContainerType.METADATA_LIBRARY_OBJECT.getContainerGUID()};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public boolean canFail()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public GUID[] getApplyingIds()
|
||||||
|
{
|
||||||
|
return APPLYING.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public Chunk read(final GUID guid, final InputStream stream, final long streamPosition) throws IOException
|
||||||
|
{
|
||||||
|
final BigInteger chunkLen = Utils.readBig64(stream);
|
||||||
|
|
||||||
|
final MetadataContainer result = new MetadataContainer(guid, streamPosition, chunkLen);
|
||||||
|
// isExtDesc will be set to true, if a extended content description
|
||||||
|
// chunk is read
|
||||||
|
// otherwise it is a metadata object, there are only slight differences
|
||||||
|
final boolean isExtDesc = result.getContainerType() == ContainerType.EXTENDED_CONTENT;
|
||||||
|
final int recordCount = Utils.readUINT16(stream);
|
||||||
|
for (int i = 0; i < recordCount; i++)
|
||||||
|
{
|
||||||
|
int languageIndex = 0;
|
||||||
|
int streamNumber = 0;
|
||||||
|
if (!isExtDesc)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* Metadata objects have a language index and a stream number
|
||||||
|
*/
|
||||||
|
languageIndex = Utils.readUINT16(stream);
|
||||||
|
assert languageIndex >= 0 && languageIndex < MetadataDescriptor.MAX_LANG_INDEX;
|
||||||
|
assert result.getContainerType() == ContainerType.METADATA_LIBRARY_OBJECT || languageIndex == 0;
|
||||||
|
streamNumber = Utils.readUINT16(stream);
|
||||||
|
assert streamNumber >= 0 && streamNumber <= MetadataDescriptor.MAX_STREAM_NUMBER;
|
||||||
|
}
|
||||||
|
final int nameLen = Utils.readUINT16(stream);
|
||||||
|
String recordName = null;
|
||||||
|
if (isExtDesc)
|
||||||
|
{
|
||||||
|
recordName = Utils.readFixedSizeUTF16Str(stream, nameLen);
|
||||||
|
}
|
||||||
|
final int dataType = Utils.readUINT16(stream);
|
||||||
|
assert dataType >= 0 && dataType <= 6;
|
||||||
|
final long dataLen = isExtDesc ? Utils.readUINT16(stream) : Utils.readUINT32(stream);
|
||||||
|
assert dataLen >= 0;
|
||||||
|
assert result.getContainerType() == ContainerType.METADATA_LIBRARY_OBJECT || dataLen <= MetadataDescriptor.DWORD_MAXVALUE;
|
||||||
|
if (!isExtDesc)
|
||||||
|
{
|
||||||
|
recordName = Utils.readFixedSizeUTF16Str(stream, nameLen);
|
||||||
|
}
|
||||||
|
final MetadataDescriptor descriptor = new MetadataDescriptor(result.getContainerType(), recordName, dataType, streamNumber, languageIndex
|
||||||
|
);
|
||||||
|
switch (dataType)
|
||||||
|
{
|
||||||
|
case MetadataDescriptor.TYPE_STRING:
|
||||||
|
descriptor.setStringValue(Utils.readFixedSizeUTF16Str(stream, (int) dataLen));
|
||||||
|
break;
|
||||||
|
case MetadataDescriptor.TYPE_BINARY:
|
||||||
|
descriptor.setBinaryValue(Utils.readBinary(stream, dataLen));
|
||||||
|
break;
|
||||||
|
case MetadataDescriptor.TYPE_BOOLEAN:
|
||||||
|
assert isExtDesc && dataLen == 4 || !isExtDesc && dataLen == 2;
|
||||||
|
descriptor.setBooleanValue(readBoolean(stream, (int) dataLen));
|
||||||
|
break;
|
||||||
|
case MetadataDescriptor.TYPE_DWORD:
|
||||||
|
assert dataLen == 4;
|
||||||
|
descriptor.setDWordValue(Utils.readUINT32(stream));
|
||||||
|
break;
|
||||||
|
case MetadataDescriptor.TYPE_WORD:
|
||||||
|
assert dataLen == 2;
|
||||||
|
descriptor.setWordValue(Utils.readUINT16(stream));
|
||||||
|
break;
|
||||||
|
case MetadataDescriptor.TYPE_QWORD:
|
||||||
|
assert dataLen == 8;
|
||||||
|
descriptor.setQWordValue(Utils.readUINT64(stream));
|
||||||
|
break;
|
||||||
|
case MetadataDescriptor.TYPE_GUID:
|
||||||
|
assert dataLen == GUID.GUID_LENGTH;
|
||||||
|
descriptor.setGUIDValue(Utils.readGUID(stream));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Unknown, hopefully the convention for the size of the
|
||||||
|
// value
|
||||||
|
// is given, so we could read it binary
|
||||||
|
descriptor.setStringValue("Invalid datatype: " + new String(Utils.readBinary(stream, dataLen)));
|
||||||
|
}
|
||||||
|
result.addDescriptor(descriptor);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the given amount of bytes and checks the last byte, if its equal to
|
||||||
|
* one or zero (true / false).<br>
|
||||||
|
* All other bytes must be zero. (if assertions enabled).
|
||||||
|
*
|
||||||
|
* @param stream stream to read from.
|
||||||
|
* @param bytes amount of bytes
|
||||||
|
* @return <code>true</code> or <code>false</code>.
|
||||||
|
* @throws IOException on I/O Errors
|
||||||
|
*/
|
||||||
|
private boolean readBoolean(final InputStream stream, final int bytes) throws IOException
|
||||||
|
{
|
||||||
|
final byte[] tmp = new byte[bytes];
|
||||||
|
stream.read(tmp);
|
||||||
|
boolean result = false;
|
||||||
|
for (int i = 0; i < bytes; i++)
|
||||||
|
{
|
||||||
|
if (i == bytes - 1)
|
||||||
|
{
|
||||||
|
result = tmp[i] == 1;
|
||||||
|
assert tmp[i] == 0 || tmp[i] == 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
assert tmp[i] == 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,94 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.asf.io;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.GUID;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Structure to tell the differences occurred by altering a chunk.
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
final class ModificationResult
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the difference of bytes.<br>
|
||||||
|
*/
|
||||||
|
private final long byteDifference;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the difference of the amount of chunks.<br>
|
||||||
|
* "-1" if the chunk disappeared upon modification.<br>
|
||||||
|
* "0" if the chunk was just modified.<br>
|
||||||
|
* "1" if a chunk has been created.<br>
|
||||||
|
*/
|
||||||
|
private final int chunkDifference;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores all GUIDs, which have been read.<br>
|
||||||
|
*/
|
||||||
|
private final Set<GUID> occuredGUIDs = new HashSet<GUID>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.<br>
|
||||||
|
*
|
||||||
|
* @param chunkCountDiff amount of chunks appeared, disappeared
|
||||||
|
* @param bytesDiffer amount of bytes added or removed.
|
||||||
|
* @param occurred all GUIDs which have been occurred, during processing
|
||||||
|
*/
|
||||||
|
public ModificationResult(final int chunkCountDiff, final long bytesDiffer, final GUID... occurred)
|
||||||
|
{
|
||||||
|
assert occurred != null && occurred.length > 0;
|
||||||
|
this.chunkDifference = chunkCountDiff;
|
||||||
|
this.byteDifference = bytesDiffer;
|
||||||
|
this.occuredGUIDs.addAll(Arrays.asList(occurred));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.<br>
|
||||||
|
*
|
||||||
|
* @param chunkCountDiff amount of chunks appeared, disappeared
|
||||||
|
* @param bytesDiffer amount of bytes added or removed.
|
||||||
|
* @param occurred all GUIDs which have been occurred, during processing
|
||||||
|
*/
|
||||||
|
public ModificationResult(final int chunkCountDiff, final long bytesDiffer, final Set<GUID> occurred)
|
||||||
|
{
|
||||||
|
this.chunkDifference = chunkCountDiff;
|
||||||
|
this.byteDifference = bytesDiffer;
|
||||||
|
this.occuredGUIDs.addAll(occurred);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the difference of bytes.
|
||||||
|
*
|
||||||
|
* @return the byte difference
|
||||||
|
*/
|
||||||
|
public long getByteDifference()
|
||||||
|
{
|
||||||
|
return this.byteDifference;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the difference of the amount of chunks.
|
||||||
|
*
|
||||||
|
* @return the chunk count difference
|
||||||
|
*/
|
||||||
|
public int getChunkCountDifference()
|
||||||
|
{
|
||||||
|
return this.chunkDifference;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all GUIDs which have been occurred during processing.
|
||||||
|
*
|
||||||
|
* @return see description.s
|
||||||
|
*/
|
||||||
|
public Set<GUID> getOccuredGUIDs()
|
||||||
|
{
|
||||||
|
return new HashSet<GUID>(this.occuredGUIDs);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.asf.io;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.RandomAccessFile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps a {@link RandomAccessFile} into an {@link InputStream}.<br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public final class RandomAccessFileInputstream extends InputStream
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The file access to read from.<br>
|
||||||
|
*/
|
||||||
|
private final RandomAccessFile source;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance that will provide {@link InputStream} functionality
|
||||||
|
* on the given {@link RandomAccessFile} by delegating calls.<br>
|
||||||
|
*
|
||||||
|
* @param file The file to read.
|
||||||
|
*/
|
||||||
|
public RandomAccessFileInputstream(final RandomAccessFile file)
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
if (file == null)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("null");
|
||||||
|
}
|
||||||
|
this.source = file;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int read() throws IOException
|
||||||
|
{
|
||||||
|
return this.source.read();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int read(final byte[] buffer, final int off, final int len) throws IOException
|
||||||
|
{
|
||||||
|
return this.source.read(buffer, off, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public long skip(final long amount) throws IOException
|
||||||
|
{
|
||||||
|
if (amount < 0)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("invalid negative value");
|
||||||
|
}
|
||||||
|
long left = amount;
|
||||||
|
while (left > Integer.MAX_VALUE)
|
||||||
|
{
|
||||||
|
this.source.skipBytes(Integer.MAX_VALUE);
|
||||||
|
left -= Integer.MAX_VALUE;
|
||||||
|
}
|
||||||
|
return this.source.skipBytes((int) left);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.asf.io;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.RandomAccessFile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps a {@link RandomAccessFile} into an {@link OutputStream}.<br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public final class RandomAccessFileOutputStream extends OutputStream
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the file to write to.
|
||||||
|
*/
|
||||||
|
private final RandomAccessFile targetFile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.<br>
|
||||||
|
*
|
||||||
|
* @param target file to write to.
|
||||||
|
*/
|
||||||
|
public RandomAccessFileOutputStream(final RandomAccessFile target)
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.targetFile = target;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void write(final byte[] bytes, final int off, final int len) throws IOException
|
||||||
|
{
|
||||||
|
this.targetFile.write(bytes, off, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void write(final int toWrite) throws IOException
|
||||||
|
{
|
||||||
|
this.targetFile.write(toWrite);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* Entagged Audio Tag library
|
||||||
|
* Copyright (c) 2004-2005 Christian Laireiter <liree@web.de>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library 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
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package com.mp3.jaudiotagger.audio.asf.io;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.Chunk;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.GUID;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.StreamBitratePropertiesChunk;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class reads the chunk containing the stream bitrate properties.<br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public class StreamBitratePropertiesReader implements ChunkReader
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GUID this reader {@linkplain #getApplyingIds() applies to}
|
||||||
|
*/
|
||||||
|
private final static GUID[] APPLYING = {GUID.GUID_STREAM_BITRATE_PROPERTIES};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should not be used for now.
|
||||||
|
*/
|
||||||
|
protected StreamBitratePropertiesReader()
|
||||||
|
{
|
||||||
|
// NOTHING toDo
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public boolean canFail()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public GUID[] getApplyingIds()
|
||||||
|
{
|
||||||
|
return APPLYING.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public Chunk read(final GUID guid, final InputStream stream, final long chunkStart) throws IOException
|
||||||
|
{
|
||||||
|
final BigInteger chunkLen = Utils.readBig64(stream);
|
||||||
|
final StreamBitratePropertiesChunk result = new StreamBitratePropertiesChunk(chunkLen);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Read the amount of bitrate records
|
||||||
|
*/
|
||||||
|
final long recordCount = Utils.readUINT16(stream);
|
||||||
|
for (int i = 0; i < recordCount; i++)
|
||||||
|
{
|
||||||
|
final int flags = Utils.readUINT16(stream);
|
||||||
|
final long avgBitrate = Utils.readUINT32(stream);
|
||||||
|
result.addBitrateRecord(flags & 0x00FF, avgBitrate);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.setPosition(chunkStart);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,191 @@
|
|||||||
|
/*
|
||||||
|
* Entagged Audio Tag library
|
||||||
|
* Copyright (c) 2004-2005 Christian Laireiter <liree@web.de>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library 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
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package com.mp3.jaudiotagger.audio.asf.io;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.*;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads and interprets the data of the audio or video stream information chunk. <br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public class StreamChunkReader implements ChunkReader
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GUID this reader {@linkplain #getApplyingIds() applies to}
|
||||||
|
*/
|
||||||
|
private final static GUID[] APPLYING = {GUID.GUID_STREAM};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shouldn't be used for now.
|
||||||
|
*/
|
||||||
|
protected StreamChunkReader()
|
||||||
|
{
|
||||||
|
// Nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public boolean canFail()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public GUID[] getApplyingIds()
|
||||||
|
{
|
||||||
|
return APPLYING.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public Chunk read(final GUID guid, final InputStream stream, final long chunkStart) throws IOException
|
||||||
|
{
|
||||||
|
StreamChunk result = null;
|
||||||
|
final BigInteger chunkLength = Utils.readBig64(stream);
|
||||||
|
// Now comes GUID indicating whether stream content type is audio or
|
||||||
|
// video
|
||||||
|
final GUID streamTypeGUID = Utils.readGUID(stream);
|
||||||
|
if (GUID.GUID_AUDIOSTREAM.equals(streamTypeGUID) || GUID.GUID_VIDEOSTREAM.equals(streamTypeGUID))
|
||||||
|
{
|
||||||
|
|
||||||
|
// A GUID is indicating whether the stream is error
|
||||||
|
// concealed
|
||||||
|
final GUID errorConcealment = Utils.readGUID(stream);
|
||||||
|
/*
|
||||||
|
* Read the Time Offset
|
||||||
|
*/
|
||||||
|
final long timeOffset = Utils.readUINT64(stream);
|
||||||
|
|
||||||
|
final long typeSpecificDataSize = Utils.readUINT32(stream);
|
||||||
|
final long streamSpecificDataSize = Utils.readUINT32(stream);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Read a bit field. (Contains stream number, and whether the stream
|
||||||
|
* content is encrypted.)
|
||||||
|
*/
|
||||||
|
final int mask = Utils.readUINT16(stream);
|
||||||
|
final int streamNumber = mask & 127;
|
||||||
|
final boolean contentEncrypted = (mask & 0x8000) != 0;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Skip a reserved field
|
||||||
|
*/
|
||||||
|
stream.skip(4);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* very important to set for every stream type. The size of bytes
|
||||||
|
* read by the specific stream type, in order to skip the remaining
|
||||||
|
* unread bytes of the stream chunk.
|
||||||
|
*/
|
||||||
|
long streamSpecificBytes;
|
||||||
|
|
||||||
|
if (GUID.GUID_AUDIOSTREAM.equals(streamTypeGUID))
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* Reading audio specific information
|
||||||
|
*/
|
||||||
|
final AudioStreamChunk audioStreamChunk = new AudioStreamChunk(chunkLength);
|
||||||
|
result = audioStreamChunk;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* read WAVEFORMATEX and format extension.
|
||||||
|
*/
|
||||||
|
final long compressionFormat = Utils.readUINT16(stream);
|
||||||
|
final long channelCount = Utils.readUINT16(stream);
|
||||||
|
final long samplingRate = Utils.readUINT32(stream);
|
||||||
|
final long avgBytesPerSec = Utils.readUINT32(stream);
|
||||||
|
final long blockAlignment = Utils.readUINT16(stream);
|
||||||
|
final int bitsPerSample = Utils.readUINT16(stream);
|
||||||
|
final int codecSpecificDataSize = Utils.readUINT16(stream);
|
||||||
|
final byte[] codecSpecificData = new byte[codecSpecificDataSize];
|
||||||
|
stream.read(codecSpecificData);
|
||||||
|
|
||||||
|
audioStreamChunk.setCompressionFormat(compressionFormat);
|
||||||
|
audioStreamChunk.setChannelCount(channelCount);
|
||||||
|
audioStreamChunk.setSamplingRate(samplingRate);
|
||||||
|
audioStreamChunk.setAverageBytesPerSec(avgBytesPerSec);
|
||||||
|
audioStreamChunk.setErrorConcealment(errorConcealment);
|
||||||
|
audioStreamChunk.setBlockAlignment(blockAlignment);
|
||||||
|
audioStreamChunk.setBitsPerSample(bitsPerSample);
|
||||||
|
audioStreamChunk.setCodecData(codecSpecificData);
|
||||||
|
|
||||||
|
streamSpecificBytes = 18 + codecSpecificData.length;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* Reading video specific information
|
||||||
|
*/
|
||||||
|
final VideoStreamChunk videoStreamChunk = new VideoStreamChunk(chunkLength);
|
||||||
|
result = videoStreamChunk;
|
||||||
|
|
||||||
|
final long pictureWidth = Utils.readUINT32(stream);
|
||||||
|
final long pictureHeight = Utils.readUINT32(stream);
|
||||||
|
|
||||||
|
// Skip unknown field
|
||||||
|
stream.skip(1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Now read the format specific data
|
||||||
|
*/
|
||||||
|
// Size of the data section (formatDataSize)
|
||||||
|
stream.skip(2);
|
||||||
|
|
||||||
|
stream.skip(16);
|
||||||
|
final byte[] fourCC = new byte[4];
|
||||||
|
stream.read(fourCC);
|
||||||
|
|
||||||
|
videoStreamChunk.setPictureWidth(pictureWidth);
|
||||||
|
videoStreamChunk.setPictureHeight(pictureHeight);
|
||||||
|
videoStreamChunk.setCodecId(fourCC);
|
||||||
|
|
||||||
|
streamSpecificBytes = 31;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Setting common values for audio and video
|
||||||
|
*/
|
||||||
|
result.setStreamNumber(streamNumber);
|
||||||
|
result.setStreamSpecificDataSize(streamSpecificDataSize);
|
||||||
|
result.setTypeSpecificDataSize(typeSpecificDataSize);
|
||||||
|
result.setTimeOffset(timeOffset);
|
||||||
|
result.setContentEncrypted(contentEncrypted);
|
||||||
|
result.setPosition(chunkStart);
|
||||||
|
/*
|
||||||
|
* Now skip remainder of chunks bytes. chunk-length - 24 (size of
|
||||||
|
* GUID and chunklen) - streamSpecificBytes(stream type specific
|
||||||
|
* data) - 54 (common data)
|
||||||
|
*/
|
||||||
|
stream.skip(chunkLength.longValue() - 24 - streamSpecificBytes - 54);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.asf.io;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.GUID;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementors can write themselves directly to an output stream, and have the
|
||||||
|
* ability to tell the size they would need, as well as determine if they are
|
||||||
|
* empty.<br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public interface WriteableChunk
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method calculates the total amount of bytes, the chunk would consume
|
||||||
|
* in an ASF file.<br>
|
||||||
|
*
|
||||||
|
* @return amount of bytes the chunk would currently need in an ASF file.
|
||||||
|
*/
|
||||||
|
long getCurrentAsfChunkSize();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the GUID of the chunk.
|
||||||
|
*
|
||||||
|
* @return GUID of the chunk.
|
||||||
|
*/
|
||||||
|
GUID getGuid();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <code>true</code> if it is not necessary to write the chunk into an ASF
|
||||||
|
* file, since it contains no information.
|
||||||
|
*
|
||||||
|
* @return <code>true</code> if no useful data will be preserved.
|
||||||
|
*/
|
||||||
|
boolean isEmpty();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the chunk into the specified output stream, as ASF stream chunk.<br>
|
||||||
|
*
|
||||||
|
* @param out stream to write into.
|
||||||
|
* @return amount of bytes written.
|
||||||
|
* @throws IOException on I/O errors
|
||||||
|
*/
|
||||||
|
long writeInto(OutputStream out) throws IOException;
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.asf.io;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.GUID;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.util.Utils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A chunk modifier which works with information provided by
|
||||||
|
* {@link WriteableChunk} objects.<br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public class WriteableChunkModifer implements ChunkModifier
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The chunk to write.
|
||||||
|
*/
|
||||||
|
private final WriteableChunk writableChunk;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.<br>
|
||||||
|
*
|
||||||
|
* @param chunk chunk to write
|
||||||
|
*/
|
||||||
|
public WriteableChunkModifer(final WriteableChunk chunk)
|
||||||
|
{
|
||||||
|
this.writableChunk = chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public boolean isApplicable(final GUID guid)
|
||||||
|
{
|
||||||
|
return guid.equals(this.writableChunk.getGuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public ModificationResult modify(final GUID guid, final InputStream chunk, OutputStream destination) throws IOException
|
||||||
|
{ // NOPMD by Christian Laireiter on 5/9/09 5:03 PM
|
||||||
|
int chunkDiff = 0;
|
||||||
|
long newSize = 0;
|
||||||
|
long oldSize = 0;
|
||||||
|
/*
|
||||||
|
* Replace the outputstream with the counting one, only if assert's are
|
||||||
|
* evaluated.
|
||||||
|
*/
|
||||||
|
assert (destination = new CountingOutputstream(destination)) != null;
|
||||||
|
if (!this.writableChunk.isEmpty())
|
||||||
|
{
|
||||||
|
newSize = this.writableChunk.writeInto(destination);
|
||||||
|
assert newSize == this.writableChunk.getCurrentAsfChunkSize();
|
||||||
|
/*
|
||||||
|
* If assert's are evaluated, we have replaced destination by a
|
||||||
|
* CountingOutpustream and can now verify if
|
||||||
|
* getCurrentAsfChunkSize() really works correctly.
|
||||||
|
*/
|
||||||
|
assert ((CountingOutputstream) destination).getCount() == newSize;
|
||||||
|
if (guid == null)
|
||||||
|
{
|
||||||
|
chunkDiff++;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
if (guid != null)
|
||||||
|
{
|
||||||
|
assert isApplicable(guid);
|
||||||
|
if (this.writableChunk.isEmpty())
|
||||||
|
{
|
||||||
|
chunkDiff--;
|
||||||
|
}
|
||||||
|
oldSize = Utils.readUINT64(chunk);
|
||||||
|
chunk.skip(oldSize - 24);
|
||||||
|
}
|
||||||
|
return new ModificationResult(chunkDiff, (newSize - oldSize), guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Entagged Audio Tag library
|
||||||
|
* Copyright (c) 2004-2005 Christian Laireiter <liree@web.de>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library 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
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package com.mp3.jaudiotagger.audio.asf.util;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.Chunk;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Comparator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is needed for ordering all types of
|
||||||
|
* {@link Chunk}s ascending by their Position. <br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public final class ChunkPositionComparator implements Comparator<Chunk>, Serializable
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private static final long serialVersionUID = -6337108235272376289L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public int compare(final Chunk first, final Chunk second)
|
||||||
|
{
|
||||||
|
return Long.valueOf(first.getPosition()).compareTo(second.getPosition());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,204 @@
|
|||||||
|
/*
|
||||||
|
* Entagged Audio Tag library
|
||||||
|
* Copyright (c) 2004-2005 Christian Laireiter <liree@web.de>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library 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
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package com.mp3.jaudiotagger.audio.asf.util;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.*;
|
||||||
|
import com.mp3.jaudiotagger.tag.FieldKey;
|
||||||
|
import com.mp3.jaudiotagger.tag.Tag;
|
||||||
|
import com.mp3.jaudiotagger.tag.asf.*;
|
||||||
|
import com.mp3.jaudiotagger.tag.reference.GenreTypes;
|
||||||
|
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class provides functionality to convert
|
||||||
|
* {@link AsfHeader}objects into
|
||||||
|
* {@link Tag}objects.<br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter (liree)
|
||||||
|
*/
|
||||||
|
public final class TagConverter
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method assigns those tags of <code>tag</code> which are defined to
|
||||||
|
* be common by jaudiotagger. <br>
|
||||||
|
*
|
||||||
|
* @param tag The tag from which the values are gathered. <br>
|
||||||
|
* Assigned values are: <br>
|
||||||
|
* @param description The extended content description which should receive the
|
||||||
|
* values. <br>
|
||||||
|
* <b>Warning: </b> the common values will be replaced.
|
||||||
|
*/
|
||||||
|
public static void assignCommonTagValues(Tag tag, MetadataContainer description)
|
||||||
|
{
|
||||||
|
assert description.getContainerType() == ContainerType.EXTENDED_CONTENT;
|
||||||
|
MetadataDescriptor tmp;
|
||||||
|
if (!Utils.isBlank(tag.getFirst(FieldKey.ALBUM)))
|
||||||
|
{
|
||||||
|
tmp = new MetadataDescriptor(description.getContainerType(), AsfFieldKey.ALBUM.getFieldName(), MetadataDescriptor.TYPE_STRING);
|
||||||
|
tmp.setStringValue(tag.getFirst(FieldKey.ALBUM));
|
||||||
|
description.removeDescriptorsByName(tmp.getName());
|
||||||
|
description.addDescriptor(tmp);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
description.removeDescriptorsByName(AsfFieldKey.ALBUM.getFieldName());
|
||||||
|
}
|
||||||
|
if (!Utils.isBlank(tag.getFirst(FieldKey.TRACK)))
|
||||||
|
{
|
||||||
|
tmp = new MetadataDescriptor(description.getContainerType(), AsfFieldKey.TRACK.getFieldName(), MetadataDescriptor.TYPE_STRING);
|
||||||
|
tmp.setStringValue(tag.getFirst(FieldKey.TRACK));
|
||||||
|
description.removeDescriptorsByName(tmp.getName());
|
||||||
|
description.addDescriptor(tmp);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
description.removeDescriptorsByName(AsfFieldKey.TRACK.getFieldName());
|
||||||
|
}
|
||||||
|
if (!Utils.isBlank(tag.getFirst(FieldKey.YEAR)))
|
||||||
|
{
|
||||||
|
tmp = new MetadataDescriptor(description.getContainerType(), AsfFieldKey.YEAR.getFieldName(), MetadataDescriptor.TYPE_STRING);
|
||||||
|
tmp.setStringValue(tag.getFirst(FieldKey.YEAR));
|
||||||
|
description.removeDescriptorsByName(tmp.getName());
|
||||||
|
description.addDescriptor(tmp);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
description.removeDescriptorsByName(AsfFieldKey.YEAR.getFieldName());
|
||||||
|
}
|
||||||
|
if (!Utils.isBlank(tag.getFirst(FieldKey.GENRE)))
|
||||||
|
{
|
||||||
|
// Write Genre String value
|
||||||
|
tmp = new MetadataDescriptor(description.getContainerType(), AsfFieldKey.GENRE.getFieldName(), MetadataDescriptor.TYPE_STRING);
|
||||||
|
tmp.setStringValue(tag.getFirst(FieldKey.GENRE));
|
||||||
|
description.removeDescriptorsByName(tmp.getName());
|
||||||
|
description.addDescriptor(tmp);
|
||||||
|
Integer genreNum = GenreTypes.getInstanceOf().getIdForName(tag.getFirst(FieldKey.GENRE));
|
||||||
|
// ..and if it is one of the standard genre types used the id as
|
||||||
|
// well
|
||||||
|
if (genreNum != null)
|
||||||
|
{
|
||||||
|
tmp = new MetadataDescriptor(description.getContainerType(), AsfFieldKey.GENRE_ID.getFieldName(), MetadataDescriptor.TYPE_STRING);
|
||||||
|
tmp.setStringValue("(" + genreNum + ")");
|
||||||
|
description.removeDescriptorsByName(tmp.getName());
|
||||||
|
description.addDescriptor(tmp);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
description.removeDescriptorsByName(AsfFieldKey.GENRE_ID.getFieldName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
description.removeDescriptorsByName(AsfFieldKey.GENRE.getFieldName());
|
||||||
|
description.removeDescriptorsByName(AsfFieldKey.GENRE_ID.getFieldName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method creates a {@link Tag}and fills it with the contents of the
|
||||||
|
* given {@link AsfHeader}.<br>
|
||||||
|
*
|
||||||
|
* @param source The ASF header which contains the information. <br>
|
||||||
|
* @return A Tag with all its values.
|
||||||
|
*/
|
||||||
|
public static AsfTag createTagOf(AsfHeader source)
|
||||||
|
{
|
||||||
|
// TODO do we need to copy here.
|
||||||
|
AsfTag result = new AsfTag(true);
|
||||||
|
for (int i = 0; i < ContainerType.values().length; i++)
|
||||||
|
{
|
||||||
|
MetadataContainer current = source.findMetadataContainer(ContainerType.values()[i]);
|
||||||
|
if (current != null)
|
||||||
|
{
|
||||||
|
List<MetadataDescriptor> descriptors = current.getDescriptors();
|
||||||
|
for (MetadataDescriptor descriptor : descriptors)
|
||||||
|
{
|
||||||
|
AsfTagField toAdd;
|
||||||
|
if (descriptor.getType() == MetadataDescriptor.TYPE_BINARY)
|
||||||
|
{
|
||||||
|
if (descriptor.getName().equals(AsfFieldKey.COVER_ART.getFieldName()))
|
||||||
|
{
|
||||||
|
toAdd = new AsfTagCoverField(descriptor);
|
||||||
|
}
|
||||||
|
else if (descriptor.getName().equals(AsfFieldKey.BANNER_IMAGE.getFieldName()))
|
||||||
|
{
|
||||||
|
toAdd = new AsfTagBannerField(descriptor);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
toAdd = new AsfTagField(descriptor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
toAdd = new AsfTagTextField(descriptor);
|
||||||
|
}
|
||||||
|
result.addField(toAdd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method distributes the tags fields among the
|
||||||
|
* {@linkplain ContainerType#getOrdered()} {@linkplain MetadataContainer
|
||||||
|
* containers}.
|
||||||
|
*
|
||||||
|
* @param tag the tag with the fields to distribute.
|
||||||
|
* @return distribution
|
||||||
|
*/
|
||||||
|
public static MetadataContainer[] distributeMetadata(final AsfTag tag)
|
||||||
|
{
|
||||||
|
final Iterator<AsfTagField> asfFields = tag.getAsfFields();
|
||||||
|
final MetadataContainer[] createContainers = MetadataContainerFactory.getInstance().createContainers(ContainerType.getOrdered());
|
||||||
|
boolean assigned;
|
||||||
|
AsfTagField current;
|
||||||
|
while (asfFields.hasNext())
|
||||||
|
{
|
||||||
|
current = asfFields.next();
|
||||||
|
assigned = false;
|
||||||
|
for (int i = 0; !assigned && i < createContainers.length; i++)
|
||||||
|
{
|
||||||
|
if (ContainerType.areInCorrectOrder(createContainers[i].getContainerType(), AsfFieldKey.getAsfFieldKey(current.getId()).getHighestContainer()))
|
||||||
|
{
|
||||||
|
if (createContainers[i].isAddSupported(current.getDescriptor()))
|
||||||
|
{
|
||||||
|
createContainers[i].addDescriptor(current.getDescriptor());
|
||||||
|
assigned = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert assigned;
|
||||||
|
}
|
||||||
|
return createContainers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hidden utility class constructor.
|
||||||
|
*/
|
||||||
|
private TagConverter()
|
||||||
|
{
|
||||||
|
// Nothing to do.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,536 @@
|
|||||||
|
/*
|
||||||
|
* Entagged Audio Tag library
|
||||||
|
* Copyright (c) 2004-2005 Christian Laireiter <liree@web.de>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library 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
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
package com.mp3.jaudiotagger.audio.asf.util;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.AsfHeader;
|
||||||
|
import com.mp3.jaudiotagger.audio.asf.data.GUID;
|
||||||
|
import com.mp3.jaudiotagger.logging.ErrorMessage;
|
||||||
|
|
||||||
|
import java.io.EOFException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.GregorianCalendar;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some static Methods which are used in several Classes. <br>
|
||||||
|
*
|
||||||
|
* @author Christian Laireiter
|
||||||
|
*/
|
||||||
|
public class Utils
|
||||||
|
{
|
||||||
|
|
||||||
|
public static final long DIFF_BETWEEN_ASF_DATE_AND_JAVA_DATE = 11644470000000l;
|
||||||
|
/**
|
||||||
|
* Stores the default line separator of the current underlying system.
|
||||||
|
*/
|
||||||
|
public final static String LINE_SEPARATOR = System.getProperty("line.separator"); //$NON-NLS-1$
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private static final int MAXIMUM_STRING_LENGTH_ALLOWED = 32766;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method checks given string will not exceed limit in bytes[] when
|
||||||
|
* converted UTF-16LE encoding (2 bytes per character) and checks whether
|
||||||
|
* the length doesn't exceed 65535 bytes. <br>
|
||||||
|
*
|
||||||
|
* @param value The string to check.
|
||||||
|
* @throws IllegalArgumentException If byte representation takes more than 65535 bytes.
|
||||||
|
*/
|
||||||
|
public static void checkStringLengthNullSafe(String value) throws IllegalArgumentException
|
||||||
|
{
|
||||||
|
if (value != null)
|
||||||
|
{
|
||||||
|
if (value.length() > MAXIMUM_STRING_LENGTH_ALLOWED)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException(ErrorMessage.WMA_LENGTH_OF_STRING_IS_TOO_LARGE.getMsg((value.length() * 2))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param value String to check for null
|
||||||
|
* @return true unless string is too long
|
||||||
|
*/
|
||||||
|
public static boolean isStringLengthValidNullSafe(String value)
|
||||||
|
{
|
||||||
|
if (value != null)
|
||||||
|
{
|
||||||
|
if (value.length() > MAXIMUM_STRING_LENGTH_ALLOWED)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* effectively copies a specified amount of bytes from one stream to
|
||||||
|
* another.
|
||||||
|
*
|
||||||
|
* @param source stream to read from
|
||||||
|
* @param dest stream to write to
|
||||||
|
* @param amount amount of bytes to copy
|
||||||
|
* @throws IOException on I/O errors, and if the source stream depletes before all
|
||||||
|
* bytes have been copied.
|
||||||
|
*/
|
||||||
|
public static void copy(InputStream source, OutputStream dest, long amount) throws IOException
|
||||||
|
{
|
||||||
|
byte[] buf = new byte[8192];
|
||||||
|
long copied = 0;
|
||||||
|
while (copied < amount)
|
||||||
|
{
|
||||||
|
int toRead = 8192;
|
||||||
|
if ((amount - copied) < 8192)
|
||||||
|
{
|
||||||
|
toRead = (int) (amount - copied);
|
||||||
|
}
|
||||||
|
int read = source.read(buf, 0, toRead);
|
||||||
|
if (read == -1)
|
||||||
|
{
|
||||||
|
throw new IOException("Inputstream has to continue for another " + (amount - copied) + " bytes."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
dest.write(buf, 0, read);
|
||||||
|
copied += read;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies all of the source to the destination.<br>
|
||||||
|
*
|
||||||
|
* @param source source to read from
|
||||||
|
* @param dest stream to write to
|
||||||
|
* @throws IOException on I/O errors.
|
||||||
|
*/
|
||||||
|
public static void flush(final InputStream source, final OutputStream dest) throws IOException
|
||||||
|
{
|
||||||
|
final byte[] buf = new byte[8192];
|
||||||
|
int read;
|
||||||
|
while ((read = source.read(buf)) != -1)
|
||||||
|
{
|
||||||
|
dest.write(buf, 0, read);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method will create a byte[] at the size of <code>byteCount</code>
|
||||||
|
* and insert the bytes of <code>value</code> (starting from lowset byte)
|
||||||
|
* into it. <br>
|
||||||
|
* You can easily create a Word (16-bit), DWORD (32-bit), QWORD (64 bit) out
|
||||||
|
* of the value, ignoring the original type of value, since java
|
||||||
|
* automatically performs transformations. <br>
|
||||||
|
* <b>Warning: </b> This method works with unsigned numbers only.
|
||||||
|
*
|
||||||
|
* @param value The value to be written into the result.
|
||||||
|
* @param byteCount The number of bytes the array has got.
|
||||||
|
* @return A byte[] with the size of <code>byteCount</code> containing the
|
||||||
|
* lower byte values of <code>value</code>.
|
||||||
|
*/
|
||||||
|
public static byte[] getBytes(final long value, final int byteCount)
|
||||||
|
{
|
||||||
|
byte[] result = new byte[byteCount];
|
||||||
|
for (int i = 0; i < result.length; i++)
|
||||||
|
{
|
||||||
|
result[i] = (byte) ((value >>> (i * 8)) & 0xFF);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method to convert the given string into a byte sequence which
|
||||||
|
* has the format of the charset given.
|
||||||
|
*
|
||||||
|
* @param source string to convert.
|
||||||
|
* @param charset charset to apply
|
||||||
|
* @return the source's binary representation according to the charset.
|
||||||
|
*/
|
||||||
|
public static byte[] getBytes(final String source, final Charset charset)
|
||||||
|
{
|
||||||
|
assert charset != null;
|
||||||
|
assert source != null;
|
||||||
|
final ByteBuffer encoded = charset.encode(source);
|
||||||
|
final byte[] result = new byte[encoded.limit()];
|
||||||
|
encoded.rewind();
|
||||||
|
encoded.get(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Since date values in ASF files are given in 100 ns steps since first
|
||||||
|
* january of 1601 a little conversion must be done. <br>
|
||||||
|
* This method converts a date given in described manner to a calendar.
|
||||||
|
*
|
||||||
|
* @param fileTime
|
||||||
|
* Time in 100ns since 1 jan 1601
|
||||||
|
* @return Calendar holding the date representation.
|
||||||
|
*/
|
||||||
|
/* Old method that ran very slowely and doesnt logical correct, how does dividing something
|
||||||
|
at 10-4 by 10,000 convert it to 10 -3
|
||||||
|
public static GregorianCalendar getDateOf(final BigInteger fileTime) {
|
||||||
|
final GregorianCalendar result = new GregorianCalendar(1601, 0, 1);
|
||||||
|
// lose anything beyond milliseconds, because calendar can't handle
|
||||||
|
// less value
|
||||||
|
BigInteger time = fileTime.divide(new BigInteger("10000")); //$NON-NLS-1$
|
||||||
|
final BigInteger maxInt = new BigInteger(String
|
||||||
|
.valueOf(Integer.MAX_VALUE));
|
||||||
|
while (time.compareTo(maxInt) > 0) {
|
||||||
|
result.add(Calendar.MILLISECOND, Integer.MAX_VALUE);
|
||||||
|
time = time.subtract(maxInt);
|
||||||
|
}
|
||||||
|
result.add(Calendar.MILLISECOND, time.intValue());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Date values in ASF files are given in 100 ns (10 exp -4) steps since first
|
||||||
|
*
|
||||||
|
* @param fileTime Time in 100ns since 1 jan 1601
|
||||||
|
* @return Calendar holding the date representation.
|
||||||
|
*/
|
||||||
|
public static GregorianCalendar getDateOf(final BigInteger fileTime)
|
||||||
|
{
|
||||||
|
final GregorianCalendar result = new GregorianCalendar();
|
||||||
|
|
||||||
|
// Divide by 10 to convert from -4 to -3 (millisecs)
|
||||||
|
BigInteger time = fileTime.divide(new BigInteger("10"));
|
||||||
|
// Construct Date taking into the diff between 1601 and 1970
|
||||||
|
Date date = new Date(time.longValue() - DIFF_BETWEEN_ASF_DATE_AND_JAVA_DATE);
|
||||||
|
result.setTime(date);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests if the given string is <code>null</code> or just contains
|
||||||
|
* whitespace characters.
|
||||||
|
*
|
||||||
|
* @param toTest String to test.
|
||||||
|
* @return see description.
|
||||||
|
*/
|
||||||
|
public static boolean isBlank(String toTest)
|
||||||
|
{
|
||||||
|
if (toTest == null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < toTest.length(); i++)
|
||||||
|
{
|
||||||
|
if (!Character.isWhitespace(toTest.charAt(i)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads 8 bytes from stream and interprets them as a UINT64 which is
|
||||||
|
* returned as {@link BigInteger}.<br>
|
||||||
|
*
|
||||||
|
* @param stream stream to readm from.
|
||||||
|
* @return a BigInteger which represents the read 8 bytes value.
|
||||||
|
* @throws IOException if problem reading bytes
|
||||||
|
*/
|
||||||
|
public static BigInteger readBig64(InputStream stream) throws IOException
|
||||||
|
{
|
||||||
|
byte[] bytes = new byte[8];
|
||||||
|
byte[] oa = new byte[8];
|
||||||
|
int read = stream.read(bytes);
|
||||||
|
if (read != 8)
|
||||||
|
{
|
||||||
|
// 8 bytes mandatory.
|
||||||
|
throw new EOFException();
|
||||||
|
}
|
||||||
|
for (int i = 0; i < bytes.length; i++)
|
||||||
|
{
|
||||||
|
oa[7 - i] = bytes[i];
|
||||||
|
}
|
||||||
|
return new BigInteger(oa);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads <code>size</code> bytes from the stream.<br>
|
||||||
|
*
|
||||||
|
* @param stream stream to read from.
|
||||||
|
* @param size amount of bytes to read.
|
||||||
|
* @return the read bytes.
|
||||||
|
* @throws IOException on I/O errors.
|
||||||
|
*/
|
||||||
|
public static byte[] readBinary(InputStream stream, long size) throws IOException
|
||||||
|
{
|
||||||
|
byte[] result = new byte[(int) size];
|
||||||
|
stream.read(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method reads a UTF-16 String, which length is given on the number of
|
||||||
|
* characters it consists of. <br>
|
||||||
|
* The stream must be at the number of characters. This number contains the
|
||||||
|
* terminating zero character (UINT16).
|
||||||
|
*
|
||||||
|
* @param stream Input source
|
||||||
|
* @return String
|
||||||
|
* @throws IOException read errors
|
||||||
|
*/
|
||||||
|
public static String readCharacterSizedString(InputStream stream) throws IOException
|
||||||
|
{
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
int strLen = readUINT16(stream);
|
||||||
|
int character = stream.read();
|
||||||
|
character |= stream.read() << 8;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if (character != 0)
|
||||||
|
{
|
||||||
|
result.append((char) character);
|
||||||
|
character = stream.read();
|
||||||
|
character |= stream.read() << 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (character != 0 || (result.length() + 1) > strLen);
|
||||||
|
if (strLen != (result.length() + 1))
|
||||||
|
{
|
||||||
|
throw new IllegalStateException("Invalid Data for current interpretation"); //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method reads a UTF-16 encoded String. <br>
|
||||||
|
* For the use this method the number of bytes used by current string must
|
||||||
|
* be known. <br>
|
||||||
|
* The ASF specification recommends that those strings end with a
|
||||||
|
* terminating zero. However it also says that it is not always the case.
|
||||||
|
*
|
||||||
|
* @param stream Input source
|
||||||
|
* @param strLen Number of bytes the String may take.
|
||||||
|
* @return read String.
|
||||||
|
* @throws IOException read errors.
|
||||||
|
*/
|
||||||
|
public static String readFixedSizeUTF16Str(InputStream stream, int strLen) throws IOException
|
||||||
|
{
|
||||||
|
byte[] strBytes = new byte[strLen];
|
||||||
|
int read = stream.read(strBytes);
|
||||||
|
if (read == strBytes.length)
|
||||||
|
{
|
||||||
|
if (strBytes.length >= 2)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* Zero termination is recommended but optional. So check and
|
||||||
|
* if, remove.
|
||||||
|
*/
|
||||||
|
if (strBytes[strBytes.length - 1] == 0 && strBytes[strBytes.length - 2] == 0)
|
||||||
|
{
|
||||||
|
byte[] copy = new byte[strBytes.length - 2];
|
||||||
|
System.arraycopy(strBytes, 0, copy, 0, strBytes.length - 2);
|
||||||
|
strBytes = copy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new String(strBytes, "UTF-16LE");
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Couldn't read the necessary amount of bytes.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This Method reads a GUID (which is a 16 byte long sequence) from the
|
||||||
|
* given <code>raf</code> and creates a wrapper. <br>
|
||||||
|
* <b>Warning </b>: <br>
|
||||||
|
* There is no way of telling if a byte sequence is a guid or not. The next
|
||||||
|
* 16 bytes will be interpreted as a guid, whether it is or not.
|
||||||
|
*
|
||||||
|
* @param stream Input source.
|
||||||
|
* @return A class wrapping the guid.
|
||||||
|
* @throws IOException happens when the file ends before guid could be extracted.
|
||||||
|
*/
|
||||||
|
public static GUID readGUID(InputStream stream) throws IOException
|
||||||
|
{
|
||||||
|
if (stream == null)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("Argument must not be null"); //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
int[] binaryGuid = new int[GUID.GUID_LENGTH];
|
||||||
|
for (int i = 0; i < binaryGuid.length; i++)
|
||||||
|
{
|
||||||
|
binaryGuid[i] = stream.read();
|
||||||
|
}
|
||||||
|
return new GUID(binaryGuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads 2 bytes from stream and interprets them as UINT16.<br>
|
||||||
|
*
|
||||||
|
* @param stream stream to read from.
|
||||||
|
* @return UINT16 value
|
||||||
|
* @throws IOException on I/O Errors.
|
||||||
|
*/
|
||||||
|
public static int readUINT16(InputStream stream) throws IOException
|
||||||
|
{
|
||||||
|
int result = stream.read();
|
||||||
|
result |= stream.read() << 8;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads 4 bytes from stream and interprets them as UINT32.<br>
|
||||||
|
*
|
||||||
|
* @param stream stream to read from.
|
||||||
|
* @return UINT32 value
|
||||||
|
* @throws IOException on I/O Errors.
|
||||||
|
*/
|
||||||
|
public static long readUINT32(InputStream stream) throws IOException
|
||||||
|
{
|
||||||
|
long result = 0;
|
||||||
|
for (int i = 0; i <= 24; i += 8)
|
||||||
|
{
|
||||||
|
// Warning, always cast to long here. Otherwise it will be
|
||||||
|
// shifted as int, which may produce a negative value, which will
|
||||||
|
// then be extended to long and assign the long variable a negative
|
||||||
|
// value.
|
||||||
|
result |= (long) stream.read() << i;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads long as little endian.
|
||||||
|
*
|
||||||
|
* @param stream Data source
|
||||||
|
* @return long value
|
||||||
|
* @throws IOException read error, or eof is reached before long is completed
|
||||||
|
*/
|
||||||
|
public static long readUINT64(InputStream stream) throws IOException
|
||||||
|
{
|
||||||
|
long result = 0;
|
||||||
|
for (int i = 0; i <= 56; i += 8)
|
||||||
|
{
|
||||||
|
// Warning, always cast to long here. Otherwise it will be
|
||||||
|
// shifted as int, which may produce a negative value, which will
|
||||||
|
// then be extended to long and assign the long variable a negative
|
||||||
|
// value.
|
||||||
|
result |= (long) stream.read() << i;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method reads a UTF-16 encoded String, beginning with a 16-bit value
|
||||||
|
* representing the number of bytes needed. The String is terminated with as
|
||||||
|
* 16-bit ZERO. <br>
|
||||||
|
*
|
||||||
|
* @param stream Input source
|
||||||
|
* @return read String.
|
||||||
|
* @throws IOException read errors.
|
||||||
|
*/
|
||||||
|
public static String readUTF16LEStr(InputStream stream) throws IOException
|
||||||
|
{
|
||||||
|
int strLen = readUINT16(stream);
|
||||||
|
byte[] buf = new byte[strLen];
|
||||||
|
int read = stream.read(buf);
|
||||||
|
if (read == strLen || (strLen == 0 && read == -1))
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* Check on zero termination
|
||||||
|
*/
|
||||||
|
if (buf.length >= 2)
|
||||||
|
{
|
||||||
|
if (buf[buf.length - 1] == 0 && buf[buf.length - 2] == 0)
|
||||||
|
{
|
||||||
|
byte[] copy = new byte[buf.length - 2];
|
||||||
|
System.arraycopy(buf, 0, copy, 0, buf.length - 2);
|
||||||
|
buf = copy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new String(buf, AsfHeader.ASF_CHARSET.name());
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Invalid Data for current interpretation"); //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the given value as UINT16 into the stream.
|
||||||
|
*
|
||||||
|
* @param number value to write.
|
||||||
|
* @param out stream to write into.
|
||||||
|
* @throws IOException On I/O errors
|
||||||
|
*/
|
||||||
|
public static void writeUINT16(int number, OutputStream out) throws IOException
|
||||||
|
{
|
||||||
|
if (number < 0)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("positive value expected."); //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
byte[] toWrite = new byte[2];
|
||||||
|
for (int i = 0; i <= 8; i += 8)
|
||||||
|
{
|
||||||
|
toWrite[i / 8] = (byte) ((number >> i) & 0xFF);
|
||||||
|
}
|
||||||
|
out.write(toWrite);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the given value as UINT32 into the stream.
|
||||||
|
*
|
||||||
|
* @param number value to write.
|
||||||
|
* @param out stream to write into.
|
||||||
|
* @throws IOException On I/O errors
|
||||||
|
*/
|
||||||
|
public static void writeUINT32(long number, OutputStream out) throws IOException
|
||||||
|
{
|
||||||
|
if (number < 0)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("positive value expected."); //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
byte[] toWrite = new byte[4];
|
||||||
|
for (int i = 0; i <= 24; i += 8)
|
||||||
|
{
|
||||||
|
toWrite[i / 8] = (byte) ((number >> i) & 0xFF);
|
||||||
|
}
|
||||||
|
out.write(toWrite);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the given value as UINT64 into the stream.
|
||||||
|
*
|
||||||
|
* @param number value to write.
|
||||||
|
* @param out stream to write into.
|
||||||
|
* @throws IOException On I/O errors
|
||||||
|
*/
|
||||||
|
public static void writeUINT64(long number, OutputStream out) throws IOException
|
||||||
|
{
|
||||||
|
if (number < 0)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("positive value expected."); //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
byte[] toWrite = new byte[8];
|
||||||
|
for (int i = 0; i <= 56; i += 8)
|
||||||
|
{
|
||||||
|
toWrite[i / 8] = (byte) ((number >> i) & 0xFF);
|
||||||
|
}
|
||||||
|
out.write(toWrite);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,98 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.dsf;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.StandardCharsets;
|
||||||
|
import com.mp3.jaudiotagger.audio.generic.Utils;
|
||||||
|
import com.mp3.jaudiotagger.audio.iff.IffHeaderChunk;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DSD Chunk
|
||||||
|
*/
|
||||||
|
public class DsdChunk
|
||||||
|
{
|
||||||
|
private long chunkSizeLength;
|
||||||
|
private long fileLength;
|
||||||
|
private long metadataOffset;
|
||||||
|
|
||||||
|
public static final int CHUNKSIZE_LENGTH = 8;
|
||||||
|
public static final int FILESIZE_LENGTH = 8;
|
||||||
|
public static final int METADATA_OFFSET_LENGTH = 8;
|
||||||
|
public static final int FMT_CHUNK_MIN_DATA_SIZE_ = 40;
|
||||||
|
|
||||||
|
public static final int DSD_HEADER_LENGTH = IffHeaderChunk.SIGNATURE_LENGTH + CHUNKSIZE_LENGTH + FILESIZE_LENGTH + METADATA_OFFSET_LENGTH;
|
||||||
|
|
||||||
|
public static DsdChunk readChunk(ByteBuffer dataBuffer)
|
||||||
|
{
|
||||||
|
String type = Utils.readFourBytesAsChars(dataBuffer);
|
||||||
|
if (DsfChunkType.DSD.getCode().equals(type))
|
||||||
|
{
|
||||||
|
return new DsdChunk(dataBuffer);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DsdChunk(ByteBuffer dataBuffer)
|
||||||
|
{
|
||||||
|
chunkSizeLength = dataBuffer.getLong();
|
||||||
|
fileLength = dataBuffer.getLong();
|
||||||
|
metadataOffset = dataBuffer.getLong();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
|
||||||
|
return "ChunkSize:"+chunkSizeLength
|
||||||
|
+ ":fileLength:"+fileLength
|
||||||
|
+ ":metadata:"+metadataOffset;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getChunkSizeLength()
|
||||||
|
{
|
||||||
|
return chunkSizeLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChunkSizeLength(long chunkSizeLength)
|
||||||
|
{
|
||||||
|
this.chunkSizeLength = chunkSizeLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getFileLength()
|
||||||
|
{
|
||||||
|
return fileLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFileLength(long fileLength)
|
||||||
|
{
|
||||||
|
this.fileLength = fileLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getMetadataOffset()
|
||||||
|
{
|
||||||
|
return metadataOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMetadataOffset(long metadataOffset)
|
||||||
|
{
|
||||||
|
this.metadataOffset = metadataOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write new DSDchunk to buffer
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public ByteBuffer write()
|
||||||
|
{
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocateDirect(DSD_HEADER_LENGTH);
|
||||||
|
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
buffer.put(DsfChunkType.DSD.getCode().getBytes(StandardCharsets.US_ASCII));
|
||||||
|
buffer.putLong(chunkSizeLength);
|
||||||
|
buffer.putLong(fileLength);
|
||||||
|
buffer.putLong(metadataOffset);
|
||||||
|
buffer.flip();
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package com.mp3.jaudiotagger.audio.dsf;
|
||||||
|
|
||||||
|
import com.mp3.jaudiotagger.tag.Tag;
|
||||||
|
import com.mp3.jaudiotagger.tag.TagOptionSingleton;
|
||||||
|
import com.mp3.jaudiotagger.tag.id3.ID3v22Tag;
|
||||||
|
import com.mp3.jaudiotagger.tag.id3.ID3v23Tag;
|
||||||
|
import com.mp3.jaudiotagger.tag.id3.ID3v24Tag;
|
||||||
|
import com.mp3.jaudiotagger.tag.reference.ID3V2Version;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by Paul on 28/01/2016.
|
||||||
|
*/
|
||||||
|
public class Dsf
|
||||||
|
{
|
||||||
|
public static Tag createDefaultTag()
|
||||||
|
{
|
||||||
|
if(TagOptionSingleton.getInstance().getID3V2Version()== ID3V2Version.ID3_V24)
|
||||||
|
{
|
||||||
|
return new ID3v24Tag();
|
||||||
|
}
|
||||||
|
else if(TagOptionSingleton.getInstance().getID3V2Version()==ID3V2Version.ID3_V23)
|
||||||
|
{
|
||||||
|
return new ID3v23Tag();
|
||||||
|
}
|
||||||
|
else if(TagOptionSingleton.getInstance().getID3V2Version()==ID3V2Version.ID3_V22)
|
||||||
|
{
|
||||||
|
return new ID3v22Tag();
|
||||||
|
}
|
||||||
|
//Default in case not set somehow
|
||||||
|
return new ID3v24Tag();
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user