问题
How to properly create Volume Envelopes for the AVAudioUnitSampler?
Basically I want to add a Attack Phase where the volume fades in over a couple of milliseconds to avoid clicks. Additionally I want to add a release phase so that the sound volume fades out after stoping to play the note.
What I did so far:
I set up a global Timer that represents my sample rate. For this I used a NSTimer with a interval length of 1 sample (for a sample rate of 44100 one sample duration is 1/44100). That way one should not hear volume "jumps" causing "clicks". Maybe I need to oversample to twice the sample rate.
The timer would call a function that incremented the masterGain as long as the gain is below a certain threshold. The amount I incremented was calculated by dividing the difference in desired and current gain by the sample rate then dividing this amount by the desired fade time. (In the example below I use a fixed value for easy reading)
After the threshold is reached I remove the timer and stop the note.
-> I think this approach is quite complex especially when I use a release phase too.
Please check my example code. I used a UIButton touchDown to trigger the played note. Please tell me if you know a much easier way to automate a value over time/ build a volume fade in/out.
Thanks
import UIKit
import AVFoundation
class ViewController: UIViewController {
var engine: AVAudioEngine!
var sampler: AVAudioUnitSampler!
var error: NSError?
var timer: NSTimer = NSTimer()
override func viewDidLoad() {
super.viewDidLoad()
engine = AVAudioEngine()
sampler = AVAudioUnitSampler()
engine.attachNode(sampler)
engine.connect(sampler, to: engine.mainMixerNode, format: nil)
if !engine!.startAndReturnError(&error) {
if let error = error {
println("Engine could not start!")
println(error.localizedDescription)
}
}
}
@IBAction func touchDown(sender: UIButton, forEvent event: UIEvent) {
sampler.startNote(66, withVelocity: 66, onChannel: 0)
sampler.masterGain = -90
timer = NSTimer.scheduledTimerWithTimeInterval (1/44100, target: self, selector: "updateVolume", userInfo: nil, repeats: true)
}
func updateVolume() {
if sampler.masterGain <= -10 {
sampler.masterGain = sampler.masterGain + 0.1
println("volume updated")
} else {
timer.invalidate()
}
}
}
回答1:
Wow! That's a pretty complicated way of doing volume envelopes!
Have you thought about making it a bit simpler? Rather than trying to control the gain control, I would just modify the actual audio coming out. First I would just have my sample stored as a vector. Then, I would create another vector that would represent my volume envelope. Then, I would create a new sample for playback, which would just be an element by element multiplication of the sample and amplitude vectors. Here's an example in matlab.
%Load your sample
[x, Fs] = audioread('filename.wav');
%get the length of your sample
sampSize = length(x);
%Create volume envelope. (Im just using a Gaussian window here. You can
%replace it with any type of envelope you want so long as the values are
%between 0 and 1.)
ampEnv = gausswin(length(x));
%Multiply element by element of your two vectors.
newSample = x.*ampEnv;
%Plot your new sound.
plot(newSample);
Alternatively, if you're looking to do it in real time it becomes a bit more tricky. If delay isn't a massive issue here, I suggest having a slight lookahead window which would store the outcoming samples of you sounds and begin applying amplitude volume methods there.
回答2:
The cleanest way to do it is to set up a performance parameter, check out AUSampler - Controlling the Settings of the AUSampler in Real Time. Then just set your attack. You can also hard code such values when you create your preset, but a parameter is much more flexible.
EDIT
The AULab in Yosemite is currently broken, in the meantime, I inspected the control connection needed for attack from one of my own presets. The preset format is fairly specific so I wouldn't be surprised if there were some conflict, but this works for a basic setup.
This approach is just a temporary fix. What it does is retrieve the preset (an NSDictionary of array of dictionaries of arrays of dictionaries), then adds a control (an NSDictionary) to the "Controls" array. Then you set your samplers preset. This is done after you have loaded your preset or samples on the underlying AUSampler which is accessed from the audioUnit property of the AVAudioUnitSampler.
//this is the connection we will be adding
NSMutableDictionary *attackConnection = [NSMutableDictionary dictionaryWithDictionary:
@{@"ID" :@0,
@"control" :@0,
@"destination":@570425344,
@"enabled" :[NSNumber numberWithBool:1],
@"inverse" :[NSNumber numberWithBool:0],
@"scale" :@10,
@"source" :@73,
@"transform" :@1,
}];
AVAudioUnitSampler *sampler;//already initialized and loaded with samples or this won't work
CFPropertyListRef presetPlist;
UInt32 presetSize = sizeof(CFPropertyListRef);
AudioUnitGetProperty(sampler.audioUnit, kAudioUnitProperty_ClassInfo, kAudioUnitScope_Global, 0, &presetPlist, &presetSize);
NSMutableDictionary *mutablePreset = [NSMutableDictionary dictionaryWithDictionary:(__bridge NSDictionary *)presetPlist];
CFRelease(presetPlist);
NSMutableDictionary *instrument = [NSMutableDictionary dictionaryWithDictionary: mutablePreset[@"Instrument"]];
NSArray *existingLayers = instrument[@"Layers"];
if (existingLayers.count) {
NSMutableArray *layers = [[NSMutableArray alloc]init];
for (NSDictionary *layer in existingLayers){
NSMutableDictionary *mutableLayer = [NSMutableDictionary dictionaryWithDictionary:layer];
NSArray *existingConections = mutableLayer[@"Connections"];
if (existingConections) {
attackConnection[@"ID"] = [NSNumber numberWithInteger:existingConections.count];
NSMutableArray *connections = [NSMutableArray arrayWithArray:existingConections];
[connections addObject:attackConnection];
[mutableLayer setObject:connections forKey:@"Connections"];
}
else{
attackConnection[@"ID"] = [NSNumber numberWithInteger:0];
[mutableLayer setObject:@[attackConnection] forKey:@"Connections"];
}
[layers addObject:mutableLayer];
}
[instrument setObject:layers forKeyedSubscript:@"Layers"];
}
else{
instrument[@"Layers"] = @[@{@"Connections":@[attackConnection]}];
}
[mutablePreset setObject:instrument forKey:@"Instrument"];
CFPropertyListRef editedPreset = (__bridge CFPropertyListRef)mutablePreset;
AudioUnitSetProperty(sampler.audioUnit,kAudioUnitProperty_ClassInfo,kAudioUnitScope_Global,0,&editedPreset,sizeof(presetPlist));
Then after this connection has been made you set the attack like this.
uint8_t value = 100; //0 -> 127
[sampler sendController:73 withValue:value onChannel:0];
来源:https://stackoverflow.com/questions/29880528/proper-volume-envelopes-for-avaudiounitsampler-with-avfoundation