changeset 1362:3dbf77ca78b9

did: refactor loading, adding threaded background loading (still missing stuff though)
author sam <sam@basx.dev>
date Mon, 04 Nov 2024 23:27:30 +0700
parents 48df70e8aeed
children ce92db0b8f50
files semicongine.nim semicongine/audio.nim semicongine/audio/generators.nim semicongine/audio/mixer.nim semicongine/audio/mixer_module.nim semicongine/audio/resources.nim semicongine/background_loader.nim semicongine/gltf.nim semicongine/image.nim semicongine/loaders.nim semicongine/resources.nim
diffstat 11 files changed, 569 insertions(+), 516 deletions(-) [+]
line wrap: on
line diff
--- a/semicongine.nim	Mon Nov 04 00:06:30 2024 +0700
+++ b/semicongine.nim	Mon Nov 04 23:27:30 2024 +0700
@@ -4,6 +4,12 @@
 import ./semicongine/resources
 export resources
 
+import ./semicongine/loaders
+export loaders
+
+import ./semicongine/background_loader
+export background_loader
+
 import ./semicongine/image
 export image
 
--- a/semicongine/audio.nim	Mon Nov 04 00:06:30 2024 +0700
+++ b/semicongine/audio.nim	Mon Nov 04 23:27:30 2024 +0700
@@ -1,5 +1,10 @@
-include ./audio/mixer
-include ./audio/generators
-include ./audio/resources
+import ./audio/mixer_module
+export mixer_module
+
+import ./audio/generators
+export generators
+
+import ./audio/resources
+export resources
 
 startMixerThread()
--- a/semicongine/audio/generators.nim	Mon Nov 04 00:06:30 2024 +0700
+++ b/semicongine/audio/generators.nim	Mon Nov 04 23:27:30 2024 +0700
@@ -1,3 +1,7 @@
+import std/math
+
+import ./mixer_module
+
 proc sinewave(f: float): proc(x: float): float =
   proc ret(x: float): float =
     sin(x * 2 * Pi * f)
--- a/semicongine/audio/mixer.nim	Mon Nov 04 00:06:30 2024 +0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,353 +0,0 @@
-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"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/semicongine/audio/mixer_module.nim	Mon Nov 04 23:27:30 2024 +0700
@@ -0,0 +1,354 @@
+import std/os
+import std/locks
+import std/logging
+import std/math
+import std/monotimes
+import std/strformat
+import std/tables
+import std/times
+
+import ../core/globals
+
+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
+
+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"
--- a/semicongine/audio/resources.nim	Mon Nov 04 00:06:30 2024 +0700
+++ b/semicongine/audio/resources.nim	Mon Nov 04 23:27:30 2024 +0700
@@ -1,11 +1,14 @@
 import std/endians
 import std/os
 import std/streams
+import std/strformat
 import std/strutils
 
 import ../core
 import ../resources
 
+import ./mixer_module
+
 type
   Encoding {.size: sizeof(uint32).} = enum
     # Unspecified = 0
@@ -113,21 +116,3 @@
       "Only support mono and stereo audio at the moment (1 or 2 channels), but found " &
         $channels,
     )
-
-proc loadAudio*(path: string, package = DEFAULT_PACKAGE): SoundData =
-  if path.splitFile().ext.toLowerAscii == ".au":
-    loadResource_intern(path, package = package).readAU()
-  elif path.splitFile().ext.toLowerAscii == ".ogg":
-    loadResource_intern(path, package = package).readVorbis()
-  else:
-    raise newException(Exception, "Unsupported audio file type: " & path)
-
-proc loadAudio*(
-    path: static string, package: static string = DEFAULT_PACKAGE
-): SoundData =
-  if path.splitFile().ext.toLowerAscii == ".au":
-    loadResource_intern(path, package = package).readAU()
-  elif path.splitFile().ext.toLowerAscii == ".ogg":
-    loadResource_intern(path, package = package).readVorbis()
-  else:
-    raise newException(Exception, "Unsupported audio file type: " & path)
--- a/semicongine/background_loader.nim	Mon Nov 04 00:06:30 2024 +0700
+++ b/semicongine/background_loader.nim	Mon Nov 04 23:27:30 2024 +0700
@@ -2,51 +2,57 @@
 import std/tables
 
 type
-  LoaderThreadArgs[T] =
-    (ptr Channel[string], ptr Channel[LoaderResponse[T]], proc(f: string): T {.gcsafe.})
+  LoaderThreadArgs[T] = (
+    ptr Channel[(string, string)],
+    ptr Channel[LoaderResponse[T]],
+    proc(f, p: string): T {.gcsafe.},
+  )
   LoaderResponse[T] = object
-    file: string
+    path: string
+    package: string
     data: T
     error: string
 
   BackgroundLoader[T] = object
-    loadRequestCn: Channel[string] # used for sending load requests
+    loadRequestCn: Channel[(string, string)] # used for sending load requests
     responseCn: Channel[LoaderResponse[T]] # used for sending back loaded data
     worker: Thread[LoaderThreadArgs[T]] # does the actual loading from the disk
     responseTable: Table[string, LoaderResponse[T]] # stores results
 
 proc loader[T](args: LoaderThreadArgs[T]) {.thread.} =
   while true:
-    let file = args[0][].recv()
+    let (path, package) = args[0][].recv()
     try:
-      args[1][].send(LoaderResponse[T](file: file, data: args[2](file)))
+      args[1][].send(
+        LoaderResponse[T](path: path, package: package, data: args[2](path, package))
+      )
     except Exception as e:
-      args[1][].send(LoaderResponse[T](file: file, error: e.msg))
+      args[1][].send(LoaderResponse[T](path: path, package: package, error: e.msg))
 
 proc fetchAll*(ld: var BackgroundLoader) =
   var (hasData, response) = ld.responseCn.tryRecv()
   while hasData:
-    ld.responseTable[response.file] = response
+    ld.responseTable[response.package & ":" & response.path] = response
     (hasData, response) = ld.responseCn.tryRecv()
 
-proc requestLoading*(ld: var BackgroundLoader, file: string) =
-  ld.loadRequestCn.send(file)
+proc requestLoading*(ld: var BackgroundLoader, path, package: string) =
+  ld.loadRequestCn.send((path, package))
 
-proc isLoaded*(ld: var BackgroundLoader, file: string): bool =
-  ld.fetchAll * ()
-  file in ld.responseTable
+proc isLoaded*(ld: var BackgroundLoader, path, package: string): bool =
+  fetchAll(ld)
+  (package & ":" & path) in ld.responseTable
 
-proc getLoaded*[T](ld: var BackgroundLoader[T], file: string): T =
+proc getLoadedData*[T](ld: var BackgroundLoader[T], path, package: string): T =
   var item: LoaderResponse[T]
-  doAssert ld.responseTable.pop(file, item)
+  doAssert ld.responseTable.pop(package & ":" & path, item)
   if item.error != "":
     raise newException(Exception, item.error)
   result = item.data
 
 proc initBackgroundLoader*[T](
-    loadFn: proc(f: string): T {.gcsafe.}
+    loadFn: proc(path, package: string): T {.gcsafe.}
 ): ptr BackgroundLoader[T] =
-  result = cast[ptr BackgroundLoader[T]](allocShared0(sizeof(BackgroundLoader[T])))
+  result = createShared(BackgroundLoader[T])
   open(result.loadRequestCn)
   open(result.responseCn)
   createThread[LoaderThreadArgs[T]](
@@ -54,14 +60,3 @@
     loader[T],
     (addr result.loadRequestCn, addr result.responseCn, loadFn),
   )
-
-# threaded background loaders
-
-proc rawLoaderFunc(f: string): seq[byte] {.gcsafe.} =
-  cast[seq[byte]](toSeq(f.readFile()))
-
-proc audioLoaderFunc(f: string): seq[byte] {.gcsafe.} =
-  cast[seq[byte]](toSeq(f.readFile()))
-
-var rawLoader = initBackgroundLoader(rawLoaderFunc)
-var rawLoader = initBackgroundLoader(rawLoaderFunc)
--- a/semicongine/gltf.nim	Mon Nov 04 00:06:30 2024 +0700
+++ b/semicongine/gltf.nim	Mon Nov 04 23:27:30 2024 +0700
@@ -410,28 +410,3 @@
         for nodeId in items(scene["nodes"]):
           nodes.add nodeId.getInt()
         result.scenes.add nodes
-
-proc loadMeshes*[TMesh, TMaterial](
-    path: string,
-    meshAttributesMapping: static MeshAttributeNames,
-    materialAttributesMapping: static MaterialAttributeNames,
-    package = DEFAULT_PACKAGE,
-): GltfData[TMesh, TMaterial] =
-  ReadglTF[TMesh, TMaterial](
-    stream = loadResource_intern(path, package = package),
-    meshAttributesMapping = meshAttributesMapping,
-    materialAttributesMapping = materialAttributesMapping,
-  )
-
-# static version, for better checks
-proc loadMeshes*[TMesh, TMaterial](
-    path: static string,
-    meshAttributesMapping: static MeshAttributeNames,
-    materialAttributesMapping: static MaterialAttributeNames,
-    package: static string = DEFAULT_PACKAGE,
-): GltfData[TMesh, TMaterial] =
-  ReadglTF[TMesh, TMaterial](
-    stream = loadResource_intern(path, package = package),
-    meshAttributesMapping = meshAttributesMapping,
-    materialAttributesMapping = materialAttributesMapping,
-  )
--- a/semicongine/image.nim	Mon Nov 04 00:06:30 2024 +0700
+++ b/semicongine/image.nim	Mon Nov 04 23:27:30 2024 +0700
@@ -93,18 +93,6 @@
       swap(result.data[i][0], result.data[i][2])
 
 # TODO: static versions to check for existing of files during compilation
-proc loadImage*[T: PixelType](path: string, package = DEFAULT_PACKAGE): Image[T] =
-  assert path.splitFile().ext.toLowerAscii == ".png",
-    "Unsupported image type: " & path.splitFile().ext.toLowerAscii
-  when T is Gray:
-    let pngType = 0.cint
-  elif T is BGRA:
-    let pngType = 6.cint
-
-  let (width, height, data) =
-    loadImageData[T](loadResource_intern(path, package = package).readAll())
-  result = Image[T](width: width, height: height, data: data)
-
 proc addImage*[T: PixelType](imageArray: var ImageArray[T], image: sink Image[T]) =
   assert image.width == imageArray.width,
     &"Image needs to have same dimension as ImageArray to be added (array has {imageArray.width}x{imageArray.height} but image has {image.width}x{image.height})"
@@ -114,61 +102,6 @@
   inc imageArray.nLayers
   imageArray.data.add image.data
 
-proc loadImageArray*[T: PixelType](
-    paths: openArray[string], package = DEFAULT_PACKAGE
-): ImageArray[T] =
-  assert paths.len > 0, "Image array cannot contain 0 images"
-  for path in paths:
-    assert path.splitFile().ext.toLowerAscii == ".png",
-      "Unsupported image type: " & path.splitFile().ext.toLowerAscii
-  when T is Gray:
-    let pngType = 0.cint
-  elif T is BGRA:
-    let pngType = 6.cint
-
-  let (width, height, data) =
-    loadImageData[T](loadResource_intern(paths[0], package = package).readAll())
-  result =
-    ImageArray[T](width: width, height: height, data: data, nLayers: paths.len.uint32)
-  for path in paths[1 .. ^1]:
-    let (w, h, data) =
-      loadImageData[T](loadResource_intern(path, package = package).readAll())
-    assert w == result.width,
-      "New image layer has dimension {(w, y)} but image has dimension {(result.width, result.height)}"
-    assert h == result.height,
-      "New image layer has dimension {(w, y)} but image has dimension {(result.width, result.height)}"
-    result.data.add data
-
-proc loadImageArray*[T: PixelType](
-    path: string, tilesize: uint32, package = DEFAULT_PACKAGE
-): ImageArray[T] =
-  assert path.splitFile().ext.toLowerAscii == ".png",
-    "Unsupported image type: " & path.splitFile().ext.toLowerAscii
-  when T is Gray:
-    let pngType = 0.cint
-  elif T is BGRA:
-    let pngType = 6.cint
-
-  let (width, height, data) =
-    loadImageData[T](loadResource_intern(path, package = package).readAll())
-  let
-    tilesX = width div tilesize
-    tilesY = height div tilesize
-  result = ImageArray[T](width: tilesize, height: tilesize)
-  var tile = newSeq[T](tilesize * tilesize)
-  for ty in 0 ..< tilesY:
-    for tx in 0 ..< tilesY:
-      var hasNonTransparent = when T is BGRA: false else: true
-      let baseI = ty * tilesize * width + tx * tilesize
-      for y in 0 ..< tilesize:
-        for x in 0 ..< tilesize:
-          tile[y * tilesize + x] = data[baseI + y * width + x]
-          when T is BGRA:
-            hasNonTransparent = hasNonTransparent or tile[y * tilesize + x].a > 0
-      if hasNonTransparent:
-        result.data.add tile
-        result.nLayers.inc
-
 proc `[]`*(image: Image, x, y: uint32): auto =
   assert x < image.width, &"{x} < {image.width} is not true"
   assert y < image.height, &"{y} < {image.height} is not true"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/semicongine/loaders.nim	Mon Nov 04 23:27:30 2024 +0700
@@ -0,0 +1,171 @@
+import std/json
+import std/parsecfg
+import std/strutils
+import std/sequtils
+import std/os
+import std/streams
+
+import ./audio
+import ./background_loader
+import ./core
+import ./gltf
+import ./image
+import ./resources
+
+proc loadBytes*(path, package: string): seq[byte] {.gcsafe.} =
+  cast[seq[byte]](toSeq(path.loadResource_intern(package = package).readAll()))
+
+proc loadJson*(path: string, package = DEFAULT_PACKAGE): JsonNode {.gcsafe.} =
+  path.loadResource_intern(package = package).readAll().parseJson()
+
+proc loadConfig*(path: string, package = DEFAULT_PACKAGE): Config {.gcsafe.} =
+  path.loadResource_intern(package = package).loadConfig(filename = path)
+
+proc loadImage*[T: PixelType](
+    path: string, package = DEFAULT_PACKAGE
+): Image[T] {.gcsafe.} =
+  assert path.splitFile().ext.toLowerAscii == ".png",
+    "Unsupported image type: " & path.splitFile().ext.toLowerAscii
+  when T is Gray:
+    let pngType = 0.cint
+  elif T is BGRA:
+    let pngType = 6.cint
+
+  let (width, height, data) =
+    loadImageData[T](loadResource_intern(path, package = package).readAll())
+  result = Image[T](width: width, height: height, data: data)
+
+proc loadImageArray*[T: PixelType](
+    paths: openArray[string], package = DEFAULT_PACKAGE
+): ImageArray[T] {.gcsafe.} =
+  assert paths.len > 0, "Image array cannot contain 0 images"
+  for path in paths:
+    assert path.splitFile().ext.toLowerAscii == ".png",
+      "Unsupported image type: " & path.splitFile().ext.toLowerAscii
+  when T is Gray:
+    let pngType = 0.cint
+  elif T is BGRA:
+    let pngType = 6.cint
+
+  let (width, height, data) =
+    loadImageData[T](loadResource_intern(paths[0], package = package).readAll())
+  result =
+    ImageArray[T](width: width, height: height, data: data, nLayers: paths.len.uint32)
+  for path in paths[1 .. ^1]:
+    let (w, h, data) =
+      loadImageData[T](loadResource_intern(path, package = package).readAll())
+    assert w == result.width,
+      "New image layer has dimension {(w, y)} but image has dimension {(result.width, result.height)}"
+    assert h == result.height,
+      "New image layer has dimension {(w, y)} but image has dimension {(result.width, result.height)}"
+    result.data.add data
+
+proc loadImageArray*[T: PixelType](
+    path: string, tilesize: uint32, package = DEFAULT_PACKAGE
+): ImageArray[T] {.gcsafe.} =
+  assert path.splitFile().ext.toLowerAscii == ".png",
+    "Unsupported image type: " & path.splitFile().ext.toLowerAscii
+  when T is Gray:
+    let pngType = 0.cint
+  elif T is BGRA:
+    let pngType = 6.cint
+
+  let (width, height, data) =
+    loadImageData[T](loadResource_intern(path, package = package).readAll())
+  let
+    tilesX = width div tilesize
+    tilesY = height div tilesize
+  result = ImageArray[T](width: tilesize, height: tilesize)
+  var tile = newSeq[T](tilesize * tilesize)
+  for ty in 0 ..< tilesY:
+    for tx in 0 ..< tilesY:
+      var hasNonTransparent = when T is BGRA: false else: true
+      let baseI = ty * tilesize * width + tx * tilesize
+      for y in 0 ..< tilesize:
+        for x in 0 ..< tilesize:
+          tile[y * tilesize + x] = data[baseI + y * width + x]
+          when T is BGRA:
+            hasNonTransparent = hasNonTransparent or tile[y * tilesize + x].a > 0
+      if hasNonTransparent:
+        result.data.add tile
+        result.nLayers.inc
+
+proc loadAudio*(path: string, package = DEFAULT_PACKAGE): SoundData {.gcsafe.} =
+  if path.splitFile().ext.toLowerAscii == ".au":
+    loadResource_intern(path, package = package).readAU()
+  elif path.splitFile().ext.toLowerAscii == ".ogg":
+    loadResource_intern(path, package = package).readVorbis()
+  else:
+    raise newException(Exception, "Unsupported audio file type: " & path)
+
+proc loadMeshes*[TMesh, TMaterial](
+    path: string,
+    meshAttributesMapping: static MeshAttributeNames,
+    materialAttributesMapping: static MaterialAttributeNames,
+    package = DEFAULT_PACKAGE,
+): GltfData[TMesh, TMaterial] {.gcsafe.} =
+  ReadglTF[TMesh, TMaterial](
+    stream = loadResource_intern(path, package = package),
+    meshAttributesMapping = meshAttributesMapping,
+    materialAttributesMapping = materialAttributesMapping,
+  )
+
+# background loaders
+
+type ResourceType =
+  seq[byte] | JsonNode | Config | Image[Gray] | Image[BGRA] | SoundData
+
+var rawLoader = initBackgroundLoader(loadBytes)
+var jsonLoader = initBackgroundLoader(loadJson)
+var configLoader = initBackgroundLoader(loadConfig)
+var grayImageLoader = initBackgroundLoader(loadImage[Gray])
+var imageLoader = initBackgroundLoader(loadImage[BGRA])
+var audioLoader = initBackgroundLoader(loadAudio)
+
+proc loadAsync*[T: ResourceType](path: string, package = DEFAULT_PACKAGE) =
+  when T is seq[byte]:
+    requestLoading(rawLoader[], path, package)
+  elif T is JsonNode:
+    requestLoading(jsonLoader[], path, package)
+  elif T is Config:
+    requestLoading(configLoader[], path, package)
+  elif T is Image[Gray]:
+    requestLoading(grayImageLoader[], path, package)
+  elif T is Image[BGRA]:
+    requestLoading(imageLoader[], path, package)
+  elif T is SoundData:
+    requestLoading(audioLoader[], path, package)
+  else:
+    {.error: "Unknown type".}
+
+proc isLoaded*[T: ResourceType](path: string, package = DEFAULT_PACKAGE): bool =
+  when T is seq[byte]:
+    isLoaded(rawLoader[], path, package)
+  elif T is JsonNode:
+    isLoaded(jsonLoader[], path, package)
+  elif T is Config:
+    isLoaded(configLoader[], path, package)
+  elif T is Image[Gray]:
+    isLoaded(grayImageLoader[], path, package)
+  elif T is Image[BGRA]:
+    isLoaded(imageLoader[], path, package)
+  elif T is SoundData:
+    isLoaded(audioLoader[], path, package)
+  else:
+    {.error: "Unknown type".}
+
+proc getLoaded*[T: ResourceType](path: string, package = DEFAULT_PACKAGE): T =
+  when T is seq[byte]:
+    getLoadedData(rawLoader[], path, package)
+  elif T is JsonNode:
+    getLoadedData(jsonLoader[], path, package)
+  elif T is Config:
+    getLoadedData(configLoader[], path, package)
+  elif T is Image[Gray]:
+    getLoadedData(grayImageLoader[], path, package)
+  elif T is Image[BGRA]:
+    getLoadedData(imageLoader[], path, package)
+  elif T is SoundData:
+    getLoadedData(audioLoader[], path, package)
+  else:
+    {.error: "Unknown type".}
--- a/semicongine/resources.nim	Mon Nov 04 00:06:30 2024 +0700
+++ b/semicongine/resources.nim	Mon Nov 04 23:27:30 2024 +0700
@@ -164,28 +164,6 @@
 proc loadResource*(path: string, package = DEFAULT_PACKAGE): Stream =
   loadResource_intern(path, package = package)
 
-proc loadJson*(path: string, package = DEFAULT_PACKAGE): JsonNode =
-  path.loadResource_intern(package = package).readAll().parseJson()
-
-proc loadConfig*(path: string, package = DEFAULT_PACKAGE): Config =
-  path.loadResource_intern(package = package).loadConfig(filename = path)
-
-# static versions of the above 3 calls
-proc loadResource*(
-    path: static string, package: static string = DEFAULT_PACKAGE
-): Stream =
-  loadResource_intern(path, package = package)
-
-proc loadJson*(
-    path: static string, package: static string = DEFAULT_PACKAGE
-): JsonNode =
-  path.loadResource_intern(package = package).readAll().parseJson()
-
-proc loadConfig*(
-    path: static string, package: static string = DEFAULT_PACKAGE
-): Config =
-  path.loadResource_intern(package = package).loadConfig(filename = path)
-
 proc packages*(): seq[string] =
   modList_intern()