├── .gitattributes ├── .gitignore ├── LICENSE.md ├── asl-thread.lua ├── asl.lua └── readme.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows thumbnail cache files 2 | Thumbs.db 3 | ehthumbs.db 4 | ehthumbs_vista.db 5 | 6 | # Folder config file 7 | Desktop.ini 8 | 9 | # Recycle Bin used on file shares 10 | $RECYCLE.BIN/ 11 | 12 | # Windows Installer files 13 | *.cab 14 | *.msi 15 | *.msm 16 | *.msp 17 | 18 | # Windows shortcuts 19 | *.lnk 20 | 21 | # ========================= 22 | # Operating System Files 23 | # ========================= 24 | 25 | battery.lua 26 | conf.lua 27 | main.lua 28 | *.ttf 29 | run.bat 30 | run_debug.bat 31 | temp.lua 32 | test* 33 | *.mp3 34 | *.wav 35 | *.ogg 36 | *.love 37 | *.jpg 38 | *.png 39 | *.lnk -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2018-2021 zorg 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /asl-thread.lua: -------------------------------------------------------------------------------- 1 | -- Advanced Source Library 2 | -- Processing Thread 3 | -- by zorg § ISC @ 2018-2023 4 | 5 | 6 | 7 | --[[ 8 | Internal Variables 9 | - ProcThread Class: 10 | - instances -> Table needed to keep track of all active sources. 11 | - Keys should be gapless integers so we can quickly iterate over them 12 | - Times the table is modified: 13 | - Add new element -> goes to end, that's fine 14 | - Del one element -> can be any element, shifting table or better yet, replacing with 15 | the last should be fine; this shouldn't change id-s though. 16 | - Times the table is accessed: 17 | - Thread update loop -> goes over all elements, should be fast. 18 | - Instance method calls -> goes by id number, should be relatively fast. 19 | - love.audio.pause -> requests id-s that were stopped by this event. 20 | Verdict: An int-keyed gapless table w/ an inverse lookup table seems like the best choice. 21 | 22 | - ProcThread Instances: 23 | See the constructor function definition. 24 | The methods may throw 4 kinds of errors resulting from: 25 | - nil checks, 26 | - type checks, 27 | - range checks, 28 | - enumeration checks. 29 | --]] 30 | 31 | 32 | 33 | -- Needed löve modules in this thread. 34 | require('love.thread') 35 | require('love.sound') 36 | require('love.audio') 37 | require('love.timer') 38 | require('love.math') 39 | 40 | 41 | 42 | -- The thread itself, needed so we can have one unique processing thread. 43 | local procThread 44 | 45 | -- Shared inbound communication channel to this thread. 46 | local toProc = ... 47 | 48 | -- List of ASource objects; keys are contiguous integers, however, they aren't unique indices. 49 | -- We also define a reverse-lookup table: keys are the unique indices, vals are the above keys. 50 | local ASourceList = {} -- number:table 51 | local ASourceIMap = {} -- number:number 52 | 53 | 54 | 55 | -- Enumerations (Not stored in instances, no need for reverse lookup tables.) 56 | local PitchUnit = {['ratio'] = true, ['semitones'] = true} 57 | local TimeUnit = {['seconds'] = true, ['samples'] = true} 58 | local BufferUnit = {['milliseconds'] = true, ['samples'] = true} -- Also used for Frame size. 59 | local VarianceUnit = {['milliseconds'] = true, ['samples'] = true, ['percentage'] = true} 60 | 61 | 62 | 63 | -- Monkeypatching into the math library (of this thread only). 64 | local math = math 65 | math.sgn = function(x) return x<0.0 and -1.0 or 1.0 end 66 | math.clamp = function(x,min,max) return math.max(math.min(x, max), min) end 67 | local invSqrtTwo = 1.0 / math.sqrt(2.0) 68 | local halfpi = math.pi / 2.0 69 | 70 | -- Default panning laws and reverse-lookup table. 71 | local PanLawFunc = { 72 | [0] = function(pan) return 1.0 - pan, pan end, 73 | [1] = function(pan) return math.cos(math.pi / 2.0 * pan), math.sin(math.pi / 2.0 * pan) end 74 | -- [2] is locally stored as functions in each instance separately. 75 | } 76 | local PanLawList = {[0] = 'gain', 'power'} 77 | local PanLawIMap = {}; for i=0,#PanLawList do PanLawIMap[PanLawList[i]] = i end 78 | 79 | -- Interpolation method list and reverse-lookup table. 80 | local ItplMethodList = {[0] = 'nearest', 'linear', 'cubic', 'sinc'} 81 | local ItplMethodIMap = {}; for i=0,#ItplMethodList do ItplMethodIMap[ItplMethodList[i]] = i end 82 | 83 | -- Sinc function needed for the related interpolation functionality. 84 | local function lanczos_window(x,a) 85 | if x == 0 then 86 | return 1 -- division by zero protection. 87 | elseif x >= -a and x < a then 88 | return (a * math.sin(math.pi * x) * math.sin(math.pi * x / a)) / (math.pi ^ 2 * x ^ 2) 89 | else 90 | return 0 -- brickwall edges outside the region we're using. 91 | end 92 | end 93 | 94 | -- TSM buffer mixing method list and reverse-lookup table. 95 | -- Technically these are also interpolation methods, but 2 input ones only, and an automatic mode. 96 | local MixMethodList = {[0] = 'auto', 'linear', 'sqroot', 'cosine'} 97 | local MixMethodIMap = {}; for i=0,#MixMethodList do MixMethodIMap[MixMethodList[i]] = i end 98 | 99 | -- Buffer variance distribution method list and reverse-lookup table. 100 | local VarDistType = {[0] = 'uniform', 'normal'} 101 | local VarDistIMap = {}; for i=0,#VarDistType do VarDistIMap[VarDistType[i]] = i end 102 | 103 | 104 | 105 | ---------------------------------------------------------------------------------------------------- 106 | 107 | -- Helper functions that need to be called to recalculate multiple internals from varied methods. 108 | 109 | -- Makes pitch shifting and time stretching possible. 110 | local function calculateTSMCoefficients(instance) 111 | -- Set offsets 112 | instance.innerOffset = instance.resampleRatio 113 | * instance.pitchShift * math.sgn(instance.timeStretch) 114 | instance.outerOffset = instance.resampleRatio * 115 | ( instance.timeStretch * math.sgn(instance.resampleRatio) 116 | - instance.pitchShift * math.sgn(instance.timeStretch)) 117 | -- Me avoiding responsibilities :c 118 | instance.frameAdvance = instance.timeStretch * math.abs(instance.resampleRatio) 119 | end 120 | 121 | -- Pre-calculates how stereo sources should have their panning set. 122 | local function calculatePanningCoefficients(instance) 123 | instance.panL, instance.panR = instance.panLawFunc(instance.panning) 124 | end 125 | 126 | -- Pre-calculates values needed for uniformly random buffer resizing. 127 | local function calculateFrameCoefficients(instance) 128 | local lLimit = math.floor( 1 * 0.001 * instance.samplingRate + 0.5) -- 1 ms 129 | local uLimit = math.floor(10 * instance.samplingRate + 0.5) -- 10 s 130 | instance.minFrameSize = math.max(lLimit, instance.frameSize - instance.frameVariance) 131 | instance.maxFrameSize = math.min(uLimit, instance.frameSize + instance.frameVariance) 132 | end 133 | 134 | 135 | 136 | ---------------------------------------------------------------------------------------------------- 137 | 138 | -- TODO: Placeholder until we implement the full push-style version with all processing included. 139 | local Queue = function(instance, ...) 140 | instance.source:queue(...) 141 | -- No play call here; vanilla QSources didn't automatically play either. 142 | return true 143 | end 144 | 145 | 146 | 147 | ---------------------------------------------------------------------------------------------------- 148 | 149 | -- Defining processing functions that fill the internal buffer with generated samplepoints. 150 | 151 | local Process = {} 152 | 153 | 154 | 155 | Process.static = function(instance) 156 | -- Localize length of the input SoundData for less table accesses and function calls. 157 | -- This is only used for wrapping in the sampling functions (getSample). 158 | local N = instance.data:getSampleCount() 159 | 160 | -- Copy samplepoints to buffer. 161 | for b = 0, instance.bufferSize - 1 do 162 | 163 | -- The current frame offset. 164 | local frameOffset = instance.playbackOffset 165 | -- Calculate the offset of the frame we want to mix with the current one. 166 | local mixFrameOffset = instance.curFrameSize * instance.outerOffset 167 | -- The above two don't need to be wrapped due to them only being used in one location only. 168 | 169 | -- TSM frames aren't aligned to the buffer's size, so we keep track of that separately. 170 | local i = instance._frameIndex 171 | 172 | -- Normalized linear weight applied to the two samplepoints we're mixing each time. 173 | local mix = i / (instance.curFrameSize - 1) 174 | -- Fix potential NaN issue if curFrameSize is 1. 175 | mix = mix == mix and mix or 0 176 | 177 | -- Current fractional samplepoint offset into the input SoundData. 178 | local smpOffset = frameOffset --+ i * instance.innerOffset 179 | -- Calculate the offset of the samplepoint we want to mix with the current one. 180 | local mixSmpOffset = smpOffset --+ mixFrameOffset 181 | -- The above two also don't need to be wrapped. 182 | 183 | -- If we left the SoundData's region, and looping is off... 184 | if not instance.looping then 185 | 186 | smpOffset = frameOffset + i * instance.innerOffset 187 | mixSmpOffset = smpOffset + mixFrameOffset 188 | 189 | if smpOffset < 0 or smpOffset >= N then 190 | 191 | -- Fill the rest of the buffer with silence, 192 | for j = b, instance.bufferSize - 1 do 193 | for ch=1, instance.channelCount do 194 | instance.buffer:setSample(j, ch, 0.0) 195 | end 196 | end 197 | 198 | -- Stop the instance. 199 | instance:stop() 200 | 201 | -- Reset TSM frame index 202 | instance._frameIndex = 0 203 | 204 | -- Break out early of the for loop. 205 | break 206 | end 207 | 208 | else--if instance.looping then 209 | 210 | local disjunct = instance.loopRegionB < instance.loopRegionA 211 | 212 | -- Initial playback or seeking was performed. 213 | if not instance.loopRegionEntered then 214 | 215 | -- Check if we're inside any loop regions now, if so, set the above parameter. 216 | if not disjunct then 217 | if smpOffset >= instance.loopRegionA and smpOffset <= instance.loopRegionB then 218 | instance.loopRegionEntered = true 219 | end 220 | 221 | else--if disjunct then 222 | --if smpOffset <= instance.loopRegionB or smpOffset >= instance.loopRegionA then 223 | if (smpOffset >= 0 and smpOffset <= instance.loopRegionB) or 224 | (smpOffset <= N-1 and smpOffset >= instance.loopRegionA) then 225 | instance.loopRegionEntered = true 226 | end 227 | end 228 | end 229 | 230 | -- If we're in the loop, make sure we don't leave it with neither of the two pointers. 231 | if instance.loopRegionEntered then 232 | 233 | -- Adjust both offsets to adhere to loop region bounds as well as SoundData length. 234 | local loopRegionSize 235 | if not disjunct then 236 | 237 | -- One contiguous region between A and B. 238 | -- Minimal region size is 1 samplepoints. 239 | loopRegionSize = instance.loopRegionB - instance.loopRegionA + 1 240 | 241 | else--if disjunct then 242 | 243 | -- Two separate regions between 0 and B, and A and N-1 respectively. 244 | -- Minimal region size is 2 samplepoints. (if it was 1, it would be conjunct...) 245 | loopRegionSize = (1 + instance.loopRegionB) + (N - instance.loopRegionA) 246 | 247 | end 248 | 249 | -- One algorithm to rule them all 250 | -- ...mathed out by Vörnicus, thank you once again~ 251 | 252 | smpOffset = (((smpOffset - instance.loopRegionA) % N 253 | + i * instance.innerOffset) % loopRegionSize 254 | + instance.loopRegionA) % N 255 | 256 | mixSmpOffset = (((smpOffset - instance.loopRegionA) % N 257 | + mixFrameOffset) % loopRegionSize 258 | + instance.loopRegionA) % N 259 | 260 | else 261 | 262 | smpOffset = frameOffset + i * instance.innerOffset 263 | mixSmpOffset = smpOffset + mixFrameOffset 264 | 265 | end 266 | end 267 | 268 | -- Currently, outerOffset can't change inside this function, so we hoist automatic TSM 269 | -- buffer mixing method selection out of the inner loops to here. 270 | local mixMethod = instance.mixMethodIdx 271 | -- Automatic mode: if outerOffset is effectively 0.0, use linear, otherwise use cosine. 272 | -- Note: Linear mode is inadequate, we still need to adjust the volume to be lower. 273 | if mixMethod == 0 then 274 | -- Epsilon chosen so that for most sensible values of the TSM parameters, zero will 275 | -- result in the right combinations, meaning we're just doing resampling. 276 | -- 2^-32 is good enough for more than 8 decimal digits of precision. 277 | if math.abs(instance.outerOffset) < 2^-32 then 278 | mixMethod = 0 279 | else 280 | mixMethod = 3 281 | end 282 | end 283 | 284 | -- Unroll loops based on input channel count. 285 | if instance.channelCount == 1 then 286 | local A, B = 0.0, 0.0 287 | 288 | -- Using interpolation, calculate the two samplepoints for each input channel. 289 | local itplMethodIdx = instance.itplMethodIdx 290 | if itplMethodIdx == 0 then 291 | -- 0th order hold / nearest-neighbour 292 | A = instance.data:getSample(math.floor(smpOffset + 0.5) % N) 293 | 294 | B = instance.data:getSample(math.floor(mixSmpOffset + 0.5) % N) 295 | 296 | elseif itplMethodIdx == 1 then 297 | -- 1st order / linear 298 | local int,frac 299 | 300 | int = math.floor(smpOffset) 301 | frac = smpOffset - int 302 | A = instance.data:getSample( int % N) * (1.0 - frac) + 303 | instance.data:getSample((int + 1) % N) * frac 304 | 305 | int = math.floor(mixSmpOffset) 306 | frac = mixSmpOffset - int 307 | B = instance.data:getSample( int % N) * (1.0 - frac) + 308 | instance.data:getSample((int + 1) % N) * frac 309 | 310 | elseif itplMethodIdx == 2 then 311 | -- 3rd order / cubic hermite spline 312 | local int, frac 313 | 314 | -- https://blog.demofox.org/2015/08/08/cubic-hermite-interpolation/ but simplified. 315 | local x, y, z, w 316 | local X, Y, Z, W 317 | 318 | int = math.floor(smpOffset) 319 | frac = smpOffset - int 320 | x = instance.data:getSample(math.floor(smpOffset - 1) % N) 321 | y = instance.data:getSample(math.floor(smpOffset ) % N) 322 | z = instance.data:getSample(math.floor(smpOffset + 1) % N) 323 | w = instance.data:getSample(math.floor(smpOffset + 2) % N) 324 | X = -(x * 0.5) + (y * 1.5) - (z * 1.5) + w * 0.5 325 | Y = (x ) - (y * 2.5) + (z * 2.0) - w * 0.5 326 | Z = -(x * 0.5) + (z * 0.5) 327 | W = y 328 | A = X * frac^3 + Y * frac^2 + Z * frac + W 329 | 330 | int = math.floor(mixSmpOffset) 331 | frac = mixSmpOffset - int 332 | x = instance.data:getSample(math.floor(mixSmpOffset - 1) % N) 333 | y = instance.data:getSample(math.floor(mixSmpOffset ) % N) 334 | z = instance.data:getSample(math.floor(mixSmpOffset + 1) % N) 335 | w = instance.data:getSample(math.floor(mixSmpOffset + 2) % N) 336 | X = -(x * 0.5) + (y * 1.5) - (z * 1.5) + w * 0.5 337 | Y = (x ) - (y * 2.5) + (z * 2.0) - w * 0.5 338 | Z = -(x * 0.5) + (z * 0.5) 339 | W = y 340 | B = X * frac^3 + Y * frac^2 + Z * frac + W 341 | 342 | else--if itplMethodIdx == 3 then 343 | -- 16-tap / sinc (lanczos) 344 | local taps = 16 345 | local offset, result 346 | 347 | result = 0.0 348 | offset = smpOffset % N 349 | for t = -taps+1, taps do 350 | local i = math.floor(smpOffset + t) % N 351 | result = result + instance.data:getSample(i) * 352 | lanczos_window(offset - i, taps) 353 | end 354 | A = result 355 | 356 | result = 0.0 357 | offset = mixSmpOffset % N 358 | for t = -taps+1, taps do 359 | local i = math.floor(mixSmpOffset + t) % N 360 | result = result + instance.data:getSample(i) * 361 | lanczos_window(offset - i, taps) 362 | end 363 | B = result 364 | end 365 | 366 | -- Apply attenuation to result in a linear or cosine mix through the buffer. 367 | if mixMethod == 0 then 368 | A = A * (1.0 - mix) 369 | B = B * mix 370 | A = A * invSqrtTwo 371 | B = B * invSqrtTwo 372 | elseif mixMethod == 1 then 373 | A = A * (1.0 - mix) 374 | B = B * mix 375 | elseif mixMethod == 2 then 376 | A = A * math.sqrt(1.0 - mix) 377 | B = B * math.sqrt( mix) 378 | A = A * invSqrtTwo 379 | B = B * invSqrtTwo 380 | else--if mixMethod == 3 then 381 | A = A * math.cos( mix * halfpi) 382 | B = B * math.cos((1.0 - mix) * halfpi) 383 | A = A * invSqrtTwo 384 | B = B * invSqrtTwo 385 | end 386 | 387 | if instance.outputAurality == 1 then 388 | 389 | -- Mix the values by simple summing. 390 | local O = A+B 391 | 392 | -- Set the samplepoint value(s) with clamping for safety. 393 | instance.buffer:setSample(b, math.clamp(O, -1.0, 1.0)) 394 | 395 | else--if instance.outputAurality == 2 then 396 | 397 | -- Mix the values by simple summing. 398 | local L, R = A+B, A+B 399 | 400 | -- Apply stereo separation. 401 | local M = (L + R) * 0.5 402 | local S = (L - R) * 0.5 403 | -- New range in [0.0,2.0] which is perfect for the implementation here. 404 | local separation = instance.separation + 1.0 405 | 406 | L = M * (2.0 - separation) + S * separation 407 | R = M * (2.0 - separation) - S * separation 408 | 409 | -- Apply panning. 410 | L = L * instance.panL 411 | R = R * instance.panR 412 | 413 | -- Set the samplepoint value(s) with clamping for safety. 414 | instance.buffer:setSample(b, 1, math.clamp(L, -1.0, 1.0)) 415 | instance.buffer:setSample(b, 2, math.clamp(R, -1.0, 1.0)) 416 | 417 | end 418 | 419 | else--if instance.channelCount == 2 then 420 | local AL, BL = 0.0, 0.0 421 | local AR, BR = 0.0, 0.0 422 | 423 | -- Using interpolation, calculate the two samplepoints for each input channel. 424 | local itplMethodIdx = instance.itplMethodIdx 425 | if itplMethodIdx == 0 then 426 | -- 0th order hold / nearest-neighbour 427 | AL = instance.data:getSample(math.floor(smpOffset + 0.5) % N, 1) 428 | AR = instance.data:getSample(math.floor(smpOffset + 0.5) % N, 2) 429 | 430 | BL = instance.data:getSample(math.floor(mixSmpOffset + 0.5) % N, 1) 431 | BR = instance.data:getSample(math.floor(mixSmpOffset + 0.5) % N, 2) 432 | 433 | elseif itplMethodIdx == 1 then 434 | -- 1st order / linear 435 | local int,frac 436 | 437 | int = math.floor(smpOffset) 438 | frac = smpOffset - int 439 | AL = instance.data:getSample( int % N, 1) * (1.0 - frac) + 440 | instance.data:getSample((int + 1) % N, 1) * frac 441 | AR = instance.data:getSample( int % N, 2) * (1.0 - frac) + 442 | instance.data:getSample((int + 1) % N, 2) * frac 443 | 444 | int = math.floor(mixSmpOffset) 445 | frac = mixSmpOffset - int 446 | BL = instance.data:getSample( int % N, 1) * (1.0 - frac) + 447 | instance.data:getSample((int + 1) % N, 1) * frac 448 | BR = instance.data:getSample( int % N, 2) * (1.0 - frac) + 449 | instance.data:getSample((int + 1) % N, 2) * frac 450 | 451 | elseif itplMethodIdx == 2 then 452 | -- 3rd order / cubic hermite spline 453 | local int, frac 454 | 455 | -- https://blog.demofox.org/2015/08/08/cubic-hermite-interpolation/ but simplified. 456 | local x, y, z, w 457 | local X, Y, Z, W 458 | 459 | int = math.floor(smpOffset) 460 | frac = smpOffset - int 461 | x = instance.data:getSample(math.floor(smpOffset - 1) % N, 1) 462 | y = instance.data:getSample(math.floor(smpOffset ) % N, 1) 463 | z = instance.data:getSample(math.floor(smpOffset + 1) % N, 1) 464 | w = instance.data:getSample(math.floor(smpOffset + 2) % N, 1) 465 | X = -(x * 0.5) + (y * 1.5) - (z * 1.5) + w * 0.5 466 | Y = (x ) - (y * 2.5) + (z * 2.0) - w * 0.5 467 | Z = -(x * 0.5) + (z * 0.5) 468 | W = y 469 | AL = X * frac^3 + Y * frac^2 + Z * frac + W 470 | x = instance.data:getSample(math.floor(smpOffset - 1) % N, 2) 471 | y = instance.data:getSample(math.floor(smpOffset ) % N, 2) 472 | z = instance.data:getSample(math.floor(smpOffset + 1) % N, 2) 473 | w = instance.data:getSample(math.floor(smpOffset + 2) % N, 2) 474 | X = -(x * 0.5) + (y * 1.5) - (z * 1.5) + w * 0.5 475 | Y = (x ) - (y * 2.5) + (z * 2.0) - w * 0.5 476 | Z = -(x * 0.5) + (z * 0.5) 477 | W = y 478 | AR = X * frac^3 + Y * frac^2 + Z * frac + W 479 | 480 | int = math.floor(mixSmpOffset) 481 | frac = mixSmpOffset - int 482 | x = instance.data:getSample(math.floor(mixSmpOffset - 1) % N, 1) 483 | y = instance.data:getSample(math.floor(mixSmpOffset ) % N, 1) 484 | z = instance.data:getSample(math.floor(mixSmpOffset + 1) % N, 1) 485 | w = instance.data:getSample(math.floor(mixSmpOffset + 2) % N, 1) 486 | X = -(x * 0.5) + (y * 1.5) - (z * 1.5) + w * 0.5 487 | Y = (x ) - (y * 2.5) + (z * 2.0) - w * 0.5 488 | Z = -(x * 0.5) + (z * 0.5) 489 | W = y 490 | BL = X * frac^3 + Y * frac^2 + Z * frac + W 491 | x = instance.data:getSample(math.floor(mixSmpOffset - 1) % N, 2) 492 | y = instance.data:getSample(math.floor(mixSmpOffset ) % N, 2) 493 | z = instance.data:getSample(math.floor(mixSmpOffset + 1) % N, 2) 494 | w = instance.data:getSample(math.floor(mixSmpOffset + 2) % N, 2) 495 | X = -(x * 0.5) + (y * 1.5) - (z * 1.5) + w * 0.5 496 | Y = (x ) - (y * 2.5) + (z * 2.0) - w * 0.5 497 | Z = -(x * 0.5) + (z * 0.5) 498 | W = y 499 | BR = X * frac^3 + Y * frac^2 + Z * frac + W 500 | 501 | else--if itplMethodIdx == 3 then 502 | -- 16-tap / sinc (lanczos) 503 | local taps = 16 504 | local offset, resultL, resultR 505 | 506 | resultL, resultR = 0.0, 0.0 507 | offset = smpOffset % N 508 | for t = -taps+1, taps do 509 | local i = math.floor(smpOffset + t) % N 510 | resultL = resultL + instance.data:getSample(i, 1) * 511 | lanczos_window(offset - i, taps) 512 | resultR = resultR + instance.data:getSample(i, 2) * 513 | lanczos_window(offset - i, taps) 514 | end 515 | AL, AR = resultL, resultR 516 | 517 | resultL, resultR = 0.0, 0.0 518 | offset = mixSmpOffset % N 519 | for t = -taps+1, taps do 520 | local i = math.floor(mixSmpOffset + t) % N 521 | resultL = resultL + instance.data:getSample(i, 1) * 522 | lanczos_window(offset - i, taps) 523 | resultR = resultR + instance.data:getSample(i, 2) * 524 | lanczos_window(offset - i, taps) 525 | end 526 | BL, BR = resultL, resultR 527 | end 528 | 529 | -- Apply attenuation to result in a linear or cosine mix through the buffer. 530 | if mixMethod == 0 then 531 | AL, AR = AL * (1.0 - mix), AR * (1.0 - mix) 532 | BL, BR = BL * ( mix), BR * ( mix) 533 | AL, AR = AL * invSqrtTwo, AR * invSqrtTwo 534 | BL, BR = BL * invSqrtTwo, BR * invSqrtTwo 535 | elseif mixMethod == 1 then 536 | AL, AR = AL * (1.0 - mix), AR * (1.0 - mix) 537 | BL, BR = BL * ( mix), BR * ( mix) 538 | elseif mixMethod == 2 then 539 | AL, AR = AL * math.sqrt(1.0 - mix), AR * math.sqrt(1.0 - mix) 540 | BL, BR = BL * math.sqrt( mix), BR * math.sqrt( mix) 541 | AL, AR = AL * invSqrtTwo, AR * invSqrtTwo 542 | BL, BR = BL * invSqrtTwo, BR * invSqrtTwo 543 | else--if mixMethod == 3 then 544 | AL, AR = AL * math.cos( mix * halfpi), AR * math.cos( mix * halfpi) 545 | BL, BR = BL * math.cos((1.0 - mix) * halfpi), BR * math.cos((1.0 - mix) * halfpi) 546 | AL, AR = AL * invSqrtTwo, AR * invSqrtTwo 547 | BL, BR = BL * invSqrtTwo, BR * invSqrtTwo 548 | end 549 | 550 | -- Mix the values by simple summing. 551 | local L, R = AL+BL, AR+BR 552 | 553 | -- Apply stereo separation. 554 | local M = (L + R) * 0.5 555 | local S = (L - R) * 0.5 556 | -- New range in [0.0,2.0] which is perfect for the implementation here. 557 | local separation = instance.separation + 1.0 558 | 559 | L = M * (2.0 - separation) + S * separation 560 | R = M * (2.0 - separation) - S * separation 561 | 562 | -- Apply panning. 563 | L = L * instance.panL 564 | R = R * instance.panR 565 | 566 | if instance.outputAurality == 1 then 567 | -- Downmix to mono. 568 | local O = (L+R) * 0.5 569 | -- Set the samplepoint value(s) with clamping for safety. 570 | instance.buffer:setSample(b, math.clamp(O, -1.0, 1.0)) 571 | else--if instance.outputAurality == 2 then 572 | -- Set the samplepoint value(s) with clamping for safety. 573 | instance.buffer:setSample(b, 1, math.clamp(L, -1.0, 1.0)) 574 | instance.buffer:setSample(b, 2, math.clamp(R, -1.0, 1.0)) 575 | end 576 | end 577 | 578 | -- Increase TSM frame index. 579 | instance._frameIndex = instance._frameIndex + 1 580 | 581 | -- If we are at the last one, calculate next frame offsets. 582 | if instance._frameIndex > instance.curFrameSize - 1 then 583 | 584 | -- Reset index. 585 | instance._frameIndex = 0 586 | 587 | -- Calculate next frame offset. 588 | local nextPlaybackOffset = instance.playbackOffset + instance.curFrameSize 589 | * instance.frameAdvance 590 | 591 | -- Apply loop region bounding, if applicable. 592 | if instance.looping then 593 | 594 | local disjunct = instance.loopRegionB < instance.loopRegionA 595 | 596 | -- If we're in the loop, make sure we don't leave it with neither of the two pointers. 597 | if instance.loopRegionEntered then 598 | 599 | -- Adjust both offsets to adhere to loop region bounds as well as SoundData length. 600 | local loopRegionSize 601 | if not disjunct then 602 | -- One contiguous region between A and B. 603 | loopRegionSize = instance.loopRegionB - instance.loopRegionA + 1 604 | 605 | else--if disjunct then 606 | -- Two separate regions between 0 and B, and A and N-1 respectively. 607 | loopRegionSize = (1 + instance.loopRegionB) + (N - instance.loopRegionA) 608 | end 609 | 610 | -- One algorithm to rule them all 611 | -- ...mathed out by Vörnicus, thank you once again~ 612 | 613 | nextPlaybackOffset = ( 614 | ((instance.playbackOffset - instance.loopRegionA) % N 615 | + instance.curFrameSize * instance.frameAdvance) 616 | % loopRegionSize + instance.loopRegionA) % N 617 | end 618 | end 619 | 620 | -- Set instance's offset to what it should be, with wrapping by the input SoundData's size. 621 | instance.playbackOffset = nextPlaybackOffset % N 622 | 623 | -- If needed, recalculate TSM coefficients; needs to be done here to avoid mid-frame 624 | -- changes which would result in glitches. 625 | if instance._recalcTSMCoefficients then 626 | calculateTSMCoefficients(instance) 627 | instance._recalcTSMCoefficients = false 628 | end 629 | 630 | -- Randomize frame size. 631 | if instance.frameVarianceDistributionIdx == 0 then 632 | instance.curFrameSize = love.math.random( 633 | instance.minFrameSize, 634 | instance.maxFrameSize) 635 | else 636 | local avgFrameSize = (instance.minFrameSize + instance.maxFrameSize) / 2.0 637 | local normal = love.math.randomNormal(avgFrameSize / 2.0, avgFrameSize) 638 | instance.curFrameSize = math.floor(math.clamp( 639 | normal, instance.minFrameSize, instance.maxFrameSize)) 640 | end 641 | end 642 | end 643 | 644 | -- Queue up the buffer we just calculated, and ensure it gets played even if the 645 | -- Source object has already underrun, and hence stopped. 646 | instance.source:queue( 647 | instance.buffer, instance.bufferSize * (instance.bitDepth/8) * instance.outputAurality) 648 | instance.source:play() 649 | end 650 | 651 | 652 | 653 | Process.stream = function() 654 | -- Not Yet Implemented. 655 | end 656 | 657 | 658 | 659 | Process.queue = function() 660 | -- Not Yet Implemented. 661 | end 662 | 663 | 664 | 665 | ---------------------------------------------------------------------------------------------------- 666 | 667 | -- Class 668 | 669 | local ASource = {} 670 | 671 | 672 | 673 | ---------------------------------------------------------------------------------------------------- 674 | 675 | -- Metatable 676 | 677 | local mtASource = {__index = ASource} 678 | 679 | 680 | 681 | ---------------------------------------------------------------------------------------------------- 682 | 683 | -- Constructor (not part of the class) 684 | 685 | local function new(a,b,c,d,e) 686 | -- Decode parameters. 687 | local sourcetype 688 | if type(a) == 'nil' then 689 | error("ASource constructor: Missing 1st parameter; it can be one of the following:\n" .. 690 | "string, File, FileData, Decoder, SoundData; use number for a queue-type instance.") 691 | 692 | elseif type(a) == 'number' then 693 | -- Queueable type. 694 | if type(b) ~= 'number' then 695 | error(("ASource constructor: 2nd parameter must be a number; " .. 696 | "got %s instead."):format(type(b))) 697 | end 698 | if type(c) ~= 'number' then 699 | error(("ASource constructor: 3rd parameter must be a number; " .. 700 | "got %s instead."):format(type(c))) 701 | end 702 | if type(d) ~= 'number' then 703 | error(("ASource constructor: 4th parameter must be a number; " .. 704 | "got %s instead."):format(type(d))) 705 | end 706 | if not ({[8] = true, [16] = true})[b] then 707 | error(("ASource constructor: 2nd parameter must be either 8 or 16; " .. 708 | "got %f instead."):format(b)) 709 | end 710 | if not ({[1] = true, [2] = true})[c] then 711 | error(("ASource constructor: 3rd parameter must be either 1 or 2; " .. 712 | "got %f instead."):format(c)) 713 | end 714 | sourcetype = 'queue' 715 | 716 | elseif type(a) == 'string' or 717 | a.type and a:type() == 'File' or 718 | a.type and a:type() == 'DroppedFile' or 719 | a.type and a:type() == 'FileData' or 720 | a.type and a:type() == 'Decoder' then 721 | -- Static/stream types (excluding static ones from a SoundData object). 722 | if type(b) ~= 'string' then 723 | error(("ASource constructor: 2nd parameter must be a string; " .. 724 | "got %s instead."):format(type(b))) 725 | end 726 | if not ({static = true, stream = true})[b] then 727 | error(("ASource constructor: 2nd parameter must be either `static` or `stream`; " .. 728 | "got %s instead."):format(tostring(b))) 729 | end 730 | sourcetype = b 731 | 732 | elseif a.type and a:type() == 'SoundData' then 733 | -- Can only be static type. 734 | sourcetype = 'static' 735 | 736 | else 737 | error(("ASource constructor: 1st parameter was %s; must be one of the following:\n" .. 738 | "string, File, FileData, Decoder, SoundData, number."):format(tostring(a))) 739 | end 740 | 741 | 742 | 743 | -- /!\ ONLY STATIC TYPE SUPPORTED CURRENTLY. 744 | if sourcetype ~= 'static' then 745 | error("Can't yet create streaming or queue type sources!") 746 | end 747 | 748 | 749 | 750 | -- The instance we're constructing. 751 | local instance = {} 752 | 753 | -- Load in initial data. 754 | if sourcetype == 'queue' then 755 | -- We only have format parameters. 756 | instance.data = false 757 | instance.samplingRate = a 758 | instance.bitDepth = b 759 | instance.channelCount = c 760 | instance.OALBufferCount = d 761 | else 762 | if sourcetype == 'static' then 763 | if type(a) == 'string' or 764 | a.type and a:type() == 'File' or 765 | a.type and a:type() == 'DroppedFile' or 766 | a.type and a:type() == 'FileData' 767 | then 768 | -- Load from given path, file object reference, or memory-mapped file data object. 769 | -- This might not work for File and/or FileData, but it will by inserting 770 | -- a Decoder in-between. Source says this already does that, however. 771 | instance.data = love.sound.newSoundData(a) 772 | elseif a.type and a:type() == 'Decoder' 773 | then 774 | -- We create a new SoundData from the given Decoder object; fully decoded. 775 | instance.data = love.sound.newSoundData(a) 776 | elseif a.type and a:type() == 'SoundData' 777 | then 778 | -- We set the data to the provided object. 779 | instance.data = a 780 | end 781 | else--if sourcetype == 'stream' then 782 | if type(a) == 'string' or 783 | a.type and a:type() == 'File' or 784 | a.type and a:type() == 'DroppedFile' or 785 | a.type and a:type() == 'FileData' 786 | then 787 | -- Load from given path, file object reference, or memory-mapped file data object. 788 | -- Initial decoder object will return default sized SoundData objects. 789 | instance.data = love.sound.newDecoder(a) 790 | elseif a.type and a:type() == 'Decoder' 791 | then 792 | -- We store the provided Decoder object. 793 | instance.data = a 794 | end 795 | end 796 | 797 | -- Fill out format parameters. 798 | instance.samplingRate = instance.data:getSampleRate() 799 | instance.bitDepth = instance.data:getBitDepth() 800 | instance.channelCount = instance.data:getChannelCount() 801 | 802 | -- Check for optional OAL buffer count parameter; nil for default value otherwise. 803 | instance.OALBufferCount = nil 804 | if type(a) == 'string' or 805 | a.type and a:type() == 'File' or 806 | a.type and a:type() == 'DroppedFile' or 807 | a.type and a:type() == 'FileData' or 808 | a.type and a:type() == 'Decoder' 809 | then 810 | if type(c) == 'number' then 811 | instance.OALBufferCount = c 812 | elseif type(c) ~= 'nil' then 813 | error(("3rd, optional parameter expected to be a number between 1 and 64; " .. 814 | "got %s instead."):format(type(c))) 815 | end 816 | elseif a.type and a:type() == 'SoundData' 817 | then 818 | if type(b) == 'number' then 819 | instance.OALBufferCount = b 820 | elseif type(b) ~= 'nil' then 821 | error(("2nd, optional parameter expected to be a number between 1 and 64; " .. 822 | "got %s instead."):format(type(b))) 823 | end 824 | end 825 | end 826 | 827 | -- Check for optional output aurality param.; default is input aurality, a.k.a. channel count. 828 | instance.outputAurality = instance.channelCount 829 | if sourcetype == 'queue' then 830 | instance.outputAurality = type(e) == 'number' and e 831 | else 832 | if type(a) == 'string' or 833 | a.type and a:type() == 'File' or 834 | a.type and a:type() == 'DroppedFile' or 835 | a.type and a:type() == 'FileData' or 836 | a.type and a:type() == 'Decoder' 837 | then 838 | if type(d) == 'number' then 839 | instance.outputAurality = d 840 | elseif type(d) ~= 'nil' then 841 | error(("4th, optional parameter expected to be a number, either 1 or 2; " .. 842 | "got %s instead."):format(type(d))) 843 | end 844 | elseif a.type and a:type() == 'SoundData' 845 | then 846 | if type(c) == 'number' then 847 | instance.outputAurality = c 848 | elseif type(c) ~= 'nil' then 849 | error(("3rd, optional parameter expected to be a number, either 1 or 2; " .. 850 | "got %s instead."):format(type(c))) 851 | end 852 | end 853 | end 854 | 855 | -- Set up fields not coming from the constructor arguments. 856 | do 857 | -- The type of this instance. 858 | instance.type = sourcetype 859 | 860 | -- Size of the buffer used for processing. 861 | instance.bufferSize = 50 * 0.001 * instance.samplingRate 862 | 863 | -- Size of the frame we use for applying effects onto the data, without variance. 864 | instance.frameSize = 35 * 0.001 * instance.samplingRate 865 | -- The amount of frame size variance centered on the above value, in smp-s. 866 | instance.frameVariance = 17 * 0.001 * instance.samplingRate 867 | -- The random distribution to use for varying the buffer's size. 868 | instance.frameVarianceDistributionIdx = 1 869 | -- The pre-calculated min, max and currently applied frame size, for performance reasons. 870 | instance.minFrameSize = nil 871 | instance.maxFrameSize = nil 872 | calculateFrameCoefficients(instance) 873 | -- The variable holding the final TSM frame size that gets used. 874 | instance.curFrameSize = instance.frameSize 875 | -- Store the current point in the TSM frame we're at, due to this not being aligned with 876 | -- the used buffer size. 877 | instance._frameIndex = 0 878 | 879 | -- The playback state; stopped by default. 880 | instance.playing = false 881 | -- The playback pointer in smp-s that is used as an offset into the data; can be fractional. 882 | instance.playbackOffset = 0.0 883 | 884 | -- Whether the playback should loop. 885 | instance.looping = false 886 | -- The start and end points in smp-s that define the loop region; can be disjunct by having 887 | -- the endpoint before the startpoint. Points are inclusive. 888 | -- By default, the loop region is either the entirety of the given input, or the frame size 889 | -- for queue-type sources. 890 | instance.loopRegionA = 0 891 | instance.loopRegionB = 0 892 | -- Helper variable to guarantee playback not being initially locked between the loop region. 893 | -- False by default, stopping and redefining the loop region resets the field to false. 894 | -- If no loop region is defined, then this turns true instantly when starting playback. 895 | instance.loopRegionEntered = false 896 | 897 | -- The indice of the interpolation method used when rendering data into the buffer. 898 | -- Default is 1 for linear. 899 | instance.itplMethodIdx = 1 900 | 901 | -- The indice of the TSM mixing method used. Default is 0 for automatic mode. 902 | -- Reasoning: For smaller buffer sizes, cosine interpolation does introduce some modulation 903 | -- noise due to nonlinearity, however, for all situations where pitch shift and time stretch 904 | -- are not related (i.e. it's just a change in resampling), cosine gets rid of audible 905 | -- amplitude fluctuations, which may be better; auto mode sets the method used automatically 906 | -- based on the above. 907 | instance.mixMethodIdx = 0 908 | 909 | -- Resampling ratio; the simple way of combined speed and pitch modification. 910 | instance.resampleRatio = 1.0 911 | -- Time stretching; Uses Time-Scale Modification as the method. 912 | instance.timeStretch = 1.0 913 | -- Pitch shifting; stored both as a ratio, and in semitones for accuracy. 914 | instance.pitchShift = 1.0 915 | instance.pitchShiftSt = 0 916 | -- The pre-calculated playback rate coefficients, for performance reasons. 917 | -- The rate of smp advancement in one frame and the rate of frame advancement, respectively. 918 | instance.innerOffset = nil 919 | instance.outerOffset = nil 920 | instance.frameAdvance = nil -- Me avoiding responsibilities :c 921 | calculateTSMCoefficients(instance) 922 | -- Flag to know we need to recalculate the coefficients 923 | -- Note: Easier to only do this at Frame borders vs. mathing out what this does mid-frame. 924 | instance._recalcTSMCoefficients = false 925 | 926 | -- The panning law string and the function in use. 927 | instance.panLaw = 0 928 | instance.panLawFunc = PanLawFunc[instance.panLaw] 929 | 930 | -- Panning value; operates on input so even with mono output, this has an impact. 931 | -- Can go from 0.0 to 1.0; default is 0.5 for centered. 932 | instance.panning = 0.5 933 | -- The pre-calculated left and right panning coefficients, for performance reasons. 934 | instance.panL = nil 935 | instance.panR = nil 936 | calculatePanningCoefficients(instance) 937 | 938 | -- Stereo separation value; operates on input so even with mono output, this has an impact. 939 | -- Can go from -100% to 100%, from mid ch. downmix to original to side ch. downmix. 940 | instance.separation = 0.0 941 | end 942 | 943 | -- Create internal buffer; format adheres to output. 944 | instance.buffer = love.sound.newSoundData( 945 | instance.bufferSize, 946 | instance.samplingRate, 947 | instance.bitDepth, 948 | instance.outputAurality 949 | ) 950 | 951 | -- Create internal queueable source; format adheres to output. 952 | instance.source = love.audio.newQueueableSource( 953 | instance.samplingRate, 954 | instance.bitDepth, 955 | instance.outputAurality, 956 | instance.OALBufferCount 957 | ) 958 | 959 | -- Set default loop region's end point. 960 | if instance.type == 'queue' then 961 | instance.loopRegionB = instance.frameSize - 1 962 | elseif instance.type == 'static' then 963 | instance.loopRegionB = instance.data:getSampleCount() - 1 964 | else--if instance.type == 'stream' then 965 | instance.loopRegionB = instance.data:getDuration() * instance.samplingRate - 1 966 | end 967 | 968 | -- Set up method calls. 969 | setmetatable(instance, mtASource) 970 | 971 | -- Make the instance have an unique id, and add instance to the internal tables. 972 | local id = #ASourceList + 1 973 | instance.id = id 974 | ASourceList[id] = instance 975 | ASourceIMap[id] = id 976 | 977 | -- Return the id number so a proxy instance can be constructed on the caller thread. 978 | return id 979 | end 980 | 981 | 982 | 983 | ---------------------------------------------------------------------------------------------------- 984 | 985 | -- Copy-constructor 986 | 987 | function ASource.clone(instance) 988 | local clone = {} 989 | 990 | -- Shallow-copy over all parameters as an initial step. 991 | for k,v in pairs(instance) do 992 | clone[k] = v 993 | end 994 | 995 | -- Set the playback state to stopped, which also resets the pointer to the start. 996 | -- Due to reverse playback support, the start point is dependent on the playback direction. 997 | clone._isPlaying = false 998 | if clone.type == 'static' then 999 | clone.pointer = clone.timeStretch >= 0 and 0 or math.max(0, clone.data:getSampleCount()-1) 1000 | elseif clone.type == 'stream' then 1001 | -- Not Yet Implemented. 1002 | else--if clone.type == 'queue' then 1003 | -- Not Yet Implemented. 1004 | end 1005 | 1006 | -- Data source object: SoundData gets referenced by the shallow-copy above; 1007 | -- Decoder gets cloned, Queue has none. 1008 | if clone.type == 'stream' then 1009 | -- This should work even if the Decoder was created from a DroppedFile. 1010 | clone.data = instance.data:clone() 1011 | end 1012 | 1013 | -- Buffer object: Create an unique one. 1014 | clone.buffer = love.sound.newSoundData( 1015 | clone.bufferSize, 1016 | clone.samplingRate, 1017 | clone.bitDepth, 1018 | clone.channelCount 1019 | ) 1020 | 1021 | -- Internal QSource object: Clone it. 1022 | -- BUG: Cloning a queueable source doesn't give back a functioning one; temporary fix. 1023 | --clone.source = instance.source:clone() 1024 | clone.source = love.audio.newQueueableSource( 1025 | clone.samplingRate, 1026 | clone.bitDepth, 1027 | clone.outputAurality, 1028 | clone.OALBufferCount 1029 | ) 1030 | -- Due to the above duct tape, we also need to manually copy over QSource internals that aren't 1031 | -- touched by the library: 1032 | local fxlist = instance.source:getActiveEffects() 1033 | for i,name in ipairs(fxlist) do 1034 | local filtersettings = instance.source:getEffect(name) 1035 | if filtersettings then 1036 | clone.source:setEffect(name, filtersettings) 1037 | else 1038 | clone.source:setEffect(name, true) 1039 | end 1040 | end 1041 | clone.source:setFilter( instance.source:getFilter()) 1042 | clone.source:setAirAbsorption( instance.source:getAirAbsorption()) 1043 | clone.source:setAttenuationDistances( instance.source:getAttenuationDistances()) 1044 | clone.source:setCone( instance.source:getCone()) 1045 | clone.source:setDirection( instance.source:getDirection()) 1046 | clone.source:setPosition( instance.source:getPosition()) 1047 | clone.source:setRolloff( instance.source:getRolloff()) 1048 | clone.source:setVelocity( instance.source:getVelocity()) 1049 | clone.source:setRelative( instance.source:isRelative()) 1050 | clone.source:setVolumeLimits( instance.source:getVolumeLimits()) 1051 | clone.source:setVolume( instance.source:getVolume()) 1052 | 1053 | -- Make sure all library-specific internals are configured correctly. 1054 | calculateTSMCoefficients(clone) 1055 | calculatePanningCoefficients(clone) 1056 | calculateFrameCoefficients(clone) 1057 | 1058 | -- Set instance metatable. 1059 | setmetatable(clone, mtASource) 1060 | 1061 | -- Make clone have an unique id, and add instance to the internal tables. 1062 | local id = #ASourceList + 1 1063 | clone.id = id 1064 | ASourceList[id] = clone 1065 | ASourceIMap[id] = id 1066 | 1067 | -- Return the id number so a proxy instance can be constructed on the caller thread. 1068 | return id 1069 | end 1070 | 1071 | 1072 | 1073 | ---------------------------------------------------------------------------------------------------- 1074 | 1075 | -- Base class overrides (Object) 1076 | 1077 | function ASource.type(instance) 1078 | return 'ASource' 1079 | end 1080 | 1081 | function ASource.typeOf(instance, type) 1082 | if type == 'ASource' or type == 'Source' or type == 'Object' then 1083 | return true 1084 | end 1085 | return false 1086 | end 1087 | 1088 | function ASource.release(instance) 1089 | -- Clean up the whole instance itself. 1090 | local id = instance.id 1091 | if instance.data then instance.data:release() end 1092 | instance.buffer:release() 1093 | instance.source:release() 1094 | for k,v in pairs(instance) do k = nil end 1095 | 1096 | -- Remove the instance from both tables we do book-keeping in. 1097 | local this = ASourceList[ASourceIMap[id]] 1098 | local last = ASourceList[#ASourceList] 1099 | if this == last then 1100 | -- There's only one instance, or we're deleting the last one in the list. 1101 | ASourceList[ASourceIMap[id]] = nil 1102 | else 1103 | -- Remove the instance, move the last one in the list to the removed one's position. 1104 | -- Also update the indice mapping table as well. 1105 | local lastid = ASourceList[#ASourceList].id 1106 | ASourceList[ASourceIMap[id]] = ASourceList[#ASourceList] 1107 | ASourceList[#ASourceList] = nil 1108 | ASourceIMap[lastid] = ASourceIMap[id] 1109 | end 1110 | ASourceIMap[id] = nil 1111 | end 1112 | 1113 | 1114 | 1115 | ---------------------------------------------------------------------------------------------------- 1116 | 1117 | -- Internally used across threads 1118 | 1119 | function ASource.getInternalSource(instance) 1120 | return instance.source 1121 | end 1122 | 1123 | 1124 | 1125 | ---------------------------------------------------------------------------------------------------- 1126 | 1127 | -- Deprecations 1128 | 1129 | function ASource.setPitch(instance) 1130 | error("Function deprecated by Advanced Source Library; " .. 1131 | "for the same functionality, use setResamplingRatio instead.") 1132 | end 1133 | 1134 | function ASource.getPitch(instance) 1135 | error("Function deprecated by Advanced Source Library; " .. 1136 | "for the same functionality, use getResamplingRatio instead.") 1137 | end 1138 | 1139 | 1140 | 1141 | ---------------------------------------------------------------------------------------------------- 1142 | 1143 | -- Queue related 1144 | 1145 | function ASource.queue(instance, ...) 1146 | if instance.type ~= 'queue' then 1147 | error("Cannot call queue on a non-queueable ASource instance.") 1148 | end 1149 | 1150 | return Queue(instance, ...) 1151 | end 1152 | 1153 | 1154 | 1155 | ---------------------------------------------------------------------------------------------------- 1156 | 1157 | -- Format related 1158 | 1159 | function ASource.getType(instance) 1160 | return instance.type 1161 | end 1162 | 1163 | function ASource.getSampleRate(instance) 1164 | return instance.samplingRate 1165 | end 1166 | 1167 | function ASource.getBitDepth(instance) 1168 | return instance.bitDepth 1169 | end 1170 | 1171 | function ASource.getChannelCount(instance) 1172 | -- The user needs the output format, not the input, so we return that instead. 1173 | return instance.outputAurality --instance.channelCount 1174 | end 1175 | 1176 | 1177 | 1178 | ---------------------------------------------------------------------------------------------------- 1179 | 1180 | -- Buffer related (Warning: Don't resize the buffer each frame, that might tank performance.) 1181 | 1182 | function ASource.getBufferSize(instance, unit) 1183 | unit = unit or 'milliseconds' 1184 | 1185 | if not BufferUnit[unit] then 1186 | error(("1st parameter must be `milliseconds`, `samples` or left empty; " .. 1187 | "got %s instead."):format(tostring(unit))) 1188 | end 1189 | 1190 | if unit == 'samples' then 1191 | return instance.bufferSize 1192 | else--if unit == 'milliseconds' then 1193 | return instance.bufferSize / instance.samplingRate * 1000 1194 | end 1195 | end 1196 | 1197 | function ASource.setBufferSize(instance, size, unit) 1198 | unit = unit or 'milliseconds' 1199 | 1200 | if not BufferUnit[unit] then 1201 | error(("2nd parameter must be `milliseconds`, `samples` or left empty; " .. 1202 | "got %s instead."):format(tostring(unit))) 1203 | end 1204 | 1205 | if not size then 1206 | error("Missing 1st parameter, must be a non-negative number.") 1207 | end 1208 | if type(size) ~= 'number' then 1209 | error(("1st parameter must be a non-negative number; " .. 1210 | "got %s instead."):format(tostring(size))) 1211 | end 1212 | 1213 | local min, max 1214 | if unit == 'samples' then 1215 | min = 1 * 0.001 * instance.samplingRate -- 1 ms 1216 | max = 10 * instance.samplingRate -- 10 s 1217 | else--if unit = 'milliseconds' then 1218 | min = 1 -- 1 ms 1219 | max = 10 * 1000 -- 10 s 1220 | end 1221 | if size < min or size > max then 1222 | error(("1st parameter out of range; " .. 1223 | "min/given/max: %f < [%f] < %f (%s)."):format(min, size, max, unit)) 1224 | end 1225 | 1226 | if unit == 'samples' then 1227 | instance.bufferSize = size 1228 | else--if unit = 'milliseconds' then 1229 | instance.bufferSize = math.floor(size * 0.001 * instance.samplingRate + 0.5) 1230 | end 1231 | 1232 | -- Recreate internal buffer. 1233 | -- Note: We can assume that this call never happens mid-processing, so we never lose 1234 | -- previous buffer contents, no copying necessary. 1235 | instance.buffer:release() 1236 | instance.buffer = love.sound.newSoundData( 1237 | instance.bufferSize, 1238 | instance.samplingRate, 1239 | instance.bitDepth, 1240 | instance.outputAurality 1241 | ) 1242 | end 1243 | 1244 | 1245 | 1246 | ---------------------------------------------------------------------------------------------------- 1247 | 1248 | -- TSM Frame related 1249 | 1250 | function ASource.getFrameSize(instance, unit) 1251 | unit = unit or 'milliseconds' 1252 | 1253 | if not BufferUnit[unit] then 1254 | error(("1st parameter must be `milliseconds`, `samples` or left empty; " .. 1255 | "got %s instead."):format(tostring(unit))) 1256 | end 1257 | 1258 | if unit == 'samples' then 1259 | return instance.frameSize, 1260 | instance.curFrameSize 1261 | else--if unit == 'milliseconds' then 1262 | return instance.frameSize / instance.samplingRate * 1000, 1263 | instance.curFrameSize / instance.samplingRate * 1000 1264 | end 1265 | end 1266 | 1267 | function ASource.setFrameSize(instance, size, unit) 1268 | unit = unit or 'milliseconds' 1269 | 1270 | if not BufferUnit[unit] then 1271 | error(("2nd parameter must be `milliseconds`, `samples` or left empty; " .. 1272 | "got %s instead."):format(tostring(unit))) 1273 | end 1274 | 1275 | if not size then 1276 | error("Missing 1st parameter, must be a non-negative number.") 1277 | end 1278 | if type(size) ~= 'number' then 1279 | error(("1st parameter must be a non-negative number; " .. 1280 | "got %s instead."):format(tostring(size))) 1281 | end 1282 | 1283 | local min, max 1284 | if unit == 'samples' then 1285 | min = 1 * 0.001 * instance.samplingRate -- 1 ms 1286 | max = 10 * instance.samplingRate -- 10 s 1287 | else--if unit = 'milliseconds' then 1288 | min = 1 -- 1 ms 1289 | max = 10 * 1000 -- 10 s 1290 | end 1291 | if size < min or size > max then 1292 | error(("1st parameter out of range; " .. 1293 | "min/given/max: %f < [%f] < %f (%s)."):format(min, size, max, unit)) 1294 | end 1295 | 1296 | if unit == 'samples' then 1297 | instance.frameSize = size 1298 | else--if unit = 'milliseconds' then 1299 | instance.frameSize = math.floor(size * 0.001 * instance.samplingRate + 0.5) 1300 | end 1301 | 1302 | calculateFrameCoefficients(instance) 1303 | end 1304 | 1305 | function ASource.getFrameVariance(instance, unit) 1306 | unit = unit or 'milliseconds' 1307 | 1308 | if not VarianceUnit[unit] then 1309 | error(("1st parameter must be `milliseconds`, `samples`, `percentage` or left empty; " .. 1310 | "got %s instead."):format(tostring(unit))) 1311 | end 1312 | 1313 | if unit == 'samples' then 1314 | return instance.frameVariance 1315 | elseif unit == 'milliseconds' then 1316 | return instance.frameVariance / instance.samplingRate * 1000 1317 | else--if unit == 'percentage' then 1318 | return instance.frameVariance / instance.frameSize 1319 | end 1320 | end 1321 | 1322 | function ASource.setFrameVariance(instance, variance, unit) 1323 | unit = unit or 'milliseconds' 1324 | 1325 | if not VarianceUnit[unit] then 1326 | error(("2nd parameter must be `milliseconds`, `samples`, `percentage` or left empty; " .. 1327 | "got %s instead."):format(tostring(unit))) 1328 | end 1329 | 1330 | if not variance then 1331 | error("Missing 1st parameter, must be a non-negative number.") 1332 | end 1333 | if type(variance) ~= 'number' then 1334 | error(("1st parameter must be a non-negative number; " .. 1335 | "got %s instead."):format(tostring(variance))) 1336 | end 1337 | if variance < 0 then 1338 | error(("1st parameter must be a non-negative number; " .. 1339 | "got %f instead."):format(variance)) 1340 | end 1341 | 1342 | if unit == 'percentage' and variance > 1.0 then 1343 | error(("1st parameter as a percentage can't be more than 100%; " .. 1344 | "got %f%% (%f) instead."):format(math.floor(variance*100), variance)) 1345 | end 1346 | 1347 | if unit == 'samples' then 1348 | instance.frameVariance = variance 1349 | elseif unit == 'milliseconds' then 1350 | instance.frameVariance = math.floor(variance * 0.001 * instance.samplingRate + 0.5) 1351 | else --if unit == 'percentage' then 1352 | instance.frameVariance = math.floor(instance.frameSize * variance + 0.5) 1353 | end 1354 | 1355 | calculateFrameCoefficients(instance) 1356 | end 1357 | 1358 | function ASource.getFrameVarianceDistribution(instance) 1359 | return VarDistType[instance.frameVarianceDistributionIdx] 1360 | end 1361 | 1362 | function ASource.setFrameVarianceDistribution(instance, distribution) 1363 | if not VarDistIMap[distribution] then 1364 | error(("1st parameter is not a supported buffer variance distribution; got %s.\n" .. 1365 | "Supported: `uniform`, `normal`"):format(tostring(distribution))) 1366 | end 1367 | instance.frameVarianceDistributionIdx = VarDistIMap[distribution] 1368 | 1369 | --calculateFrameCoefficients(instance) 1370 | end 1371 | 1372 | 1373 | 1374 | ---------------------------------------------------------------------------------------------------- 1375 | 1376 | -- Playback state related 1377 | 1378 | function ASource.isPlaying(instance) 1379 | return instance.playing 1380 | end 1381 | 1382 | function ASource.play(instance) 1383 | instance.playing = true 1384 | return true 1385 | end 1386 | 1387 | --[[ 1388 | function ASource.isPaused(instance) 1389 | if instance.playing then 1390 | return false 1391 | end 1392 | 1393 | if instance.timeStretch >= 0 then 1394 | if instance:tell() == 0 then 1395 | return false 1396 | end 1397 | else--if instance.timeStretch < 0 then 1398 | local limit 1399 | 1400 | if instance.type == 'static' then 1401 | limit = math.max(0, instance.data:getSampleCount() - 1) 1402 | else--if instance.type == 'stream' then 1403 | limit = math.max(0, instance.data:getDuration() * instance.samplingRate - 1) 1404 | end 1405 | 1406 | if instance:tell() == limit then 1407 | return false 1408 | end 1409 | end 1410 | 1411 | return true 1412 | end 1413 | --]] 1414 | 1415 | function ASource.pause(instance) 1416 | instance.playing = false 1417 | return true 1418 | end 1419 | 1420 | --[[ 1421 | function ASource.isStopped(instance) 1422 | if instance.playing then 1423 | return false 1424 | end 1425 | 1426 | if instance.timeStretch >= 0 then 1427 | if instance:tell() ~= 0 then 1428 | return false 1429 | end 1430 | else--if instance.timeStretch < 0 then 1431 | local limit 1432 | 1433 | if instance.type == 'static' then 1434 | limit = math.max(0, instance.data:getSampleCount() - 1) 1435 | else--if instance.type == 'stream' then 1436 | limit = math.max(0, instance.data:getDuration() * instance.samplingRate - 1) 1437 | end 1438 | 1439 | if instance:tell() ~= limit then 1440 | return false 1441 | end 1442 | end 1443 | 1444 | return true 1445 | end 1446 | --]] 1447 | 1448 | function ASource.stop(instance) 1449 | instance:pause() 1450 | instance:rewind() 1451 | return true 1452 | end 1453 | 1454 | 1455 | 1456 | function ASource.tell(instance, unit) 1457 | unit = unit or 'seconds' 1458 | 1459 | if not TimeUnit[unit] then 1460 | error(("1st parameter must be `seconds`, `samples` or left empty; " .. 1461 | "got %s instead."):format(tostring(unit))) 1462 | end 1463 | 1464 | if instance.type == 'queue' then 1465 | -- Tell the source object. 1466 | return instance.source:tell(unit) 1467 | 1468 | else 1469 | if unit == 'samples' then 1470 | return instance.playbackOffset 1471 | else 1472 | return instance.playbackOffset / instance.samplingRate 1473 | end 1474 | end 1475 | end 1476 | 1477 | function ASource.seek(instance, position, unit) 1478 | unit = unit or 'seconds' 1479 | 1480 | if not TimeUnit[unit] then 1481 | error(("2nd parameter must be `seconds`, `samples` or left empty; " .. 1482 | "got %s instead."):format(tostring(unit))) 1483 | end 1484 | 1485 | if not position then 1486 | error("Missing 1st parameter, must be a non-negative number.") 1487 | end 1488 | if type(position) ~= 'number' then 1489 | error(("1st parameter must be a non-negative number; " .. 1490 | "got %s instead."):format(tostring(position))) 1491 | end 1492 | if position < 0 then 1493 | error(("1st parameter must be a non-negative number; " .. 1494 | "got %f instead."):format(position)) 1495 | end 1496 | 1497 | if instance.type == 'queue' then 1498 | -- Seek the source object. 1499 | instance.source:seek(position, unit) 1500 | 1501 | else 1502 | if unit == 'samples' then 1503 | local limit 1504 | 1505 | if instance.type == 'static' then 1506 | limit = instance.data:getSampleCount() 1507 | else--if instance.type == 'stream' then 1508 | limit = instance.data:getDuration() * instance.samplingRate 1509 | end 1510 | 1511 | if position >= limit then 1512 | error(("1st parameter outside of data range; " .. 1513 | "%f > %f."):format(position, limit)) 1514 | end 1515 | 1516 | instance.playbackOffset = position 1517 | 1518 | else--if unit == 'seconds' then 1519 | local limit 1520 | 1521 | if instance.type == 'static' then 1522 | limit = instance.data:getDuration() 1523 | else--if instance.type == 'stream' then 1524 | limit = instance.data:getDuration() 1525 | end 1526 | 1527 | if position >= limit then 1528 | error(("1st parameter outside of data range; " .. 1529 | "%f > %f."):format(position, limit)) 1530 | end 1531 | 1532 | instance.playbackOffset = position * instance.samplingRate 1533 | end 1534 | 1535 | -- Seeking resets initial loop state. 1536 | instance.loopRegionEntered = false 1537 | end 1538 | end 1539 | 1540 | function ASource.getDuration(instance, unit) 1541 | unit = unit or 'seconds' 1542 | 1543 | if not TimeUnit[unit] then 1544 | error(("1st parameter must be `seconds`, `samples` or left empty; " .. 1545 | "got %s instead."):format(tostring(unit))) 1546 | end 1547 | 1548 | if instance.type == 'queue' then 1549 | -- Get the duration of the source object. 1550 | return instance.source:getDuration(unit) 1551 | else 1552 | if unit == 'samples' then 1553 | if instance.type == 'static' then 1554 | return instance.data:getSampleCount() 1555 | else--if instance.type == 'stream' then 1556 | return instance.data:getDuration() * instance.samplingRate 1557 | end 1558 | else--if unit == 'seconds' then 1559 | if instance.type == 'static' then 1560 | return instance.data:getDuration() 1561 | else--if instance.type == 'stream' then 1562 | return instance.data:getDuration() 1563 | end 1564 | end 1565 | end 1566 | end 1567 | 1568 | function ASource.rewind(instance) 1569 | if instance.type == 'queue' then 1570 | -- Rewind the source object. 1571 | instance.source:seek(0) 1572 | else 1573 | -- Use the seek method. 1574 | if instance.timeStretch >= 0 then 1575 | instance:seek(0, 'samples') 1576 | else 1577 | if instance.type == 'static' then 1578 | instance:seek(math.max(0, 1579 | instance.data:getSampleCount() - 1), 'samples') 1580 | else--if instance.type == 'stream' then 1581 | instance:seek(math.max(0, 1582 | instance.data:getDuration() * instance.samplingRate - 1), 'samples') 1583 | end 1584 | end 1585 | end 1586 | return true 1587 | end 1588 | 1589 | 1590 | 1591 | ---------------------------------------------------------------------------------------------------- 1592 | 1593 | -- Looping related 1594 | 1595 | function ASource.isLooping(instance) 1596 | -- Löve also just returns false for queue-type Sources as well. 1597 | return instance.looping 1598 | end 1599 | 1600 | function ASource.setLooping(instance, state) 1601 | if instance.type == 'queue' then 1602 | error("Can't set looping behaviour on queue-type Sources.") 1603 | end 1604 | 1605 | if type(state) ~= 'boolean' then 1606 | error(("1st parameter must be boolean; " .. 1607 | "got %s instead."):format(type(state))) 1608 | end 1609 | 1610 | instance.looping = state 1611 | 1612 | -- Setting loop state resets initial loop state. 1613 | instance.loopRegionEntered = false 1614 | end 1615 | 1616 | function ASource.getLoopPoints(instance) 1617 | -- Let's just return the default values even with queue-type Sources. 1618 | return instance.loopRegionA, instance.loopRegionB 1619 | end 1620 | 1621 | function ASource.setLoopPoints(instance, pointA, pointB) 1622 | if instance.type == 'queue' then 1623 | error("Can't set looping region on queue-type Sources.") 1624 | end 1625 | 1626 | if (pointA == nil) and (pointB == nil) then 1627 | error("At least one of the endpoints of the looping region must be given.") 1628 | end 1629 | 1630 | local limit 1631 | if instance.type == 'static' then 1632 | limit = instance.data:getSampleCount() 1633 | else--if instance.type == 'stream' then 1634 | limit = instance.data:getDuration() * instance.samplingRate 1635 | end 1636 | 1637 | if pointA ~= nil then 1638 | if (type(pointA) ~= 'number' or pointA < 0) then 1639 | error(("1st parameter must be a non-negative number; " .. 1640 | "got %s of type %s instead."):format(pointA, type(pointA))) 1641 | end 1642 | 1643 | if pointA >= limit then 1644 | error(("1st parameter must be less than the length of the data; " .. 1645 | "%f > %f."):format(pointA, limit)) 1646 | end 1647 | 1648 | instance.loopRegionA = pointA 1649 | end 1650 | if pointB ~= nil then 1651 | if (type(pointB) ~= 'number' or pointB < 0) then 1652 | error(("2nd parameter must be a non-negative number; " .. 1653 | "got %s of type %s instead."):format(pointB, type(pointB))) 1654 | end 1655 | 1656 | if pointB >= limit then 1657 | error(("2nd parameter must be less than the length of the data; " .. 1658 | "%f > %f."):format(pointB, limit)) 1659 | end 1660 | 1661 | instance.loopRegionB = pointB 1662 | end 1663 | 1664 | -- Setting loop state resets initial loop state. 1665 | instance.loopRegionEntered = false 1666 | end 1667 | 1668 | 1669 | 1670 | ---------------------------------------------------------------------------------------------------- 1671 | 1672 | -- Interpolation related 1673 | 1674 | function ASource.getInterpolationMethod(instance) 1675 | return ItplMethodList[instance.itplMethodIdx] 1676 | end 1677 | 1678 | function ASource.setInterpolationMethod(instance, method) 1679 | if not ItplMethodIMap[method] then 1680 | error(("1st parameter not a supported interpolation method; got %s.\n" .. 1681 | "Supported: `nearest`, `linear`, `cubic`, `sinc`."):format(tostring(method))) 1682 | end 1683 | instance.itplMethodIdx = ItplMethodIMap[method] 1684 | end 1685 | 1686 | 1687 | 1688 | ---------------------------------------------------------------------------------------------------- 1689 | 1690 | -- TSM related (TSM buffer mixing also uses interpolation) 1691 | 1692 | function ASource.getMixMethod(instance) 1693 | return MixMethodList[instance.mixMethodIdx] 1694 | end 1695 | 1696 | function ASource.setMixMethod(instance, method) 1697 | if not MixMethodIMap[method] then 1698 | error(("1st parameter not a supported mixing method; got %s.\n" .. 1699 | "Supported: `auto`, `linear`, `sqroot`, 'cosine'."):format(tostring(method))) 1700 | end 1701 | instance.mixMethodIdx = MixMethodIMap[method] 1702 | end 1703 | 1704 | function ASource.getResamplingRatio(instance) 1705 | return instance.resampleRatio 1706 | end 1707 | 1708 | function ASource.setResamplingRatio(instance, ratio) 1709 | if not ratio then 1710 | error("Missing 1st parameter, must be a number.") 1711 | end 1712 | if type(ratio) ~= 'number' then 1713 | error(("1st parameter must be a number; " .. 1714 | "got %s instead."):format(tostring(ratio))) 1715 | end 1716 | 1717 | instance.resampleRatio = ratio 1718 | 1719 | instance._recalcTSMCoefficients = true 1720 | end 1721 | 1722 | function ASource.getTimeStretch(instance) 1723 | return instance.timeStretch 1724 | end 1725 | 1726 | function ASource.setTimeStretch(instance, ratio) 1727 | if not ratio then 1728 | error("Missing 1st parameter, must be a number.") 1729 | end 1730 | if type(ratio) ~= 'number' then 1731 | error(("1st parameter must be a number; " .. 1732 | "got %s instead."):format(tostring(ratio))) 1733 | end 1734 | 1735 | instance.timeStretch = ratio 1736 | 1737 | instance._recalcTSMCoefficients = true 1738 | end 1739 | 1740 | function ASource.getPitchShift(instance, unit) 1741 | unit = unit or 'ratio' 1742 | 1743 | if not PitchUnit[unit] then 1744 | error(("1st parameter must be `ratio`, `semitones` or left empty; " .. 1745 | "got %s instead."):format(tostring(unit))) 1746 | end 1747 | 1748 | if unit == 'ratio' then 1749 | return instance.pitchShift 1750 | else--if unit == 'semitones' then 1751 | return instance.pitchShiftSt 1752 | end 1753 | end 1754 | 1755 | function ASource.setPitchShift(instance, amount, unit) 1756 | unit = unit or 'ratio' 1757 | 1758 | if not PitchUnit[unit] then 1759 | error(("2nd parameter must be `ratio`, `semitones` or left empty; " .. 1760 | "got %s instead."):format(tostring(unit))) 1761 | end 1762 | 1763 | if not amount then 1764 | error("Missing 1st parameter, must be a number.") 1765 | end 1766 | if type(amount) ~= 'number' then 1767 | error(("1st parameter must be a number; " .. 1768 | "got %s instead."):format(tostring(amount))) 1769 | end 1770 | 1771 | if unit == 'ratio' then 1772 | if amount <= 0 then 1773 | error(("1st parameter must be a positive number as a ratio; " .. 1774 | "got %f instead."):format(amount)) 1775 | end 1776 | instance.pitchShift = amount 1777 | instance.pitchShiftSt = (math.log(amount)/math.log(2))*12 1778 | else--if unit == 'semitones' then 1779 | instance.pitchShift = 2^(amount/12) 1780 | instance.pitchShiftSt = amount 1781 | end 1782 | 1783 | instance._recalcTSMCoefficients = true 1784 | end 1785 | 1786 | 1787 | 1788 | ---------------------------------------------------------------------------------------------------- 1789 | 1790 | -- Panning & stereo separation related 1791 | 1792 | function ASource.getPanLaw(instance) 1793 | -- Returning the custom function one might have defined is not supported. It's "custom". 1794 | if instance.panLaw == 2 then 1795 | return 'custom' 1796 | end 1797 | return PanLawList[instance.panLaw] 1798 | end 1799 | 1800 | function ASource.setPanLaw(instance, law) 1801 | if not law then 1802 | error("Missing 1st parameter, must be `gain`, `power` or a function " .. 1803 | "with one input and two output parameters: [0,1]->[0,1],[0,1].") 1804 | end 1805 | 1806 | if type(law) == 'string' then 1807 | if not PanLawIMap[law] then 1808 | error(("1st parameter as a string must be `gain` or `power`; " .. 1809 | "got %s instead."):format(law)) 1810 | end 1811 | 1812 | instance.panLaw = PanLawIMap[law] 1813 | instance.panLawFunc = PanLawFunc[PanLawIMap[law]] 1814 | 1815 | elseif type(law) == 'function' then 1816 | -- Minimal testing done on the given function. 1817 | for i,v in ipairs{0.00, 0.25, 0.33, 0.50, 0.67, 1.00} do 1818 | local ok, l,r = pcall(law, v) 1819 | if not ok then 1820 | error(("The given pan law function is errorenous: %s"):format(l)) 1821 | end 1822 | if type(l) ~= 'number' or type(r) ~= 'number' then 1823 | error(("The given pan law function must return two numbers; " .. 1824 | "got %s and %s instead."):format(type(l), type(r))) 1825 | end 1826 | if l < 0.0 or l > 1.0 or r < 0.0 or r > 1.0 then 1827 | error(("The given pan law function's return values must be in [0.0,1.0]; " .. 1828 | "got %f and %f instead."):format(l, r)) 1829 | end 1830 | 1831 | instance.panLaw = PanLawIMap['custom'] 1832 | instance.panLawFunc = law 1833 | end 1834 | 1835 | else 1836 | error(("1st parameter must be a string or a function; " .. 1837 | "got %s instead."):format(type(law))) 1838 | end 1839 | 1840 | calculatePanningCoefficients(instance) 1841 | end 1842 | 1843 | function ASource.getPanning(instance) 1844 | return instance.panning 1845 | end 1846 | 1847 | function ASource.setPanning(instance, pan) 1848 | if not pan then 1849 | error("Missing 1st parameter, must be a number between 0 and 1 inclusive.") 1850 | end 1851 | if type(pan) ~= 'number' then 1852 | error(("1st parameter must be a number between 0 and 1 inclusive; " .. 1853 | "got %s instead."):format(tostring(pan))) 1854 | end 1855 | if pan < 0 or pan > 1 then 1856 | error(("1st parameter must be a number between 0 and 1 inclusive; " .. 1857 | "got %f instead."):format(pan)) 1858 | end 1859 | 1860 | instance.panning = pan 1861 | 1862 | calculatePanningCoefficients(instance) 1863 | end 1864 | 1865 | function ASource.getStereoSeparation(instance) 1866 | return instance.separation 1867 | end 1868 | 1869 | function ASource.setStereoSeparation(instance, sep) 1870 | if not sep then 1871 | error("Missing 1st parameter, must be a number between -1 and 1 inclusive.") 1872 | end 1873 | if type(sep) ~= 'number' then 1874 | error(("1st parameter must be a number between -1 and 1 inclusive; " .. 1875 | "got %s instead."):format(tostring(sep))) 1876 | end 1877 | if sep < -1.0 or sep > 1.0 then 1878 | error(("1st parameter must be a number between -1 and 1 inclusive; " .. 1879 | "got %f instead."):format(sep)) 1880 | end 1881 | 1882 | instance.separation = sep 1883 | end 1884 | 1885 | 1886 | 1887 | ---------------------------------------------------------------------------------------------------- 1888 | 1889 | -- Methods that aren't modified; instead of overcomplicated metatable stuff, just have them here. 1890 | 1891 | function ASource.getFreeBufferCount(instance, ...) 1892 | return instance.source:getFreeBufferCount(...) 1893 | end 1894 | 1895 | function ASource.getEffect(instance, ...) 1896 | return instance.source:getEffect(...) 1897 | end 1898 | function ASource.setEffect(instance, ...) 1899 | return instance.source:setEffect(...) 1900 | end 1901 | function ASource.getFilter(instance, ...) 1902 | return instance.source:getFilter(...) 1903 | end 1904 | function ASource.setFilter(instance, ...) 1905 | return instance.source:setFilter(...) 1906 | end 1907 | function ASource.getActiveEffects(instance, ...) 1908 | return instance.source:getActiveEffects(...) 1909 | end 1910 | 1911 | function ASource.getAirAbsorption(instance, ...) 1912 | return instance.source:getAirAbsorption(...) 1913 | end 1914 | function ASource.setAirAbsorption(instance, ...) 1915 | return instance.source:setAirAbsorption(...) 1916 | end 1917 | function ASource.getAttenuationDistances(instance, ...) 1918 | return instance.source:getAttenuationDistances(...) 1919 | end 1920 | function ASource.setAttenuationDistances(instance, ...) 1921 | return instance.source:setAttenuationDistances(...) 1922 | end 1923 | function ASource.getCone(instance, ...) 1924 | return instance.source:getCone(...) 1925 | end 1926 | function ASource.setCone(instance, ...) 1927 | return instance.source:setCone(...) 1928 | end 1929 | function ASource.getDirection(instance, ...) 1930 | return instance.source:getDirection(...) 1931 | end 1932 | function ASource.setDirection(instance, ...) 1933 | return instance.source:setDirection(...) 1934 | end 1935 | function ASource.getPosition(instance, ...) 1936 | return instance.source:getPosition(...) 1937 | end 1938 | function ASource.setPosition(instance, ...) 1939 | return instance.source:setPosition(...) 1940 | end 1941 | function ASource.getRolloff(instance, ...) 1942 | return instance.source:getRolloff(...) 1943 | end 1944 | function ASource.setRolloff(instance, ...) 1945 | return instance.source:setRolloff(...) 1946 | end 1947 | function ASource.getVelocity(instance, ...) 1948 | return instance.source:getVelocity(...) 1949 | end 1950 | function ASource.setVelocity(instance, ...) 1951 | return instance.source:setVelocity(...) 1952 | end 1953 | function ASource.isRelative(instance, ...) 1954 | return instance.source:isRelative(...) 1955 | end 1956 | function ASource.setRelative(instance, ...) 1957 | return instance.source:setRelative(...) 1958 | end 1959 | function ASource.getVolumeLimits(instance, ...) 1960 | return instance.source:getVolumeLimits(...) 1961 | end 1962 | function ASource.setVolumeLimits(instance, ...) 1963 | return instance.source:setVolumeLimits(...) 1964 | end 1965 | 1966 | function ASource.getVolume(instance, ...) 1967 | return instance.source:getVolume(...) 1968 | end 1969 | function ASource.setVolume(instance, ...) 1970 | return instance.source:setVolume(...) 1971 | end 1972 | 1973 | 1974 | ---------------------------------------------------------------------------------------------------- 1975 | 1976 | -- Main thread loop 1977 | 1978 | while true do 1979 | -- Handle messages in inbound queue. Atomic operations in the other threads should guarantee 1980 | -- the ordering of inbound messages. 1981 | local msg = toProc:pop() 1982 | 1983 | if msg == 'procThread!' then 1984 | -- Initialization of this thread successful, next value is the reference to it. 1985 | procThread = toProc:pop() 1986 | 1987 | elseif msg == 'procThread?' then 1988 | -- This thread already initialized, send back its reference; 1989 | -- Next value in queue is the channel to the querying thread. 1990 | local ch = toProc:pop() 1991 | ch:push(procThread) 1992 | 1993 | elseif msg == 'pauseall' then 1994 | -- Support love.audio.pause call variant, with return values. 1995 | local ch = toProc:pop() 1996 | local idList = {} 1997 | for i=1, #ASourceList do 1998 | if ASourceList[i]:isPlaying() then 1999 | ASourceList[i]:pause() 2000 | table.insert(idList, ASourceList[i].id) 2001 | end 2002 | end 2003 | ch:performAtomic(function(ch) 2004 | ch:push(#idList) 2005 | if #idList > 0 then 2006 | for i=1, #idList do 2007 | ch:push(idList[i]) 2008 | end 2009 | end 2010 | end) 2011 | 2012 | elseif msg == 'stopall' then 2013 | -- Support love.audio.stop call variant. 2014 | local ch = toProc:pop() 2015 | for i=1, #ASourceList do ASourceList[i]:stop() end 2016 | ch:push(true) 2017 | 2018 | elseif msg == 'new' then 2019 | -- Construct a new ASource using parameters popped from the inbound queue, 2020 | -- then send back the instance's id so a proxy instance can be constructed there. 2021 | local ch, a, b, c, d, e, id 2022 | ch = toProc:pop() 2023 | a = toProc:pop() 2024 | b = toProc:pop() 2025 | c = toProc:pop() 2026 | d = toProc:pop() 2027 | e = toProc:pop() 2028 | 2029 | id = new(a, b, c, d, e) 2030 | 2031 | ch:push(id) 2032 | 2033 | elseif ASource[msg] then 2034 | -- Redefined methods and ones we "reverse-inherit" from the internally used QSource. 2035 | -- (above), , , , [, ..., ] 2036 | local ch = toProc:pop() 2037 | local id = toProc:pop() 2038 | local paramCount = toProc:pop() 2039 | local parameters = {} 2040 | if paramCount > 0 then 2041 | for i=1, paramCount do 2042 | parameters[i] = toProc:pop() 2043 | end 2044 | end 2045 | 2046 | -- setPanLaw is the only method that potentially supports as parameter a function, 2047 | -- we need to parse that here so it's not a dumped string anymore. 2048 | if msg == 'setPanLaw' and not PanLawIMap[parameters[1]] then 2049 | local success, _ = loadstring(parameters[1]) 2050 | if success then parameters[1] = success end 2051 | end 2052 | 2053 | -- Get instance based on id. 2054 | local instance = ASourceList[ASourceIMap[id]] 2055 | 2056 | -- Execute. 2057 | local result = {instance[msg](instance, unpack(parameters))} 2058 | 2059 | -- Return results to the querying thread. 2060 | -- , , [, ..., ] 2061 | ch:performAtomic(function(ch) 2062 | ch:push(#result) 2063 | if #result > 0 then 2064 | for i=1, #result do 2065 | ch:push(result[i]) 2066 | end 2067 | end 2068 | end) 2069 | end 2070 | 2071 | -- Update active instances. 2072 | for i = 1, #ASourceList do 2073 | local instance = ASourceList[i] 2074 | 2075 | -- If instance is not in the playing state, then skip queueing more data, since we don't 2076 | -- want to have silent sources occupying any active source slots. 2077 | if instance.playing then 2078 | 2079 | -- If there is at least one empty internal buffer, do work. 2080 | -- Cheap bugfix for now, refactoring will deal with this later. 2081 | if instance.source:getFreeBufferCount() > 0 then 2082 | 2083 | -- Process data. 2084 | Process[instance.type](instance) 2085 | end 2086 | end 2087 | end 2088 | 2089 | -- Don't hog a core. 2090 | love.timer.sleep(0.001) 2091 | end -------------------------------------------------------------------------------- /asl.lua: -------------------------------------------------------------------------------- 1 | -- Advanced Source Library 2 | -- The /other/ A/S/L. :3 3 | -- by zorg § ISC @ 2018-2023 4 | 5 | 6 | 7 | --[[ 8 | Internal Variables 9 | - Proxy Class: 10 | None, even love.audio.pause just queries the processing thread for a list of id-s it paused. 11 | - Proxy Instances: 12 | - id -> Uniquely identifies sources, even across threads. 13 | - callStack -> A table containing the method calls in LIFO order; needed due to how mt-s work. 14 | --]] 15 | 16 | 17 | 18 | -- Safeguards. 19 | do 20 | local M,m,r = love.getVersion() 21 | assert((M == 11 and m >= 3) or M >= 12, 22 | "This library needs at least LÖVE 11.3 to function.") 23 | assert(love.audio and love.sound, 24 | "This library needs both love.audio and love.sound enabled to function.") 25 | end 26 | 27 | 28 | 29 | -- Relative require the thread code. 30 | local path = ... 31 | do 32 | if not path:find('%.') then 33 | path = '' 34 | else 35 | path = path:gsub('%.[^%.]+$', '') 36 | end 37 | end 38 | 39 | 40 | 41 | -- The processing thread. 42 | local procThread 43 | 44 | -- Channel to receive data through. 45 | local toHere = love.thread.newChannel() 46 | 47 | -- Use a named channel to test for the existence of the processing thread; if it exists, use that. 48 | local toProc = love.thread.getChannel('zorg.asl.procThread') 49 | do 50 | toProc:performAtomic(function(ch) 51 | ch:push('procThread?') 52 | ch:push(toHere) 53 | end) 54 | 55 | -- Timeout if no thread responds. 56 | procThread = toHere:demand(0.1) 57 | 58 | if not procThread then 59 | -- clear toProc queue since query events never got processed. 60 | toProc:clear() 61 | 62 | procThread = love.thread.newThread(path:gsub('%.','%/') .. '/asl-thread.lua') 63 | procThread:start(toProc) 64 | toProc:performAtomic(function(ch) 65 | ch:push('procThread!') 66 | ch:push(procThread) 67 | end) 68 | end 69 | end 70 | 71 | 72 | 73 | -- List of methods supported by an ASource; no need to store parameter and retval counts. 74 | local method = {} 75 | do 76 | method.clone = true 77 | 78 | method.type = true 79 | method.typeOf = true 80 | method.release = true 81 | 82 | method.getInternalSource = true -- Not really for usage outside of love.audio monkeypatching... 83 | 84 | method.getPitch = true -- Replaced with getResamplingRatio; still allowed for custom error msg. 85 | method.setPitch = true -- Replaced with setResamplingRatio; still allowed for custom error msg. 86 | 87 | method.queue = true 88 | 89 | method.getType = true 90 | method.getSampleRate = true 91 | method.getBitDepth = true 92 | method.getChannelCount = true 93 | 94 | method.getBufferSize = true 95 | method.setBufferSize = true 96 | 97 | method.getFrameSize = true 98 | method.setFrameSize = true 99 | method.getFrameVariance = true 100 | method.setFrameVariance = true 101 | method.getFrameVarianceDistribution = true 102 | method.setFrameVarianceDistribution = true 103 | 104 | method.isPlaying = true 105 | method.play = true 106 | --method.isPaused = false 107 | method.pause = true 108 | --method.isStopped = false 109 | method.stop = true 110 | 111 | method.tell = true 112 | method.seek = true 113 | method.getDuration = true 114 | method.rewind = true 115 | 116 | method.isLooping = true 117 | method.setLooping = true 118 | method.getLoopPoints = true 119 | method.setLoopPoints = true 120 | 121 | method.getInterpolationMethod = true 122 | method.setInterpolationMethod = true 123 | method.getMixMethod = true 124 | method.setMixMethod = true 125 | 126 | method.getResamplingRatio = true 127 | method.setResamplingRatio = true 128 | method.getTimeStretch = true 129 | method.setTimeStretch = true 130 | method.getPitchShift = true 131 | method.setPitchShift = true 132 | 133 | method.getPanLaw = true 134 | method.setPanLaw = true 135 | method.getPanning = true 136 | method.setPanning = true 137 | method.getStereoSeparation = true 138 | method.setStereoSeparation = true 139 | 140 | -- These are the methods löve/openalsoft itself gives that aren't reimplemented by the library. 141 | method.getFreeBufferCount = true 142 | 143 | method.getEffect = true 144 | method.setEffect = true 145 | method.getFilter = true 146 | method.setFilter = true 147 | method.getActiveEffects = true 148 | 149 | method.getAirAbsorption = true 150 | method.setAirAbsorption = true 151 | method.getAttenuationDistances = true 152 | method.setAttenuationDistances = true 153 | method.getCone = true 154 | method.setCone = true 155 | method.getDirection = true 156 | method.setDirection = true 157 | method.getPosition = true 158 | method.setPosition = true 159 | method.getRolloff = true 160 | method.setRolloff = true 161 | method.getVelocity = true 162 | method.setVelocity = true 163 | method.isRelative = true 164 | method.setRelative = true 165 | method.getVolumeLimits = true 166 | method.setVolumeLimits = true 167 | 168 | method.getVolume = true 169 | method.setVolume = true 170 | end 171 | 172 | 173 | 174 | -- Declare mt as a local before the transfer function. Both reference each other, so 175 | local mt 176 | 177 | 178 | 179 | -- The function handling passing queries to, and receiving data from the processing thread. 180 | local transfer = function(instance, ...) 181 | local arg = {...} 182 | 183 | -- We'll need this later for the clone method. 184 | local methodName = instance.callStack[1] 185 | 186 | -- Do thread stuff: 187 | toProc:performAtomic(function(ch) 188 | -- The method name... hackish; see below in the mt metatable; 189 | ch:push(methodName) 190 | -- Send retvals (if any) back into this thread; 191 | ch:push(toHere) 192 | -- Which ASource object we're referring to; 193 | ch:push(instance.id) 194 | -- Push number of parameters given to method; 195 | ch:push(#arg) 196 | -- Push parameters, if they exist. 197 | if #arg>0 then 198 | for i=1, #arg do 199 | if methodName == 'setPanLaw' and type(arg[i]) == 'function' then 200 | -- Can't send functions to other threads through channels directly. 201 | ch:push(string.dump(arg[i])) 202 | else 203 | ch:push(arg[i]) 204 | end 205 | end 206 | end 207 | end) 208 | 209 | -- We're using a stack; again, see the hack in the metatable definition below. 210 | table.remove(instance.callStack, 1) 211 | 212 | -- Return values returned, in order, by the proc. thread; combined with error handling, 213 | -- because threads are asnync & demanding would freeze the current thread. 214 | local retvalCount 215 | local retval = {} 216 | 217 | -- The moment this is done, we have all needed parameters, since it's atomic on the other side. 218 | while not retvalCount do 219 | retvalCount = toHere:pop() 220 | local threadError = procThread:getError() 221 | if threadError then error(threadError) end 222 | end 223 | 224 | for i=1, retvalCount do 225 | table.insert(retval, toHere:pop()) 226 | end 227 | 228 | -- Cloning a source is neither a getter or a setter; need to handle it here separately. 229 | if methodName == 'clone' then 230 | local asource = {} 231 | asource.callStack = {} 232 | setmetatable(asource, mt) 233 | asource.id = retval[1] 234 | return asource 235 | else 236 | -- Allow chaining calls as long as they're not getters. 237 | if retvalCount == 0 then return instance end 238 | return unpack(retval) 239 | end 240 | end 241 | 242 | 243 | 244 | -- Route all method calls to the transfer function. 245 | mt = {__index = function(instance, m) 246 | if method[m] then 247 | -- Hack: Add the current method's name to the proxy instance so we can refer to that in 248 | -- the transfer function... also due to how metamethod indexing works, we actually 249 | -- need a stack for this to work correctly. 250 | table.insert(instance.callStack, 1, m) 251 | return transfer 252 | end 253 | end} 254 | 255 | 256 | 257 | -- Returns a proxy object that utilizes metamethods to transfer calls to methods over to 258 | -- the processing thread. 259 | local new = function(a,b,c,d,e) 260 | -- Send construction request to processing thread. 261 | toProc:performAtomic(function(ch) 262 | ch:push('new') 263 | ch:push(toHere) 264 | ch:push(a) 265 | ch:push(b) 266 | ch:push(c) 267 | ch:push(d) 268 | ch:push(e) 269 | end) 270 | 271 | -- Create proxy instance. 272 | local asource = {} 273 | 274 | -- Add internal method name stack, because metatable calls are apparently instantaneous with 275 | -- chained functions; `AS:set(AS:get())` runs in the order [set, get], so it calls get twice. 276 | asource.callStack = {} 277 | 278 | -- Make this work more or less like a regular source. 279 | setmetatable(asource, mt) 280 | 281 | -- Save the returned object id since we'll be using that. 282 | while not asource.id do 283 | asource.id = toHere:pop() 284 | local threadError = procThread:getError() 285 | if threadError then 286 | error(threadError) 287 | end 288 | end 289 | 290 | -------------- 291 | return asource 292 | end 293 | 294 | 295 | 296 | -- Monkeypatch love.audio.play/pause/stop in the thread we loaded the library in so it can 297 | -- correctly handle ASources as well. (They are the only functions that need to be messed with...) 298 | local oldLAPlay, oldLAPause, oldLAStop = love.audio.play, love.audio.pause, love.audio.stop 299 | 300 | function love.audio.play(...) 301 | local temp = select(1, ...) 302 | if type(temp) == 'table' and temp.typeOf then temp = {...} end 303 | for i,v in ipairs(temp) do 304 | if v:typeOf('ASource') then 305 | v:play() 306 | temp[i] = v:getInternalSource() 307 | temp[i]:stop() -- Sync test... doesn't seem to work. 308 | v.playbackOffset = 0.0 -- Sync test #2 309 | end 310 | end 311 | return oldLAPlay(temp) -- Returns true if all Sources succeeded in being started/resumed. 312 | end 313 | 314 | function love.audio.pause(...) 315 | if select('#', ...) == 0 then 316 | -- Pause all of the ASources. 317 | toProc:performAtomic(function(ch) 318 | ch:push('pauseall') 319 | ch:push(toHere) 320 | end) 321 | -- Get response. 322 | local pausedCount 323 | while not pausedCount do 324 | pausedCount = toHere:pop() 325 | local threadError = procThread:getError() 326 | if threadError then error(threadError) end 327 | end 328 | -- Iterate over id-s for sources we paused with the above call. 329 | local temp = {} 330 | for i=1, pausedCount do 331 | local id = toHere:pop() 332 | local asource = {} 333 | asource.callStack = {} 334 | setmetatable(asource, mt) 335 | asource.id = id 336 | table.insert(temp, asource) 337 | end 338 | -- Call original function, remove returned sources that are part of ASource objects. 339 | local temp2 = oldLAPause() 340 | for i,v in ipairs(temp) do 341 | for j,w in ipairs(temp2) do 342 | if v:getInternalSource() == w then 343 | temp2[j] = false 344 | end 345 | end 346 | end 347 | for i=#temp2, 1, -1 do 348 | if temp2[i] == false then table.remove(temp2, i) end 349 | end 350 | -- Return combined table. 351 | for i,v in ipairs(temp2) do table.insert(temp, v) end 352 | return temp 353 | else 354 | -- Only pause select ASources. 355 | local temp = select(1, ...) 356 | if type(temp) == 'table' and temp.typeOf then temp = {...} end 357 | for i,v in ipairs(temp) do 358 | if v:typeOf('ASource') then 359 | v:pause() 360 | temp[i] = v:getInternalSource() 361 | end 362 | end 363 | return oldLAPause(temp) 364 | end 365 | end 366 | 367 | function love.audio.stop(...) 368 | if select('#', ...) == 0 then 369 | -- Stop all of the ASources. 370 | toProc:performAtomic(function(ch) 371 | ch:push('stopall') 372 | ch:push(toHere) 373 | end) 374 | -- Get response. 375 | local done 376 | while not done do 377 | done = toHere:pop() 378 | local threadError = procThread:getError() 379 | if threadError then error(threadError) end 380 | end 381 | return oldLAStop() 382 | else 383 | -- Only stop select ASources. 384 | local temp = select(1, ...) 385 | if type(temp) == 'table' and temp.typeOf then temp = {...} end 386 | for i,v in ipairs(temp) do 387 | if v:typeOf('ASource') then 388 | v:stop() 389 | temp[i] = v:getInternalSource() 390 | end 391 | end 392 | return oldLAStop(temp) 393 | end 394 | end 395 | 396 | 397 | 398 | -- Successfully loaded library. 399 | return new -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Advanced Source Library 2 | ---------------------------------------------------------- 3 | 4 | ### Info 5 | 6 | ASL is a threaded wrapper on Löve (Audio) Source objects; it adds extra functionality to them. 7 | 8 | Currently supported Löve version(s): 11.3, 11.4 (current) 9 | 10 | ### Usage 11 | 12 | - `local new = require 'asl'` which returns the constructor that makes ASource objects. 13 | 14 | For those that want to, they can always add the function themselves into löve's own table structure: 15 | - `love.audio.newAdvancedSource = require 'asl'` 16 | 17 | ### Considerations 18 | 19 | - Since this library does monkeypatch a few things internally, please don't bug the löve2D developers with issues if you're using this library, before doing some due diligence making sure the error doesn't stem from this library (in which case, do create an issue here for me to solve!) 20 | 21 | ### Additions to Source functionality 22 | 23 | - Threaded implementation, can be used from multiple threads as well, it will ever only use one internal processing thread. 24 | - QueueableSource and Buffer-based timing for accurate playback and position tracking. 25 | - Implements custom loop points. 26 | - Implements panning and stereo separation support. 27 | - Implements time-domain time-stretching and pitch-shifting methods. 28 | - Implements dynamic buffer resizing based on uniform noise to mitigate specific harmonic distortion noise, for convenience. 29 | - Implements reverse playback. 30 | - Implements different interpolation methods during modified playback (resampled, stretched, and/or shifted) 31 | - Implements the ability to use stereo sound files as mono, for simpler 3D spatialization support. 32 | - All methods that don't return any values are chainable. 33 | 34 | ### API Changes 35 | 36 | #### Deprecations 37 | - `Source:getPitch` is now `Source:getResamplingRatio`. 38 | - `Source:setPitch` is now `Source:setResamplingRatio`. 39 | 40 | #### Additions 41 | - `Source:rewind` re-added, which is syntax sugar for `Source:seek(0)` or `Source:seek(Source:getSampleCount()-1`, depending on playback direction. 42 | 43 | - `Source:getLoopPoints` added, returns `startpoint` and `endpoint`, in samplepoints. 44 | - `Source:setLoopPoints` added, with parameters `startpoint` and `endpoint`; in samplepoints. 45 | 46 | - `Source:getBitDepth` and `Source:getSampleRate` added. 47 | 48 | - `Source:getBufferSize` added, with parameter `unit`; in either `samples`(samplepoints) or `milliseconds`, the latter being default. 49 | - `Source:setBufferSize` added, with parameters `amount` and `unit`; in either `samples`(samplepoints) or `milliseconds`, the latter being default; buffer sizes can be between 1 milliseconds and 10 seconds long; default is ~50 ms equivalent, rounded down. 50 | 51 | - `Source:getPitchShift` added, with parameter `unit`; in either as a non-negative `ratio`, or in `semitones`. 52 | - `Source:setPitchShift` added, with parameters `amount` and `unit`; in either as a non-negative `ratio`, or in `semitones`; modifies pitch only. 53 | 54 | - `Source:getResamplingRatio` added. 55 | - `Source:setResamplingRatio` added, with parameter `ratio` as a ratio; modifies both playback speed and pitch. 56 | 57 | - `Source:getTimeStretch` added. 58 | - `Source:setTimeStretch` added, with parameter `ratio` as a ratio; modifies playback speed only. 59 | 60 | - `Source:getInterpolationMethod` added. 61 | - `Source:setInterpolationMethod` added, with parameter `method`; which can be one of the following strings: `nearest, linear, cubic, sinc`. 62 | 63 | - `Source:getPanning` added. 64 | - `Source:setPanning` added, with parameter `pan`; 0.0 is full left, 1.0 is full right, 0.5 is centered; the curve is defined by the specific panning law selected. Default is `0.5`. 65 | 66 | - `Source:getPanLaw` added. 67 | - `Source:setPanLaw` added, with parameter `law`; which can be one of the following strings: `gain`, `power` or a custom function taking a number within the range [0,1] as input, and returning two numbers in the domain [0,1] that scale the left and right channels respectively. Default is `gain`. 68 | The two laws are constant-gain/amplitude and constant-power/loudness laws, the first attentuates the volume at the center by -6dB (50%), the second only by -3dB (1/sqrt(2)). 69 | 70 | - `Source:getStereoSeparation` added. 71 | - `Source:setStereoSeparation` added, with parameter `amount`; -1.0 means mid channel output only, 0.0 means original, 1.0 means side channel output only. Default is `0.0`. 72 | 73 | - `Source:getFrameSize` added, with parameter `unit`; in either `samples`(samplepoints) or `milliseconds`, the latter being default. 74 | - `Source:setFrameSize` added, with parameters `amount` and `unit`; in either `samples`(samplepoints) or `milliseconds`, the latter being default; buffer sizes can be between 1 milliseconds and 10 seconds long; default is ~35 milliseconds. 75 | 76 | - `Source:getFrameVariance` added, with parameter `unit`, in either `samples`(samplepoints), `milliseconds`, or as a `percentage`; milliseconds being default. 77 | - `Source:setFrameVariance` added, with parameters `amount` and `unit`; in either `samples`(samplepoints), `milliseconds`, or as a `percentage`; milliseconds being default. Setting variance above 0 will vary the TSM frame size within the signed limits given, as long as they're within 1 millisecond and 10 seconds inclusive. 78 | 79 | - `Source:getFrameVarianceDistribution` added. 80 | - `Source:setFrameVarianceDistribution` added, with parameters `uniform` and `normal`, with the latter being default. 81 | 82 | - `Source:getMixMethod` added. 83 | - `Source:setMixMethod` added, with parameters `auto`, `linear`, `sqroot`, `cosine` and `noise`, with the first being default. It's best to leave this alone for most use-cases. 84 | 85 | #### Modifications 86 | - `Source:queue` may now be defined as a "pull-style" callback; if it isn't, it will work as the vanilla "push-style" method. (Note: queue type source support not yet implemented.) 87 | 88 | - `Object:release` modified to release all extra internals of the new Objects; **must be called explicitly if one doesn't want dead objects cluttering up the processing thread.** 89 | - `Object:type` modified to return the string `ASource`. 90 | - `Object:typeOf` modified to also return true for `ASource` as a specialization of the `Source` type. 91 | 92 | - The constructor function has different parameter ordering, and combines both `love.audio.newSource` for the first 5, and `love.audio.newQueueableSource` for the last variant: 93 | - string path, SourceType, buffercount, aurality 94 | - File, SourceType, buffercount, aurality 95 | - FileData, SourceType, buffercount, aurality 96 | - Decoder, SourceType, buffercount, aurality 97 | - SoundData, buffercount, aurality 98 | - samplerate, bitdepth, channelCount, buffercount, aurality 99 | 100 | where buffercount and aurality are optional parameters. 101 | 102 | The buffercount parameter sets how many OpenAL-side buffers get made for the internal queueable source; less means less delay, but playback may underrun (it'll start to crackle). 103 | 104 | The aurality parameter forces the internal QSource and buffers to be either mono or stereo, regardless of the channel count of the input itself; this means that ostensibly stereo data can also be used with 3D spatialization. 105 | 106 | ### Version History 107 | 108 | #### V1.00 (2018.12.09) 109 | 110 | - Still available in the `non-threaded` branch. 111 | 112 | #### V2.0 (2019.04.06) 113 | 114 | - Refactored lib to be threaded and thread-safe. 115 | 116 | #### V2.1 (2021.07.03) 117 | 118 | - Added Stereo Panning implementation (direct method, not simulated by OpenAL's spatialization APIs.) 119 | 120 | #### V2.2 (2021.07.07) 121 | 122 | - Added Stereo Separation implementation. 123 | 124 | #### V2.2.1 (2021.08.15) 125 | 126 | - Some fixes regarding range checking and default values, one missing function added (getActiveEffects). 127 | 128 | #### V3.0 (2021.12.11) 129 | 130 | - Complete reimplementation of time-scale modifications to achieve pop-less functionality; it still uses time-domain methods (time-scale modification and resampling) so depending on the settings, it might still sound weird. 131 | - Changed the behaviour of buffers; they are not recreated on size change anymore; the maximum sized ones will be created, and those can be limited to a smaller range instead. 132 | - Changed default buffer size to be equivalent to 50 ms. 133 | 134 | - Added interpolation methods for higher quality resampling, including cubic hermite spline and 32-tap lanczos sinc implementations. 135 | - Added milliseconds unit to the setter and getter of the buffer size. 136 | 137 | #### V4.0 (2022.02.15) 138 | 139 | -- Refactored library code. 140 | 141 | - Added (via monkeypatching) support for love.audio.play/pause/stop to also handle ASources correctly with all variants supported. 142 | - Added methods to have an ASource dynamically change its effective buffer size by a given amount in a specificed +/- range based on an uniform distribution to get rid of specific tonal "blend-modulation" noise that happens at small buffer sizes combined with slowed playback. 143 | - Added dates to version history in this document. 144 | 145 | - Changed the constructor a fair bit, see the API changes section above. 146 | - Now possible to create either mono or stereo sources regardless of the input data channel count (meaning stereo data can be used with OALS' spatialization functionality, although this does come with internally mixing the two channels into one.) 147 | - Changed stereo separation limit from 100% to 200%; the latter half of the range will gradually remove the mid-channel. 148 | - Changed setLoopPoints method to accept partial parameters if one only wants to modify one of the values. 149 | - Changed loop handling to allow disjunct loop regions that wrap around the end/beginning of the data. 150 | - Also made sure initial playback and seeking to arbitrary places does not lock playback into the loop region; if such functionality is needed, seek into the loop region. 151 | - Note: Disjunt loop regions are currently buggy, this is a known issue and will be fixed. 152 | 153 | - Removed library adding itself to the love.audio table... it really shouldn't do that by itself unprompted. 154 | 155 | - Fixed clone method not quite working as expected. 156 | - Fixed bug with loop points not being correctly handled depending on buffer size and/or TSM parameters. 157 | - Fixed bug regarding the handling of individual proxy instances sometimes pointing to the wrong ones. 158 | 159 | - The library currently only supports creating "static" source types, and will error otherwise. 160 | 161 | #### V4.1 (2022.02.26) 162 | 163 | - Fixed constructor bugs. 164 | - Fixed bugs relating to methods that the library doesn't override. 165 | - Fixed sinc interpolator ringing bug. 166 | 167 | #### V4.2 (2022.04.20) 168 | 169 | - Added getMixMethod/setMixMethod to select how TSM frames are mixed together, either through linear or cosine interpolation (to preserve power); default setting is automatic method selection whether TSM is active, or if just resampling, even at +/- 100%. 170 | 171 | - Fixed disjunct loop region behaviour. 172 | 173 | - Made sure loop regions are not forcefully "jumped into" while the playhead is outside said regions; looping is only really happening once we enter the region proper, during playback; "jumping out" of the region is also possible, resetting the looping behaviour like if the playhead was never in the region in the first place. 174 | 175 | #### V4.3 (2022.09.17) 176 | 177 | - Fixed monkeypatched love.audio.play/pause/stop not working on ASources. (love.audio.play does not sync up multiple ASources regarding playback though.) 178 | 179 | - Renamed "cosine" mixing method to square root, and added an actual cosine-based one, along with a white-noise based one for no real reason other than it being interesting. 180 | 181 | #### V4.4 (2023.02.19) 182 | 183 | - Made TSM frame size be independent from the chosen buffer size, meaning that frame size changes won't change how much input time delay there is. 184 | 185 | - Split setBufferSize and getBufferSize into two; the separate functions added are setFrameSize and getFrameSize. 186 | 187 | - Renamed setBufferVariance and getBufferVariance to setFrameVariance and getFrameVariance. 188 | 189 | - Added setFrameVarianceDistribution and getFrameVarianceDistribution methods to change how the buffer's length gets varied each time one is filled; can be either uniform or normal distribution (with a constant deviation). 190 | 191 | - Changed panning law internals a bit, including bugfixes; now also stored as an enumeration, like everything else. (The custom one is still stored as a per-instance function as well, if defined.) 192 | 193 | - Removed noise-based mixing method, it was not useful. 194 | 195 | #### V4.5 (2023.07.08) - CURRENT 196 | 197 | - Fixed cloning not setting Source-specific internal state. (Temporary; permanent fix will come when i figure out why calling source:clone can fail.) 198 | 199 | #### V?.? () - TODO 200 | 201 | - Move out loading code from processing thread so it never stalls... 202 | 203 | - Add callback functionality to handle processed buffers (one use-case being visualizers, for example) 204 | 205 | - Have the number of OpenAL-Soft internal buffers not change processing delay. (Note: More testing needed.) 206 | 207 | - Fix error behaviour (show handler screen instead of crashing) 208 | 209 | - Do automatic refcount across threads... possible? 210 | 211 | - Add missing versions to :queue. (rest of the parameters, that is) 212 | - Add all advanced functionality to `stream` type ASources. 213 | - Add all advanced functionality to `queue` type ASources. Probably using threading to call worker functions... 214 | - The queue method also supports all variants that löve supports, except that the lightuserdata variant does 215 | not need the format parameters, since it'll use the ASource's given format anyway. 216 | 217 | - Implement a way to have each ASource be instanceable internally (==> multiple voices). 218 | 219 | #### Remarks: 220 | 221 | - Tagging support: This library should work out-of-the-box with Tesselode's Ripple library, which implements such features, if needed... unfortunately issues have been reported, so this might not be hassle-free... TODO look into solutions. 222 | 223 | ### License 224 | This library licensed under the ISC License. 225 | --------------------------------------------------------------------------------