Java Sound Collection

Java Sound
online resources on formal explanation of java sound very good, the paper will not give examples, mainly to provide some good resources, and talk about some of my understanding, to form an overall understanding of java sound of.

A few words.
TTS: Speech-to-text, text-to-speech
OCR: optical-character-recignition Optical Character Recognition
MIDI: Musical Instrument Digital Interface, Musical Instrument Digital Interface

MIDI is the beginning of the 1980s presented by Dave Smith, the purpose is to solve the communication between electronic musical instruments. But a series of notes and other control parameters .MIDI instruction is not an audio signal transmitted MIDI + sound library synthesis of modern music is through it tells what to do .MIDI MIDI device transmitted signals are unified into MIDIMessage, asynchronous serial communications to pass through.

Tritonus: java sound is a standard, there are two sets to achieve a set of Sun's, is set in Tritonus in Java 1.3, Sun's Java standard library is included Since then, Tritonus very embarrassing... . to use Tritonus you should disable the Sun, and disable the Sun is a superfluous thing .Tritonus currently only supports Linux systems, but some Tritonus separate download plug-ins can also run on other systems.
the SPI: Service Provider . Interface provides an interface service, which is a common mode of the API the code written in the form of interfaces, some services may be implemented in accordance with these interfaces, thereby achieving pluggable programming .SPI there is another meaning: in the circuit , SPI serial peripheral interface means, serial peripheral Interface, which is a high-speed full-duplex, synchronous communication bus.

Acousitic acoustic, Reverb response, Gain Gain, Pan sound image .DAC (digital analog converter) DAC.

II. Learning Resources
jsresource
JS refers java sound, this site devoted to java sound, all-inclusive, called the Encyclopedia of java sound, there is a site that is enough, now need to do is put this site from start to finish.

oracle official website of java sound introduction
official website has always been the most important document provider, the oracle official website, there is a comprehensive strong sample, which shows the various aspects of java sound. The sample can be downloaded from this page. This sample particularly good, actually it can be used to play the piano, demonstrating the power of java sound.

http://www.oracle.com/technetwork/java/javase/downloads/index.html
This page is Oracle's official web surface, it is jdk download page. In this page, there is a demo jdk, download link doc, which are Shanghao of java learning resources, download, read javax.sound module.

http://www.javazoom.net/index.shtml
the Java Sound audio formats directly supported by very few, including only .wav (more common in windows) and .AIFF (more common in macintosh) and .au (more common in unix) three kinds format audio files. but through SPI, we do not have to modify the java code, only need to provide the appropriate SPI format can be achieved play a variety of file .javazoom website provides a set of mp3 decoder library, called JLayer.
there are sites on java Zoom java audio on multiple projects, here introduces JLayer and MP3SPI and jlGUI.JLayer launched in February 1999, the goal is to provide real-time MP3 decoder as java. it also includes JLayerME subprojects, version JLayer on the JavaME .MP3SPI is based JLayer and Tritonus Java plug-in, Tritonus is another realization of java sound standards, in order to use MP3SPI, need three jar package: mp3spi.jar and tritonus.jar and jlayer.jar, these three jar package into the class path, java sound will have the ability to play MP3 .jlGUI is a graphical music player, it is purely written in Java, depending on MP3SPI, this simple music player is simple, with the okay.

http://www.sauronsoftware.it/projects/jave/manual.php

jave (java audio video encoder) is a pure java version of the audio and video codecs.

III. Overview
AudioSystem is an important import category javax.sound package, everything to it as the center of AudioSystem default input device is a microphone, the default output device is a speaker
SourceDataLine and TargetDataLine are available .SourceDataLine AudioSystem is meant by " source data stream "refers to the AudioSystem input stream, writes the audio files to the AudioSystem, will play the audio file AudioSystem .TargetDataLine means" target data flow "refers to the output stream AudioSystem is AudioSystem the target. Therefore, when the playback file, the file contents are written SourceDataLine a AudioSystem; and when the recording of the content AudioSystem TargetDataLine is read into memory.
the clip is "cut", "fragment", indicates a complete section of the audio data in memory, It can be played over and over again, very suitable background music .Clip input port and SourceDataLine AudioSystem are playing the game.
in the java voice processing consists of four packages:

javax.sound.sample processing a digital audio
javax.sound.midi form midi audio processing
javax.sound.sample.spi sample corresponds to the type of service provider interfaces
javax.sound.midi.spi provided corresponding to types of service interfaces midi
IV. the most simple player
the AudioInputStream CIN = AudioSystem.getAudioInputStream (new new File ( "haha.wav"));
the AudioFormat = cin.getFormat the format ();
the DataLine.Info the DataLine.Info info new new = (SourceDataLine.class, the format);
SourceDataLine a = Line (SourceDataLine A) AudioSystem.getLine (info);
line.open (the format); // or line.open (); format parameters optional
line.start ();
int nBytesRead = 0;
byte [] = Buffer byte new new [512];
the while (to true) {
nBytesRead = cin.read (Buffer, 0, buffer.length);
IF (nBytesRead <= 0)
BREAK;
line.write (Buffer, 0, nBytesRead);
}
line.drain ();
line.close ();
This program can only play wav, pcm files can not play mp3 files.
The first step in building AudioInputStream cin from a file object, the cin object contains format data and audio data files.
Next, create DataLine.Info objects according to cin of AudioFormat.
the third step is to get SourceDataLine according to AudioFormat, there will be a speaker with SourceDataLine data stream, the speaker can sound a.

SourceDataLine like as a conduit, the data stream flows to AudioSystem audio system from the interior of the computer audio files, line.open () to open the duct inlet end, line.start () to open the outlet end of the conduit .line.drain () of the pipe introducing the outlet end of the pipe where the other remaining data exiled empty .line.close () shut the outlet end of the conduit.

The above code there is a place to simplify it:

  //DataLine.Info info = new DataLine.Info(SourceDataLine.class, ais.getFormat());
  //SourceDataLine sourceDataLine = (SourceDataLine) AudioSystem.getLine(info);
  SourceDataLine sourceDataLine = AudioSystem.getSourceDataLine(ais.getFormat());

The first was written:
first acquisition in accordance with audioInputStream # format DataLine.Info, then acquired by DataLine.Info SourceDataLine, this method to write long-winded.
The second written directly by audioInputStream # format structure SourceDataLine

五.最简单的录音机
File outputFile = new File(“recoder.wav”);
AudioFormat audioFormat = new AudioFormat(
AudioFormat.Encoding.PCM_SIGNED, 44100.0F, 16, 2, 4, 44100.0F,
false);
DataLine.Info info = new DataLine.Info(TargetDataLine.class,
audioFormat);
TargetDataLine targetDataLine = (TargetDataLine) AudioSystem
.getLine(info);
targetDataLine.open(audioFormat);
targetDataLine.start();
new Thread() {
public void run() {
AudioInputStream cin = new AudioInputStream(targetDataLine);
try {
AudioSystem.write(cin, AudioFileFormat.Type.WAVE,
outputFile);
System.out.println(“over”);
} catch (IOException e) {
e.printStackTrace ();
}
};
} .start ();
System.out.println ( "recording has started, after the finish, please enter the Enter key to finish recording");
System.in.read ();
TargetDataLine .close ();
like get SourceDataLine, in order to obtain from the AudioSystem TargetDataLine, need to provide good AudioFormat.
AudioInputStream has two constructors:

The AudioInputStream (TargetDataLine, Line)
the AudioInputStream (the InputStream stream, AudioFormat format, Long length)
focus here is the second function, InputStream stream refers to an audio stream, AudioFormat format refers to the form of audio, length represents the number contained in stream frame, is calculated stream.length / frameSize, frameSize where a frame represents the number of bytes occupied .frameSize = channelCount * sampleSize, a frame is recorded in each audio channel the current time value of sampling rate refers to a second the number of samples in a particular channel, i.e. the number of a frame in the second .sampleRate = frameRate.

AudioSystem.write () has two overloaded functions:

write (AudioInputStream stream, AudioFileFormat.Type fileType, File out): written to the file
write (AudioInputStream stream, AudioFileFormat.Type fileType, OutputStream out): written to the OutputStream
AudioSystem.write () function is thread blocked, as long as there is no AudioInputStream end, it will have been waiting for input, so you must open a thread to another record, otherwise we can not taped shut.

The integrated use of tape recorders and players, to achieve a Repeater, your computer spade spade.

AudioFormat format = new AudioFormat(Encoding.PCM_SIGNED, 8000, 16, 1, 2, 8000, true);
TargetDataLine dataLine = AudioSystem.getTargetDataLine(format);
dataLine.open();
dataLine.start();
SourceDataLine sourceDataLine = AudioSystem.getSourceDataLine(format);
sourceDataLine.open();
sourceDataLine.start();
while (true) {
byte[] buf = new byte[1024];
int cnt = dataLine.read(buf, 0, buf.length);
sourceDataLine.write(buf, 0, cnt);
}
六.使用Clip循环播放的小段音频
public static void main(String[] args) throws Exception {
Clip clip = AudioSystem.getClip();
clip.open(AudioSystem.getAudioInputStream(new File(“haha.wav”)));
clip.start();
clip.setLoopPoints(0, clip.getFrameLength() - 1);
while (true) {

    }
}

AudioSystem like a chip, it has three pins: SourceDataLine, TargetDataLine, Clip these three things are inherited from the interface to DataLine .SourceDataLine for playing audio, TargetDataLine for recording, Clip for an audio loop. You can set the number of cycles, etc.
when using Clip playback audio, will open a thread to play audio, so Clip is not blocked, which is different from SourceDataLine. so wrote at the end of the program an infinite loop.
Clip Gets manner except directly from AudioSystem.getClip (), AudioSystem.getClip (Info) direct access, you can get that as SourceDataLine, TargetDataLine.

AudioInputStream = AudioSystem.getAudioInputStream the AudioInputStream (clipFile);
the AudioFormat = AudioInputStream.getFormat the format ();
the DataLine.Info the DataLine.Info info new new = (Clip.class, the format);
the Clip = Clip (the Clip) AudioSystem.getLine (info);
seven play MP3 audio
when non pcm audio playback format, a corresponding decoder must be converted to the appropriate format to be able to play pcm format .pcm three formats:. PCM_FLOAT, PCM_SIGNED, PCM_UNSIGNED
JLayer is a MP3 decoder, MP3SPI is based JLayer and Tritonus of a MP3 service provider interface. jlayer.jar the jar and three tritonus.jar mp3spi.jar and on the classpath package, will automatically find the corresponding decoder to decode the decoding. this sample program relies above three jar package.

    AudioInputStream stream = AudioSystem
            .getAudioInputStream(new File("haha.mp3"));
    AudioFormat format = stream.getFormat();
    if (format.getEncoding() != AudioFormat.Encoding.PCM_SIGNED) {
        format = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,
                format.getSampleRate(), 16, format.getChannels(),
                format.getChannels() * 2, format.getSampleRate(), false);
        stream = AudioSystem.getAudioInputStream(format, stream);
    }
    DataLine.Info info = new DataLine.Info(SourceDataLine.class,
            stream.getFormat());
    SourceDataLine sourceDataLine = (SourceDataLine) AudioSystem
            .getLine(info);
    sourceDataLine.open(stream.getFormat(), sourceDataLine.getBufferSize());
    sourceDataLine.start();
    int numRead = 0;
    byte[] buf = new byte[sourceDataLine.getBufferSize()];
    while ((numRead = stream.read(buf, 0, buf.length)) >= 0) {
        int offset = 0;
        while (offset < numRead) {
            offset += sourceDataLine.write(buf, offset, numRead - offset);
        }
        System.out.println(sourceDataLine.getFramePosition() + " "
                + sourceDataLine.getMicrosecondPosition());
    }
    sourceDataLine.drain();
    sourceDataLine.stop();
    sourceDataLine.close();
    stream.close();

AudioFormat first obtain the original file, and then create a new AudioFormat according to this old AudioFormat, pay attention to the new AudioFormat addition Encoding changes, sampleSizeInBits should be changed to 16, the corresponding change .frameSize frameSize represent a frame of words used have occurred number of sections, frameSize = channelCount sampleSizeInBytes, that is, the number of channels channelCount 2.
constructed over the new AudioFormat, by AudioSystem.getAudioInputStream (format, stream) will be able to complete the decoding work this function, which calls the decoder for decoding. Once decoded complete, will get a new AudioInputStream, you can play it like to play a wav file as normal.
procedure uses the byte [] buf array above in reading using the double loop, why? the first is heavy required, which is the second heavy afraid sourceData never finish inside buf, if the write-once may cause data loss.
in the above playback, frame number and the number of milliseconds to play non-stop output to play, which is to SourceDataLine show these two functions.
another way is transcoded

; = AudioSystem.getAudioInputStream the AudioInputStream Stream (new new ( "haha.mp3") File)
Stream = AudioSystem.getAudioInputStream (AudioFormat.Encoding.PCM_SIGNED, Stream);
in this way able to transcode mp3 pcm by two words, but but given the absence of such a transcoder is come so that according to the above said method..
AudioSystem.getAudioInputStream () function has five types: read from the file is read from an InputStream is read from the URL, and the results read from the conversion to some AudioInputStream coding, read the result from the conversion to some AudioInputStream AudioFormat.

VIII. SourceDataLine for repeated playback using
said earlier Clip ideal for loop, in fact SourceDataLine also can be easily circulated to play. The key is to realize AudioInputStream the mark (int readLimit) and reset () function two .mark + a reset mechanism, are common in java flow regime, which is a mechanism: such as the current position for the first three bytes, once the mark (100) is called, a flag showing made at 3, then continue to move forward, for example, assuming that went to 66, to perform a reset (), then suddenly returned to the first three bytes; if the reset is not performed at 66 () but continued to walk forward, walk at 110 (already more than 100 steps to limit), then just at the mark mark 3 becomes ineffective. not all InputStream subclasses support mark mechanism can function to detect the current stream by calling the InputStream # markSurported () supports the mark .BufferedInputStream ByteArrayInputStream mechanism and the mechanism is to support the mark. for readLimit, not all of the flow after the finish readLimit step will mark set as invalid, but executed after the finish readLimit step A reset line mark remains on the back, in short, as is the actual readlimit max (explicit readlimit, the maximum flow in memory space).

byte[] abData = new byte[EXTERNAL_BUFFER_SIZE];
int nBytesRead = 0;
int nPlayCount = 0;
if (audioInputStream.getFrameLength() == AudioSystem.NOT_SPECIFIED ||
audioFormat.getFrameSize() == AudioSystem.NOT_SPECIFIED)
{
out(“cannot calculate length of AudioInputStream!”);
System.exit(1);
}
long lStreamLengthInBytes = audioInputStream.getFrameLength()
* audioFormat.getFrameSize();
if (lStreamLengthInBytes > Integer.MAX_VALUE)
{
out(“length of AudioInputStream exceeds 2^31, cannot properly reset stream!”);
System.exit(1);
}
int nStreamLengthInBytes = (int) lStreamLengthInBytes;

line.start();

while (nPlayCount < PLAY_COUNT)
{
nPlayCount++;
audioInputStream.mark(nStreamLengthInBytes);
nBytesRead = 0;
while (nBytesRead != -1)
{
try
{
nBytesRead = audioInputStream.read(abData, 0, abData.length);
}
catch (IOException e)
{
e.printStackTrace();
}
if (nBytesRead >= 0)
{
int nBytesWritten = line.write(abData, 0, nBytesRead);
}
}
audioInputStream.reset();
}
line.drain();
line.close();
九.关于混音器mixer
SourceDataLine and the Clip is input AudioSystem, TargetDataLine is the output of .AudioSystem AudioSystem like a black box, the box was inside what? Actually mixer. When you play two songs at the same time, the need for a mixer SourceDataLine and to a plurality of audio data of a plurality of mixing Clip.
all mixer system

public static void listMixers() {
    Mixer.Info[] a = AudioSystem.getMixerInfo();
    for (int i = 0; i < a.length; i++) {
        System.out.println(a[i].getName());
    }
}

Not all mixer supports three types of DataLine, in fact, an input mixer and output mixer are separated. Some mixers and support SourceDataLine Clip, and some mixers support TargetDataLine, some mixed What sound does not support. DataLine to get three kinds AudioSystem class by avoiding the manual programmer to see which mixer can be used for input, which can be used mixer output.

    Mixer.Info[] a = AudioSystem.getMixerInfo();
    for (int i = 0; i < a.length; i++) {
        Mixer mixer = AudioSystem.getMixer(a[i]);
        Line.Info[] b = {new Line.Info(SourceDataLine.class),
                new Line.Info(TargetDataLine.class),
                new Line.Info(Clip.class)};
        int ans = 0;
        for (int j = 0; j < b.length; j++) {
            if (mixer.isLineSupported(b[j])) {
                ans |= (1 << j);
            }
        }
        System.out.println(a[i].getName() + " " + ans);
    }

To get the mixer, then use AudioSystem # getMixer (Mixer.Info) function. To get DataLine, going through Mixer # getLine (Line.Info) function. This process is necessary to detect cases DataLine mixer support, which is very troublesome a. Fortunately, you can get DataLine want by AudioSystem # getLine (Line.Info) function directly, this function is the bottom package a bit.

View supported file types

public static void listSupportedTypes() {
    AudioFileFormat.Type[] aTypes = AudioSystem.getAudioFileTypes();
    for (int i = 0; i < aTypes.length; i++) {
        System.out.println(aTypes[i].getExtension());
    }
}

Ten view file metadata
view file metadata through three main categories: AudioFormat, AudioFileFormat, AudioInputStream.

File file = new File(“haha.wav”);
AudioFileFormat aff = AudioSystem.getAudioFileFormat(file);
AudioInputStream ais = AudioSystem.getAudioInputStream(file);
//AudioFormat既可以通过AudioInputStream获取,也可以通过AudioFileFormat获取.
AudioFormat af = ais.getFormat();// aff.getFormat()
out("---------AudioFileFormat---------");
out("Type " + aff.getType());
out("byteLength " + aff.getByteLength());
out("frame length " + aff.getFrameLength());
out(“format " + aff.getFormat());
out(“properties " + aff.properties());
out(”--------AudioFormat----------”);
out("encoding " + af.getEncoding());
out("channels " + af.getChannels());
out("sample rate "+ af.getSampleRate ());
OUT ( "the frameRate" + af.getFrameRate ());
OUT ( "Properties" + af.properties ());
OUT ( "sampleSizeInBits" + af.getSampleSizeInBits ());
OUT ( "the frameSize" + af.getFrameSize () );
OUT ( "the AudioInputStream -------------- -------");
OUT ( "frameLength" + ais.getFrameLength ());
length = frameLength / frameRate playback
frameRate the sampleRate =
the frameSize = channels sampleSizeInBytes = channels sampleSizeInBits /. 8
AudioFileFormat.getByteLength = header length + data length = file header length + frameSize * frameLength
above conclusions wav file is fully applicable to, but not for mp3 files, mp3 files are compressed as , not the original audio data.

XI. Audio file type conversion
AudioSystem.write (AudioInputStream, AudioFileFormat.Type, File) function can convert between wav, aiff, au.

XII. Other methods play audio
(1) using Applet.getAudioClip (URL) to get AudioClip

import java.applet.Applet;
import java.applet.AudioClip;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.net.MalformedURLException;
import java.net.URL;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;

public class TestAudioClip extends JPanel {

AudioClip audioClip;

TestAudioClip(String source) throws MalformedURLException {
    super(new GridLayout(1, 0, 10, 10));
    setBorder(new EmptyBorder(20, 20, 20, 20));
    JButton play = new JButton("Play");
    play.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent ae) {
            audioClip.play();
            audioClip.loop();
        }
    });
    add(play);

    JButton stop = new JButton("Stop");
    stop.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent ae) {
            audioClip.stop();
        }
    });
    add(stop);

    URL url = new URL(source);
    audioClip = Applet.newAudioClip(url);
    audioClip.play();
}

public static void main(String[] args) {
    SwingUtilities.invokeLater(new Thread() {
        public void run() {
            JFrame frame = new JFrame();
            TestAudioClip pc = null;
            try {
                pc = new TestAudioClip(
                        "file:///C:/Users/weidiao/Documents/eclipseProject/实验室java/haha.mp3");
            } catch (MalformedURLException e) {
                e.printStackTrace();
            }
            frame.getContentPane().add(pc);
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setVisible(true);
        }
    });
}

}
(2) using the sun AudioPlayer.player
sun java this packet is not the standard package, the preparation will be in the eclipse following code error:
Access Restriction: type of The 'the AudioPlayer' IS Not the API (Restriction ON requir
solution is: Right Project> Properties > buildpath, the access rule jdk this library (authority rules) to relax more.

import sun.audio.AudioPlayer;

{class TestAudioPlayer public
public static void main (String [] args) {throws a FileNotFoundException
AudioPlayer.player.start (new new the FileInputStream ( "haha.mp3"));
}
}
. thirteen playback of MIDI files
Sequencer sequencer = MidiSystem.getSequencer () ;
sequencer.open ();
sequencer.setSequence (new new FileInputStream ( "haha.mid"));
sequencer.start ();
. If you add a MP3SPI in the classpath, is likely to play without sound because mp3spi.jar dependent tritonus .jar change the runtime behavior. the solution is to remove mp3spi.jar and jlayer.jar and tritonus.jar out. It used to make me puzzled why no sound, I thought I was a bad speaker. the results of the java files in the console will be able to run audible.
run after the expiry will find this program can not be terminated, can always be seen in the task Manager. this is because the Sequencer does not close. Sequencer to add an event listener, when at the end of play, close Sequencer. this allows the program terminates normally the same problem in the sample which also exists that the whole family, you can add Event listener to listen play end event.

Sequencer = MidiSystem.getSequencer Sequencer ();
sequencer.open ();
sequencer.setSequence (new new the FileInputStream ( "haha.mid"));
sequencer.start ();
sequencer.addMetaEventListener (The MetaEventListener new new () {
@Override
public void Meta (A MetaMessage Meta) {
IF (meta.getType () == 47) {
sequencer.close ();
}
}
});
fourteen .Midi system Overview
java sound clearly divided into two sects: sample described immediately and midi.sample audio waveform data, equivalent to just tell you what made .midi should describe the audio instructions that tell the terminal should send notes, loudness, duration, etc. .MIDI to send commands directly to the terminal, which has great autonomy how to determine sound.
After the system is symmetrical sample and midi system design in the Java API. familiar set of API sample of midi look there is a sense of familiarity .midi systematically entrance class MidiSystem, quite AudioSystem status with the sample by MidiSystem can MidiDivice management, can be managed by AudioSystem AudioDivice. Sequencer may acquire (sequencer) and synthesizer (synthesizer) of MidiSystem by, sequencer Sequencer for playing a sound, a synthesized sound synthesizer.
Sequencer synthesizer and interfaces are inherited from MidiDivice interface, MidiDivice inherited from java.io.Closable.
Transmitter and Receiver are also interfaces inherited from java.io.Closable interfaces .Transmitter only a sub-interface MidiDiviceTransmitter, Receiver only a sub-interface MidiDiviceReceiver. Transmitter is a MIDI input port, use to play MIDI audio, Receiver is a MIDI output port for recording.

MidiEvent = MidiMessage (which is an entity class) + duration of the tick.
The MidiMessage three subclasses:. A ShortMessage, A MetaMessage, A SysexMessage
the MidiMessage two member variables:. Int length and byte [] data length represents a data length, data represent data. wherein the first byte of data represents a status.

In java, you will see a lot of use byte int representation, such as InputStream # read () to read a byte int return value. When the end of the reading, the return value of -1. The reason is represented using byte int, because this is the byte unsigned byte, but there is a java principle: everything Jie digital symbols so symbols can not be represented with a byte 128 to 255, so a byte unsigned int..

musical instruments, programs, pathes, timbres similar meaning, refer to certain sounds, certain instruments .midi said soundbank not the same with java in soundbank, midi in a soundbank can contain 128 kinds of musical instruments, and in java soundbank contains 16383 * 128 kinds of musical instruments is a java soundbank contains 16,383 midi bank to locate a musical instrument, java sound using Patch to locate instrument, Patch only two member methods:.. getBank (), getProgram ().

Published 15 original articles · won praise 4 · Views 2909

Guess you like

Origin blog.csdn.net/weixin_43965939/article/details/103944193