Play network audio, can first download good, get audio files, simple
Use AVAudioPlayer to play, over
Apple package under AVAudioPlayer handles local files for convenience
Just get a file url and play it
Simple mechanical understanding:
Facilitate the transmission of audio, generally using audio compression files, MP3 and so on. File pressure small volume, good transmission
Sound cards play PCM buffers
Apple helped develop a compressed format that was converted to an uncompressed raw file called PCM,
Also help the development of audio playback resource scheduling, from the PCM file to take out a section of the buffer, to the sound card consumption
(Actually there are not two steps, of course the process is parallel)
Now the manual
This article introduces the direct audio streaming media
When you receive an audio packet from the network, you play it.
Step four:
1, network audio files >> download to local audio data
Download the binary data of the audio file
Task of URLSession to retrieve network files
When you get a packet of Data, you process one
In this example, a packet, Data, corresponds to an audio packet, and corresponds to an audio buffer
This is the easy step,
Create a URLSessionDataTask and download it
It’s all in the network proxy method
Extension Downloader: URLSessionDataDelegate public func urlSession(_ session: urlSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { totalBytesCount = response.expectedContentLength CompletionHandler (.allow)} public func urlSession(_ session: urlSession, dataTask: URLSessionDataTask, didReceive data: Data) {// Update, TotalBytesReceived += Int64(data.count) progress = Float(totalBytesReceived)/Float(totalBytesCount) // Data is handed to the delegate to be parsed as an audio packet delegate? .download(self, didReceiveData: data, progress: progress)} public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { state = .completed delegate?.download(self, completedWithError: error) } }Copy the code
Audio basic understanding first:
Audio files are divided into encapsulation format (file format), and encoding format
The three levels of audio data are buffer, packet and frame
Packet, packet,
Audio packet, audio frame
Audio encoding format, generally divided into variable bit rate, and fixed bit rate
Fixed bit rate CBR, average sampling, corresponding to raw file, PCM (uncompressed file)
Variable bit rate VBR, corresponding to compressed files, such as MP3
Core Audio supports VBR, generally via the variable frame rate format VFR
VFR refers to: the volume of each packet is the same, and the number of frames in packet is different, and the audio data contained in frame is large or small
Description of data in Core Audio
Fixed rate described in ASBD AudioStreamBasicDescription
The description of ASBD refers to some configuration information, including channel number, sampling rate, bit depth…
VFR variable bit rate, with ASPD description, AudioStreamPacketDescription
VFR in compressed audio data corresponds to ASPD
Each Packet has its ASPD
ASPD contains information about the position of the packet mStartOffset, the number of frames in the packet, mVariableFramesInPacket
2, Audio data >> Audio Packet
Take Audio Queue Services, process the Audio binary data obtained in the previous step and parse it into Audio packet
2.1 Establish audio processing channel, register parse callback method
public init() throws {
letcontext = unsafeBitCast(self, to: UnsafeMutableRawPointer. Self) / / create a active audio file stream parser, Create a parser ID guard AudioFileStreamOpen (context, ParserPropertyChangeCallback ParserPacketCallback, kAudioFileMP3Type, &streamID) == noErrelse {
throw ParserError.streamCouldNotOpen
}
}
Copy the code
2.2 Pass data in and start parsing
public func parse(data: Data) throws {
let streamID = self.streamID!
let count = data.count
_ = try data.withUnsafeBytes({ (rawBufferPointer) in
let bufferPointer = rawBufferPointer.bindMemory(to: UInt8.self)
if letAddress = bufferPointer. BaseAddress {/ / the audio data to the parser / / streamID, specify the parserlet result = AudioFileStreamParseBytes(streamID, UInt32(count), address, [])
guard result == noErr else {
throw ParserError.failedToParseBytes(result)
}
}
})
}
Copy the code
2.3 Audio Information parsing
func ParserPropertyChangeCallback(_ context: UnsafeMutableRawPointer, _ streamID: AudioFileStreamID, _ propertyID: AudioFileStreamPropertyID, _ flags: UnsafeMutablePointer<AudioFileStreamPropertyFlags>) {
letParser = Unmanaged< parser >.fromopaque (context). TakeUnretainedValue ()casekAudioFileStreamProperty_DataFormat: / / get data format var format = AudioStreamBasicDescription () GetPropertyValue (& format, streamID, propertyID) parser.dataFormat = AVAudioFormat(streamDescription: &format)casekAudioFileStreamProperty_AudioDataPacketCount: GetPropertyValue(& Parser. packetCount, streamID, propertyID) default: // Audio stream file, number of packets in the separated audio data Func GetPropertyValue<T>(_ value: inout T, _ streamID:) GetPropertyValue<T>(_ value: inout T, _ streamID:) AudioFileStreamID, _ propertyID: AudioFileStreamPropertyID) { var propSize: UInt32 = 0 guard AudioFileStreamGetPropertyInfo(streamID, propertyID, &propSize, nil) == noErrelse {
return
}
guard AudioFileStreamGetProperty(streamID, propertyID, &propSize, &value) == noErr else {
return}}Copy the code
2.4 Parse callbacks and process data
func ParserPacketCallback(_ context: UnsafeMutableRawPointer, _ byteCount: UInt32, _ packetCount: UInt32, _ data: UnsafeRawPointer, _ packetDescriptions: UnsafeMutablePointer < AudioStreamPacketDescription >) {/ / back to the self (parser)let parser = Unmanaged<Parser>.fromOpaque(context).takeUnretainedValue()
letpacketDescriptionsOrNil: UnsafeMutablePointer<AudioStreamPacketDescription>? = packetDescriptions // ASPD exists, is compressed audio package // uncompressed PCM, use ASBDletisCompressed = packetDescriptionsOrNil ! = nil guardlet dataFormat = parser.dataFormat else {
return} // parser.packets = self.packets = self.packetsif isCompressed {
for i in0.. < Int(packetCount) {// Compress audio data, each packet corresponds to an ASPD, one by onelet packetDescription = packetDescriptions[i]
let packetStart = Int(packetDescription.mStartOffset)
let packetSize = Int(packetDescription.mDataByteSize)
let packetData = Data(bytes: data.advanced(by: packetStart), count: packetSize)
parser.packets.append((packetData, packetDescription))
}
} else{// Original audio data PCM, file unified configuration, calculation is relatively simplelet format = dataFormat.streamDescription.pointee
let bytesPerPacket = Int(format.mBytesPerPacket)
for i in 0 ..< Int(packetCount) {
let packetStart = i * bytesPerPacket
let packetSize = bytesPerPacket
let packetData = Data(bytes: data.advanced(by: packetStart), count: packetSize)
parser.packets.append((packetData, nil))
}
}
}
Copy the code
3, Audio packet >> audio buffer
public required init(parser: Parsing, readFormat: AVAudioFormat) throws {// Take audio data self.parser = parser guard from the previous parserlet dataFormat = parser.dataFormat else {
throw ReaderError.parserMissingDataFormat
}
let sourceFormat = dataFormat.streamDescription
let commonFormat = readFormat.streamDescription // Create audio Format converter converter // by specifying input Format, and output Format // input Format is parsed from the previous step, take output Format from paser, develop the specifiedlet result = AudioConverterNew(sourceFormat, commonFormat, &converter)
guard result == noErr else {
throw ReaderError.unableToCreateConverter(result)
}
self.readFormat = readFormat
}
Copy the code
Develop the specified output format
public var readFormat: AVAudioFormat {
return AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 44100, channels: 2, interleaved: false)! } // Bit depth, using Float32 // sample rate 44100 Hz, standard CD sound quality // minute around the channelCopy the code
The audio packet is parsed in the previous step and the audio buffer is read
public func read(_ frames: AVAudioFrameCount) throws -> AVAudioPCMBuffer {
let framesPerPacket = readFormat. StreamDescription. Pointee. MFramesPerPacket var packets = frames/framesPerPacket / / create a blank, specify the Format and capacity, Audio buffering AVAudioPCMBuffer Guardlet buffer = AVAudioPCMBuffer(pcmFormat: readFormat, frameCapacity: frames) else{throw ReaderError. FailedToCreatePCMBuffer} buffer. FrameLength = frames / / the parse the audio packets packet, convert AVAudioPCMBuffer, So AVAudioEngine plays try queue.sync {letcontext = unsafeBitCast(self, to: UnsafeMutableRawPointer. Self) / / set a good converter converter, using the callback method ReaderConverterCallback, Fill to create the data in the buffer buffer. MutableAudioBufferListletstatus = AudioConverterFillComplexBuffer(converter! , ReaderConverterCallback, context, &packets, buffer.mutableAudioBufferList, nil) guard status == noErrelse {
switch status {
case ReaderMissingSourceFormatError:
throw ReaderError.parserMissingDataFormat
case ReaderReachedEndOfDataError:
throw ReaderError.reachedEndOfFile
case ReaderNotEnoughDataError:
throw ReaderError.notEnoughData
default:
throw ReaderError.converterFailed(status)
}
}
}
return buffer
}
Copy the code
- The use of AudioConverterFillComplexBuffer pose:
AudioConverterFillComplexBuffer (format converter, the callback function, custom parameters pointer, the number of packages pointer, receive the converted data pointer, receive ASPD pointer)
AudioConverterFillComplexBuffer(converter! , ReaderConverterCallback, context, &packets, buffer.mutableAudioBufferList, nil)Copy the code
- AudioConverterFillComplexBuffer ReaderConverterCallback callback function, the use of the position:
Callback functions (format converter, number of packets pointer, pointer to receive converted data, pointer to receive ASPD, pointer to custom parameters)
That is passed to the AudioConverterFillComplexBuffer six parameters,
In addition to its callback argument itself, the other five arguments, its callback function is useful
Convert the buffer callback function to create a blank audio buffer and fill it with data
func ReaderConverterCallback(_ converter: AudioConverterRef, _ packetCount: UnsafeMutablePointer<UInt32>, _ ioData: UnsafeMutablePointer<AudioBufferList>, _ outPacketDescriptions: UnsafeMutablePointer<UnsafeMutablePointer<AudioStreamPacketDescription>? >? , _ context: UnsafeMutableRawPointer?) -> OSStatus {// restore self (reader)letreader = Unmanaged<Reader>.fromOpaque(context!) .takeunretainedValue () // Ensure that the input format is guardlet sourceFormat = reader.parser.dataFormat else {
returnReaderMissingSourceFormatError} / / this class Reader, which recorded the location of a play to currentPacket, / / play the position, // Play to the end of the package, according to the download parsing situation, divided into two situations // 1, download parsing is completed, play to the end of the package // 2, download is not completed, parsing is completed, play to the end of the package // (only two situations, because of the parsing time, Nowhere near as long as it took to download. Download completed = Parsing completed)let packetIndex = Int(reader.currentPacket)
let packets = reader.parser.packets
let isEndOfData = packetIndex >= packets.count - 1
if isEndOfData {
if reader.parser.isParsingComplete {
packetCount.pointee = 0
return ReaderReachedEndOfDataError
} else {
returnReaderNotEnoughDataError}} // The previous setting only processed one packet of audio data ata timelet packet = packets[packetIndex]
var data = packet.0
letDataCount = data. The count ioData. Pointee. MNumberBuffers = 1 / / audio data copies come over: Allocates memory, and then copy the address data ioData. Pointee. MBuffers. MData = UnsafeMutableRawPointer. The allocate (byteCount: dataCount, alignment: 0) _ = data.withUnsafeMutableBytes { (rawMutableBufferPointer)in
let bufferPointer = rawMutableBufferPointer.bindMemory(to: UInt8.self)
if letaddress = bufferPointer.baseAddress{ memcpy((ioData.pointee.mBuffers.mData? .assumingMemoryBound(to: UInt8.self))! , address, dataCount)}} ioData pointee. MBuffers. MDataByteSize = UInt32 (dataCount) / / processing compressed files to MP3, AAC ASPDlet sourceFormatDescription = sourceFormat.streamDescription.pointee
if sourceFormatDescription.mFormatID ! = kAudioFormatLinearPCM {ifoutPacketDescriptions? .pointee == nil { outPacketDescriptions? .pointee = UnsafeMutablePointer<AudioStreamPacketDescription>.allocate(capacity: 1) } outPacketDescriptions? .pointee? .pointee.mDataByteSize = UInt32(dataCount) outPacketDescriptions? .pointee? .pointee.mStartOffset = 0 outPacketDescriptions? .pointee? . Pointee. MVariableFramesInPacket = 0} packetCount. Pointee = 1 / / update the position of the broadcast to currentPacket reader. CurrentPacket = reader.currentPacket + 1return noErr;
}
Copy the code
4, using AVAudioEngine, playback and real-time sound processing
AVAudioEngine can do real-time sound processing and add effects with Effect units
4.1 play first
Set up AudioEngine, add nodes, connect nodes
func setupAudioEngine(){attachNodes() // attachNodes connectNodes() // prepare AudioEngine engine. Prepare () // AVAudioEngine data stream, Use push model // use timer, every 0.1 seconds or so, scheduling playback resourceslet interval = 1 / (readFormat.sampleRate / Double(readBufferSize))
let timer = Timer(timeInterval: interval / 2, repeats: true) {
[weak self] _ inguard self? .state ! = .stoppedelse {
return} // Allocate the buffer, schedule the playback resource self? .scheduleNextBuffer() self? .handleTimeUpdate() self? .notifyTimeUpdated() } RunLoop.current.add(timer,forMode:.common)} // Add the playback node open funcattachNodes() {engine. Attach (playerNode)} // Attach the playerNode to open funcconnectNodes() {
engine.connect(playerNode, to: engine.mainMixerNode, format: readFormat)
}
Copy the code
Schedule the playback resource and deliver the data (the audio buffer created in the previous step) to AudioEngine’s playerNode
func scheduleNextBuffer(){
guard let reader = reader else {
return} // Manage playback by status recording // playback status, is a switch guard! isFileSchedulingComplete || repeatselse {
return
}
do{// Create the audio bufferlet nextScheduledBuffer = try reader.read(readBufferSize) / / playerNode play consumed playerNode. ScheduleBuffer (nextScheduledBuffer)} catch ReaderError. ReachedEndOfFile { isFileSchedulingComplete =true
} catch { }
}
Copy the code
Open play
public func play() {// Start guard! playerNode.isPlayingelse {
return
}
if! engine.isRunning {do{try engine.start()} catch {}} // To improve user experience, mute before playingletlastVolume = volumeRampTargetValue ?? Volume Volume = 0 // PlayerNode. play() // Play swellVolume(to: lastVolume) with normal volume after 250 ms // Update the playing state =.playing}Copy the code
After 4.2 sound
Add real-time pitch and playback speed effects
// Use AVAudioUnitTimePitch to adjust the playback speed and pitch effectlet timePitchNode = AVAudioUnitTimePitch()
override func attachNodes() {// Add playback node super.attachNodes() // Add sound node engine. Attach (timePitchNode)connectNodes() {
engine.connect(playerNode, to: timePitchNode, format: readFormat)
engine.connect(timePitchNode, to: engine.mainMixerNode, format: readFormat)
}
Copy the code
Fill in the details
Duration 5
First get the number of packages, downloaded data, after parsing, add up
The first 2 minutes and 34 seconds of mp3 can be divided into 5925 packages
public var totalPacketCount: AVAudioPacketCount? {
guard let _ = dataFormat else {
returnParserPacketCallback = AVAudioPacketCount(packets.count); // ParserPacketCallback = AVAudioPacketCount(packets.count); Add data to packetsreturn max(AVAudioPacketCount(packetCount), AVAudioPacketCount(packets.count))
}
Copy the code
Get the total number of audio frames
public var totalFrameCount: AVAudioFrameCount? {
guard letframesPerPacket = dataFormat? .streamDescription.pointee.mFramesPerPacketelse {
return nil
}
guard let totalPacketCount = totalPacketCount else {
returnNil} // The total number of packets in the previous step X how many frames are in each packetreturn AVAudioFrameCount(totalPacketCount) * AVAudioFrameCount(framesPerPacket)
}
Copy the code
Calculate the audio duration
public var duration: TimeInterval? {
guard letsampleRate = dataFormat? .sampleRateelse {
return nil
}
guard let totalFrameCount = totalFrameCount else {
returnNil} // Total number of audio frames from the previous step/sampling ratereturn TimeInterval(totalFrameCount) / TimeInterval(sampleRate)
}
Copy the code
6. Adjust the current position of playback
6.1 Audio Playback manager inside the Streamer
Public func seek(to time: TimeInterval) throws {// Use the Parser audio package and reader audio buffer to play guardlet parser = parser, let reader = reader else {
return} // Take the time to calculate the relative position of the audio frame; // Take the relative position of the audio framelet frameOffset = parser.frameOffset(forTime: time),
let packetOffset = parser.packetOffset(forFrame: frameOffset) else {
return} currentTimeOffset = time isFileSchedulingComplete =false// Record the current statuslet isPlaying = playerNode.isPlaying
letlastVolume = volumeRampTargetValue ?? Playernode.stop () volume = 0 // Update the location of reader resourcesdo {
try reader.seek(packetOffset)
} catch {
return} // Just record the current state, restoreifIsPlaying {playerNode.play()} // Update UI delegate? Streamer (self, updatedCurrentTime: time) swellVolume(to: lastVolume)}Copy the code
Figure out the frame offset for the current time
public func frameOffset(forTime time: TimeInterval) -> AVAudioFramePosition? {
guard let_ = dataFormat? .streamDescription.pointee,let frameCount = totalFrameCount,
let duration = duration else {
returnNil} // Take the current time/total audio duration, calculate the ratiolet ratio = time / duration
return AVAudioFramePosition(Double(frameCount) * ratio)
}
Copy the code
Figure out the packet position of the current frame
public func packetOffset(forFrame frame: AVAudioFramePosition) -> AVAudioPacketCount? {
guard letframesPerPacket = dataFormat? .streamDescription.pointee.mFramesPerPacketelse {
returnNil} // How many frames are there in a packetreturn AVAudioPacketCount(frame) / AVAudioPacketCount(framesPerPacket)
}
Copy the code
6.2 Audio Resource scheduling in reader
Public func seek(_ packet: AVAudioPacketCount) throws {queue.sync {currentPacket = packet}}Copy the code
Record the position currentPacket, so it works
ReaderConverterCallback in Step 3
/ /... // In this example, an audio packet corresponds to an audio bufferlet packet = packets[packetIndex]
var data = packet.0
// ...
_ = data.withUnsafeMutableBytes { (rawMutableBufferPointer) in // ...
}
// ...
Copy the code
7 UI user experience is improved. Manually drag and drop the scene of playing time
It is divided into three events: finger press down, finger drag, finger lift
/ / finger press, screen refresh play the progress of the proxy method @ IBAction func progressSliderTouchedDown (_ sender: UISlider) {isSeeking =true} / / finger drag, screen refresh broadcast pace agent method, the use of gestures corresponding UI @ IBAction func progressSliderValueChanged (_ sender: UISlider) {letCurrentTime = TimeInterval(progressSlider.value) currentTimelabel.text = currentTime.tommss ()} @ibAction func progressSliderTouchedUp(_ sender: UISlider) {seek(sender) isSeeking =false
}
Copy the code
The related agent method updates the UI of the current event and progress bar based on the playback progress
If you’re dragging it, block it
func streamer(_ streamer: Streaming, updatedCurrentTime currentTime: TimeInterval) {
if! isSeeking { progressSlider.value = Float(currentTime) currentTimeLabel.text = currentTime.toMMSS() } }Copy the code
8 Single track loop mode
Step 4 During playback, distribute playback resources using a timer
Manage the logic of the two methods below
(Scheduling audio buffer and changing state after playing)
let timer = Timer(timeInterval: interval / 2, repeats: true) {
[weak self] _ in/ /... self? .scheduleNextBuffer() self? .handleTimeUpdate() // ... }Copy the code
Scheduling audio buffer,
func scheduleNextBuffer(){
guard let reader = reader else {
return} // If repeats are repeated, continue to play, regardless of the guard! isFileSchedulingComplete || repeatselse {
return} / /... The following is, the player node plays resources}Copy the code
Handle related states according to the playback situation
func handleTimeUpdate(){
guard let currentTime = currentTime, let duration = duration else {
return} // The current playback time, after the audio length, it is considered to have finished playing, to pauseifcurrentTime >= duration { try? Seek (to: 0) // If repeated, do not pauseif! repeats{ pause() } } }Copy the code