Proper Volume Envelopes for AVAudioUnitSampler with AVFoundation

蹲街弑〆低调 提交于 2019-12-08 13:26:43

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!