Using Sound Files

[Using a Filter to Modify Sound] [Spatial Sound]

This first example, based on our Motion1 starter program, shows how you can replay a single file upon each impact. The statement FAudioElement.currentTime := 0; works in Chrome, Edge, Firefox and Opera. To work around the bug in Chromium (the internal browser on Smart Mobile Studio and a useful browser for the Raspberry Pi) you can use instead FAudioElement.Load as indicated on Stack Overflow.

We use the hit-01.wav (downloaded from MEDIACOLLEGE) in the res folder. Wav files can be handled by Chrome, Chromium, Edge, Firefox and Opera browsers.

unit Unit1;

interface

uses
  System.Types, SmartCL.System, SmartCL.Components, SmartCL.Application,
  SmartCL.Game, SmartCL.GameApp, SmartCL.Graphics, SmartCL.MediaElements;

type
  TCanvasProject = class(TW3CustomGameApplication)
  private
    const SCENE_WIDTH = 100;
    const SCENE_HEIGHT = 200;
    const MOB_WIDTH = 20;
    const MOB_HEIGHT = 15;
    const MOB_Y = 100;
    FrameCount := 0;
    MobX := 5;
    GoingRight := True;
    FAudioElement: TW3AudioElement;
  public
    procedure ApplicationStarting; override;
    procedure ApplicationClosing; override;
    procedure PaintView(Canvas: TW3Canvas); override;
  end;

implementation

procedure TCanvasProject.ApplicationStarting;
begin
  inherited;
  FAudioElement := TW3AudioElement.Create;
  if FAudioElement.CanPlayTypeAsBoolean('audio/wav') then
    FAudioElement.Source := 'res/hit-01.wav';
  FAudioElement.Volume := 0.3;

  GameView.Delay := 5;
  GameView.StartSession(False);
end;

procedure TCanvasProject.PaintView(Canvas: TW3Canvas);
begin
  inc(FrameCount);
  if FrameCount < 100 then  // Give time for sound file to load
    exit;
  // Clear background with colour teal.
  Canvas.FillStyle := 'teal';
  Canvas.FillRect(0, 0, SCENE_WIDTH, SCENE_HEIGHT);
  //Draw red ellipse.
  Canvas.FillStyle := 'red';
  Canvas.BeginPath;
  Canvas.Ellipse(MobX, MOB_Y, MobX + MOB_WIDTH, MOB_Y + MOB_HEIGHT); //leftX, topY, rightX, bottomY
  Canvas.Fill;
  // Change position of mobile before next paint.
  if GoingRight then
    inc(MobX)
  else
    dec(MobX);
  if MobX + MOB_WIDTH = SCENE_WIDTH  then
    begin
      GoingRight := False;
      FAudioElement.currentTime := 0;
      FAudioElement.Play;
    end;
  if MobX = 0 then
    begin
      GoingRight := True;
      FAudioElement.currentTime := 0;
      FAudioElement.Play;
    end;
end;

procedure TCanvasProject.ApplicationClosing;
begin
  GameView.EndSession;
  inherited;
end;

end.
    

Using a Filter to Modify Sound

This example uses a low-pass filter to modify the sound of hit-pipe.wav (downloaded from MEDIACOLLEGE). The sound file contents are stored a buffer and decoded ready for use. The buffer source is the input to the gain node, and the output of the gain node feeds into the filter node. After a few impacts the filter is bypassed and you can hear the original sound for comparison. You might decide to filter sounds according to the current situation in your game.

See the possible types of filter in the code of W3C.WebAudio.pas: JBiquadFilterType = (bftLowpass, bftHighpass, bftBandpass, bftLowshelf, bftHighshelf, bftPeaking, bftNotch, bftAllpass);. The types are described in the JavaScript documentation. Note that ranges of frequency are attenuated (reduced) by the filter rather than being totally eliminated.

The Smart Pascal code of the example follows.

unit Unit1;

interface

uses
  System.Types, SmartCL.System, SmartCL.Components, SmartCL.Application,
  SmartCL.Game, SmartCL.GameApp, SmartCL.Graphics, SmartCL.MediaElements,
  W3C.WebAudio, W3C.XMLHttpRequest, W3C.TypedArray;

  type
  TCanvasProject = class(TW3CustomGameApplication)
  private
    const MOB_WIDTH = 20;
    const MOB_HEIGHT = 15;
    const MOB_Y = 100;
    const SCENE_WIDTH = 100;
    const SCENE_HEIGHT = 200;
    MobX: integer := 5;
    GoingRight: Boolean := True;
    FrameCount := 0;

    FAudioContext: JAudioContext;
    FAudioData: JAudioBuffer;
    FAudioBufferNode: JAudioBufferSourceNode;
    FGain: JGainNode;
    FFilter: JBiquadFilterNode;
  public
    procedure PlaySound;
    procedure ApplicationStarting; override;
    procedure ApplicationClosing; override;
    procedure PaintView(Canvas: TW3Canvas); override;
  end;

implementation

procedure TCanvasProject.PlaySound;
begin
  FAudioBufferNode := FAudioContext.createBufferSource;
  FAudioBufferNode.buffer := FAudioData;
  FAudioBufferNode.connect(FGain);
  FAudioBufferNode.start(0);
end;

procedure TCanvasProject.ApplicationStarting;
var
  Request: JXMLHttpRequest;
begin
  inherited;
  FAudioContext := new JAudioContext;
  Request := new JXMLHttpRequest;
  Request.open('GET', 'res/hit-pipe.wav', true);
  Request.responseType := 'arraybuffer';
  Request.OnLoad := procedure
    begin
      FAudioContext.decodeAudioData(JArrayBuffer(Request.response),
        procedure(decodedData: JAudioBuffer)
        begin
          FAudioData := decodedData;
        end);
    end;
  Request.send;

  FGain := FAudioContext.createGain;
  FGain.gain.value := 0.9;
  FFilter := FAudioContext.createBiquadFilter;
  FFilter.&type := bftLowpass;
  FFilter.frequency.value := 400;
  FGain.connect(FFilter);
  FFilter.connect(FAudioContext.destination);
  GameView.Delay := 5;
  GameView.StartSession(False);
end;

procedure TCanvasProject.PaintView(Canvas: TW3Canvas);
begin
  inc(FrameCount);
  if framecount > 500 then
    begin
      FFilter.disconnect;
      FGain.connect(FAudioContext.destination);
    end;
  Canvas.FillStyle := 'teal';
  Canvas.FillRect(0, 0, SCENE_WIDTH, SCENE_HEIGHT);
  //Draw red ellipse.
  Canvas.FillStyle := 'red';
  Canvas.BeginPath;
  Canvas.Ellipse(MobX, MOB_Y, MobX + MOB_WIDTH, MOB_Y + MOB_HEIGHT); //leftX, topY, rightX, bottomY
  Canvas.Fill;
  if GoingRight = True then
    inc(MobX)
  else
    dec(MobX);
  if MobX + MOB_WIDTH = SCENE_WIDTH  then
    begin
      GoingRight := False;
      PlaySound;
    end;
  if MobX = 0 then
    begin
      GoingRight := True;
      PlaySound;
    end;
end;

procedure TCanvasProject.ApplicationClosing;
begin
  GameView.EndSession;
  inherited;
end;

end.
    

Spatial Sound

See first the useful documentation and JavaScript demonstration, from which we have taken values of the properties of the panner and listener. The intention is that you will be able to hear from the output of stereo speakers or headphones whether the sound of each impact is coming from the left or right. This example uses the same sound file as our first demonstration above.

We hope that you will be able to learn the Smart Pascal syntax and use it for creating 3D sound in a game of your own.

unit Unit1;

interface

uses
  System.Types, SmartCL.System, SmartCL.Components, SmartCL.Application,
  SmartCL.Game, SmartCL.GameApp, SmartCL.Graphics, SmartCL.MediaElements,
  W3C.WebAudio, W3C.XMLHttpRequest, W3C.TypedArray;

  type
  TCanvasProject = class(TW3CustomGameApplication)
  private
    const MOB_WIDTH = 20;
    const MOB_HEIGHT = 15;
    const MOB_Y = 300;
    MobX: integer := 5;
    GoingRight: Boolean := True;
    FAudioContext: JAudioContext;
    FAudioData: JAudioBuffer;
    FAudioBufferNode: JAudioBufferSourceNode;
    FPanner: JPannerNode;
    FListener: JAudioListener;
  public
    procedure PlaySound;
    procedure ApplicationStarting; override;
    procedure ApplicationClosing; override;
    procedure PaintView(Canvas: TW3Canvas); override;
  end;

implementation

procedure TCanvasProject.PlaySound;
begin
  FAudioBufferNode := FAudioContext.createBufferSource;
  FAudioBufferNode.buffer := FAudioData;
  FAudioBufferNode.connect(FPanner);
  FAudioBufferNode.start(0);
end;

procedure TCanvasProject.ApplicationStarting;
var
  Request: JXMLHttpRequest;
begin
  inherited;
  FAudioContext := new JAudioContext;
  Request := new JXMLHttpRequest;
  Request.open('GET', 'res/hit-01.wav', true);
  Request.responseType := 'arraybuffer';
  Request.OnLoad := procedure
    begin
      FAudioContext.decodeAudioData(JArrayBuffer(Request.response),
        procedure(decodedData: JAudioBuffer)
        begin
          FAudioData := decodedData;
        end);
    end;
  Request.send;

  FPanner := FAudioContext.createPanner;
  FPanner.panningModel := pmtHRTF;
  FPanner.distanceModel := dmtInverse;
  FPanner.refDistance := 1;
  FPanner.maxDistance := 10000;
  FPanner.rolloffFactor := 1;
  FPanner.coneInnerAngle := 360;
  FPanner.coneOuterAngle := 0;
  FPanner.coneOuterGain := 0;
  FPanner.setOrientation(1, 0, 0);

  FListener := FAudioContext.listener;
  FListener.setPosition(683, 333.5, 300);
  FListener.dopplerFactor := 1;
  FListener.speedOfSound := 343.3;
  FListener.setOrientation(0, 0, -1, 0, 1, 0);
  FPanner.connect(FAudioContext.destination);
  GameView.Delay := 1;
  GameView.StartSession(False);
end;

procedure TCanvasProject.PaintView(Canvas: TW3Canvas);
begin
  Canvas.FillStyle := 'teal';
  Canvas.FillRect(0, 0, GameView.Width, GameView.Height);
  //Draw red ellipse.
  Canvas.FillStyle := 'red';
  Canvas.BeginPath;
  Canvas.Ellipse(MobX, MOB_Y, MobX + MOB_WIDTH, MOB_Y + MOB_HEIGHT); //leftX, topY, rightX, bottomY
  Canvas.Fill;
  if GoingRight = True then
    inc(MobX, 3)
  else
    dec(MobX, 3);
  if MobX + MOB_WIDTH >= GameView.Width  then
    begin
      GoingRight := False;
      FPanner.setPosition(688, 333.5, 300);
      PlaySound;
    end;
  if MobX <= 0 then
    begin
      GoingRight := True;
      FPanner.setPosition(678, 333.5, 300);
      PlaySound;
    end;
end;

procedure TCanvasProject.ApplicationClosing;
begin
  GameView.EndSession;
  inherited;
end;

end.   

Programming - a skill for life!

Using Audio in Smart Pascal