向Angular编写Retrowave

Web Audio API已经存在很长时间了,关于它的文章很多。因此,我们不会过多讨论API本身。我们会告诉您,如果正确介绍Web Audio和Angular,它们可能是最好的朋友。让我们开始吧!



Web Audio API , , . , , . . - :


const context = new AudioContext();
const gainNode = context.createGain();

, , . Web Audio API Angular.



Angular — , . , , :


const context = new AudioContext();
const gainNode = new GainNode(context);
const delayNode = new DelayNode(context);
const audioSource = new MediaElementAudioSourceNode(context, {
   mediaElement: audioElement.nativeElement,
});

gainNode.gain.value = 0.5;
delayNode.delayTime.value = 0.2;

audioSource.connect(gainNode);
audioSource.connect(context.destination);
gainNode.connect(delayNode);
delayNode.connect(gainNode);
delayNode.connect(context.destination);

, — . , , connect. HTML audio , , . . AudioContext Dependency Injection.


GainNode DelayNode — . , AudioParam. .


@Directive({
   selector: '[waGainNode]',
   inputs: [
     'channelCount',
     'channelCountMode',
     'channelInterpretation'
   ],
})
export class WebAudioGain extends GainNode {
   @Input('gain')
   set gainSetter(value: number) {
       this.gain.value = value;
   }

   constructor(@Inject(AUDIO_CONTEXT) context: AudioContext) {
       super(context);
   }
}

, : channelCount, channelCountMode channelInterpretation. inputs @Directive — . DelayNode . AUDIO_NODE, :


@Directive({
   selector: '[waGainNode]',
   inputs: [
     'channelCount',
     'channelCountMode',
     'channelInterpretation'
   ],
   exportAs: 'AudioNode',
   providers: [{
     provide: AUDIO_NODE,
     useExisting: forwardRef(() => WebAudioGain),
   }],
})
export class WebAudioGain extends GainNode implements OnDestroy {
   @Input('gain')
   set gainSetter(value: number) {
       this.gain.value = value;
   }

   constructor(
     @Inject(AUDIO_CONTEXT) context: BaseAudioContext,
     @SkipSelf() @Inject(AUDIO_NODE) node: AudioNode | null,
   ) {
       super(context);

       if (node) {
           node.connect(this);
       }
   }

   ngOnDestroy() {
       this.disconnect();
   }
}

DI . exportAstemplate reference variables. :


<ng-container waGainNode>
   <ng-container waDelayNode></ng-container>
</ng-container>

waAudioDestination:


@Directive({
   selector: '[waAudioDestinationNode]',
   exportAs: 'AudioNode',
})
export class WebAudioDestination extends GainNode 
   implements OnDestroy {
   constructor(
       @Inject(AUDIO_CONTEXT) context: AudioContext,
       @Inject(AUDIO_NODE) node: AudioNode | null,
   ) {
       super(context);
       this.connect(context.destination);

       if (node) {
           node.connect(this);
       }
   }

   ngOnDestroy() {
       this.disconnect();
   }
}

, , Dependency Injection. . , :


@Directive({
   selector: '[waOutput]',
})
export class WebAudioOutput extends GainNode implements OnDestroy {
   @Input()
   set waOutput(destination: AudioNode | undefined) {
       this.disconnect();

       if (destination) {
           this.connect(destination);
       }
   }

   constructor(
       @Inject(AUDIO_CONTEXT) context: AudioContext,
       @Inject(AUDIO_NODE) node: AudioNode | null,
   ) {
       super(context);

       if (node) {
           node.connect(this);
       }
   }

   ngOnDestroy() {
       this.disconnect();
   }
}

GainNode, . ngOnDestroy. , , this .



. -, . audio , MediaElementAudioSourceNode:


@Directive({
   selector: 'audio[waMediaElementAudioSourceNode]',
   exportAs: 'AudioNode',
   providers: [
       {
           provide: AUDIO_NODE,
           useExisting: forwardRef(() => WebAudioMediaSource),
       },
   ],
})
export class WebAudioMediaSource extends MediaElementAudioSourceNode
   implements OnDestroy {
   constructor(
       @Inject(AUDIO_CONTEXT) context: AudioContext,
       @Inject(ElementRef) {nativeElement}: ElementRef<HTMLMediaElement>,
   ) {
       super(context, {mediaElement: nativeElement});
   }

   ngOnDestroy() {
       this.disconnect();
   }
}

:


<audio src="/demo.wav" waMediaElementAudioSourceNode>
    <ng-container #node="AudioNode" waDelayNode [delayTime]="0.2">
        <ng-container waGainNode [gain]="0.5">
            <ng-container [waOutput]="node"></ng-container>
            <ng-container waAudioDestinationNode></ng-container>
        </ng-container>
    </ng-container>
    <ng-container waAudioDestinationNode></ng-container>
</audio>

Web Audio API , . - . HTML . AudioBufferSourceNode. — , `AudioBuffer. AudioBuffer:


@Injectable({
   providedIn: 'root',
})
export class AudioBufferService {
   private readonly cache = new Map<string, AudioBuffer>();

   constructor(
      @Inject(AUDIO_CONTEXT) private readonly context: AudioContext
   ) {}

   fetch(url: string): Promise<AudioBuffer> {
       return new Promise<AudioBuffer>((resolve, reject) => {
           if (this.cache.has(url)) {
               resolve(this.cache.get(url));

               return;
           }

           const request = new XMLHttpRequest();

           request.open('GET', url, true);
           request.responseType = 'arraybuffer';
           request.onerror = reject;
           request.onabort = reject;
           request.onload = () => {
               this.context.decodeAudioData(
                   request.response,
                   buffer => {
                       this.cache.set(url, buffer);
                       resolve(buffer);
                   },
                   reject,
               );
           };
           request.send();
       });
   }
}

AudioBufferSourceNode, AudioBuffer, :


export class WebAudioBufferSource extends AudioBufferSourceNode 
   implements OnDestroy {
   @Input('buffer')
   set bufferSetter(source: AudioBuffer | null | string) {
       this.buffer$.next(source);
   }

   readonly buffer$ = new Subject<AudioBuffer | null | string>();

   constructor(
       @Inject(AudioBufferService) service: AudioBufferService,
       @Inject(AUDIO_CONTEXT) context: AudioContext,
       @Attribute('autoplay') autoplay: string | null,
   ) {
       super(context);

       this.buffer$
           .pipe(
               switchMap(source =>
                   typeof source === 'string'
                       ? service.fetch(source)
                       : of(source),
               ),
           )
           .subscribe(buffer => {
               this.buffer = buffer;
           });

       if (autoplay !== null) {
           this.start();
       }
   }

   ngOnDestroy() {
       this.buffer$.complete();

       try {
           this.stop();
       } catch (_) {}

       this.disconnect();
   }
}

, autoplay audio, .


AudioParam


AudioParam. GainNode . . . — , . , AudioParam . :


@Input('gain')
@audioParam('gain')
gainParam?: AudioParamInput;

:


export type AudioParamDecorator<K extends string> = (
   target: AudioNodeWithParams<K>,
   propertyKey: string,
) => void;

export function audioParam<K extends string>(
   param: K,
): AudioParamDecorator<K> {
   return (target, propertyKey) => {
       Object.defineProperty(target, propertyKey, {
           set(
               this: AudioNode & Record<K, AudioParam>,
               value: AudioParamInput,
           ) {
               processAudioParam(
                   this[param],
                   value,
                   this.context.currentTime,
               );
           },
       });
   };
}

. AudioParamInput? :


export type AudioParamAutomation = Readonly<{
   value: number;
   duration: number;
   mode: 'instant' | 'linear' | 'exponential';
}>;

processAudioParam API. , . 0 , 1 , — : {value: 1, duration: 1, mode: ‘linear’}. .


, duration, . . . , , :


@Pipe({
   name: 'waAudioParam',
})
export class WebAudioParamPipe implements PipeTransform {
   transform(
       value: number,
       duration: number,
       mode: AudioParamAutomationMode = 'exponential',
   ): AudioParamAutomation {
       return {
           value,
           duration,
           mode,
       };
   }
}

AudioParam . 1, LFO — Low Frequency Oscillator. . — . waOutput, . exportAs :


<ng-container waOscillatorNode frequency="0.2" autoplay>
   <ng-container waGainNode gain="3000">
       <ng-container [waOutput]="filter.frequency"></ng-container>
   </ng-container>
</ng-container>

<ng-container waOscillatorNode autoplay [frequency]="note">
   <ng-container
       #filter="AudioNode"
       waBiquadFilterNode
       type="bandpass"
       frequency="4000"
   >
       <ng-container waAudioDestinationNode></ng-container>
   </ng-container>
</ng-container>


Web Audio API : , . :


https://stackblitz.com/edit/angular-web-audio


— -. DI:


export const TICK = new InjectionToken<Observable<number>>('Ticks', {
   factory: () => interval(250, animationFrameScheduler)
      .pipe(share()),
});

4 . beat :


kick$ = this.tick$.pipe(map(tick => tick % 4 < 2));

true false . :


<ng-container
   *ngIf="kick$ | async; else snare"
   waAudioBufferSourceNode
   autoplay
   buffer="kick.wav"
>
   <ng-container waAudioDestinationNode></ng-container>
</ng-container>
<ng-template #snare>
  <ng-container
     waAudioBufferSourceNode
     autoplay
     buffer="snare.wav"
  >
     <ng-container waAudioDestinationNode></ng-container>
  </ng-container>
</ng-template>

. , 69 — . . :


const LEAD = [
   70, 70, 70, 70, 70, 70, 70, 68,
   68, 68, 68, 68, 75, 79, 80, 87,
   87, 87, 87, 87, 87, 87, 87, 87,
   87, 87, 87, 87, 84, 80, 79, 75,
   80, 80, 80, 80, 80, 80, 80, 80,
   80, 80, 80, 80, 79, 75, 72, 70,
   70, 70, 70, 70, 70, 70, 70, 68,
   72, 75, 79, 80, 80, 79, 79, 75,
];

:


notes$ = this.tick$.pipe(map(note => toFrequency(LEAD[note % 64])));

. — ADSR-. ADSR attack, decay, sustain, release, :



, . :


@Pipe({
   name: 'adsr',
})
export class AdsrPipe implements PipeTransform {
   transform(
       value: number,
       attack: number,
       decay: number,
       sustain: number,
       release: number,
   ): AudioParamInput {
       return [
           {
               value: 0,
               duration: 0,
               mode: 'instant',
           },
           {
               value,
               duration: attack,
               mode: 'linear',
           },
           {
               value: sustain,
               duration: decay,
               mode: 'linear',
           },
           {
               value: 0,
               duration: release,
               mode: 'linear',
           },
       ];
   }
}

:


<ng-container *ngIf="notes$ | async as note">
  <ng-container 
    waOscillatorNode 
    detune="5" 
    autoplay 
    [frequency]="note"
  >
    <ng-container 
      waGainNode
      [gain]="0.1 | adsr : 0 : 0.1 : 0.02 : 0.5"
    >
      <ng-container waAudioDestinationNode></ng-container>
    </ng-container>
  </ng-container>
  <ng-container
    waOscillatorNode
    type="sawtooth"
    autoplay
    [frequency]="note"
  >
    <ng-container
      waGainNode
      [gain]="0.1 | adsr : 0 : 0.1 : 0.02 : 0.5"
    >
      <ng-container
        #feedback="AudioNode"
        waGainNode
        gain="0.7"
      >
        <ng-container waDelayNode delayTime="0.3">
          <ng-container [waOutput]="feedback"></ng-container>
        </ng-container>
        <ng-container waConvolverNode buffer="response.m4a">
          <ng-container waAudioDestinationNode></ng-container>
        </ng-container>
      </ng-container>
      <ng-container waAudioDestinationNode></ng-container>
    </ng-container>
  </ng-container>
</ng-container>

? . — ADSR-. , ConvolverNode. . ConvolverNode , . . , LFO waAudioParam.



, . Web Audio API Angular — open-source- @ng-web-apis/audio.


Web Audio API canvas , — SVG.

Web APIs for Angular, — API Angular. , , Payment Request API MIDI- — .


All Articles