-
Notifications
You must be signed in to change notification settings - Fork 16
/
WaveformTask.java
executable file
·191 lines (165 loc) · 7.9 KB
/
WaveformTask.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
/*
* This file is part of Musicott software.
*
* Musicott software is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Musicott 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Musicott. If not, see <http://www.gnu.org/licenses/>.
*
* Copyright (C) 2015 - 2017 Octavio Calleya
*/
package com.transgressoft.musicott.tasks;
import be.tarsos.transcoder.*;
import be.tarsos.transcoder.ffmpeg.*;
import com.google.inject.*;
import com.google.inject.assistedinject.*;
import com.transgressoft.musicott.model.*;
import com.transgressoft.musicott.player.*;
import com.transgressoft.musicott.view.*;
import org.slf4j.*;
import javax.sound.sampled.*;
import javax.sound.sampled.AudioFormat.*;
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.*;
import static java.nio.file.StandardCopyOption.*;
/**
* Class that extends from {@link Thread} that performs the operation of processing
* an audio file to get the waveform image. It waits for a {@link Semaphore} to process
* a new {@link Track} in an endless loop, instead of terminating the execution for
* each {@code track} process.
* <p>
* If the audio file is not WAV, is converted into WAV before get
* the wav amplitudes from the audio file.
* </p><p>
* This way is rudimentary, an inefficient. It should be improved.
* </p>
*
* @author Octavio Calleya
* @version 0.10.1-b
* @see <a href="https://github.com/JorenSix/TarsosTranscoder">Tarsos Transcoder</a>
*/
public class WaveformTask extends Thread {
private static final double WAVEFORM_HEIGHT_COEFFICIENT = 2.6; // This fits the waveform to the swing node height
private static final double WAVEFORM_WIDTH = 520.0;
private static final CopyOption[] options = new CopyOption[]{COPY_ATTRIBUTES, REPLACE_EXISTING};
private final Logger LOG = LoggerFactory.getLogger(getClass().getName());
private final PlayerFacade playerFacade;
private final WaveformsLibrary waveformsLibrary;
private final BlockingQueue<Track> tracksToProcessQueue;
private Track trackToAnalyze;
private float[] resultingWaveform;
private ErrorDialogController errorDialog;
@Inject
public WaveformTask(@Assisted BlockingQueue<Track> tracksToProcessQueue, WaveformsLibrary waveformsLibrary,
PlayerFacade playerFacade) {
this.tracksToProcessQueue = tracksToProcessQueue;
this.waveformsLibrary = waveformsLibrary;
this.playerFacade = playerFacade;
}
@Override
public void run() {
while (true) {
try {
trackToAnalyze = tracksToProcessQueue.take();
LOG.debug("Processing resultingWaveform of trackToAnalyze {}", trackToAnalyze);
String fileFormat = trackToAnalyze.getFileFormat();
if ("wav".equals(fileFormat))
resultingWaveform = processFromWavFile();
else if ("mp3".equals(fileFormat) || "m4a".equals(fileFormat))
resultingWaveform = processFromNoWavFile(fileFormat);
if (resultingWaveform != null) {
waveformsLibrary.addWaveform(trackToAnalyze.getTrackId(), resultingWaveform);
Optional<Track> currentTrack = playerFacade.getCurrentTrack();
currentTrack.ifPresent(this::checkAnalyzedTrackIsCurrentPlaying);
}
}
catch (IOException | UnsupportedAudioFileException | EncoderException | InterruptedException exception) {
LOG.warn("Error processing waveform of {}", trackToAnalyze, exception);
errorDialog.show("Error processing waveform of " + trackToAnalyze.getFileName(), null, exception);
}
}
}
private float[] processFromWavFile() throws IOException, UnsupportedAudioFileException {
File trackFile = new File(trackToAnalyze.getFileFolder(), trackToAnalyze.getFileName());
return processAmplitudes(getWavAmplitudes(trackFile));
}
private float[] processFromNoWavFile(String fileFormat) throws IOException, UnsupportedAudioFileException,
EncoderException {
int trackId = trackToAnalyze.getTrackId();
Path trackPath = FileSystems.getDefault().getPath(trackToAnalyze.getFileFolder(), trackToAnalyze.getFileName());
File temporalDecodedFile = File.createTempFile("decoded_" + trackId, ".wav");
File temporalCopiedFile = File.createTempFile("original_" + trackId, "." + fileFormat);
Files.copy(trackPath, temporalCopiedFile.toPath(), options);
transcodeToWav(temporalCopiedFile, temporalDecodedFile);
return processAmplitudes(getWavAmplitudes(temporalDecodedFile));
}
private float[] processAmplitudes(int[] sourcePcmData) {
int width = (int) WAVEFORM_WIDTH; // the width of the resulting waveform panel
float[] waveData = new float[width];
int samplesPerPixel = sourcePcmData.length / width;
for (int w = 0; w < width; w++) {
float nValue = 0.0f;
for (int s = 0; s < samplesPerPixel; s++) {
nValue += (Math.abs(sourcePcmData[w * samplesPerPixel + s]) / 65536.0f);
}
nValue /= samplesPerPixel;
waveData[w] = nValue;
}
return waveData;
}
private int[] getWavAmplitudes(File file) throws UnsupportedAudioFileException, IOException {
int[] amplitudes;
try (AudioInputStream input = AudioSystem.getAudioInputStream(file)) {
AudioFormat baseFormat = input.getFormat();
Encoding encoding = AudioFormat.Encoding.PCM_UNSIGNED;
float sampleRate = baseFormat.getSampleRate();
int numChannels = baseFormat.getChannels();
AudioFormat decodedFormat = new AudioFormat(encoding, sampleRate, 16, numChannels, numChannels * 2,
sampleRate, false);
int available = input.available();
amplitudes = new int[available];
try (AudioInputStream pcmDecodedInput = AudioSystem.getAudioInputStream(decodedFormat, input)) {
byte[] buffer = new byte[available];
pcmDecodedInput.read(buffer, 0, available);
for (int i = 0; i < available - 1; i += 2) {
amplitudes[i] = ((buffer[i + 1] << 8) | buffer[i] & 0xff) << 16;
amplitudes[i] /= 32767;
amplitudes[i] *= WAVEFORM_HEIGHT_COEFFICIENT;
}
input.close();
}
}
return amplitudes;
}
private void transcodeToWav(File sourceFile, File destinationFile) throws EncoderException {
Attributes attributes = DefaultAttributes.WAV_PCM_S16LE_STEREO_44KHZ.getAttributes();
try {
Transcoder.transcode(sourceFile.toString(), destinationFile.toString(), attributes);
}
catch (EncoderException exception) {
if (exception.getMessage().startsWith("Source and target should")) {
// even with this error message the library does the conversion, who knows why
}
else {
throw exception;
}
}
}
private void checkAnalyzedTrackIsCurrentPlaying(Track currentPlayingTrack) {
if (currentPlayingTrack.equals(trackToAnalyze))
playerFacade.setWaveform(trackToAnalyze);
}
public void setErrorDialog(ErrorDialogController errorDialog) {
this.errorDialog = errorDialog;
}
}