schmitze333
schmitze333

Reputation: 332

Web Audio API: Clicks on volume change with slider

For an application that makes one able to determine ones tinnitus frequency, the following scenario occurs:

A user 'mousedown' a html5 slider -> an oscillator at a certain frequency is started. With moving the handle the user can change the volume of the note.

Here is the (CoffeeScript-)code which is responsible for the whole audio processing:

# CoffeScript for Tuner element
# Enables playing notes via Web Audio API

class @Tuner
  constructor: () ->
    @playing = false
    @whobbling = false
    @stopping = false
    @atVolumeChange = false
    @atFadeIn = false
    @activeBtn = undefined

    if typeof AudioContext isnt "undefined"
      @audioCtx ?= new AudioContext()
    else if typeof webkitAudioContext isnt "undefined"
      @audioCtx = new webkitAudioContext()
    else
      console.log "Browser hat keine WebAudioAPI"

    # make sure listener is set correct:
    listener = @audioCtx.listener
    listener.setOrientation(0,0,-1,0,1,0)
    listener.setPosition(0,0,0)

    # Create PannerNode and set initially to L and R
    @pannerNode = @audioCtx.createPanner()
    @pannerNode.setOrientation(0,0,0)
    @pannerNode.setPosition(0,0,1)
    @pannerNode.connect( @audioCtx.destination )

    # Create GainNode and set to 0.0
    @gainNode = @audioCtx.createGain()
    @gainNode.gain.value = 0.0
    @gainNode.connect @pannerNode
    @actualFreq = 500
    @currentVolume = 0.0

  start: (volume, elem_id) ->
    if @playing or @stopping
      return

    @playing = true
    @currentVolume = volume
    @oscillator = @audioCtx.createOscillator()
    @oscillator.type = "sine"
    @oscillator.frequency.value = @actualFreq
    @oscillator.connect @gainNode

    # if whobbling is active, create LFO & LFOGain for effect
    if @whobbling
      lfo = @audioCtx.createOscillator()
      lfo.type = "sine"
      lfo.frequency.value = 5

      whobGain = @actualFreq * 5.0 / 100.0
      lfo_gain = @audioCtx.createGain()
      lfo_gain.gain.value = whobGain

      lfo.connect(lfo_gain)
      lfo_gain.connect(@oscillator.frequency)
      lfo.start(0)

    # starting process
    @activeBtn = elem_id
    matchingGUI.highlightPlay( @activeBtn ) if @activeBtn
    now = @audioCtx.currentTime
    @atFadeIn = true
    @gainNode.gain.setValueAtTime 0.0, now + 0.01
    @oscillator.start(now + 0.02)
    @gainNode.gain.setValueAtTime 0.0, now + 0.03
    @gainNode.gain.linearRampToValueAtTime volume, now + 0.2
    that = @
    setTimeout ->
      that.atFadeIn = false
    , 200

  stop: () ->
    if @playing and not @stopping
      @stopping = true
      now = @audioCtx.currentTime
      now += 0.3 if @atVolumeChange
      now += 0.3 if @atFadeIn
      @gainNode.gain.setValueAtTime @currentVolume, now + 0.01
      @gainNode.gain.linearRampToValueAtTime 0.0, now + 0.45
      @oscillator.stop( now + 0.5 )
      matchingGUI.highlightStop(@activeBtn) if @activeBtn
      that = @
      setTimeout ->
        that.stopping = false
        that.playing = false
        that.whobbling = false
      , 750

  setFrequency: (newFreq) ->
    @actualFreq = newFreq
    @oscillator.frequency.value = newFreq if @playing and not @stopping

  changeVolume: (newVolume) ->
    newVolumeDb = @linearToDb newVolume
    if not @stopping
      @atVolumeChange = true
      now = @audioCtx.currentTime
      @currentVolume = newVolumeDb
      @gainNode.gain.exponentialRampToValueAtTime @currentVolume, now + 0.333
      that = @
      setTimeout ->
        that.atVolumeChange = false
      , 300

  setWhobbling: () ->
    @whobbling = true

  setPanToLR: () ->
    @pannerNode.setPosition(0,0,1) unless @playing

  setPanToL: () ->
    @pannerNode.setPosition(-3,0,0) unless @playing

  setPanToR: () ->
    @pannerNode.setPosition(3,0,0) unless @playing

  play: (frequency, volume,  whob, elem_id) ->
    whob ?= false
    @setFrequency frequency
    @setWhobbling() if whob
    volumeDb = @linearToDb( volume )
    @start( volumeDb, elem_id )

  linearToDb: (s) ->
    dbStart = 90
    s = s * dbStart - dbStart
    return Math.pow 10, s/20

# === End Class Tuner ===
#
# export Tuner
root = exports ? window
root.Tuner = Tuner

Tuner.play() is connected to the sliders 'mousedown' callback and Tuner.changeVolume() to the sliders 'input' callback.

The problem is as follows:

Every time the slider is moved, a clicking occurs. I guess the reason for this is that every time the slider fires the 'input', the ramp, currently driven by the Tuner.changeVolume method, is overlaid by another ramp.

I experimented a lot (no scheduling, cancelScheduledValues, ...) and the version above leaves only one last clicking when the slider is moved initially for various browsers on Mac. But for browsers on a Windows machine the clicking increases significantly.

(Btw. it seems not to be possible to read out the @gainNode.gain.value when a ramp is driven.)

Any idea how to handle this problem? I'd be thankful for any hint...

Upvotes: 0

Views: 220

Answers (1)

Nick Jillings
Nick Jillings

Reputation: 122

It may be your oscillators are restarting. Not very familiar with CoffeeScript (need more coffee...) so can't directly root out the problem but if its a click and drag sliding event perhaps a browser is improperly sending multiple mousedowns with the mousemove event.

Instead why not have the oscillators always running but the gain node at 0, effectively no output. Then when the user goes over the gain ramps and tracks the user input. Generally it is good practice to use ramps, but if you trigger the volume change to occur with mousemove, most users do not move the slidders fast enough for you to get the 'zipping' effect I think you want to avoid.

Upvotes: 0

Related Questions