view semicongine/audio/mixer.nim @ 1330:b165359f45d7

fix: too small buffer size on linux
author sam <sam@basx.dev>
date Mon, 19 Aug 2024 19:48:40 +0700
parents 385dbd68a947
children 966032c7a3aa
line wrap: on
line source

import std/os
import std/locks
import std/logging
import std/math
import std/monotimes
import std/strformat
import std/tables
import std/times

const NBUFFERS = 32
# it seems that some alsa hardware has a problem with smaller buffers than 512
when defined(linux):
  const BUFFERSAMPLECOUNT = 512
else:
  const BUFFERSAMPLECOUNT = 256
const AUDIO_SAMPLE_RATE* = 44100

type
  Level* = 0'f .. 1'f
  Sample* = array[2, int16]
  SoundData* = seq[Sample]

  Playback = object
    sound: SoundData
    position: int
    loop: bool
    levelLeft: Level
    levelRight: Level
    paused: bool
  Track = object
    playing: Table[uint64, Playback]
    level: Level
    targetLevel: Level
    fadeTime: float
    fadeStep: float

proc `=copy`(dest: var Playback; source: Playback) {.error.}
proc `=copy`(dest: var Track; source: Track) {.error.}

when defined(windows):
  include ./platform/windows
when defined(linux):
  include ./platform/linux

type
  Mixer* = object
    playbackCounter: uint64
    tracks: Table[string, Track]
    sounds*: Table[string, SoundData]
    level: Level
    device: NativeSoundDevice
    lock: Lock
    buffers: seq[SoundData]
    currentBuffer: int
    lastUpdate: MonoTime

proc `=copy`(dest: var Mixer; source: Mixer) {.error.}

proc initMixer(): Mixer =
  result = Mixer(
    tracks: initTable[string, Track](),
    level: 1'f,
  )
  result.tracks[""] = Track(level: 1)
  result.lock.initLock()

proc setupDevice(mixer: var Mixer) =
  # call this inside audio thread
  var bufferaddresses: seq[ptr SoundData]
  for i in 0 ..< NBUFFERS:
    mixer.buffers.add newSeq[Sample](BUFFERSAMPLECOUNT)
  for i in 0 ..< mixer.buffers.len:
    bufferaddresses.add (addr mixer.buffers[i])
  mixer.device = OpenSoundDevice(AUDIO_SAMPLE_RATE, bufferaddresses)

# TODO: this should probably be in the load-code-stuff
# proc LoadSound*(mixer: var Mixer, name: string, resource: string) =
  # assert not (name in mixer.sounds)
  # mixer.sounds[name] = LoadAudio(resource)

proc addSound*(mixer: var Mixer, name: string, sound: SoundData) =
  assert not (name in mixer.sounds)
  mixer.sounds[name] = sound

proc replaceSound*(mixer: var Mixer, name: string, sound: SoundData) =
  assert (name in mixer.sounds)
  mixer.sounds[name] = sound

proc addTrack*(mixer: var Mixer, name: string, level: Level = 1'f) =
  assert not (name in mixer.tracks)
  mixer.lock.withLock():
    mixer.tracks[name] = Track(level: level)

proc play*(mixer: var Mixer, soundName: string, track = "", stopOtherSounds = false, loop = false, levelLeft, levelRight: Level): uint64 =
  assert track in mixer.tracks, &"Track '{track}' does not exists"
  assert soundName in mixer.sounds, soundName & " not loaded"
  mixer.lock.withLock():
    if stopOtherSounds:
      mixer.tracks[track].playing.clear()
    mixer.tracks[track].playing[mixer.playbackCounter] = Playback(
      sound: mixer.sounds[soundName],
      position: 0,
      loop: loop,
      levelLeft: levelLeft,
      levelRight: levelRight,
      paused: false,
    )
  result = mixer.playbackCounter
  inc mixer.playbackCounter

proc play*(mixer: var Mixer, soundName: string, track = "", stopOtherSounds = false, loop = false, level: Level = 1'f): uint64 =
  play(
    mixer = mixer,
    soundName = soundName,
    track = track,
    stopOtherSounds = stopOtherSounds,
    loop = loop,
    levelLeft = level,
    levelRight = level
  )

proc stop*(mixer: var Mixer) =
  mixer.lock.withLock():
    for track in mixer.tracks.mvalues:
      track.playing.clear()

proc getLevel*(mixer: var Mixer): Level = mixer.level
proc getLevel*(mixer: var Mixer, track: string): Level = mixer.tracks[track].level
proc getLevel*(mixer: var Mixer, playbackId: uint64): (Level, Level) =
  for track in mixer.tracks.mvalues:
    if playbackId in track.playing:
      return (track.playing[playbackId].levelLeft, track.playing[playbackId].levelRight)

proc setLevel*(mixer: var Mixer, level: Level) = mixer.level = level
proc setLevel*(mixer: var Mixer, track: string, level: Level) =
  mixer.lock.withLock():
    mixer.tracks[track].level = level
proc setLevel*(mixer: var Mixer, playbackId: uint64, levelLeft, levelRight: Level) =
  mixer.lock.withLock():
    for track in mixer.tracks.mvalues:
      if playbackId in track.playing:
        track.playing[playbackId].levelLeft = levelLeft
        track.playing[playbackId].levelRight = levelRight
proc setLevel*(mixer: var Mixer, playbackId: uint64, level: Level) =
  setLevel(mixer, playbackId, level, level)

proc stop*(mixer: var Mixer, track: string) =
  assert track in mixer.tracks
  mixer.lock.withLock():
    mixer.tracks[track].playing.clear()

proc stop*(mixer: var Mixer, playbackId: uint64) =
  mixer.lock.withLock():
    for track in mixer.tracks.mvalues:
      if playbackId in track.playing:
        track.playing.del(playbackId)
        break

proc pause*(mixer: var Mixer, value: bool) =
  mixer.lock.withLock():
    for track in mixer.tracks.mvalues:
      for playback in track.playing.mvalues:
        playback.paused = value

proc pause*(mixer: var Mixer, track: string, value: bool) =
  mixer.lock.withLock():
    for playback in mixer.tracks[track].playing.mvalues:
      playback.paused = value

proc pause*(mixer: var Mixer, playbackId: uint64, value: bool) =
  mixer.lock.withLock():
    for track in mixer.tracks.mvalues:
      if playbackId in track.playing:
        track.playing[playbackId].paused = value

proc pause*(mixer: var Mixer) = mixer.pause(true)
proc pause*(mixer: var Mixer, track: string) = mixer.pause(track, true)
proc pause*(mixer: var Mixer, playbackId: uint64) = mixer.pause(playbackId, true)
proc unpause*(mixer: var Mixer) = mixer.pause(false)
proc unpause*(mixer: var Mixer, track: string) = mixer.pause(track, false)
proc unpause*(mixer: var Mixer, playbackId: uint64) = mixer.pause(playbackId, false)

proc fadeTo*(mixer: var Mixer, track: string, level: Level, time: float) =
  mixer.tracks[track].targetLevel = level
  mixer.tracks[track].fadeTime = time
  mixer.tracks[track].fadeStep = level.float - mixer.tracks[track].level.float / time

proc isPlaying*(mixer: var Mixer): bool =
  mixer.lock.withLock():
    for track in mixer.tracks.mvalues:
      for playback in track.playing.values:
        if not playback.paused:
          return true
  return false

proc isPlaying*(mixer: var Mixer, track: string): bool =
  mixer.lock.withLock():
    if mixer.tracks.contains(track):
      for playback in mixer.tracks[track].playing.values:
        if not playback.paused:
          return true
    return false

func applyLevel(sample: Sample, levelLeft, levelRight: Level): Sample =
  [int16(float(sample[0]) * levelLeft), int16(float(sample[1]) * levelRight)]

func clip(value: int32): int16 =
  int16(max(min(int32(high(int16)), value), int32(low(int16))))

# used for combining sounds
func mix(a, b: Sample): Sample =
  [
    clip(int32(a[0]) + int32(b[0])),
    clip(int32(a[1]) + int32(b[1])),
  ]

proc updateSoundBuffer(mixer: var Mixer) =
  let t = getMonoTime()

  let dt = (t - mixer.lastUpdate).inNanoseconds.float64 / 1_000_000_000'f64
  mixer.lastUpdate = t

  # update fadings
  for track in mixer.tracks.mvalues:
    if track.fadeTime > 0:
      track.fadeTime -= dt
      track.level = (track.level.float64 + track.fadeStep.float64 * dt).clamp(Level.low, Level.high)
      if track.fadeTime <= 0:
        track.level = track.targetLevel
  # mix
  var hasData = false
  for i in 0 ..< mixer.buffers[mixer.currentBuffer].len:
    var mixedSample = [0'i16, 0'i16]
    mixer.lock.withLock():
      for track in mixer.tracks.mvalues:
        var stoppedSounds: seq[uint64]
        for (id, playback) in track.playing.mpairs:
          if playback.paused:
            continue
          let sample = applyLevel(
            playback.sound[playback.position],
            mixer.level * track.level * playback.levelLeft,
            mixer.level * track.level * playback.levelRight,
          )
          mixedSample = mix(mixedSample, sample)
          hasData = true
          inc playback.position
          if playback.position >= playback.sound.len:
            if playback.loop:
              playback.position = 0
            else:
              stoppedSounds.add id
        for id in stoppedSounds:
          track.playing.del(id)
      mixer.buffers[mixer.currentBuffer][i] = mixedSample
  # send data to sound device
  if hasData:
    mixer.device.WriteSoundData(mixer.currentBuffer)
    mixer.currentBuffer = (mixer.currentBuffer + 1) mod mixer.buffers.len

# DSP functions
# TODO: finish implementation, one day

#[
#
proc lowPassFilter(data: var SoundData, cutoff: int) =
  let alpha = float(cutoff) / AUDIO_SAMPLE_RATE
  var value = data[0]
  for i in 0 ..< data.len:
    value[0] += int16(alpha * float(data[i][0] - value[0]))
    value[1] += int16(alpha * float(data[i][1] - value[1]))
    data[i] = value

  proc downsample(data: var SoundData, n: int) =
    let newLen = (data.len - 1) div n + 1
    for i in 0 ..< newLen:
      data[i] = data[i * n]
    data.setLen(newLen)

  proc upsample(data: var SoundData, m: int) =
    data.setLen(data.len * m)
    var i = data.len - 1
    while i < 0:
      if i mod m == 0:
        data[i] = data[i div m]
      else:
        data[i] = [0, 0]
      i.dec

    proc slowdown(data: var SoundData, m, n: int) =
      data.upsample(m)
      # TODO
      # data.lowPassFilter(m)
      data.downsample(n)

    ]#

proc destroy(mixer: var Mixer) =
  mixer.lock.deinitLock()
  mixer.device.CloseSoundDevice()

var
  mixer* = createShared(Mixer)
  audiothread: Thread[void]

proc audioWorker() {.thread.} =
  mixer[].setupDevice()
  onThreadDestruction(proc() = mixer[].lock.withLock(mixer[].destroy()); freeShared(mixer))
  while true:
    mixer[].updateSoundBuffer()

# for thread priority (really necessary?)
when defined(windows):
  import ../thirdparty/winim/winim/inc/winbase
when defined(linux):
  import std/posix

proc startMixerThread() =
  mixer[] = initMixer()
  audiothread.createThread(audioWorker)
  debug "Created audio thread"