Created
February 8, 2025 14:05
-
-
Save ObuchiYuki/9997568f6ca65c94ff5e72698bd6bee5 to your computer and use it in GitHub Desktop.
By setting the gain on the AVPlayerItem, you can play audio at a volume level higher than 1.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// GainOverOnePlayer.swift | |
// MFileViewer | |
// | |
// Created by yuki on 2025/02/08. | |
// | |
import AVFoundation | |
import CoreMedia | |
import AudioToolbox | |
extension AVPlayerItem { | |
private enum __ { static var __ = 0 } | |
func setGain(_ gain: Float) async throws { | |
try await self.playerGain.gain = gain | |
} | |
func getGain() async throws -> Float { | |
return try await self.playerGain.gain | |
} | |
private var playerGain: AVPlayerGain { | |
get async throws { | |
if let gain = objc_getAssociatedObject(self, &__.__) as? AVPlayerGain { | |
return gain | |
} | |
let gain = try await createPlayerGain() | |
objc_setAssociatedObject(self, &__.__, gain, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) | |
return gain | |
} | |
} | |
private func createPlayerGain() async throws -> AVPlayerGain { | |
let gain = AVPlayerGain() | |
let mix = try await makeAudioMix(for: self, gain: gain) | |
self.audioMix = mix | |
return gain | |
} | |
} | |
private final class AVPlayerGain { | |
var gain: Float = 1.0 | |
} | |
private func makeAudioMix(for item: AVPlayerItem, gain: AVPlayerGain) async throws -> AVAudioMix { | |
let audioMix = AVMutableAudioMix() | |
var inputParametersArray = [AVMutableAudioMixInputParameters]() | |
for track in try await item.asset.loadTracks(withMediaType: .audio) { | |
let inputParams = AVMutableAudioMixInputParameters(track: track) | |
var callbacks = MTAudioProcessingTapCallbacks( | |
version: kMTAudioProcessingTapCallbacksVersion_0, | |
clientInfo: UnsafeMutableRawPointer(Unmanaged.passUnretained(gain).toOpaque()), | |
init: tapInit, | |
finalize: tapFinalize, | |
prepare: tapPrepare, | |
unprepare: tapUnprepare, | |
process: tapProcess | |
) | |
var tap: Unmanaged<MTAudioProcessingTap>? | |
let err = MTAudioProcessingTapCreate(kCFAllocatorDefault, | |
&callbacks, | |
kMTAudioProcessingTapCreationFlag_PostEffects, | |
&tap) | |
if err == noErr, let tapProcessor = tap?.takeRetainedValue() { | |
inputParams.audioTapProcessor = tapProcessor | |
} else { | |
print("MTAudioProcessingTapCreate error: \(err)") | |
} | |
inputParametersArray.append(inputParams) | |
} | |
audioMix.inputParameters = inputParametersArray | |
return audioMix | |
} | |
private func tapInit(tap: MTAudioProcessingTap, | |
clientInfo: UnsafeMutableRawPointer?, | |
tapStorageOut: UnsafeMutablePointer<UnsafeMutableRawPointer?>) -> Void { | |
tapStorageOut.pointee = clientInfo | |
} | |
private func tapFinalize(tap: MTAudioProcessingTap) {} | |
private func tapPrepare(tap: MTAudioProcessingTap, | |
maxFrames: CMItemCount, | |
processingFormat: UnsafePointer<AudioStreamBasicDescription>) {} | |
private func tapUnprepare(tap: MTAudioProcessingTap) {} | |
private func tapProcess(tap: MTAudioProcessingTap, | |
numberFrames: CMItemCount, | |
flags: MTAudioProcessingTapFlags, | |
bufferListInOut: UnsafeMutablePointer<AudioBufferList>, | |
numberFramesOut: UnsafeMutablePointer<CMItemCount>, | |
flagsOut: UnsafeMutablePointer<MTAudioProcessingTapFlags>) { | |
var localFlags = MTAudioProcessingTapFlags(0) | |
let err = MTAudioProcessingTapGetSourceAudio(tap, | |
numberFrames, | |
bufferListInOut, | |
&localFlags, | |
nil, | |
numberFramesOut) | |
if err != noErr { | |
print("MTAudioProcessingTapGetSourceAudio error: \(err)") | |
return | |
} | |
let storage = MTAudioProcessingTapGetStorage(tap) | |
let playerGain = Unmanaged<AVPlayerGain>.fromOpaque(storage).takeUnretainedValue() | |
let gain = playerGain.gain | |
let audioBufferList = UnsafeMutableAudioBufferListPointer(bufferListInOut) | |
for buffer in audioBufferList { | |
let frameCount = Int(buffer.mDataByteSize) / MemoryLayout<Float>.size | |
if let dataPtr = buffer.mData?.assumingMemoryBound(to: Float.self) { | |
for frame in 0..<frameCount { | |
dataPtr[frame] *= gain | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment