作业
打印输出所有的“水仙花数”,所谓“水仙花数”是指一个3位数,其中各位数字立方和等于该数本身。例如,153是一个“水仙花数”。
import java.util.Arrays; import java.util.stream.IntStream; public class Main { private static void NarcissisticNumbers() { IntStream.range(100, 1000).filter(i -> Arrays.stream((i + "").split("")).mapToDouble(j -> Math.pow(Integer.parseInt(j), 3)).sum() == i).forEach(System.out::println); } public static void main(String[] args) { NarcissisticNumbers(); } }
编写Java程序,求13-23+33-43+…+973-983+993-1003的值。
class Main { public static int Sum(Integer x) { return x > 0 ? (x % 2 == 0 ? -1 : 1) * (x * 10 + 3) + Sum(x - 1) : 0; } public static void main(String[] args) { System.out.println(Sum(100)); } }
编程求1!+2!+3!+…+20!。
import java.util.function.Function; class Main { interface Rec<F> extends Function<Rec<F>, F> { } private static <A,B> Function<A,B> Y(Function<Function<A,B>, Function<A,B>> f) { Rec<Function<A,B>> r = w -> f.apply(x -> w.apply(w).apply(x)); return r.apply(r); } public static Long FactorialSum(Long input) { Function<Long, Long> fac = Y(f -> n -> n > 1 ? n * f.apply(n - 1) : 1); Function<Long,Long> facSum = Y(f -> n -> n > 0 ? fac.apply(n) + f.apply(n - 1) : 0); return facSum.apply(input); } public static void main(String[] args) { System.out.println(FactorialSum((long)20)); } }
编写Java程序,计算8+88+888+…前10项之和。
import java.util.Collections; class Main { public static long Sum(Integer x) { return x > 0 ? Long.parseLong(String.join("", Collections.nCopies(x, "8"))) + Sum(x - 1) : 0; } public static void main(String[] args) { System.out.println(Sum(10)); } }
一个数如果恰好等于它的因子之和,这个数就称为完数。编写程序输出1000以内的所有完数。
import java.util.stream.IntStream; class Main { private static void PerfectNumber() { IntStream.range(1, 1000).filter(f -> IntStream.rangeClosed(1, f / 2).filter(sf -> f % sf == 0).sum() == f).forEach(System.out::println); } public static void main(String[] args) { PerfectNumber(); } }
编写应用程序,输出满足1+2+3+…+n<8888的最大正整数。
import java.util.stream.IntStream; class Main { public static int Sum(Integer n) { return IntStream.range(1, n).filter(f -> IntStream.rangeClosed(1, f).sum() < n).max().getAsInt(); } public static void main(String[] args) { System.out.println(Sum(8888)); } }
使用for循环打印下面的图形。
import java.util.Collections; import java.util.stream.IntStream; class Main { public static void Show() { IntStream.rangeClosed(1, 5).forEach(a -> System.out.println(String.join("", Collections.nCopies(5 - a, " ")) + String.join(" ", Collections.nCopies(a, "*")))); } public static void main(String[] args) { Show(); } }
写一个名为Rectangle的类表示矩形。其属性包括宽width、高height和颜色color,width和height都是double型的,而color则是String类型的。要求该类具有:(1) 使用构造函数完成各属性的初始赋值(2) 使用get…()和set…()的形式完成属性的访问及修改(3) 提供计算面积的getArea()方法和计算周长的getLength()方法
class Rectangle { private double width; private double height; private String color; /** * @return the width */ public double getWidth() { return width; } /** * @param width the width to set */ public void setWidth(double width) { this.width = width; } /** * @return the height */ public double getHeight() { return height; } /** * @param height the height to set */ public void setHeight(double height) { this.height = height; } /** * @return the color */ public String getColor() { return color; } /** * @param color the color to set */ public void setColor(String color) { this.color = color; } public Rectangle(double width,double height,String color) { this.width = width; this.height = height; this.color = color; } public double getArea() { return width*height; } public double getLength() { return 2 * (width + height); } } public class Main { public static void main(String[] args) { Rectangle r = new Rectangle(1, 2, "black"); r.setColor("red"); System.out.println(r.getColor()); r.setHeight(4); System.out.println(r.getHeight()); r.setWidth(4); System.out.println(r.getWidth()); System.out.println(r.getArea()); System.out.println(r.getLength()); } }
银行的账户记录Account有账户的唯一性标识(11个长度的字符和数字的组合),用户的姓名,开户日期,账户密码(六位的数字,可以用0开头),当前的余额。银行规定新开一个账户时,银行方面提供一个标识符、账户初始密码123456,客户提供姓名,开户时客户可以直接存入一笔初始账户金额,不提供时初始余额为0。定义该类,并要求该类提供如下方法:存款、取款、变更密码、可以分别查询账户的标识、姓名、开户日期、当前余额等信息。
import java.util.Date; import java.util.stream.IntStream; class Account { private String id; private String name; private String date; private String password; private double bankBalance; /** * @return the id */ public String getId() { return id; } /** * @return the name */ public String getName() { return name; } /** * @return the date */ public String getDate() { return date; } /** * @param password the password to set */ public void setPassword(String password) { this.password = password; } /** * @return the bankBalance */ public double getBankBalance() { return bankBalance; } public Account(String name, double money) { this.name = name; bankBalance = money; id = IntStream.range(0, 11).map(x -> (int)(48 + Math.random() * 43)).boxed().map(x -> (char)(x < 58 || x > 64 ? x : x-7) + "").reduce((a, b) -> a + b).get(); password="123456"; date=new Date().toString(); } public void Deposit(double money) { bankBalance+=money; } public void Draw(double money) { bankBalance-=money; } } public class Main { public static void main(String[] args) { Account a = new Account("王成浩",9); a.Deposit(90.9); a.Draw(0.9); a.setPassword("654321"); System.out.println(a.getId()); System.out.println(a.getName()); System.out.println(a.getDate()); System.out.println(a.getBankBalance()); } }
已知字符串:”this is a test of java”.按要求执行以下操作:①统计该字符串中字母s出现的次数。②统计该字符串中子串“is”出现的次数。③统计该字符串中单词“is”出现的次数。④实现该字符串的倒序输出。
import java.util.Arrays; public class Main { public static void main(String[] args) { String s = "this is a test of java"; System.out.println(s.split("s").length - 1); System.out.println(s.split("is").length - 1); System.out.println(Arrays.asList(s.split(" ")).stream().filter(i -> i.equals("is")).count()); System.out.println(new StringBuffer(s).reverse().toString()); } }
请编写一个程序,使用下述算法加密或解密用户输入的英文字串。
import java.util.Arrays; import java.util.Scanner; public class Main { public static void main(String[] args) { System.out.println(Arrays.stream(new Scanner(System.in).nextLine().split("")).map(i -> Character.toString((char)(i.toCharArray()[0] + 3))).reduce((a, b) -> a + b).get()); System.out.println(Arrays.stream(new Scanner(System.in).nextLine().split("")).map(i -> Character.toString((char)(i.toCharArray()[0] - 3))).reduce((a, b) -> a + b).get()); } }
已知字符串“ddejidsEFALDFfnef2357 3ed”。输出字符串里的大写字母数,小写英文字母数,非英文字母数。
public class Main { public static void main(String[] args) { String s = "ddejidsEFALDFfnef2357 3ed"; System.out.println(s.split("[A-Z]").length - 1); System.out.println(s.split("[a-z]").length - 1); System.out.println(s.split("[^A-Za-z]").length - 1); } }
import java.util.*; class Circle { private double r; public Circle() { r = 0; } public Circle(double r) { this.r = r; } public double getRadius() { return r; } public double getPerimeter() { return 2 * Math.PI * r; } public void disp() { System.out.println(r); System.out.println(getPerimeter()); System.out.println(Math.PI * r * r); } } class Cylinder extends Circle { private double h; public Cylinder(double r, double h) { super(r); this.h = h; } public double getHeight() { return h; } public double getVol() { return Math.PI * getRadius() * getRadius() * h; } public void dispVol() { System.out.println(getVol()); } } public class Main { public static void main(String[] args) { new Cylinder(new Scanner(System.in).nextDouble(), new Scanner(System.in).nextDouble()).dispVol(); } }
设计一个类层次,定义一个抽象类–形状,其中包括有求形状的面积的抽象方法。继承该抽象类定义三角型、矩形、圆。分别创建一个三角形、矩形、圆存对象,将各类图形的面积输出。注:三角形面积s=sqrt(p*(p-a)*(p-b)*(p-c)) 其中,a,b,c为三条边,p=(a+b+c)/2
abstract class Shape { abstract double Area(); } class Triangle extends Shape { public double a; public double b; public double c; public Triangle(double a, double b,double c) { this.a=a; this.b=b; this.c=c; } public double Area() { return Math.sqrt( ((a + b + c) / 2) * (((a + b + c) / 2) - a) * (((a + b + c) / 2) - b) * (((a + b + c) / 2) - c)); } } class Rectangle extends Shape { public double a; public double b; public Rectangle(double a, double b) { this.a=a; this.b=b; } public double Area() { return a * b; } } class Circle extends Shape { public double r; public Circle(double r) { this.r=r; } public double Area() { return Math.PI * Math.pow(r, 2); } } public class Main { public static void main(String[] args) { System.out.println(new Triangle(3, 4, 5).Area()); System.out.println(new Rectangle(1, 1).Area()); System.out.println(new Circle(1).Area()); } }
定义接口Shape,其中包括一个方法size(),设计“直线”、“圆”、类实现Shape接口。分别创建一个“直线”、“圆”对象,将各类图形的大小输出。
interface Shape { double size(); } class Line implements Shape { public double l; public Line(double l) { this.l = l; } public double size() { return l; } } class Circle implements Shape { public double r; public Circle(double r) { this.r=r; } public double size() { return 2 * Math.PI * r; } } public class Main { public static void main(String[] args) { System.out.println(new Line(1).size()); System.out.println(new Circle(1).size()); } }
class DangerException extends Exception { public void toshow() { System.out.println("危险品"); } } class Goods { private boolean isDanger; /** * @return the isDanger */ public boolean getIsDanger() { return isDanger; } /** * @param isDanger the isDanger to set */ public void setIsDanger(boolean isDanger) { this.isDanger = isDanger; } public Goods(boolean danger) { isDanger = danger; } } class Machine { public void checkBag(Goods goods) throws DangerException { if(goods.getIsDanger()) throw new DangerException(); } } public class Main { public static void main(String[] args) { Goods goods = new Goods(true); try { new Machine().checkBag(goods); } catch(DangerException exception) { exception.toshow(); } } }
记事本
from tkinter import * import tkinter.messagebox import tkinter.filedialog import os filename='' root=Tk() root.title('无标题 - 记事本') root.geometry("800x600") def openfile(): global filename filename=tkinter.filedialog.askopenfilename() if filename != '': textPad.delete(1.0,END) with open(filename,'r') as f: textPad.insert(1.0,f.read()) root.title(filename+' - 记事本') def new(): global filename root.title("无标题 - 记事本") filename='' textPad.delete(1.0,END) def save(): global filename print(filename) if filename == '': saveas() else: with open(filename,'w') as f: f.write(textPad.get(1.0,END)) def saveas(): global filename filename=tkinter.filedialog.asksaveasfilename() if filename != '': with open(filename,'w') as f: f.write(textPad.get(1.0,END)) root.title(filename+' - 记事本') def selectAll(): textPad.tag_add('sel','1.0',END) menubar=Menu(root) root.config(menu=menubar) filemenu=Menu(menubar,tearoff=False) filemenu.add_command(label='新建',accelerator='Ctrl+N',command=new) filemenu.add_command(label='打开',accelerator='Ctrl+O',command=openfile) filemenu.add_command(label='保存',accelerator='Ctrl+S',command=save) filemenu.add_command(label='另存为',command=saveas) menubar.add_cascade(label='文件',menu=filemenu) editmenu=Menu(menubar,tearoff=False) editmenu.add_command(label='撤消',accelerator='Ctrl+z',command=lambda : textPad.event_generate('<<Undo>>')) editmenu.add_command(label='重做',accelerator='Ctrl+y',command=lambda : textPad.event_generate('<<Redo>>')) editmenu.add_separator() editmenu.add_command(label='剪切',accelerator='Ctrl+X',command=lambda : textPad.event_generate('<<Cut>>')) editmenu.add_command(label='复制',accelerator='Ctrl+C',command=lambda : textPad.event_generate('<<Copy>>')) editmenu.add_command(label='粘贴',accelerator='Ctrl+V',command=lambda : textPad.event_generate('<<Paste>>')) editmenu.add_separator() editmenu.add_command(label='全选',accelerator='Ctrl+A',command=lambda : textPad.tag_add('sel','1.0',END)) menubar.add_cascade(label='编辑',menu=editmenu) textPad=Text(root,undo=True) textPad.pack(expand=YES,fill=BOTH) scroll=Scrollbar(textPad) textPad.config(yscrollcommand=scroll.set) scroll.config(command=textPad.yview) scroll.pack(side=RIGHT,fill=Y) root.bind('<Button-3>',lambda event: editmenu.post(event.x_root,event.y_root)) root.bind_all("<Control-n>", lambda event: new()) root.bind_all("<Control-o>", lambda event: openfile()) root.bind_all("<Control-s>", lambda event: save()) root.mainloop()
计算器界面
import javax.swing.*; import java.awt.*; import com.intellij.uiDesigner.core.*; public class CC { private JLabel Result; private JPanel JProot; private JPanel JP0; private JPanel JP1; private JPanel JP2; private JButton MCButton; private JButton mButton; private JButton MSButton; private JButton MRButton; private JButton backspaceButton; private JButton cButton; private JButton CEButton; private JButton a7Button; private JButton a8Button; private JButton a9Button; private JButton button11; private JButton sqrtButton; private JButton a0Button; private JButton a4Button; private JButton a1Button; private JButton a5Button; private JButton a6Button; private JButton button18; private JButton button19; private JButton a1XButton; private JButton button21; private JButton a3Button; private JButton a2Button; private JButton button24; private JButton button25; private JButton button26; private JButton button27; public static void main(String[] args) { JFrame frame = new JFrame("CC"); frame.setContentPane(new CC().JProot); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.pack(); frame.setVisible(true); } { // GUI initializer generated by IntelliJ IDEA GUI Designer // >>> IMPORTANT!! <<< // DO NOT EDIT OR ADD ANY CODE HERE! $$setupUI$$(); } /** * Method generated by IntelliJ IDEA GUI Designer * >>> IMPORTANT!! <<< * DO NOT edit this method OR call it in your code! * * @noinspection ALL */ private void $$setupUI$$() { JProot = new JPanel(); JProot.setLayout(new GridLayoutManager(3, 3, new Insets(0, 0, 0, 0), -1, -1)); Result = new JLabel(); Result.setText("0"); JProot.add(Result, new GridConstraints(0, 2, 1, 1, GridConstraints.ANCHOR_EAST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); JP0 = new JPanel(); JP0.setLayout(new GridLayoutManager(1, 3, new Insets(0, 0, 0, 0), -1, -1)); JProot.add(JP0, new GridConstraints(1, 1, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); backspaceButton = new JButton(); backspaceButton.setText("Backspace"); JP0.add(backspaceButton, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); cButton = new JButton(); cButton.setText("C"); JP0.add(cButton, new GridConstraints(0, 2, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); CEButton = new JButton(); CEButton.setText("CE"); JP0.add(CEButton, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); JP1 = new JPanel(); JP1.setLayout(new GridLayoutManager(4, 1, new Insets(0, 0, 0, 0), -1, -1)); JProot.add(JP1, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); MCButton = new JButton(); MCButton.setText("MC"); JP1.add(MCButton, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); mButton = new JButton(); mButton.setText("M+"); JP1.add(mButton, new GridConstraints(3, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); MSButton = new JButton(); MSButton.setText("MS"); JP1.add(MSButton, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); MRButton = new JButton(); MRButton.setText("MR"); JP1.add(MRButton, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); JP2 = new JPanel(); JP2.setLayout(new GridLayoutManager(4, 5, new Insets(0, 0, 0, 0), -1, -1)); JProot.add(JP2, new GridConstraints(2, 1, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); a7Button = new JButton(); a7Button.setText("7"); JP2.add(a7Button, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); a8Button = new JButton(); a8Button.setText("8"); JP2.add(a8Button, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); a9Button = new JButton(); a9Button.setText("9"); JP2.add(a9Button, new GridConstraints(0, 2, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); button11 = new JButton(); button11.setText("/"); JP2.add(button11, new GridConstraints(0, 3, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); sqrtButton = new JButton(); sqrtButton.setText("sqrt"); JP2.add(sqrtButton, new GridConstraints(0, 4, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); a0Button = new JButton(); a0Button.setText("0"); JP2.add(a0Button, new GridConstraints(3, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); a4Button = new JButton(); a4Button.setText("4"); JP2.add(a4Button, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); a1Button = new JButton(); a1Button.setText("1"); JP2.add(a1Button, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); a5Button = new JButton(); a5Button.setText("5"); JP2.add(a5Button, new GridConstraints(1, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); a6Button = new JButton(); a6Button.setText("6"); JP2.add(a6Button, new GridConstraints(1, 2, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); button18 = new JButton(); button18.setText("*"); JP2.add(button18, new GridConstraints(1, 3, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); button19 = new JButton(); button19.setText("%"); JP2.add(button19, new GridConstraints(1, 4, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); a1XButton = new JButton(); a1XButton.setText("1/x"); JP2.add(a1XButton, new GridConstraints(2, 4, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); button21 = new JButton(); button21.setText("-"); JP2.add(button21, new GridConstraints(2, 3, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); a3Button = new JButton(); a3Button.setText("3"); JP2.add(a3Button, new GridConstraints(2, 2, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); a2Button = new JButton(); a2Button.setText("2"); JP2.add(a2Button, new GridConstraints(2, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); button24 = new JButton(); button24.setText("+/-"); JP2.add(button24, new GridConstraints(3, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); button25 = new JButton(); button25.setText("."); JP2.add(button25, new GridConstraints(3, 2, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); button26 = new JButton(); button26.setText("+"); JP2.add(button26, new GridConstraints(3, 3, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); button27 = new JButton(); button27.setText("="); JP2.add(button27, new GridConstraints(3, 4, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); } /** * @noinspection ALL */ public JComponent $$getRootComponent$$() { return JProot; } }
MP3的解析与播放
1 前言
MP3是一个数据压缩格式。它舍弃脉冲编码调制(PCM)音频数据中,对人类听觉不重要的数据(类似于JPEG,是一个有损图像的压缩格式),从而达到了压缩成小得多的文件大小。
在MP3中使用了许多技术,其中包括心理声学,以确定音频的哪一部分可以丢弃。MP3音频可以按照不同的比特率进行压缩,提供了权衡数据大小和音质之间的依据。
MP3格式使用了混合的转换机制将时域信号转换成频域信号:
尽管有许多创造和推广其他格式的重要努力,如 MPEG 标准中的 AAC(Advanced Audio Coding)和 IETF 开放标准中的 Opus。然而,由于MP3的空前的流通,在当前来说,其他格式不可能威胁其地位。MP3不仅有广泛的用户端软体支持,也有很多的硬件支持,比如便携式数字音频播放器(泛指MP3播放器)、移动电话、数字多功能影音光盘和CD播放器。
1.1 程序描述
1.1.1 设计背景
MPEG-1 Audio Layer II编码开始时是德国Deutsche Forschungs- und Versuchsanstalt für Luft- und Raumfahrt(后来称为Deutsches Zentrum für Luft- und Raumfahrt, 德国太空中心)Egon Meier-Engelen管理的数字声音广播(DAB)项目。这个项目是欧盟作为EUREKA研究项目资助的,它的名字通常称为数字声音广播。EU-147的研究期间是1987年到1994年。
到了1991年,就已经出现了两个提案:MPEG-1 Audio Layer II(称为Layer 2)和ASPEC(自适应频谱感知熵编码)。荷兰飞利浦公司、法国CCETT和德国Institut für Rundfunktechnik提出的Musicam方法由于它的简单、出错时的稳定性以及在高质量压缩时较少的计算量而被选中。基于子带编码的Musicam格式是确定MPEG音频压缩格式(采样率、帧结构、数据头、每帧采样点)的一个关键因素。这项技术和它的设计思路完全融合到了ISO MPEG Audio Layer I、II以及后来的Layer III(MP3)格式的定义中。在Mussmann教授(汉诺威大学)的主持下,标准的制定由Leon van de Kerkhof(Layer I)和Gerhard Stoll(Layer II)完成。
一个由荷兰Leon Van de Kerkhof、德国Gerhard Stoll、法国Yves-François Dehery和德国Karlheinz Brandenburg组成的工作小组吸收了Musicam和ASPEC的设计思想,并添加了他们自己的设计思想从而开发出了MP3,MP3能够在128码率单位达到MP2 192kbit/s音质。
所有这些算法最终都在1992年成为了MPEG的第一个标准组MPEG-1的一部分,并且生成了1993年公布的国际标准ISO/IEC 11172-3。MPEG音频上的更进一步的工作最终成为了1994年制定的第二个MPEG标准组MPEG-2标准的一部分,这个标准正式的称呼是1995年首次公布的ISO/IEC 13818-3。
编码器的压缩效率通常由比特率定义,因为压缩率依赖于位数(bit depth)和输入信号的采样率。然而,经常有产品使用CD参数(44.1赫兹、两个通道、每通道16位或者称为2×16位)作为压缩率参考,使用这个参考的压缩率通常较高,这也说明了压缩率对于有损压缩存在的问题。
Karlheinz Brandenburg使用CD介质的苏珊娜·薇佳的歌曲Tom’s Diner来评价MP3压缩算法。使用这首歌是因为这首歌的柔和、简单旋律使得在回放时更容易听到压缩格式中的缺陷。一些人开玩笑地将Suzanne Vega称为“MP3之母”。来自于EBU V3/SQAM参考CD的更多一些严肃和critical音频选段(钟琴,三角铁,手风琴,…)被专业音频工程师用来评价MPEG音频格式的主观感受质量。
为了生成位兼容的MPEG Audio文件(Layer 1、Layer 2、Layer 3),ISO MPEG Audio委员会成员用C语言开发的一个称为ISO 11172-5的参考模拟软件。在一些非实时操作系统上它能够演示第一款压缩音频基于DSP的实时硬件解码。一些其他的MPEG Audio实时开发出来用于面向消费接收机和机顶盒的数字广播(无线电DAB和电视DVB)。
后来,1994年7月7日弗劳恩霍夫协会发布了第一个称为l3enc的MP3编码器。
Fraunhofer开发组在1995年7月14日选定扩展名:”.mp3″(以前扩展名是”.bit”)。使用第一款实时软件MP3播放器Winplay3(1995年9月9日发布)许多人能够在自己的个人计算机上编码和回放MP3文件。由于当时的硬盘相对较小(如500MB),这项技术对于在计算机上存储娱乐音乐来说是至关重要的。
1993年10月,MPEG-1 Audio Layer II(MPEG-1 Audio Layer 2)文件在因特网上出现,它们经常使用Xing MPEG Audio Player播放,后来又出现了Tobias Bading为UNIX开发的MAPlay。MAPlay于1994年2月22日首次发布,现在已经移植到微软视窗平台上。
刚开始仅有的MP2编码器产品是Xing Encoder和CDDA2WAV,CDDA2WAV是一个将CD音轨转换成WAV格式的CD抓取器。
Internet Underground Music Archive(IUMA)通常被认为是在线音乐革命的鼻祖,IUMA是因特网上第一个高保真音乐网站,在MP3和网络流行之前它有数千首授权的MP2录音。
从1995年上半年开始直到整个九十年代后期,MP3开始在因特网上蓬勃发展。MP3的流行主要得益于如Nullsoft于1997年发布的Winamp和于1999年发布的Napster,这样的公司和软件包的成功,并且它们相互促进发展。这些程序使得普通用户很容易地播放、制作、共享和收集MP3文件。
关于MP3文件的点对点技术文件共享的争论在最近几年迅速蔓延—这主要是由于压缩使得文件共享成为可能,未经压缩的文件过于庞大难于共享。由于MP3文件通过因特网大量传播,一些主要唱片厂商通过法律起诉Napster来保护它们的版权(参见知识产权)。
如ITunes Store这样的商业在线音乐发行服务通常选择其他或者专有的支持数字版权管理(DRM)的音乐文件格式以控制和限制数字音乐的使用。支持DRM的格式的使用是为了防止受版权保护的素材免被侵犯版权,但是大多数的保护机制都能被一些方法破解。这些方法能够被计算机高手用来生成能够自由复制的解锁文件。如果希望得到一个压缩的音频文件,这个录制的音频流必须进行压缩且代价是音质的降低。
1.1.2 设计目标及任务
- 实现ID3v2, ID3v1, Frame的解析。
- 实现MP3的播放。
- 对HTTP/HTTPS通信的实现。
1.2 程序开发工具
- Android
Android,常见的非官方中文名称为安卓,是一个基于Linux内核的开放源代码移动操作系统,由Google成立的Open Handset Alliance(OHA,开放手持设备联盟)持续领导与开发,主要设计用于触摸屏移动设备如智能手机和平板电脑与其他便携式设备。
本程序选用Android 8.1,最低版本Android 7.0。
- Gradle
Gradle是一个基于Apache Ant和Apache Maven概念的项目自动化建构工具。它使用一种基于Groovy的特定领域语言来声明项目设置,而不是传统的XML。
本程序选用Gradle 4.0。
- OkHttp
HTTP是现代应用常用的一种交换数据和媒体的网络方式,高效地使用HTTP能让资源加载更快,节省带宽。OkHttp是一个高效的HTTP客户端,它有以下默认特性:
- 支持HTTP/2,允许所有同一个主机地址的请求共享同一个socket连接
- 连接池减少请求延时
- 透明的GZIP压缩减少响应数据的大小
- 缓存响应内容,避免一些完全重复的请求
当网络出现问题的时候OkHttp依然坚守自己的职责,它会自动恢复一般的连接问题,如果你的服务有多个IP地址,当第一个IP请求失败时,OkHttp会交替尝试你配置的其他IP,OkHttp使用现代TLS技术(SNI, ALPN)初始化新的连接,当握手失败时会回退到TLS 1.0。
本程序选用OkHttp 3.12.1。
- jCharDet
jCharDet是mozilla自动字符集检测算法的java接口。
本程序选用jCharDet 1.1。
- JDK
Java Development Kit(JDK)是太阳微系统针对Java开发人员发布的免费软件开发工具包(SDK,Software development kit)。自从Java推出以来,JDK已经成为使用最广泛的Java SDK。由于JDK的一部分特性采用商业许可证,而非开源。因此,2006年太阳微系统宣布将发布基于GPL的开源JDK,使JDK成为自由软件。在去掉了少量闭源特性之后,太阳微系统最终促成了GPL的OpenJDK的发布。
本程序选用JDK 1.8。
- Visual Studio Code
Visual Studio Code(简称VS Code)是一个由微软开发的,同时支持Windows、Linux和macOS系统且开放源代码的代码编辑器,它支持测试,并内置了Git 版本控制功能,同时也具有开发环境功能,例如代码补全(类似于 IntelliSense)、代码片段和代码重构等,该编辑器支持用户个人化配置,例如改变主题颜色、键盘快捷方式等各种属性和参数,还在编辑器中内置了扩展程序管理的功能。
本程序选用Visual Studio Code 1.29,选用插件Language Support for Java(TM) by Red Hat,Debugger for Java,Java Test Runner,Maven for Java,Java Extension Pack。
- IntelliJ IDEA
IntelliJ IDEA是一种商业化销售的Java集成开发环境(Integrated Development Environment,IDE)工具软件,由JetBrains软件公司(前称为IntelliJ)发展,提供Apache 2.0开放式授权的社区版本以及专有软件的商业版本,开发者可选择其所需来下载使用。
本程序选用IntelliJ IDEA 2017.3 Build: 173.3727.127。
- Visual Studio
Microsoft Visual Studio(简称VS或MSVS)是微软公司的开发工具包系列产品。VS是一个基本完整的开发工具集,它包括了整个软件生命周期中所需要的大部分工具,如UML工具、代码管控工具、集成开发环境(IDE)等等。所写的目标代码适用于微软支持的所有平台,包括Microsoft Windows、Windows Phone、Windows CE、.NET Framework、.NET Compact Framework和Microsoft Silverlight。
本程序选用Visual Studio 2017。
- Android Studio
Android Studio是一个为Android平台开发程序的集成开发环境。2013年5月16日在Google I/O上发布,可供开发者免费使用。2013年5月发布早期预览版本,版本号为0.1。2014年6月发布0.8版本,至此进入beta阶段。第一个稳定版本1.0于2014年12月8日发布。Android Studio基于JetBrains IntelliJ IDEA,为Android开发特殊定制,并在Windows、OS X和Linux平台上均可运行。
本程序使用Android Studio 3.2.1。
- Android Emulator
可使用各种配置运行 Android Emulator 来模拟不同的设备。 每个配置称为虚拟设备。
2 需求分析
2.1 概述
MP3文件是由帧(frame)构成的,帧是MP3文件最小的组成单位。MP3的全称应为MPEG1 Layer-3音频文件, MPEG(Moving Picture Experts Group)在汉语中译为活动图像专家组,特指活动影音压缩标准,MPEG音频文件是MPEG1标准中的声音部分,也叫MPEG音频层,它根据压缩质量和编码复杂程度划分为三层,即Layer-1、Layer-2、 Layer-3, 且分别对应MP1、MP2、MP3这三种声音文件,并根据不同的用途,使用不同层次的编码。MPEG音频编码的层次越高,编码器越复杂,压缩率也越高,MP1和MP2的压缩率分别为4:1和6:1- 8:1,而MP3的压缩率则高达10:1-12:1,也就是说,一分钟CD音质的音乐,未经压缩需要10MB的存储空间,而经过MP3压缩编码后只有1MB左右。不过MP3对音频信号采用的是有损压缩方式,为了降低声音失真度,MP3采取了“感官编码技术”,即编码时先对音频文件进行频谱分析,然后用过滤器滤掉噪音电平,接着通过量化的方式将剩下的每一位打散排列(应该说是Huffman无损压缩编码),最后形成具有较高压缩比的MP3文件,并使压缩后的文件在回放时能够达到比较接近原音的声音效果。
2.2 MP3文件结构
MP3文件大体分为三部分:TAG_V2(ID3V2), Frame, TAG_V1(ID3V1)
ID3V2 | 包含了作者,作曲,专辑等信息,长度不固定,扩展了ID3V1的信息量。 |
Frame | 一系列的帧,个数由文件大小和帧长决定
每个FRAME的长度可能不固定,也可能固定,由位率bitrate决定 每个FRAME又分为帧头和数据实体两部分 帧头记录了mp3的位率,采样率,版本等信息,每个帧之间相互独立 |
ID3V1 | 包含了作者,作曲,专辑等信息,长度为128BYTE。 |
数据帧帧头
每个MP3数据帧有一个帧头FRAMEHEADER,长度是4BYTE(32bit),帧头后面可能有两个字节的CRC校验,这两个字节的是否存在决定于FRAMEHEADER信息的第16bit,为0则帧头后面无校验,为1则有校验,校验值长度为2个字节,紧跟在FRAMEHEADER后面,接着就是帧的实体数据了,格式如下:
FRAMEHEADER | CRC(free) | MAIN_DATA |
4 BYTE | 0 OR 2 BYTE | 长度由帧头计算得出 |
帧头FRAMEHEADER格式如下:
Bytes | Bits | |||||||
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | |
0 | A | |||||||
1 | A | B | C | D | ||||
2 | E | F | G | H | ||||
3 | I | J | K | L | M |
- 同步信息(全设为1)
- 版本
- 00 – MPEG version 2.5
- 01 – reserved
- 10 – MPEG version 2
- 11 – MPEG version 1
- 层
- 00 – reserved
- 01 – Layer III
- 10 – Layer II
- 11 – Layer I
- CRC校验
- 0 – 校验(帧头后面有16bit的CRC校验)
- 1 – 不校验
- 位率(kbps)
Bits | L1 | L2 | L3, V1 | L3, V2 | L3, V2.5 |
0000 | Free | Free | Free | Free | Free |
0001 | 32 | 32 | 32 | 8 | 8 |
0010 | 64 | 48 | 40 | 16 | 16 |
0011 | 96 | 56 | 48 | 24 | 24 |
0100 | 128 | 64 | 56 | 32 | 32 |
0101 | 160 | 80 | 64 | 40 | 40 |
0110 | 192 | 96 | 80 | 48 | 48 |
0111 | 224 | 112 | 96 | 56 | 56 |
1000 | 256 | 128 | 112 | 64 | 64 |
1001 | 288 | 160 | 128 | 80 | Bad |
1010 | 320 | 192 | 160 | 96 | Bad |
1011 | 352 | 224 | 192 | 112 | Bad |
1100 | 384 | 256 | 224 | 128 | Bad |
1101 | 416 | 320 | 256 | 144 | Bad |
1110 | 448 | 384 | 320 | 160 | Bad |
1111 | Bad | Bad | Bad | Bad | Bad |
L1 – Layer I L2 – Layer II L3 – Layer III
V1 – MPEG version 1 V2 – MPEG version 2 V2.5 – MPEG version 2.5
- 采样率(Hz)
Bits | MPEG version 1 | MPEG version 2 | MPEG version 2.5 |
00 | 44100 | 22050 | 11025 |
01 | 48000 | 24000 | 12000 |
10 | 32000 | 16000 | 8000 |
11 | reserved | reserved | reserved |
- 帧长调节
- 0 – 没有填充
- 1 – 填充1bit
- 保留
- 声道模式
- 00 – Stereo
- 01 – Joint stereo (Stereo)
- 10 – Dual channel (Stereo)
- 11 – Single Channel (Mono)
- 扩充模式(仅在Joint stereo模式下)
Bits | Intensity stereo | MS stereo |
00 | Off | Off |
01 | On | Off |
10 | Off | On |
11 | On | On |
- 版权
- 0 – 不受版权保护
- 1 – 受版权保护
- 原版标志
- 0 – 非原版
- 1 – 原版
- 强调方式
- 00 – none
- 01 – 50/15 ms
- 10 – reserved
- 11 – CCIT J.17
- 每帧的播放时间:无论帧长是多少,每帧的播放时间都是26ms。
- 数据帧大小:FrameSize=((MPEGVersion == MPEG1 ? 144 : 72) * Bitrate) / SamplingRate + PaddingBit
2.3 ID3v1
ID3V1比较简单,它是存放在MP3文件的末尾,用16进制的编辑器打开一个MP3文件,查看其未尾的128个顺序存放字节,数据结构定义如下:
struct ID3v1_TAG
{
char id[3];
char title[30];
char artist[30];
char album[30];
char year[4];
char comment[28];
int8_t zero;
uint8_t track;
enum ID3_GENRES genre;
};
区域 | 长度 | 描述 |
开头 | 3 | “TAG”,标签。 |
标题 | 30 | 歌曲标题,最多30个英文字符。 |
艺术家 | 30 | 作曲或演唱者的名字,最多30个英文字符。 |
专辑 | 30 | 专辑名称,最多30个英文字符。 |
年分 | 4 | 西元年分,四个数字。 |
评论 | 28或30 | 就是评论。 |
零字节 | 1 | 如果有存储曲目,那么这个字节会存储一个二进制的0。 |
曲目 | 1 | 这首歌在该专辑中的曲目,或者为0。若前一个字节非零,则此栏内容无效。 |
艺术类型 | 1 | 一系列艺术类型清单中的编号数,默认为255。 |
ID3V1的各项信息都是顺序存放,没有任何标识将其分开,比如标题信息不足30个字节,则使用’\O’补足,否则将造成信息错误。Genre 使用原码表示,对照表如下:
ubyte | 英文 | 中文 |
0 | Blues | 蓝调 |
1 | Classic Rock | 古典摇滚乐 |
2 | Country | 乡村音乐 |
3 | Dance | 舞曲 |
4 | Disco | 迪斯科 |
5 | Funk | 放克 |
6 | Grunge | 油渍摇滚 |
7 | Hip-Hop | 嘻哈 |
8 | Jazz | 爵士乐 |
9 | Metal | 重金属音乐 |
10 | New Age | 新世纪音乐 |
11 | Oldies | |
12 | Other | |
13 | Pop | 流行 (音乐类型) |
14 | R&B | 节奏布鲁斯 |
15 | Rap | 饶舌 |
16 | Reggae | 雷鬼音乐 |
17 | Rock | 摇滚乐 |
18 | Techno | 铁克诺音乐 |
19 | Industrial | |
20 | Alternative | 另类摇滚 |
21 | Ska | 斯卡曲风 |
22 | Death Metal | 死亡金属音乐 |
23 | Pranks | |
24 | Soundtrack | 原声音乐 |
25 | Euro-Techno | |
26 | Ambient | 氛围音乐 |
27 | Trip-Hop | 神游舞曲 |
28 | Vocal | 声乐 |
29 | Jazz+Funk | 爵士乐+放克 |
30 | Fusion | 融合爵士乐 |
31 | Trance | 出神音乐 |
32 | Classical | 古典音乐 |
33 | Instrumental | 器乐 |
34 | Acid | |
35 | House | 浩室音乐 |
36 | Game | |
37 | Sound Clip | 音效及声音片段 |
38 | Gospel | 福音音乐 |
39 | Noise | 噪音音乐 |
40 | AlternRock | |
41 | Bass | 电贝斯 |
42 | Soul | 灵魂乐 |
43 | Punk | 朋克文化 |
44 | Space | Space |
45 | Meditative | 冥想音乐 |
46 | Instrumental Pop | |
47 | Instrumental Rock | |
48 | Ethnic | |
49 | Gothic | |
50 | Darkwave | |
51 | Techno-Industrial | |
52 | Electronic | 电子音乐 |
53 | Pop-Folk | |
54 | Eurodance | 欧陆舞曲 |
55 | Dream | |
56 | Southern Rock | |
57 | Comedy | 喜剧 |
58 | Cult | |
59 | Gangsta | |
60 | Top 40 | |
61 | Christian Rap | |
62 | Pop/Funk | 流行 (音乐类型)/放克 |
63 | Jungle | 早期丛林舞曲 |
64 | Native American | |
65 | Cabaret | 卡巴莱 |
66 | New Wave | 新浪潮 |
67 | Psychadelic | |
68 | Rave | 锐舞 |
69 | Showtunes | |
70 | Trailer | |
71 | Lo-Fi | |
72 | Tribal | |
73 | Acid Punk | |
74 | Acid Jazz | 酸爵士 |
75 | Polka | 波尔卡 |
76 | Retro | |
77 | Musical | |
78 | Rock & Roll | 摇滚 |
79 | Hard Rock | 硬式摇滚 |
80 | Folk | 民俗音乐 |
81 | Folk-Rock | 民谣摇滚 |
82 | National Folk | |
83 | Swing | |
84 | Fast Fusion | |
85 | Bebob | 咆勃爵士乐 |
86 | Latin | 拉丁舞 |
87 | Revival | |
88 | Celtic | 凯尔特音乐 |
89 | Bluegrass | 蓝草音乐 |
90 | Avantgarde | 前卫 |
91 | Gothic Rock | 哥德摇滚 |
92 | Progressive Rock | 前卫摇滚 |
93 | Psychedelic Rock | 迷幻摇滚 |
94 | Symphonic Rock | 前卫摇滚 |
95 | Slow Rock | |
96 | Big Band | 大乐团 |
97 | Chorus | 副歌 |
98 | Easy Listening | |
99 | Acoustic | 原音乐 |
100 | Humour | 幽默 |
101 | Speech | 语音 |
102 | Chanson | 香颂 |
103 | Opera | 歌剧 |
104 | Chamber Music | 室内乐 |
105 | Sonata | 奏鸣曲 |
106 | Symphony | 交响曲 |
107 | Booty Bass | |
108 | Primus | 讽刺 |
109 | Porn Groove | |
110 | Satire | |
111 | Slow Jam | |
112 | Club | 电子舞曲 |
113 | Tango | 探戈 |
114 | Samba | 桑巴 |
115 | Folklore | 民俗学 |
116 | Ballad | 谣曲 |
117 | Power Ballad | |
118 | Rhythmic Soul | |
119 | Freestyle | |
120 | Duet | |
121 | Punk Rock | 朋克摇滚 |
122 | Drum Solo | |
123 | A capella | 无伴奏合唱 |
124 | Euro-House | 浩室音乐 |
125 | Dance Hall | |
126 | Goa | |
127 | Drum&Bass | |
128 | Club-House | |
129 | Hardcore | |
130 | Terror | |
131 | Indie | |
132 | BritPop | |
133 | Negerpunk | |
134 | PolskPunk | |
135 | Beat | |
136 | ChristianGangstaRap | |
137 | Heavyl | |
138 | Blackl | |
139 | Crossover | |
140 | ContemporaryChristian | |
141 | ChristianRock | |
142 | Merengue | |
143 | Salsa | |
144 | Trashl | |
145 | Anime | |
146 | JPop | |
147 | Synthpop |
2.4 ID3v2
- 标签头
ID3v2标签有各种不同的大小,而且经常位于文件开头,以运用于流媒体中。举例来说,TIT2数据帧架包含标题,而WOAR数据帧架则包含该艺术家的网站URL链接。数据帧架最大可达16MB,不过标签总大小上限为256MB。标签也不再局限于西欧编码,而支持Unicode,解决了国际化的问题。文字框架会由一个编码字节标记,以得知其是由哪种编码存入的。
- 00 – ISO-8859-1
- 01 – UCS-2 (UTF-16 encoded Unicode with BOM)
- 02 – UTF-16BE encoded Unicode without BOM
- 03 – UTF-8 encoded Unicode
每个ID3V2.3的标签都一个标签头和若干个标签帧或一个扩展标签头组成。关于曲目的信息如标题、作者等都存放在不同的标签帧中,扩展标签头和标签帧并不是必要的,但每个标签至少要有一个标签帧。标签头和标签帧一起顺序存放在MP3文件的首部。
在文件的首部顺序记录10个字节的ID3V2.3的头部。数据结构如下:
struct ID3v2_HEADER
{
char head[3];
uint8_t ver_major;
uint8_t ver_revision;
struct FLAGS
{
uint8_t UNSYNCHRONISATION_USED : 1;
uint8_t EXTENDED_HEADER_PRESENT : 1;
uint8_t EXPERIMENTAL_TAG : 1;
};
struct synchsafe_integer
{
uint8_t raw[4];
};
};
- 标志字节
标志字节一般为0,定义如下:
abc00000
- 表示是否使用Unsynchronisation
- 表示是否有扩展头部
- 表示是否为测试标签
- 标签大小
一共四个字节,但每个字节只用7位,最高位不使用恒为0。所以格式如下0xxxxxx, 0xxxxxxx, 0xxxxxxx, 0xxxxxxx,计算大小时要将0去掉,得到一个28位的二进制数,就是标签大小,计算公式如下:
Total_size = (size[0] & 0x7f) * 0x200000 + (size[1] & 0x7f) * 0x400 + (size[2] & 0x7f) * 0x80 + (size[3] & 0x7f)
- 标签帧
每个标签帧都有-一个10个字节的帧头和至少一个字节的不固定长度的内容组成。它们也是顺序存放在文件中,和标签头和其他的标签帧也没有特殊的字符分隔。得到一个完整的帧的内容只有从帧头中的到内容大小后才能读出,读取时要注意大小,不要将其他帧的内容或帧头读入。
帧头的定义如下:
struct ID3v2_FRAME
{
char id[4];
uint32_t size;
struct FRAME_FLAGS
{
uint8_t TAG_ALTER_PRESERV : 1;
uint8_t FILE_ALTER_PRESERV : 1;
uint8_t READ_ONLY_FRAME : 1;
uint8_t zero : 5;
uint8_t COMPRESSED_FRAME : 1;
uint8_t ENCRYPTED_FRAME : 1;
uint8_t GROUP_MEMBER_FRAME : 1;
};
char frame_data[size];
};
- 帧标识
用四个字符标识一个帧,说明一个帧的内容含义
- 大小
每个字节的8位全用
FSize = size[0] * 0x100000000 + size[1] * 0x10000 + size[2] * 0x100 + size[3]
- 标志
只定义了6位,另外的10位为0。格式如下:
abc00000 def00000
- 标签保护标志,设置时认为此帧作废
- 文件保护标志,设置时认为此帧作废
- 只读标志,设置时认为此帧不能修改
- 压缩标志,设置时一个字节存放两个BCD码表示数字
- 加密标志
- 组标志,设置时说明此帧和其他的某帧是一组
- APIC
该帧包含与音频文件直接相关的图片。图像格式是图像的MIME类型和子类型。如果省略MIME媒体类型名称,将隐含“image /”。需要互操作性时,应使用“image / png”或“image / jpeg”图片格式。描述是图片的简短描述,表示为已终止的文本字符串。描述的最大长度为64个字符,但可能为空。可能有多个图片附加到一个文件,每个图片在其各自的“APIC”帧中,但只有一个具有相同的内容描述符。可能只有一张图片的图片类型分别声明为图片类型$ 01和$ 02。可以使用“MIME类型”“ – >”仅为图像文件添加链接 并拥有完整的URL而不是图片数据。但是,由于存在文件分离的风险,因此应谨慎使用链接文件。
<‘附图’的标题,ID:“APIC”>
文本编码$ xx
MIME类型<文本字符串> $ 00
图片类型$ xx
描述<根据编码的文本字符串> $ 00(00)
图片数据<二进制数据>
3 详细设计
4 程序实现
build.gradle
apply plugin: 'com.android.application' android { compileSdkVersion 28 defaultConfig { applicationId "cc.iriszero.mmp" minSdkVersion 24 targetSdkVersion 28 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility = '1.8' targetCompatibility = '1.8' } } dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation 'com.android.support:appcompat-v7:28.0.0' implementation 'com.android.support.constraint:constraint-layout:1.1.3' implementation 'com.android.support:design:28.0.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' implementation files('libs/chardet.jar') implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '3.12.1' }
activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <android.support.design.widget.AppBarLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="@style/AppTheme.AppBarOverlay"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" app:popupTheme="@style/AppTheme.PopupOverlay" /> </android.support.design.widget.AppBarLayout> <include layout="@layout/content_main" /> <android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_margin="@dimen/fab_margin" app:srcCompat="@drawable/ic_cab_done_mtrl_alpha" /> </android.support.design.widget.CoordinatorLayout>
content_main.xml
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:context=".MainActivity" tools:showIn="@layout/activity_main"> <EditText android:id="@+id/editText" style="@style/Base.Widget.MaterialComponents.TextInputEditText" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:layout_marginEnd="8dp" android:ems="10" android:inputType="textUri" android:singleLine="true" android:text="https://tohodog.top/Studio%20%E2%80%9CSyrup%20Comfiture%E2%80%9D%20-%20it%27s%20your%20world%20in%20scarlet.mp3" android:textAppearance="@style/TextAppearance.AppCompat" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/Output" android:layout_width="0dp" android:layout_height="0dp" android:layout_marginStart="8dp" android:layout_marginEnd="8dp" android:layout_marginBottom="8dp" android:scrollbars="vertical" android:singleLine="false" android:textAppearance="@style/TextAppearance.AppCompat" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/editText" /> </android.support.constraint.ConstraintLayout>
colors.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <color name="colorPrimary">#008577</color> <color name="colorPrimaryDark">#00574B</color> <color name="colorAccent">#008577</color> </resources>
menu_main.xml
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" tools:context="cc.iriszero.mmp.MainActivity"> <item android:id="@+id/showAPIC" android:orderInCategory="100" android:title="APIC" app:showAsAction="never" /> </menu>
MainActivity.java
package cc.iriszero.mmp; import android.app.Dialog; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.ColorDrawable; import android.media.AudioManager; import android.media.MediaPlayer; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.design.widget.FloatingActionButton; import android.support.design.widget.Snackbar; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.text.method.ScrollingMovementMethod; import android.util.Log; import android.view.View; import android.view.Menu; import android.view.MenuItem; import android.view.ViewGroup; import android.view.Window; import android.widget.EditText; import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Objects; import okhttp3.Call; import okhttp3.Callback; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; public class MainActivity extends AppCompatActivity { MP3Reader mp3Reader; Dialog dialog; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final EditText path = findViewById(R.id.editText); TextView Output = findViewById(R.id.Output); Output.setMovementMethod(ScrollingMovementMethod.getInstance()); Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); FloatingActionButton fab = findViewById(R.id.fab); Thread player = new Thread(() -> { try { MediaPlayer mediaPlayer = new MediaPlayer(); mediaPlayer.setDataSource(path.getText().toString().trim()); mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); mediaPlayer.prepareAsync(); mediaPlayer.setOnPreparedListener(mp -> mediaPlayer.start()); } catch (Exception e) { e.printStackTrace(); } }); fab.setOnClickListener(view -> { final View view1 = view; Snackbar.make(view, "Now Loading...", Snackbar.LENGTH_INDEFINITE).setAction("Action", null).show(); new Thread(() -> { if(path.getText().toString().contains("http")) { try { OkHttpClient okHttpClient = new OkHttpClient(); final Request request = new Request.Builder().url(path.getText().toString().trim()).get().build(); Call call = okHttpClient.newCall(request); call.enqueue(new Callback() { @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { Log.e("error",e.getMessage()); } @Override public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { mp3Reader = new MP3Reader(response.body() != null ? response.body().bytes() : new byte[0]); Snackbar.make(view1, "Done.", Snackbar.LENGTH_SHORT).setAction("Action", null).show(); MainActivity.this.runOnUiThread(() -> Output.setText(mp3Reader.toString())); player.start(); } }); } catch (Exception e) { Log.e("error:",e.getMessage()); } } else { try { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { mp3Reader = new MP3Reader(Files.readAllBytes(Paths.get(path.getText().toString().trim()))); Snackbar.make(view1, "Done.", Snackbar.LENGTH_SHORT).setAction("Action", null).show(); MainActivity.this.runOnUiThread(() -> Output.setText(mp3Reader.toString())); player.start(); } } catch (Exception e) { Log.e("error:",e.getMessage()); } } }).start(); }); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.showAPIC) { new Thread(this::showImage).start(); return true; } return super.onOptionsItemSelected(item); } public void showImage() { int i; for(i = 0;i<mp3Reader.id3v2_tag.tf.size();++i) if (mp3Reader.id3v2_tag.tf.get(i).id == MP3Reader.FrameID.APIC) break; byte[] raw = mp3Reader.id3v2_tag.tf.get(i).frame_data; int start; for(start = 0;start<raw.length;++start) if((raw[start] == -1 && raw[start + 1]==-40)||(raw[start]==-119 && raw[start+1]==80)) break; byte[] data = new byte[raw.length - start]; System.arraycopy(raw, start, data, 0, data.length); Bitmap bitmap = BitmapFactory.decodeByteArray(data,0,data.length); MainActivity.this.runOnUiThread(()-> { Dialog builder = new Dialog(MainActivity.this); builder.setCanceledOnTouchOutside(true); builder.requestWindowFeature(Window.FEATURE_NO_TITLE); Objects.requireNonNull(builder.getWindow()).setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT)); ImageView imageView = new ImageView(this); imageView.setImageBitmap(bitmap); builder.addContentView(imageView, new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); builder.show(); }); } }
MP3Reader.java
package cc.iriszero.mmp; import android.support.annotation.NonNull; import org.mozilla.intl.chardet.HtmlCharsetDetector; import org.mozilla.intl.chardet.nsDetector; import org.mozilla.intl.chardet.nsICharsetDetectionObserver; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; public class MP3Reader { public ID3v2_TAG id3v2_tag; public List<MPEG_FRAME> mf; public ID3v1_TAG id3v1_tag; public enum ID3_GENRES { Blues, ClassicRock, Country, Dance, Disco, Funk, Grunge, HipHop, Jazz, Metal, NewAge, Oldies, Other, Pop, RhythmAndBlues, Rap, Reggae, Rock, Techno, Industrial, Alternative, Ska, DeathMetal, Pranks, Soundtrack, EuroTechno, Ambient, TripHop, Vocal, JazzAndFunk, Fusion, Trance, Classical, Instrumental, Acid, House, Game, SoundClip, Gospel, Noise, AlternativeRock, Bass, Soul, Punk, Space, Meditative, InstrumentalPop, InstrumentalRock, Ethnic, Gothic, Darkwave, TechnoIndustrial, Electronic, PopFolk, Eurodance, Dream, SouthernRock, Comedy, Cult, Gangsta, Top40, ChristianRap, PopFunk, Jungle, NativeUS, Cabaret, NewWave, Psychedelic, Rave, ShowTunes, Trailer, LoFi, Tribal, AcidPunk, AcidJazz, Polka, Retro, Musical, RockAndRoll, HardRock, Folk, FolkRock, NationalFolk, Swing, FastFusion, Bebop, Latin, Revival, Celtic, Bluegrass, Avantgarde, GothicRock, ProgressiveRock, PsychedelicRock, SymphonicRock, Slowrock, BigBand, Chorus, EasyListening, Acoustic, Humour, Speech, Chanson, Opera, ChamberMusic, Sonata, Symphony, BootyBass, Primus, PornGroove, Satire, SlowJam, Club, Tango, Samba, Folklore, Ballad, PowerBallad, RhythmicSoul, Freestyle, Duet, PunkRock, DrumSolo, ACappella, EuroHouse, DanceHall, Goa, DrumAndBass, ClubHouse, HardcoreTechno, Terror, Indie, BritPop, Negerpunk, PolskPunk, Beat, ChristianGangstaRap, HeavyMetal, BlackMetal, Crossover, ContemporaryChristian, ChristianRock, Merengue, Salsa, ThrashMetal, Anime, Jpop, Synthpop, Abstract, ArtRock, Baroque, Bhangra, BigBeat, Breakbeat, Chillout, Downtempo, Dub, EBM, Eclectic, Electro, Electroclash, Emo, Experimental, Garage, Global, IDM, Illbient, IndustroGoth, JamBand, Krautrock, Leftfield, Lounge, MathRock, NewRomantic, NuBreakz, PostPunk, PostRock, Psytrance, Shoegaze, SpaceRock, TropRock, WorldMusic, Neoclassical, Audiobook, Audiotheatre, NeueDeutscheWelle, Podcast, IndieRock, GFunk, Dubstep, GarageRock, Psybient } public enum FrameID { AENC, ASPI, APIC, COMM, COMR, ENCR, EQU2, ETCO, GEOB, GRID, TIPL, LINK, MCDI, MLLT, OWNE, PRIV, PCNT, POPM, POSS, RBUF, RVA2, RVRB, SEEK, SIGN, SYLT, SYTC, TALB, TBPM, TCOM, TCON, TCOP, TDRC, TDEN, TDLY, TDRL, TDTG, TENC, TEXT, TFLT, TIT1, TIT2, TIT3, TKEY, TLAN, TLEN, TMCL, TMED, TMOO, TOAL, TOFN, TOLY, TOPE, TDOR, TOWN, TPE1, TPE2, TPE3, TPE4, TPOS, TPRO, TPUB, TRCK, TRSN, TRSO, TSIZ, TSOA, TSOP, TSOT, TSRC, TSSE, TSST, TXXX, UFID, USER, USLT, WCOM, WCOP, WOAF, WOAR, WOAS, WORS, WPAY, WPUB, WXXX, EQUA, IPLS, RVAD, TDAT, TIME, TORY, TRDA, TYER } public enum MPEGAudioVersion { MPEG_25, MPEG_2, MPEG_1 } public enum LayerDescription { LayerIII, LayerII, LayerI } public enum ChannelMode { Stereo, JointStereo, DualChannel, SingleChannel } public enum Emphasis { none, _5015ms, CCITJ17 } public class ID3v2_TAG { public ID3v2_HEADER hdr; public List<ID3v2_FRAME> tf; public class ID3v2_HEADER { public short ver_major; public short ver_revision; public boolean UNSYNCHRONISATTON_USED; public boolean EXTENDED_HEADER_PRESENT; public boolean EXPERIMENTAL_TAG; public int size; public ID3v2_HEADER(byte[] raw, int start) { start += 3; ver_major = UbyteToShort(raw[start++]); ver_revision = UbyteToShort(raw[start++]); UNSYNCHRONISATTON_USED = UbyteBitToBool(raw[start], 7); EXTENDED_HEADER_PRESENT = UbyteBitToBool(raw[start], 6); EXPERIMENTAL_TAG = UbyteBitToBool(raw[start++], 5); size = ((raw[start] & 0x7f) << 21) | ((raw[start + 1] & 0x7f) << 14) | ((raw[start + 2] & 0x7f) << 7) | (raw[start + 3] & 0x7f); } @NonNull public String toString() { return "ver_major = " + ver_major + "\n" + "ver_revision = " + ver_revision + "\n" + "UNSYNCHRONISATTON_USED = " + UNSYNCHRONISATTON_USED + "\n" + "EXTENDED_HEADER_PRESENT = " + EXTENDED_HEADER_PRESENT + "\n" + "EXPERIMENTAL_TAG = " + EXPERIMENTAL_TAG + "\n" + "size = " + size + "\n"; } } public class ID3v2_FRAME { public FrameID id; public int size; public boolean TAG_ALTER_PRESERV; public boolean FILE_ALTER_PRESERV; public boolean READ_ONLY_FRAME; public boolean COMPRESSED_FRAME; public boolean ENCRYPTED_FRAME; public boolean GROUP_MEMBER_FRAME; public byte[] frame_data; public ID3v2_FRAME(byte[] raw, int start) { id = FrameID.valueOf(new String(new char[]{(char)raw[start],(char)raw[start+1],(char)raw[start+2],(char)raw[start+3]})); start += 4; size = (raw[start] & 0xff) << 24 | (raw[start + 1] & 0xff) << 16 | (raw[start + 2] & 0xff) << 8 | raw[start + 3] & 0xff; start += 4; TAG_ALTER_PRESERV = UbyteBitToBool(raw[start], 7); FILE_ALTER_PRESERV = UbyteBitToBool(raw[start], 6); READ_ONLY_FRAME = UbyteBitToBool(raw[start++], 5); COMPRESSED_FRAME = UbyteBitToBool(raw[start], 7); ENCRYPTED_FRAME = UbyteBitToBool(raw[start], 6); GROUP_MEMBER_FRAME = UbyteBitToBool(raw[start++], 5); frame_data = Read(raw, start, size); } @NonNull public String toString() { return "id = " + id + "\n" + "size = " + size + "\n" + "TAG_ALTER_PRESERV = " + TAG_ALTER_PRESERV + "\n" + "FILE_ALTER_PRESERV = " + FILE_ALTER_PRESERV + "\n" + "READ_ONLY_FRAME = " + READ_ONLY_FRAME + "\n" + "COMPRESSED_FRAME = " + COMPRESSED_FRAME + "\n" + "ENCRYPTED_FRAME = " + ENCRYPTED_FRAME + "\n" + "GROUP_MEMBER_FRAME = " + GROUP_MEMBER_FRAME + "\n" + (id != FrameID.APIC ? "frame_data = " + ReadString(frame_data,1,frame_data.length - 1) + "\n" : ""); } } public ID3v2_TAG(byte[] raw, int start) { hdr = new ID3v2_HEADER(raw, start); start += 0xa; tf = new ArrayList<>(); for(int i = 0; start < hdr.size && raw[start] != 0; ++i) { tf.add(new ID3v2_FRAME(raw, start)); start += 0xa + tf.get(i).size; } } @NonNull public String toString() { StringBuilder out = new StringBuilder("----- ID3v2_HEADER -----\n" + hdr.toString() + "\n"); for(int i = 0;i<tf.size();++i) out.append("----- ID3v2_FRAME ").append(i).append(" -----\n").append(tf.get(i).toString()).append("\n"); return out.toString(); } } public class ID3v1_TAG { public String title; public String artist; public String album; public String year; public String comment; public int track; public ID3_GENRES genre; public ID3v1_TAG(byte[] raw, int start) { start += 3; title = ReadString(raw, start, 30); start += 30; artist = ReadString(raw, start, 30); start += 30; album = ReadString(raw, start, 30); start += 30; year = ReadString(raw, start, 4); start += 4; comment = ReadString(raw, start, 28); start += 28; ++start; track = UbyteToShort(raw[start++]); for(short gen = UbyteToShort(raw[start]), i = 1; gen<184 && i-- == 1;) genre = ID3_GENRES.values()[gen]; } @NonNull public String toString() { return "----- ID3v1_TAG -----\n" + "title = " + title + "\n" + "artist = " + artist + "\n" + "album = " + album + "\n" + "year = " + year + "\n" + "comment = " + comment + "\n" + "track = " + track + "\n" + "genre = " + genre + "\n\n"; } } public class MPEG_FRAME { public MPEG_HEADER mpeg_hdr; public byte[] mpeg_frame_data; public class MPEG_HEADER { public final short[] BitrateTable = new short[]{8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 192, 224, 256, 320, 352, 384, 416, 448}; public MPEGAudioVersion mpeg_id; public LayerDescription layer_id; public boolean protection_bit; public short bitrate; public int frequency; public boolean padding_bit; public boolean private_bit; public ChannelMode channel_mode; public boolean IntensityStereo; public boolean MSStereo; public boolean copyright; public boolean original; public Emphasis emphasis; public int frame_size; public MPEG_HEADER(byte[] raw, int start) { ++start; switch((raw[start] & 0x18) >> 3) { case 0b00: mpeg_id = MPEGAudioVersion.MPEG_25; break; case 0b10: mpeg_id = MPEGAudioVersion.MPEG_2; break; case 0b11: mpeg_id = MPEGAudioVersion.MPEG_1; break; } switch((raw[start] & 0x6) >> 1) { case 0b01: layer_id = LayerDescription.LayerIII; break; case 0b10: layer_id = LayerDescription.LayerII; break; case 0b11: layer_id = LayerDescription.LayerI; break; } protection_bit = !UbyteBitToBool(raw[start++],0); if(layer_id == LayerDescription.LayerI) { if(HByte(raw[start]) == 1) bitrate = BitrateTable[3]; else if(HByte(raw[start]) == 2) bitrate = BitrateTable[7]; else if(HByte(raw[start]) == 3) bitrate = BitrateTable[9]; else if(HByte(raw[start]) == 4) bitrate = BitrateTable[11]; else if(HByte(raw[start]) > 4 && HByte(raw[start]) < 15) bitrate = BitrateTable[8 + HByte(raw[start])]; } else if(layer_id == LayerDescription.LayerII) { if(HByte(raw[start]) == 1) bitrate = BitrateTable[3]; else if(HByte(raw[start]) > 1 && HByte(raw[start]) < 9) bitrate = BitrateTable[3 + HByte(raw[start])]; else if(HByte(raw[start]) > 8 && HByte(raw[start]) < 15) bitrate = BitrateTable[4 + HByte(raw[start])]; } else { if(mpeg_id == MPEGAudioVersion.MPEG_1) { System.out.println(raw[start]); if(HByte(raw[start]) < 10) bitrate = BitrateTable[2 + HByte(raw[start])]; else if(HByte(raw[start]) > 9 && HByte(raw[start]) < 15) bitrate = BitrateTable[3 + HByte(raw[start])]; } else if(HByte(raw[start]) < (mpeg_id == MPEGAudioVersion.MPEG_2 ? 15 : 9)) bitrate = BitrateTable[HByte(raw[start])]; } if(mpeg_id == MPEGAudioVersion.MPEG_1) { switch((UbyteBitToByte(raw[start],3) << 1) | UbyteBitToByte(raw[start],2)) { case 0b00: frequency = 44100; break; case 0b01: frequency = 48000; break; case 0b10: frequency = 32000; break; } } else if(mpeg_id == MPEGAudioVersion.MPEG_2) { switch((UbyteBitToByte(raw[start],3) << 1) | UbyteBitToByte(raw[start],2)) { case 0b00: frequency = 22050; break; case 0b01: frequency = 24000; break; case 0b10: frequency = 16000; break; } } else { switch((UbyteBitToByte(raw[start],3) << 1) | UbyteBitToByte(raw[start],2)) { case 0b00: frequency = 11025; break; case 0b01: frequency = 12000; break; case 0b10: frequency = 8000; break; } } padding_bit = UbyteBitToBool(raw[start],1); private_bit = UbyteBitToBool(raw[start++],0); switch(HByte(raw[start]) >> 2) { case 0b00: channel_mode = ChannelMode.Stereo; break; case 0b01: channel_mode = ChannelMode.JointStereo; break; case 0b10: channel_mode = ChannelMode.DualChannel; break; case 0b11: channel_mode = ChannelMode.SingleChannel; break; } if (channel_mode == ChannelMode.JointStereo) { switch(HByte(raw[start]) & 0x3) { case 0b00: IntensityStereo = false; MSStereo = false; break; case 0b01: IntensityStereo = true; MSStereo = false; break; case 0b10: IntensityStereo = false; MSStereo = true; break; case 0b11: IntensityStereo = true; MSStereo = true; break; } } copyright = UbyteBitToBool(raw[start++],3); original = UbyteBitToBool(raw[start++],2); switch(raw[start] & 0x3) { case 0b00: emphasis = Emphasis.none; break; case 0b01: emphasis = Emphasis._5015ms; break; case 0b11: emphasis = Emphasis.CCITJ17; break; } frame_size = (((mpeg_id == MPEGAudioVersion.MPEG_1 ? 144 : 72) * bitrate * 1000) / frequency) + (padding_bit ? 1 : 0); } @NonNull public String toString() { return "mpeg_id = " + mpeg_id + "\n" + "layer_id = " + layer_id + "\n" + "protection_bit = " + protection_bit + "\n" + "bitrate = " + bitrate + "\n" + "frequency = " + frequency + "\n" + "padding_bit = " + padding_bit + "\n" + "private_bit = " + private_bit + "\n" + "channel_mode = " + channel_mode + "\n" + "IntensityStereo = " + IntensityStereo + "\n" + "MSStereo = " + MSStereo + "\n" + "copyright = " + copyright + "\n" + "original = " + original + "\n" + "emphasis = " + emphasis + "\n" + "frame_size = " + frame_size + "\n"; } } public MPEG_FRAME(byte[] raw, int start) { mpeg_hdr = new MPEG_HEADER(raw, start); start += 32; mpeg_frame_data = Read(raw,start,mpeg_hdr.frame_size); } @NonNull public String toString() { return mpeg_hdr.toString(); } } public MP3Reader(byte[] raw) { for(int i = 0; i < raw.length; ++i) { if(raw[i] == 73 && raw[i + 1] == 68 && raw[i+2]==51) { id3v2_tag = new ID3v2_TAG(raw, i); break; } } for(int i = raw.length - 2;i>0;--i) { if(raw[i] == 84 && raw[i + 1] == 65 && raw[i+2]==71) { id3v1_tag = new ID3v1_TAG(raw, i); break; } } mf = new ArrayList<>(); for (int start = id3v2_tag == null ? 0 : id3v2_tag.hdr.size + 0xa,i=0; start < raw.length - 128;) { if (raw[start] == -1 && ((raw[start+1] >> 5) == -1)) { mf.add(new MPEG_FRAME(raw, start)); start += mf.get(i).mpeg_hdr.frame_size; break; } } /* for (int start = id3v2_tag.hdr.size + 0xa,i=0; start < raw.length - 128;) { if (raw[start] == -1 && ((raw[start+1] >> 5) == -1)) { mf.add(new MPEG_FRAME(raw, start)); start += mf.get(i).mpeg_hdr.frame_size; ++i; } }*/ } @NonNull public String toString() { String out = ""; out += id3v2_tag == null ? "" : id3v2_tag.toString(); out += id3v1_tag == null ? "" : id3v1_tag.toString(); out += "----- MPEG_HEADER 0 -----\n" + mf.get(0).toString(); return out; } private short UbyteToShort(byte ubyte) { return (short)(ubyte & 0xff); } private byte HByte(byte x) { return (byte)((x & 0xff) >> 4); } private boolean UbyteBitToBool(byte ubyte, int bit) { return ((1 << bit) & ubyte) != 0; } private byte UbyteBitToByte(byte ubyte, int bit) { return (byte)((1 << bit) & ubyte); } private String ReadString(byte[] raw, int start, int count) { byte[] temp = Read(raw, start, count); nsICharsetDetectionObserver cbo = arg0 -> HtmlCharsetDetector.found = true; nsDetector det = new nsDetector(); det.Init(cbo); det.DoIt(temp, temp.length,false); det.DataEnd(); String set = det.getProbableCharsets()[0]; return new String(temp, Charset.forName(set)); } private byte[] Read(byte[] raw, int start, int count) { byte[] res = new byte[count]; System.arraycopy(raw, start, res, 0, count); return res; } }