Efeitos de áudio personalizados

Este artigo descreve como criar um componente do Windows RunTime que implementa a interface IBasicAudioEffect para permitir que você crie efeitos personalizados para fluxos de áudio. Os efeitos personalizados podem ser usados com várias APIs diferentes do Windows Runtime, incluindo MediaCapture, que fornece acesso à câmera de um dispositivo, MediaComposition, que permite criar composições complexas a partir de clipes de mídia, e AudioGraph, que permite montar rapidamente um gráfico de vários nós de entrada, saída e submistura de áudio.

Adicionar um efeito personalizado ao seu aplicativo

Um efeito de áudio personalizado é definido em uma classe que implementa a interface IBasicAudioEffect. Essa classe não pode ser incluída diretamente no projeto do seu aplicativo. Em vez disso, você deve usar um componente do Windows Runtime para hospedar sua classe de efeito de áudio.

Adicionar um componente do Windows Runtime para seu efeito de áudio

  1. No Microsoft Visual Studio, com sua solução aberta, vá para o menu Arquivo e selecione Adicionar> Novo Projeto.
  2. Selecione o tipo de projeto Componente do Windows Runtime (Universal Windows).
  3. Para este exemplo, nomeie o projeto como AudioEffectComponent. Esse nome será referenciado no código posteriormente.
  4. Clique em OK.
  5. O modelo de projeto cria uma classe chamada Class1.cs. No Solution Explorer, clique com o botão direito do mouse no ícone de Class1.cs e escolha Renomear.
  6. Renomeie o arquivo como ExampleAudioEffect.cs. O Visual Studio mostrará um prompt perguntando se você deseja atualizar todas as referências para o novo nome. Clique em Sim.
  7. Abra o ExampleAudioEffect.cs e atualize a definição de classe para implementar a interface IBasicAudioEffect.
public sealed class ExampleAudioEffect : IBasicAudioEffect

Você precisa incluir os namespaces a seguir em seu arquivo de classe effect para acessar todos os tipos usados nos exemplos deste artigo.

using Windows.Media.Effects;
using Windows.Media.MediaProperties;
using Windows.Foundation.Collections;
using System.Runtime.InteropServices;
using Windows.Media;
using Windows.Foundation;

Implementar a interface IBasicAudioEffect

Seu efeito de áudio deve implementar todos os métodos e propriedades da interface IBasicAudioEffect. Esta seção traz orientações sobre como fazer uma implementação simples dessa interface para criar um efeito de eco básico.

Propriedade SupportedEncodingProperties

O sistema verifica a propriedade SupportedEncodingProperties para determinar quais propriedades de codificação são suportadas pelo seu efeito. Se o consumidor do seu efeito não puder codificar áudio usando as propriedades especificadas, o sistema chamará Close no seu efeito e removerá o efeito do pipeline de áudio. Neste exemplo, os objetos AudioEncodingProperties são criados e adicionados à lista retornada para oferecer suporte a 44,1 kHz e 48 kHz, float de 32 bits, codificação mono.

public IReadOnlyList<AudioEncodingProperties> SupportedEncodingProperties
{
    get
    {
        var supportedEncodingProperties = new List<AudioEncodingProperties>();
        AudioEncodingProperties encodingProps1 = AudioEncodingProperties.CreatePcm(44100, 1, 32);
        encodingProps1.Subtype = MediaEncodingSubtypes.Float;
        AudioEncodingProperties encodingProps2 = AudioEncodingProperties.CreatePcm(48000, 1, 32);
        encodingProps2.Subtype = MediaEncodingSubtypes.Float;

        supportedEncodingProperties.Add(encodingProps1);
        supportedEncodingProperties.Add(encodingProps2);

        return supportedEncodingProperties;
        
    }
}

MétodoSetEncodingProperties

O sistema chama SetEncodingProperties em seu efeito para que você saiba as propriedades de codificação para o fluxo de áudio no qual o efeito está operando. Para implementar um efeito de eco, este exemplo usa um buffer para armazenar um segundo de dados de áudio. Esse método fornece a oportunidade de inicializar o tamanho do buffer para o número de amostras em um segundo de áudio, com base na taxa de amostragem na qual o áudio é codificado. O efeito de atraso também usa um contador inteiro para controlar a posição atual no buffer de atraso. Como SetEncodingProperties é chamado sempre que o efeito é adicionado ao pipeline de áudio, este é um bom momento para inicializar esse valor como 0. Você também pode querer capturar o objeto AudioEncodingProperties passado para esse método para usar em outro lugar em seu efeito.

private float[] echoBuffer;
private int currentActiveSampleIndex;
private AudioEncodingProperties currentEncodingProperties;
public void SetEncodingProperties(AudioEncodingProperties encodingProperties)
{
    currentEncodingProperties = encodingProperties;
    echoBuffer = new float[encodingProperties.SampleRate]; // exactly one second delay
    currentActiveSampleIndex = 0;
}

Método SetProperties

O método SetProperties permite que o aplicativo que está usando seu efeito ajuste os parâmetros de efeito. As propriedades são passadas como um mapa IPropertySet de nomes e valores de propriedades.

IPropertySet configuration;
public void SetProperties(IPropertySet configuration)
{
    this.configuration = configuration;
}

Este exemplo simples misturará a amostra de áudio atual com um valor do buffer de atraso de acordo com o valor da propriedade Mix. Uma propriedade é declarada e TryGetValue é usado para obter o valor definido pelo aplicativo de chamada. Se nenhum valor foi definido, um valor padrão de .5 será usado. Essa propriedade é somente leitura. O valor da propriedade deve ser definido usando SetProperties.

public float Mix
{
    get
    {
        object val;
        if (configuration != null && configuration.TryGetValue("Mix", out val))
        {
            return (float)val;
        }
        return .5f;
    }
}

Método ProcessFrame

O método ProcessFrame é onde seu efeito modifica os dados de áudio do fluxo. O método é chamado uma vez por quadro e é passado um objeto ProcessAudioFrameContext. Esse objeto contém um objeto AudioFrame de entrada, que contém o quadro de entrada a ser processado, e um objeto AudioFrame de saída, no qual você grava dados de áudio que serão passados para o restante do pipeline de áudio. Um quadro de áudio é um buffer de amostras de áudio que representa uma pequena fatia de dados de áudio.

O acesso ao buffer de dados de um AudioFrame requer interoperabilidade com COM, portanto, você deve incluir o namespace System.Runtime.InteropServices no arquivo de classe de efeito e, em seguida, adicionar o seguinte código dentro do namespace para que seu efeito importe a interface para acessar o buffer de áudio.

[ComImport]
[Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
unsafe interface IMemoryBufferByteAccess
{
    void GetBuffer(out byte* buffer, out uint capacity);
}

Observação

Como essa técnica acessa um buffer de imagem nativo e não gerenciado, você precisará configurar seu projeto para permitir código não seguro.

  1. No Solution Explorer, clique com o botão direito do mouse no projeto AudioEffectComponent e selecione Propriedades.
  2. Selecione a guia Compilar.
  3. Marque a caixa de seleção Permitir código não seguro.

 

Agora você pode adicionar a implementação do método ProcessFrame ao seu efeito. Primeiro, esse método obtém um objeto AudioBuffer dos quadros de áudio de entrada e saída. O quadro de saída é aberto para gravação e a entrada para leitura. Em seguida, um IMemoryBufferReference é obtido para cada buffer chamando CreateReference. Depois, o buffer de dados real é obtido convertendo os objetos IMemoryBufferReference como a interface de interoperabilidade COM definida acima, IMemoryByteAccess e, em seguida, chamando GetBuffer.

Agora que os buffers de dados foram obtidos, você pode ler a partir do buffer de entrada e gravar no buffer de saída. Para cada amostra no buffer de entrada, o valor é obtido e multiplicado por 1 - Mix para definir o valor do sinal seco do efeito. Em seguida, uma amostra é recuperada da posição atual no buffer de eco e multiplicada por Mix para definir o valor úmido do efeito. A amostra de saída é definida como a soma dos valores seco e úmido. Finalmente, cada amostra de entrada é armazenada no buffer de eco e o índice de amostra atual é incrementado.

unsafe public void ProcessFrame(ProcessAudioFrameContext context)
{
    AudioFrame inputFrame = context.InputFrame;
    AudioFrame outputFrame = context.OutputFrame;

    using (AudioBuffer inputBuffer = inputFrame.LockBuffer(AudioBufferAccessMode.Read),
                        outputBuffer = outputFrame.LockBuffer(AudioBufferAccessMode.Write))
    using (IMemoryBufferReference inputReference = inputBuffer.CreateReference(),
                                    outputReference = outputBuffer.CreateReference())
    {
        byte* inputDataInBytes;
        byte* outputDataInBytes;
        uint inputCapacity;
        uint outputCapacity;

        ((IMemoryBufferByteAccess)inputReference).GetBuffer(out inputDataInBytes, out inputCapacity);
        ((IMemoryBufferByteAccess)outputReference).GetBuffer(out outputDataInBytes, out outputCapacity);

        float* inputDataInFloat = (float*)inputDataInBytes;
        float* outputDataInFloat = (float*)outputDataInBytes;

        float inputData;
        float echoData;

        // Process audio data
        int dataInFloatLength = (int)inputBuffer.Length / sizeof(float);

        for (int i = 0; i < dataInFloatLength; i++)
        {
            inputData = inputDataInFloat[i] * (1.0f - this.Mix);
            echoData = echoBuffer[currentActiveSampleIndex] * this.Mix;
            outputDataInFloat[i] = inputData + echoData;
            echoBuffer[currentActiveSampleIndex] = inputDataInFloat[i];
            currentActiveSampleIndex++;

            if (currentActiveSampleIndex == echoBuffer.Length)
            {
                // Wrap around (after one second of samples)
                currentActiveSampleIndex = 0;
            }
        }
    }
}

Método Close

O sistema chamará o método CloseClose em sua classe quando o efeito for desligado. Você deve usar esse método para descartar todos os recursos que criou. O argumento para o método é um MediaEffectClosedReason, que permite saber se o efeito foi fechado normalmente, se ocorreu um erro ou se o efeito não oferece suporte ao formato de codificação necessário.

public void Close(MediaEffectClosedReason reason)
{
    // Dispose of effect resources
    echoBuffer = null;
}

Método DiscardQueuedFrames

O método DiscardQueuedFrames é chamado quando seu efeito deve ser redefinido. Um cenário típico para isso é se seu efeito armazena quadros processados anteriormente para usar no processamento do quadro atual. Quando esse método é chamado, você deve descartar o conjunto de quadros anteriores que você salvou. Esse método pode ser usado para redefinir qualquer estado relacionado a quadros anteriores, não apenas quadros de áudio acumulados.

public void DiscardQueuedFrames()
{
    // Reset contents of the samples buffer
    Array.Clear(echoBuffer, 0, echoBuffer.Length - 1);
    currentActiveSampleIndex = 0;
}

Propriedade TimeIndependent

A propriedade TimeIndependent TimeIndependent permite que o sistema saiba se seu efeito não requer tempo uniforme. Quando definido como verdadeiro, o sistema pode usar otimizações que melhoram o desempenho do efeito.

public bool TimeIndependent { get { return true; } }

Propriedade UseInputFrameForOutput

Defina a propriedade UseInputFrameForOutput como true para informar ao sistema que seu efeito gravará sua saída no buffer de áudio do InputFrame do ProcessAudioFrameContext passado para ProcessFrame em vez de gravar no OutputFrame.

public bool UseInputFrameForOutput { get { return false; } }

Adicionando um efeito personalizado ao seu aplicativo

Para usar o efeito de áudio do aplicativo, você deve adicionar uma referência ao projeto de efeito ao aplicativo.

  1. No Solution Explorer, no projeto do app, clique com o botão direito do mouse em Referências e selecione Adicionar Referência.
  2. Expanda a guia Projetos , selecione Solução e marque a caixa de seleção do nome do projeto de efeito. Para este exemplo, o nome é AudioEffectComponent.
  3. Clique em OK

Se sua classe de efeito de áudio for declarada como um namespace diferente, certifique-se de incluir esse namespace no arquivo de código.

using AudioEffectComponent;

Adicionar seu efeito personalizado a um nó do AudioGraph

Para mais informações sobre como usar gráficos de áudio, consulte Gráficos de áudio. O trecho de código a seguir mostra como adicionar o efeito de eco de exemplo mostrado neste artigo a um nó de gráfico de áudio. Primeiro, um PropertySet é criado e um valor para a propriedade Mix, definido pelo efeito, é definido. Em seguida, o consultor AudioEffectDefinition é chamado, passando o nome completo da classe do tipo de efeito personalizado e o conjunto de propriedades. Finalmente, a definição de efeito é adicionada à propriedade EffectDefinitions de um FileInputNode, fazendo com que o áudio emitido seja processado pelo efeito personalizado.

// Create a property set and add a property/value pair
PropertySet echoProperties = new PropertySet();
echoProperties.Add("Mix", 0.5f);

// Instantiate the custom effect defined in the 'AudioEffectComponent' project
AudioEffectDefinition echoEffectDefinition = new AudioEffectDefinition(typeof(ExampleAudioEffect).FullName, echoProperties);
fileInputNode.EffectDefinitions.Add(echoEffectDefinition);

Depois de ser adicionado a um nó, o efeito personalizado pode ser desabilitado chamando DisableEffectsByDefinition e passando o objeto AudioEffectDefinition. Para mais informações sobre como usar gráficos de áudio em seu aplicativo, consulte AudioGraph.

Adicionar seu efeito personalizado a um clipe em um MediaComposition

O trecho de código a seguir mostra como adicionar o efeito de áudio personalizado a um clipe de vídeo e uma faixa de áudio em segundo plano em uma composição de mídia. Para saber mais sobre como criar composições de mídia a partir de clipes de vídeo e adicionar faixas de áudio em segundo plano, consulte Composições e edição de mídia.

// Create a property set and add a property/value pair
PropertySet echoProperties = new PropertySet();
echoProperties.Add("Mix", 0.5f);

// Instantiate the custom effect defined in the 'AudioEffectComponent' project
AudioEffectDefinition echoEffectDefinition = new AudioEffectDefinition(typeof(ExampleAudioEffect).FullName, echoProperties);

// Add custom audio effect to the current clip in the timeline
var currentClip = composition.Clips.FirstOrDefault(
    mc => mc.StartTimeInComposition <= mediaPlayerElement.MediaPlayer.PlaybackSession.Position &&
    mc.EndTimeInComposition >= mediaPlayerElement.MediaPlayer.PlaybackSession.Position);
currentClip.AudioEffectDefinitions.Add(echoEffectDefinition);

// Add custom audio effect to the first background audio track
if (composition.BackgroundAudioTracks.Count > 0)
{
    composition.BackgroundAudioTracks[0].AudioEffectDefinitions.Add(echoEffectDefinition);
}