This article has authorized the wechat public account: Hongyang (hongyangAndroid) in the wechat public account platform original launch.
App & Technology introduction
This app uses THE MD specification, has a simple interface style, and has strong practicability in making mp3 cutting ringtones. Although the function is simple, but technically the project is “small sparrow, all five organs”. Here are some brief introductions from the technical level:
- The home page uses the classic MD design style of CoordinatorLayout+AppBarLayout+DrawerLayout+NavigationView.
- The overall project adopts MVP+ Databinding + RXJAVA2 + RxAndroid2 + Dagger2 framework design, data cache using Greendao.
- The audio spectrum is mainly drawn through the waveform data obtained in Visualizer.
- In terms of cutting function, the core function of MP3 cutting uses the Jaudiotagger JAR package to obtain the byte position of MP3 metadata and perform file IO operations to generate target files. This feature is the focus of this article and will be explained in detail later.
- For animations, the welcome page uses Lottie animations. If you’re interested, this blog post goes through the detailed steps to create Lottie animations and apply them to android projects. The file selection page and about page in the project uses the property animation and the property animation component AVLoadingIndicatorView.
- Android CustomRangeSeekBar CustomRangeSeekBar CustomRangeSeekBar
Instructions + GIF
Step1. Select the mp3 file
Step2. Select the shear range through the slider and click the shear button
Tips: You can see three buttons on the main interface, with functions from left to right:
- Play \ Pause
- Switch the slider for playback (switch the current playing position, front slider or back slider)
- Music shear
Mp3 cut to realize the idea
The realization idea has two main points
- Gets the file byte position of the mp3 start time (the start time to cut) and the file byte position of the end time
- Generate our target file by combining the source file with the start time byte position and end time byte position
Mp3 cut technical points
So how to get the byte position of the file where mp3 starts? Jaudiotagger.jar is used here. Its home page describes it this way
Jaudiotagger is a Java API for audio metatagging. Both a common API and format specific APIs are available, Currently supports reading and writing metadata for:Mp3, Flac, OggVorbis, Mp4, Aiff, Wav, Wma, Dsf
It is a Java library of audio meta tags that support read and write metadata operations in specific formats such as MP3.
Mp3 cut implementation details:
First, we need to do through Jaudiotagger to get the metadata of MP3, through the metadata to get the first frame byte position and bit rate of MP3. The byte position of the corresponding file can then be determined based on the byte position of the first frame as well as the bit rate and start time. Finally, the start byte position and the end byte position are obtained.
- Obtain the MP3 metadata
MP3File mp3 = new MP3File(this.mp3File); MP3AudioHeader Header = (MP3AudioHeader) mp3.getaudioheader ();Copy the code
- Get the MP3 bit rate based on metadata
/ / based on metadata for bit rate long bitRateKbps = header. GetBitRateAsNumber ();Copy the code
You may ask, what is bitrate?
The bit rate is the number of bits (bits) transmitted per second
Consider the way in which we take mp3 bitRate annotation long bitRate = header. GetBitRateAsNumber (); See the method source comment below:
/ * * * * @return bitrate in kbps, no indicator is provided as to
* whether or not it is vbr
*/
public long getBitRateAsNumber()
{
return bitrate;
}
Copy the code
The comments show that this method returns the bit rate in KBPS (kilobytes per second), whereas the bit rate we want is (bits per millisecond). The next step is the unit conversion calculation.
- GetBitRateAsNumber () *1024L / 8L / 1000L getBitRateAsNumber() *1024L / 8L / 1000L The code is as follows:
Long bitRatebpm = bitRateKbps *1024L / 8L / 1000L * beginTime;Copy the code
- Is this value the byte position of the file where the start time is? Of course not, our MP3 files not only contain music data, but also contain music information header data. Also we can get our first frame byte position of mp3 from the scratch information. The first frame byte position + bits per millisecond is the unit bit rate, which is the starting byte position of mp3. The code is as follows:
long firstFrameByte = header.getMp3StartByte();
long beginByte = firstFrameByte + beginBitRateBpm;
Copy the code
- Similarly, start byte beginType+ time difference (clipping end time – start time) bit rate (in milliseconds) calculated above can be used to calculate the end byte position, code in the following:
Long endByte = beginByte + convertKbpsToBpm(bitRateKbps) * (endTime - beginTime);Copy the code
Long endIndex = beginIndex + bitRate *1024L / 8L / 1000L (bitRate per millisecond) * (endTime -) BeginTime) (truncated time in milliseconds);
Second, with the start time byte position and the end time byte position, we can combine the source file to generate our target file pull. We can use RandomAccessFile to implement random read and write operations, using randomAccessFile.seek () to call the specified location.
- If the MP3 file we are trying to manipulate is too big, for example, if the size of the byte we are trying to capture is 100MB, then our app will crash because of OOM. My solution here is to use a cache array to limit the size of each read/write operation, so that no matter how big the file is, we will not have OOM problems.
- First, we write a tool method to generate the target file in the way of cache, the source file reads the specified size of the data read and write to the target file, the code is as follows:
/** ** * @param targetFile Output file * @paramsourceFile File to be read * @param buffer Buffer container for input and output * @param offset Offset value of seek when reading files */ private static void writeSourceToTargetFile(RandomAccessFile targetFile, RandomAccessFilesourceFile,
byte buffer[], long offset) throws Exception {
sourceFile.seek(offset);
sourceFile.read(buffer); long fileLength = targetFile.length(); // Move the write pointer to the end of the file. targetFile.seek(fileLength); targetFile.write(buffer); }Copy the code
- The size of the file to be cut is smaller than or equal to the size of the cache. The code is as follows:
private static void writeSourceToTargetFileWithBuffer(RandomAccessFile targetFile, RandomAccessFile sourceFile, long totalSize, long offset) throws Exception {// Cache size to prevent memory leaks when specified data is written int bufferSize = BUFFER_SIZE; long count = totalSize / buffersize;ifWriteSourceToTargetFile (targetFile,sourceFile, new byte[(int) totalSize], offset);
} elseLong remainSize = totalSize % bufferSize; byte data[] = new byte[buffersize]; // The offset of seek when reading the filefor (int i = 0; i < count; i++) {
writeSourceToTargetFile(targetFile, sourceFile, data, offset); offset += BUFFER_SIZE; } // Write the rest of the dataif (remainSize > 0) {
writeSourceToTargetFile(targetFile, sourceFile, new byte[(int) remainSize], offset); }}}Copy the code
- Finally, consider not only to write the data related to the MP3 music frame, but also to write the header information, the code is as follows:
/** * Generate target MP3 file ** @param targetFile * @param beginByte * @param endByte * @param firstFrameByte * @throws Exception */ private void generateTargetMp3File(RandomAccessFile targetFile, long beginByte, long endByte, long firstFrameByte) throws Exception { RandomAccessFilesourceFile = new RandomAccessFile(mSourceMp3File, "rw");
try {
//write mp3 header info
writeSourceToTargetFileWithBuffer(targetFile, sourceFile, firstFrameByte, 0);
//write mp3 frame info
int size = (int) (endByte - beginByte);
writeSourceToTargetFileWithBuffer(targetFile, sourceFile, size, beginByte);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (sourceFile ! = null)sourceFile.close(); }}Copy the code
Here is the end, ability is limited, write wrong good place, please give more comments. The project plan will continue to maintain and upgrade, thank you for your attention!!
Source & apk
-
Code has been uploaded to Github
-
Making APK download
-
Dandelion APK download
Unit testing
If you do not have a mobile phone or other reasons not convenient to use the app. Unit tests and MP3 files are provided in the project, so you can experience the MP3 clipping function through unit tests.
- Laozi. The mp3 is source mp3
- Test. mp3 is an MP3 file generated after a unit test is run.
- StartTime and endTime indicate the startTime and endTime of clipping
subsequent
After the post was published by Hongyang, Github received a lot of attention. Some people raised an issue, “Some MP3 files failed to be cut”. The reason is that the previous MP3 cut only to do constant bit rate support, in the variable bit rate that piece of logic is not realized, directly throw an exception.
public void generateNewMp3ByTime(String targetFileStr, long beginTime, long endTime) throws Exception {
MP3File mp3 = new MP3File(this.mSourceMp3File);
MP3AudioHeader header = (MP3AudioHeader) mp3.getAudioHeader();
if (header.isVariableBitRate()) {
throw new Exception("This is nonsupport variableBitRate!!!");
} else{... }}Copy the code
You can see that the previous version did not support variable bit rates. Here’s an idea for implementing variable bit rate MP3 clipping.
- One important point: the time of each frame is equal
- Formula: Bit size per frame = (sampling times per frame x bit rate (bit/s) ÷ 8 ÷ sampling rate) + Padding
- Mp3 total bit size = number of MP3 frames x bit size per frame
- Ratio of Start time to total duration = Start time/total mp3 duration, ratio of End time to total DURATION = End time/total MP3 duration
- Bits corresponding to start time = Total size of MP3 bits * Proportion of the start time to the total duration; Bits corresponding to end time = Total size of MP3 bits * Proportion of the end time to the total duration
The code:
/** * Generate MP3 files based on time and source file (source file MP3 bit rate is VBR variable bit rate) ** @param header * @param targetFileStr * @param beginTime * @param endTime * @throws IOException */ private void generateMp3ByTimeAndVBR(MP3AudioHeader header, String targetFileStr, long beginTime, long endTime) throws IOException { long frameCount = header.getNumberOfFrames(); int sampleRate = header.getSampleRateAsNumber(); int sampleCount = 1152; //header.getNoOfSample(); int paddingLength = header.isPadding() ? 1:0; // Frame size = (sampling times per frame x bit rate (bit/s) ÷ 8 ÷ sampling rate) + Padding //getBitRateAsNumber returns KBPS, so *1000floatframeSize = sampleCount * header.getBitRateAsNumber() / 8f / sampleRate * 1000 + paddingLength; Int trackLengthMs = header.getTrackLength() * 1000; // The ratio of start time to total timefloat beginRatio = (float) beginTime / (float) trackLengthMs; // The ratio of end time to total timefloat endRatio = (float) endTime / (float) trackLengthMs; long startFrameSize = (long) (beginRatio * frameCount * frameSize); long endFrameSize = (long) (endRatio * frameCount * frameSize); // Return the first byte of music data long firstFrameByte = header.getmp3StartByte (); generateTargetMp3File(targetFileStr, startFrameSize, endFrameSize, firstFrameByte); }Copy the code
Thank you
- jaudiotagger
- RXJava
- RxAndroid
- greendao
- StatusBarUtil
- Dagger2
- PermissionsDispatcher
- logger
- AVLoadingIndicatorView
- baseAdapter
- CustomRangeSeekBar
License
Mp3Cutter is under CC BY-NC-SA license.