Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Raw audio streaming

examples/audio/raw_stream.livemd

Raw audio streaming

Mix.install([
  {:zexray, github: "jn-jairo/zexray", depth: 1}
])

Code example

Example complexity rating: [★★★☆] 3/4

defmodule ExampleState do
  require Record

  Record.defrecord(:example_state,
    # Cycles per second (hz)
    frequency: 440,

    # Audio frequency, for smoothing
    audio_frequency: 440,

    # Previous value, used to test if sine needs to be rewritten, and to smoothly modulate frequency
    old_frequency: 1,

    # Index for audio rendering
    sine_idx: 0,

    # Buffer for the single cycle waveform we are synthesizing
    data: [],

    # Raw audio stream
    stream: nil
  )

  @type example_state ::
          record(:example_state,
            frequency: number,
            audio_frequency: number,
            old_frequency: number,
            sine_idx: number,
            data: [integer],
            stream: Zexray.Type.AudioStream.t()
          )
end

defmodule Example do
  use Zexray.Enum
  use Zexray.Type

  import ExampleState

  @screen_width 800
  @screen_height 450
  @title "zexray [audio] example - raw audio streaming"

  @max_samples 512
  @buffer_size 4096

  def init do
    # Initialize window
    Zexray.Window.with_window(@screen_width, @screen_height, @title, fn ->
      # Initialize audio device
      Zexray.Audio.with_audio(fn ->
        # Set our game to run at 30 frames-per-second
        Zexray.Timing.set_target_fps(30)

        Zexray.Audio.set_buffer_size(@buffer_size)

        # Manage resources loading/unloading
        Zexray.Resource.with_resource(
          fn ->
            # Init raw audio stream (sample rate: 44100, sample size: 16bit-short, channels: 1-mono) 
            stream =
              Zexray.Audio.load_stream(44_100, 16, 1, :resource)
              |> Zexray.Audio.play()

            example_state(stream: stream)
          end,
          &loop/1
        )
      end)
    end)
  end

  defp loop(state) do
    # Detect window close button or ESC key
    if Zexray.Window.should_close?() do
      :ok
    else
      # Update

      example_state(
        frequency: frequency,
        audio_frequency: audio_frequency,
        old_frequency: old_frequency,
        sine_idx: sine_idx,
        data: data,
        stream: stream
      ) = state

      # Sample mouse input
      type_vector2(
        x: mouse_position_x,
        y: mouse_position_y
      ) = Zexray.Mouse.get_position()

      frequency =
        if Zexray.Mouse.down?(enum_mouse_button(:left)) do
          frequency = 40 + mouse_position_y
          pan = 1 - mouse_position_x / @screen_width
          Zexray.Audio.set_pan(stream, pan)

          frequency
        else
          frequency
        end

      # Rewrite the sine wave
      {old_frequency, data} =
        if frequency != old_frequency do
          # Compute wavelength. Limit size in both directions
          wave_length = max(min(trunc(22_050 / frequency), trunc(@max_samples / 2)), 1)

          # Write sine wave
          data =
            0..(@screen_width - 1)
            |> Enum.map(fn i ->
              trunc(:math.sin(2 * :math.pi() * i / wave_length) * 32_000)
            end)

          {frequency, data}
        else
          {old_frequency, data}
        end

      # Refill audio stream if required
      {audio_frequency, sine_idx} =
        if Zexray.Audio.processed?(stream) do
          audio_frequency = frequency + (audio_frequency - frequency) * 0.95
          incr = audio_frequency / 44_100

          {samples, sine_idx} =
            1..@buffer_size
            |> Enum.reduce({[], sine_idx}, fn _, {samples, sine_idx} ->
              d = trunc(32_000 * :math.sin(2 * :math.pi() * sine_idx))

              sine_idx = sine_idx + incr

              sine_idx = if sine_idx > 1, do: sine_idx - 1, else: sine_idx

              {[d | samples], sine_idx}
            end)

          samples = Enum.reverse(samples)

          # Copy finished frame to audio stream
          Zexray.Audio.update(stream, samples)

          {audio_frequency, sine_idx}
        else
          {audio_frequency, sine_idx}
        end

      # Draw

      Zexray.Drawing.with_drawing(fn ->
        Zexray.Drawing.clear_background(enum_color(:raywhite))

        Zexray.Text.draw(
          "sine frequency: #{trunc(frequency)}",
          @screen_width - 220,
          10,
          20,
          enum_color(:red)
        )

        Zexray.Text.draw(
          "click mouse button to change frequency or pan",
          10,
          10,
          20,
          enum_color(:darkgray)
        )

        0..(@screen_width - 1)
        |> Enum.each(fn x ->
          y = trunc(250 + 50 * Enum.at(data, trunc(x * @max_samples / @screen_width), 0) / 32_000)

          Zexray.Shape.draw_pixel(
            x,
            y,
            enum_color(:red)
          )
        end)
      end)

      state
      |> example_state(
        frequency: frequency,
        audio_frequency: audio_frequency,
        old_frequency: old_frequency,
        sine_idx: sine_idx,
        data: data,
        stream: stream
      )
      |> loop()
    end
  end
end
Example.init()