Recentemente, voltei a me interessar por áudio gerado, mas estou com um pouco de dificuldade. Tenho seguido este tutorial e convertido para Swift:
https://gist.github.com/gcatlin/0dd61f19d40804173d015c01a80461b8
No entanto, quando reproduzo por áudio, tudo o que obtenho são alguns efeitos de ruído branco bastante desagradáveis, em vez do tom puro que eu esperava. Aqui está o código que estou usando para criar a unidade de tom:
private func createToneUnit() throws {
// Configure the search parameters to find the default playback output unit
var outputDesc = AudioComponentDescription()
outputDesc.componentType = kAudioUnitType_Output
outputDesc.componentSubType = kAudioUnitSubType_RemoteIO
outputDesc.componentManufacturer = kAudioUnitManufacturer_Apple
outputDesc.componentFlags = 0
outputDesc.componentFlagsMask = 0
// Get the default playback output unit
guard let output = AudioComponentFindNext(nil, &outputDesc) else {
throw AudioError.cannotFindOutput
}
// Create a new unit based on this that we'll use for output
var error = AudioComponentInstanceNew(output, &toneUnit)
guard let toneUnit = toneUnit, error == noErr else {
throw AudioError.cannotCreateComponent
}
// Set our tone rendering function on the unit
var callback = AURenderCallbackStruct()
callback.inputProcRefCon = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
callback.inputProc = {
(userData, actionFlags, timeStamp, busNumber, frameCount, data) -> OSStatus in
let _self = Unmanaged<MainViewController>.fromOpaque(userData).takeUnretainedValue()
return _self.renderTone(actionFlags: actionFlags, timeStamp: timeStamp, busNumber: busNumber, frameCount: frameCount, data: data)
}
error = AudioUnitSetProperty(
toneUnit,
kAudioUnitProperty_SetRenderCallback,
kAudioUnitScope_Input,
0,
&callback,
UInt32(MemoryLayout.size(ofValue: callback))
)
guard error == noErr else {
throw AudioError.cannotSetCallback
}
// Set the format to 32 bit, single channel, floating point, linear PCM
var streamFormat = AudioStreamBasicDescription()
streamFormat.mSampleRate = sampleRate
streamFormat.mFormatID = kAudioFormatLinearPCM
streamFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked
streamFormat.mFramesPerPacket = 1
streamFormat.mChannelsPerFrame = 1
streamFormat.mBitsPerChannel = 16
streamFormat.mBytesPerFrame = streamFormat.mChannelsPerFrame * streamFormat.mBitsPerChannel / 8
streamFormat.mBytesPerPacket = streamFormat.mBytesPerFrame * streamFormat.mFramesPerPacket
error = AudioUnitSetProperty(
toneUnit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input,
0,
&streamFormat,
UInt32(MemoryLayout<AudioStreamBasicDescription>.size)
)
guard error == noErr else {
throw AudioError.cannotSetStreamFormat
}
}
E aqui está a função render:
func renderTone(
actionFlags: UnsafeMutablePointer<AudioUnitRenderActionFlags>,
timeStamp: UnsafePointer<AudioTimeStamp>,
busNumber: UInt32,
frameCount: UInt32,
data: UnsafeMutablePointer<AudioBufferList>?
) -> OSStatus {
// Get buffer
let bufferList = UnsafeMutableAudioBufferListPointer(data!)
let increment = MainViewController.fullCycle * frequency / sampleRate
// Generate samples
for buffer in bufferList {
for frame in 0 ..< frameCount {
if let audioData = buffer.mData?.assumingMemoryBound(to: Float64.self) {
audioData[Int(frame)] = sin(theta) * amplitude
}
// Note: this would NOT work for a stereo output
theta += increment
while theta > MainViewController.fullCycle {
theta -= MainViewController.fullCycle
}
}
}
return noErr;
}
Alguém vê algo obviamente ruim nisso? Eu realmente preferiria usar Swift em vez de Obj C, mas não consigo encontrar um exemplo prático de como fazer isso, apenas alguns exemplos parciais (reconhecidamente úteis) sobre como configurar coisas que não executam nenhuma renderização de tom.
Você informou à unidade de áudio IO remota que iria fornecer dados inteiros:
Mas então você deu a ele dados float, o que soa ruim quando interpretado como inteiro (e também sobrecarrega a memória porque
Float64
s são maiores queInt16
s).Portanto, você deve fazer com que os dois concordem. Uma maneira é fazer com que o retorno de chamada de renderização corresponda ao formato do fluxo, produzindo dados inteiros de 16 bits:
Ou você poderia continuar produzindo dados flutuantes (talvez não flutuantes de 64 bits, não tenho certeza se isso é suportado) e fazer o formato do fluxo corresponder a isso. Você provavelmente precisará de algumas conversões rápidas para corrigir.
O que me leva a outro problema provável. Tenho quase certeza de que o Swift ainda quebra as regras de áudio em tempo real que você pode ler neste artigo (observação: o artigo é anterior ao Swift), então, ao usar o Swift em um retorno de chamada de renderização ou API semelhante, você pode ouvir cliques/estalos e interrupções.