Skip to content

Instantly share code, notes, and snippets.

@ObuchiYuki
Created February 8, 2025 14:05
Show Gist options
  • Save ObuchiYuki/9997568f6ca65c94ff5e72698bd6bee5 to your computer and use it in GitHub Desktop.
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.
//
// 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