可以实现的声音种类:萝莉、大叔、肥仔、搞怪、熊孩子、慢吞吞、网红女、困兽、重机械、感冒、空灵等。
本方法是通过github开源的项目 TarsosDSP
废话不多说,先上代码
这里从maven上找了一个fork TarsosDSP打包的jar包,源码一样。也可以直接从TarsosDSP项目上把jar包下载到本地
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-core</artifactId>
<version>2.4.6</version>
</dependency>
public static void main(String[] args) throws Exception {
//这里返回的是pcm格式的音频
byte[] pcmBytes = speechPitchShiftMp3("/home/li/Music/silk2pcm/file.mp3", 0.6, 0.6);
//如果需要转成wav则需要给pcmBytes增加一个头部信息
//TarsosDSP中也有输出Wav格式音频的处理器,这里没有使用。
byte[] wavHeader = pcm2wav(bytes);
OutputStream wavOutPut = new FileOutputStream(tempFile);
wavOutPut.write(wavHeader);
wavOutPut.write(bytes);
wavOutPut.flush();
wavOutPut.close();
// 对于各种声音类型,以及所需添加的处理器,还有处理器参数代码,将在本文最后给出。
//如果需要转mp3格式的,也可以给我留言,我会加上。
}
/**
* 变声
* @param speedFactor 变速率 (0,2) 大于1为加快语速,小于1为放慢语速
* @param rateFactor 音调变化率 (0,2) 大于1为降低音调(深沉),小于1为提升音调(尖锐)
* @return 变声后的MP3数据输入流
*/
public static byte[] speechPitchShiftMp3(String fileUrl, double rateFactor, double speedFactor) throws IOException, UnsupportedAudioFileException {
WaveformSimilarityBasedOverlapAdd w = new WaveformSimilarityBasedOverlapAdd(WaveformSimilarityBasedOverlapAdd.Parameters.speechDefaults(rateFactor, 16000));
int inputBufferSize = w.getInputBufferSize();
int overlap = w.getOverlap();
AudioDispatcher dispatcher = AudioDispatcherFactory.fromPipe(fileUrl,16000,inputBufferSize,overlap);
w.setDispatcher(dispatcher);
dispatcher.addAudioProcessor(w);
/** 采样率转换器。 使用插值更改采样率, 与时间拉伸器一起可用于音高转换。 **/
dispatcher.addAudioProcessor(new RateTransposer(speedFactor));
AudioOutputToByteArray out = new AudioOutputToByteArray();
/** 声音速率转换器 -- 失败 **/
/*SoundTouchRateTransposer soundTouchRateTransposer = new SoundTouchRateTransposer(2);
soundTouchRateTransposer.setDispatcher(dispatcher);
dispatcher.addAudioProcessor(soundTouchRateTransposer);*/
/** 正弦波发生器 -- 无反应 **/
/*SineGenerator sineGenerator = new SineGenerator(0.5, 0.5);
dispatcher.addAudioProcessor(sineGenerator);*/
/** 音调转换器 -- 无效果 **/
// dispatcher.addAudioProcessor(new PitchShifter(0.1,16000,448,overlap));
/** 制粒机使用颗粒合成回放样本。方法可用于控制播放速率,音高,颗粒大小, -- 无效果 **/
// dispatcher.addAudioProcessor(new OptimizedGranulator(16000, 448));
/** 噪音产生器 -- 有效果 **/
// dispatcher.addAudioProcessor(new NoiseGenerator(0.2 ));
/** 增益处理器 增益为1,则无任何反应。 增益大于1表示音量增加a -- 有反应 **/
// dispatcher.addAudioProcessor(new GainProcessor(10));
/**镶边效果 -- 有反应 **/
// dispatcher.addAudioProcessor(new FlangerEffect(64, 0.3, 16000, 16000));// 回声效果
// dispatcher.addAudioProcessor(new FlangerEffect(1 << 4, 0.8, 8000, 2000));// 感冒
// dispatcher.addAudioProcessor(new ZeroCrossingRateProcessor());//感冒
/** 淡出 --声音慢慢变小 **/
// dispatcher.addAudioProcessor(new FadeOut(5));
/** 淡入-- 声音慢慢变大 **/
// dispatcher.addAudioProcessor(new FadeIn(5));
/** 在信号上添加回声效果。echoLength以秒为单位 elay回声的衰减,介于0到1之间的值。1表示无衰减,0表示立即衰减 **/
dispatcher.addAudioProcessor(new DelayEffect(0.2, 0.24, 12000) );
/** 调幅噪声 -- 将声音转换为噪声**/
// dispatcher.addAudioProcessor(new AmplitudeModulatedNoise());
/** 振幅LFO -- 声音波动 **/
// dispatcher.addAudioProcessor(new AmplitudeLFO());
dispatcher.addAudioProcessor(out);
dispatcher.run();
// return new ByteArrayInputStream(out.getData());
return out.getData();
}
public static byte[] pcm2wav(byte[] bytes) throws IOException {
//填入参数,比特率等等。这里用的是16位单声道 8000 hz
WaveHeader header = new WaveHeader();
//长度字段 = 内容的大小(PCMSize) + 头部字段的大小(不包括前面4字节的标识符RIFF以及fileLength本身的4字节)
header.fileLength = bytes.length + (44 - 8);
header.FmtHdrLeth = 16;
header.BitsPerSample = 16;
header.Channels = 1;
header.FormatTag = 0x0001;
header.SamplesPerSec = 16000;
header.BlockAlign = (short)(header.Channels * header.BitsPerSample / 8);
header.AvgBytesPerSec = header.BlockAlign * header.SamplesPerSec;
header.DataHdrLeth = bytes.length;
byte[] h = header.getHeader();
assert h.length == 44; //WAV标准,头部应该是44字节
return h;
}
AudioOutputToByteArray代码
import be.tarsos.dsp.AudioEvent;
import be.tarsos.dsp.AudioProcessor;
import org.tritonus.share.sampled.file.AudioOutputStream;
import java.io.ByteArrayOutputStream;
public class AudioOutputToByteArray implements AudioProcessor {
private boolean isDone = false;
private byte[] out = null;
private ByteArrayOutputStream bos;
private AudioOutputStream outputStream;
public AudioOutputToByteArray() {
bos = new ByteArrayOutputStream();
}
public ByteArrayOutputStream getBos() {
return bos;
}
public byte[] getData() {
while (!isDone && out == null) {
try {
Thread.sleep(10);
} catch (InterruptedException ignored) {}
}
return out;
}
@Override
public boolean process(AudioEvent audioEvent) {
bos.write(audioEvent.getByteBuffer(),0,audioEvent.getByteBuffer().length);
return true;
}
@Override
public void processingFinished() {
out = bos.toByteArray().clone();
bos = null;
isDone = true;
}
}
WaveHeader代码
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class WaveHeader {
public final char fileID[] = {'R', 'I', 'F', 'F'};
public int fileLength;
public char wavTag[] = {'W', 'A', 'V', 'E'};;
public char FmtHdrID[] = {'f', 'm', 't', ' '};
public int FmtHdrLeth;
public short FormatTag;
public short Channels;
public int SamplesPerSec;
public int AvgBytesPerSec;
public short BlockAlign;
public short BitsPerSample;
public char DataHdrID[] = {'d','a','t','a'};
public int DataHdrLeth;
public byte[] getHeader() throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
WriteChar(bos, fileID);
WriteInt(bos, fileLength);
WriteChar(bos, wavTag);
WriteChar(bos, FmtHdrID);
WriteInt(bos,FmtHdrLeth);
WriteShort(bos,FormatTag);
WriteShort(bos,Channels);
WriteInt(bos,SamplesPerSec);
WriteInt(bos,AvgBytesPerSec);
WriteShort(bos,BlockAlign);
WriteShort(bos,BitsPerSample);
WriteChar(bos,DataHdrID);
WriteInt(bos,DataHdrLeth);
bos.flush();
byte[] r = bos.toByteArray();
bos.close();
return r;
}
private void WriteShort(ByteArrayOutputStream bos, int s) throws IOException {
byte[] mybyte = new byte[2];
mybyte[1] =(byte)( (s << 16) >> 24 );
mybyte[0] =(byte)( (s << 24) >> 24 );
bos.write(mybyte);
}
private void WriteInt(ByteArrayOutputStream bos, int n) throws IOException {
byte[] buf = new byte[4];
buf[3] =(byte)( n >> 24 );
buf[2] =(byte)( (n << 8) >> 24 );
buf[1] =(byte)( (n << 16) >> 24 );
buf[0] =(byte)( (n << 24) >> 24 );
bos.write(buf);
}
private void WriteChar(ByteArrayOutputStream bos, char[] id) {
for (int i=0; i<id.length; i++) {
char c = id[i];
bos.write(c);
}
}
各种变声器参数
import be.tarsos.dsp.AudioDispatcher;
import be.tarsos.dsp.WaveformSimilarityBasedOverlapAdd;
import be.tarsos.dsp.ZeroCrossingRateProcessor;
import be.tarsos.dsp.effects.DelayEffect;
import be.tarsos.dsp.io.jvm.AudioDispatcherFactory;
import be.tarsos.dsp.resample.RateTransposer;
import com.bleege.recordingsound.utils.AudioOutputToByteArray;
import com.bleege.recordingsound.utils.WaveHeader;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.IOException;
import java.util.Optional;
import java.util.function.Consumer;
@Slf4j
public enum SoundEnum {
LUOLI(0.6, 0.6, "萝莉", 1, dispatcher -> {}),
DASHU(1.2, 1.2, "大叔", 2, dispatcher -> {}),
FEIZAI(1.5, 1.5, "肥仔", 3, dispatcher -> {}),
GAOGUAI(1.5, 0.8, "搞怪", 4, dispatcher -> {}),
XIONGHAIZI(0.73, 0.73, "熊孩子", 5, dispatcher -> {}),
MANTUNTUN(0.35,1, "慢吞吞",6 , dispatcher -> {}),
WANGHONGNV(1.2,0.7, "网红女",7 , dispatcher -> {}),
/**
* dispatcher.addAudioProcessor(new DelayEffect(0.2, 0.24, 12000) );
*/
KUNSHOU(1.55,1.55, "困兽", 8, dispatcher -> dispatcher.addAudioProcessor(new DelayEffect(0.2, 0.24, 12000))),
/**
* dispatcher.addAudioProcessor(new DelayEffect(0.2, 0.24, 12000) );
*/
ZHONGJIXIE(1.50,1.50, "重机械", 9, dispatcher -> dispatcher.addAudioProcessor(new DelayEffect(0.2, 0.24, 12000))),
/**
* dispatcher.addAudioProcessor(new FlangerEffect(1 << 4, 0.8, 8000, 2000));
* dispatcher.addAudioProcessor(new ZeroCrossingRateProcessor());
*/
GANMAO(1.05,1.05, "感冒", 10, dispatcher -> {
dispatcher.addAudioProcessor(new DelayEffect(0.2, 0.24, 12000));
dispatcher.addAudioProcessor(new ZeroCrossingRateProcessor());
}),
/**
* dispatcher.addAudioProcessor(new DelayEffect(0.8, 0.5, 12000) );
* dispatcher.addAudioProcessor(new DelayEffect(0.5, 0.3, 8000) );
*/
KONGLING(1, 1, "空灵", 11, dispatcher -> {
dispatcher.addAudioProcessor(new DelayEffect(0.8, 0.5, 12000) );
dispatcher.addAudioProcessor(new DelayEffect(0.5, 0.3, 8000) );
});
/**
* @param speedFactor 变速率 (0,2) 大于1为加快语速,小于1为放慢语速
* @param rateFactor 音调变化率 (0,2) 大于1为降低音调(深沉),小于1为提升音调(尖锐)
*/
SoundEnum(double rateFactor, double speedFactor, String name, int type, Consumer<AudioDispatcher> consumer){
this.rateFactor = rateFactor;
this.speedFactor = speedFactor;
this.name = name;
this.type = type;
this.consumer = consumer;
}
private double rateFactor;
private double speedFactor;
private String name;
private int type;
private Consumer consumer;
public byte[] run(String fileUrl){
WaveformSimilarityBasedOverlapAdd w = new WaveformSimilarityBasedOverlapAdd(WaveformSimilarityBasedOverlapAdd.Parameters.speechDefaults(rateFactor, 16000));
int inputBufferSize = w.getInputBufferSize();
int overlap = w.getOverlap();
AudioDispatcher dispatcher = AudioDispatcherFactory.fromPipe(fileUrl,16000,inputBufferSize,overlap);
w.setDispatcher(dispatcher);
dispatcher.addAudioProcessor(w);
/** 采样率转换器。 使用插值更改采样率, 与时间拉伸器一起可用于音高转换。 **/
dispatcher.addAudioProcessor(new RateTransposer(speedFactor));
AudioOutputToByteArray out = new AudioOutputToByteArray();
consumer.accept(dispatcher);
dispatcher.addAudioProcessor(out);
dispatcher.run();
return out.getData();
}
public static byte[] pcm2wav(byte[] bytes) {
try {
//填入参数,比特率等等。这里用的是16位单声道 8000 hz
WaveHeader header = new WaveHeader();
//长度字段 = 内容的大小(PCMSize) + 头部字段的大小(不包括前面4字节的标识符RIFF以及fileLength本身的4字节)
header.fileLength = bytes.length + (44 - 8);
header.FmtHdrLeth = 16;
header.BitsPerSample = 16;
header.Channels = 1;
header.FormatTag = 0x0001;
header.SamplesPerSec = 16000;
header.BlockAlign = (short)(header.Channels * header.BitsPerSample / 8);
header.AvgBytesPerSec = header.BlockAlign * header.SamplesPerSec;
header.DataHdrLeth = bytes.length;
byte[] h = header.getHeader();
assert h.length == 44; //WAV标准,头部应该是44字节
return h;
} catch (IOException e) {
log.error("pcm2wav-error", e);
}
return null;
}
public static Optional<SoundEnum> getInstance(int type){
for (int i = 0; i < SoundEnum.values().length; i++) {
if(SoundEnum.values()[i].type == type)
return Optional.of(SoundEnum.values()[i]);
}
return Optional.empty();
}
}