├── .gitignore ├── test.lua └── wav.lua /.gitignore: -------------------------------------------------------------------------------- 1 | bad_apple.wav 2 | out.wav 3 | audio.ass -------------------------------------------------------------------------------- /test.lua: -------------------------------------------------------------------------------- 1 | dofile("wav.lua") 2 | 3 | -- Write audio file 4 | local samples, freq = {n = 0}, math.pi * 2 * 500 5 | for i = 0, 44100*3 do 6 | for c = 1, 2 do 7 | samples.n = samples.n + 1 8 | samples[samples.n] = math.sin(i % 44100 / 44099 * freq) * 32767 9 | end 10 | end 11 | local writer = wav.create_context("out.wav", "w") 12 | writer.init(2, 44100, 16) 13 | writer.write_samples_interlaced(samples) 14 | writer.finish() 15 | 16 | -- Read audio file 17 | local reader = wav.create_context("out.wav", "r") 18 | print("Filename: " .. reader.get_filename()) 19 | print("Mode: " .. reader.get_mode()) 20 | print("File size: " .. reader.get_file_size()) 21 | print("Channels: " .. reader.get_channels_number()) 22 | print("Sample rate: " .. reader.get_sample_rate()) 23 | print("Byte rate: " .. reader.get_byte_rate()) 24 | print("Block align: " .. reader.get_block_align()) 25 | print("Bitdepth: " .. reader.get_bits_per_sample()) 26 | print("Samples per channel: " .. reader.get_samples_per_channel()) 27 | print("Sample at 500ms: " .. reader.get_sample_from_ms(500)) 28 | print("Milliseconds from 3rd sample: " .. reader.get_ms_from_sample(3)) 29 | print(string.format("Min- & maximal amplitude: %d <-> %d", reader.get_min_max_amplitude())) 30 | reader.set_position(256) 31 | print("Sample 256, channel 2: " .. reader.get_samples(1)[2][1]) 32 | 33 | -- Get first frequencies 34 | reader.set_position(0) 35 | local samples = reader.get_samples(1024)[1] 36 | for i=1, samples.n do 37 | samples[i] = samples[i] / 32768 38 | end 39 | local analyzer = wav.create_frequency_analyzer(samples, reader.get_sample_rate()) 40 | print("\nFrequency weight 400-600Hz: " .. analyzer.get_frequency_range_weight(400,600)) 41 | print("FREQUENCY: WEIGHT") 42 | for _, frequency in ipairs(analyzer.get_frequencies()) do 43 | print(string.format("%.2f: %f", frequency.freq, frequency.weight)) 44 | end 45 | 46 | -- Audio samples to ASS 47 | local filename, ms_to_play = "out.wav", 1000 48 | local file = io.open("audio.ass", "w") 49 | file:write(string.format([[[Script Info] 50 | Title: Audio to ASS 51 | ScriptType: v4.00+ 52 | WrapStyle: 0 53 | ScaledBorderAndShadow: yes 54 | PlayResX: 1280 55 | PlayResY: 720 56 | Video file: ?dummy:25.000000:2250:1280:720:0:0:0: 57 | Audio file: %s 58 | Audio URI: %s 59 | 60 | [V4+ Styles] 61 | Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding 62 | Style: Default,Arial,30,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,7,0,0,0,1 63 | 64 | [Events] 65 | Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text]], filename, filename)) 66 | local function ass_timestamp(t) 67 | local ms = t % 1000 68 | t = math.floor(t / 1000) 69 | local s = t % 60 70 | t = math.floor(t / 60) 71 | local m = t % 60 72 | t = math.floor(t / 60) 73 | local h = t 74 | return string.format("%02d:%02d:%02d.%02d", h, m, s, ms / 10) 75 | end 76 | reader = wav.create_context(filename, "r") 77 | local chunk_size = reader.get_sample_from_ms(40) 78 | for ms = 0, ms_to_play-1, 40 do 79 | reader.set_position(math.floor(reader.get_sample_from_ms(ms))) 80 | file:write(string.format("\nDialogue: 0,%s,%s,Default,,0,0,0,,{\\pos(10,300)\\c&H4040FF&\\p1}%s", ass_timestamp(ms), ass_timestamp(ms+40), audio_to_ass(reader.get_samples(chunk_size)[1], 1000, 1/162, 6))) 81 | end -------------------------------------------------------------------------------- /wav.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Library for simple audio reading, writing and analysing. 3 | 4 | Copyright © 2014, Christoph "Youka" Spanknebel 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | ]] 9 | wav = { 10 | --[[ 11 | Reads or writes audio file in WAVE format with PCM integer samples. 12 | 13 | Function 'create_context' requires 2 arguments: a filename and a mode, which can be "r" (read) or "w" (write). 14 | A call returns one table with methods depending on the used mode. 15 | On reading, following methods are callable: 16 | - get_filename() 17 | - get_mode() 18 | - get_file_size() 19 | - get_channels_number() 20 | - get_sample_rate() 21 | - get_byte_rate() 22 | - get_block_align() 23 | - get_bits_per_sample() 24 | - get_samples_per_channel() 25 | - get_sample_from_ms(ms) 26 | - get_ms_from_sample(sample) 27 | - get_min_max_amplitude() 28 | - get_position() 29 | - set_position(pos) 30 | - get_samples_interlaced(n) 31 | - get_samples(n) 32 | On writing, following methods are callable: 33 | - get_filename() 34 | - get_mode() 35 | - init(channels_number, sample_rate, bits_per_sample) 36 | - write_samples_interlaced(samples) 37 | - finish() 38 | 39 | (WAVE format: https://ccrma.stanford.edu/courses/422/projects/WaveFormat/) 40 | ]] 41 | create_context = function(filename, mode) 42 | -- Check function parameters 43 | if type(filename) ~= "string" or not (mode == "r" or mode == "w") then 44 | error("invalid function parameters, expected filename and mode \"r\" or \"w\"", 2) 45 | end 46 | -- Audio file handle 47 | local file = io.open(filename, mode == "r" and "rb" or "wb") 48 | if not file then 49 | error(string.format("couldn't open file %q", filename), 2) 50 | end 51 | -- Byte-string(unsigend integer,little endian)<->Lua-number converters 52 | local function bton(s) 53 | local bytes = {s:byte(1,#s)} 54 | local n, bytes_n = 0, #bytes 55 | for i = 0, bytes_n-1 do 56 | n = n + bytes[1+i] * 2^(i*8) 57 | end 58 | return n 59 | end 60 | local unpack = table.unpack or unpack -- Lua 5.1 or 5.2 table unpacker 61 | local function ntob(n, len) 62 | local n, bytes = math.max(math.floor(n), 0), {} 63 | for i=1, len do 64 | bytes[i] = n % 256 65 | n = math.floor(n / 256) 66 | end 67 | return string.char(unpack(bytes)) 68 | end 69 | -- Check for integer 70 | local function isint(n) 71 | return type(n) == "number" and n == math.floor(n) 72 | end 73 | -- Initialize read process 74 | if mode == "r" then 75 | -- Audio meta informations 76 | local file_size, channels_number, sample_rate, byte_rate, block_align, bits_per_sample, samples_per_channel 77 | -- Audio samples file area 78 | local data_begin, data_end 79 | -- Read file type 80 | if file:read(4) ~= "RIFF" then 81 | error("not a RIFF file", 2) 82 | end 83 | file_size = file:read(4) 84 | if not file_size then 85 | error("file header incomplete (file size)") 86 | end 87 | file_size = bton(file_size) + 8 88 | if file:read(4) ~= "WAVE" then 89 | error("not a WAVE file", 2) 90 | end 91 | -- Read file chunks 92 | local chunk_id, chunk_size 93 | while true do 94 | -- Read chunk header 95 | chunk_id, chunk_size = file:read(4), file:read(4) 96 | if not chunk_size then 97 | break 98 | end 99 | chunk_size = bton(chunk_size) 100 | -- Identify chunk type 101 | if chunk_id == "fmt " then 102 | -- Read format informations 103 | local bytes = file:read(2) 104 | if not bytes or bton(bytes) ~= 1 then 105 | error("data must be in PCM format", 2) 106 | end 107 | bytes = file:read(2) 108 | if not bytes then 109 | error("channels number not found", 2) 110 | end 111 | channels_number = bton(bytes) 112 | bytes = file:read(4) 113 | if not bytes then 114 | error("sample rate not found", 2) 115 | end 116 | sample_rate = bton(bytes) 117 | bytes = file:read(4) 118 | if not bytes then 119 | error("byte rate not found", 2) 120 | end 121 | byte_rate = bton(bytes) 122 | bytes = file:read(2) 123 | if not bytes then 124 | error("block align not found", 2) 125 | end 126 | block_align = bton(bytes) 127 | bytes = file:read(2) 128 | if not bytes then 129 | error("bits per sample not found") 130 | end 131 | bits_per_sample = bton(bytes) 132 | if bits_per_sample ~= 8 and bits_per_sample ~= 16 and bits_per_sample ~= 24 and bits_per_sample ~= 32 then 133 | error("bits per sample must be 8, 16, 24 or 32", 2) 134 | end 135 | file:seek("cur", chunk_size-16) 136 | elseif chunk_id == "data" then 137 | -- Read samples 138 | if not block_align then 139 | error("format informations must be defined before sample data", 2) 140 | end 141 | samples_per_channel = chunk_size / block_align 142 | data_begin = file:seek() 143 | data_end = data_begin + chunk_size 144 | break -- Stop here for later reading 145 | else 146 | -- Skip chunk 147 | file:seek("cur", chunk_size) 148 | end 149 | end 150 | -- Enough informations available? 151 | if not bits_per_sample then 152 | error("no format informations found", 2) 153 | end 154 | -- Return audio handler 155 | local obj 156 | obj = { 157 | get_filename = function() 158 | return filename 159 | end, 160 | get_mode = function() 161 | return mode 162 | end, 163 | get_file_size = function() 164 | return file_size 165 | end, 166 | get_channels_number = function() 167 | return channels_number 168 | end, 169 | get_sample_rate = function() 170 | return sample_rate 171 | end, 172 | get_byte_rate = function() 173 | return byte_rate 174 | end, 175 | get_block_align = function() 176 | return block_align 177 | end, 178 | get_bits_per_sample = function() 179 | return bits_per_sample 180 | end, 181 | get_samples_per_channel = function() 182 | return samples_per_channel 183 | end, 184 | get_sample_from_ms = function(ms) 185 | if not isint(ms) or ms < 0 then 186 | error("positive integer expected", 2) 187 | end 188 | return ms * 0.001 * sample_rate 189 | end, 190 | get_ms_from_sample = function(sample) 191 | if not isint(sample) or sample < 0 then 192 | error("positive integer expected", 2) 193 | end 194 | return sample / sample_rate * 1000 195 | end, 196 | get_min_max_amplitude = function() 197 | local half_level = 2^bits_per_sample / 2 198 | return -half_level, half_level - 1 199 | end, 200 | get_position = function() 201 | if not data_begin then 202 | error("no samples available", 2) 203 | end 204 | return (file:seek() - data_begin) / block_align 205 | end, 206 | set_position = function(pos) 207 | if not isint(pos) or pos < 0 then 208 | error("positive integer expected", 2) 209 | elseif not data_begin then 210 | error("no samples available", 2) 211 | elseif data_begin + pos * block_align > data_end then 212 | error("tried to set position behind data end", 2) 213 | end 214 | file:seek("set", data_begin + pos * block_align) 215 | end, 216 | get_samples_interlaced = function(n) 217 | if not isint(n) or n <= 0 then 218 | error("positive integer greater zero expected", 2) 219 | elseif not data_begin then 220 | error("no samples available", 2) 221 | elseif file:seek() + n * block_align > data_end then 222 | error("tried to read over data end", 2) 223 | end 224 | local bytes, sample, output = file:read(n * block_align), nil, {n = 0} 225 | local bytes_n = #bytes 226 | if bits_per_sample == 8 then 227 | for i=1, bytes_n, 1 do 228 | sample = bton(bytes:sub(i,i)) 229 | output.n = output.n + 1 230 | output[output.n] = sample > 127 and sample - 256 or sample 231 | end 232 | elseif bits_per_sample == 16 then 233 | for i=1, bytes_n, 2 do 234 | sample = bton(bytes:sub(i,i+1)) 235 | output.n = output.n + 1 236 | output[output.n] = sample > 32767 and sample - 65536 or sample 237 | end 238 | elseif bits_per_sample == 24 then 239 | for i=1, bytes_n, 3 do 240 | sample = bton(bytes:sub(i,i+2)) 241 | output.n = output.n + 1 242 | output[output.n] = sample > 8388607 and sample - 16777216 or sample 243 | end 244 | else -- if bits_per_sample == 32 then 245 | for i=1, bytes_n, 4 do 246 | sample = bton(bytes:sub(i,i+3)) 247 | output.n = output.n + 1 248 | output[output.n] = sample > 2147483647 and sample - 4294967296 or sample 249 | end 250 | end 251 | return output 252 | end, 253 | get_samples = function(n) 254 | local success, samples = pcall(obj.get_samples_interlaced, n) 255 | if not success then 256 | error(samples, 2) 257 | end 258 | local output, channel_samples = {n = channels_number} 259 | for c=1, output.n do 260 | channel_samples = {n = samples.n / channels_number} 261 | for s=1, channel_samples.n do 262 | channel_samples[s] = samples[c + (s-1) * channels_number] 263 | end 264 | output[c] = channel_samples 265 | end 266 | return output 267 | end 268 | } 269 | return obj 270 | -- Initialize write process 271 | else 272 | -- Audio meta informations 273 | local channels_number_private, bytes_per_sample 274 | -- Return audio handler 275 | return { 276 | get_filename = function() 277 | return filename 278 | end, 279 | get_mode = function() 280 | return mode 281 | end, 282 | init = function(channels_number, sample_rate, bits_per_sample) 283 | -- Check function parameters 284 | if not isint(channels_number) or channels_number < 1 or 285 | not isint(sample_rate) or sample_rate < 2 or 286 | not (bits_per_sample == 8 or bits_per_sample == 16 or bits_per_sample == 24 or bits_per_sample == 32) then 287 | error("valid channels number, sample rate and bits per sample expected", 2) 288 | -- Already finished? 289 | elseif not file then 290 | error("already finished", 2) 291 | -- Already initialized? 292 | elseif file:seek() > 0 then 293 | error("already initialized", 2) 294 | end 295 | -- Write file type 296 | file:write("RIFF????WAVE") -- file size to insert later 297 | -- Write format chunk 298 | file:write("fmt ", 299 | ntob(16, 4), 300 | ntob(1, 2), 301 | ntob(channels_number, 2), 302 | ntob(sample_rate, 4), 303 | ntob(sample_rate * channels_number * (bits_per_sample / 8), 4), 304 | ntob(channels_number * (bits_per_sample / 8), 2), 305 | ntob(bits_per_sample, 2)) 306 | -- Write data chunk (so far) 307 | file:write("data????") -- data size to insert later 308 | -- Set format memory 309 | channels_number_private, bytes_per_sample = channels_number, bits_per_sample / 8 310 | end, 311 | write_samples_interlaced = function(samples) 312 | -- Check function parameters 313 | if type(samples) ~= "table" then 314 | error("samples table expected", 2) 315 | end 316 | local samples_n = #samples 317 | if samples_n == 0 or samples_n % channels_number_private ~= 0 then 318 | error("valid number of samples expected (multiple of channels)", 2) 319 | -- Already finished? 320 | elseif not file then 321 | error("already finished", 2) 322 | -- Already initialized? 323 | elseif file:seek() == 0 then 324 | error("initialize before writing samples", 2) 325 | end 326 | -- All samples are numbers? 327 | for i=1, samples_n do 328 | if type(samples[i]) ~= "number" then 329 | error("samples have to be numbers", 2) 330 | end 331 | end 332 | -- Write samples to file 333 | local sample 334 | if bytes_per_sample == 1 then 335 | for i=1, samples_n do 336 | sample = samples[i] 337 | file:write(ntob(sample < 0 and sample + 256 or sample, 1)) 338 | end 339 | elseif bytes_per_sample == 2 then 340 | for i=1, samples_n do 341 | sample = samples[i] 342 | file:write(ntob(sample < 0 and sample + 65536 or sample, 2)) 343 | end 344 | elseif bytes_per_sample == 3 then 345 | for i=1, samples_n do 346 | sample = samples[i] 347 | file:write(ntob(sample < 0 and sample + 16777216 or sample, 3)) 348 | end 349 | else -- if bytes_per_sample == 4 then 350 | for i=1, samples_n do 351 | sample = samples[i] 352 | file:write(ntob(sample < 0 and sample + 4294967296 or sample, 4)) 353 | end 354 | end 355 | end, 356 | finish = function() 357 | -- Already finished? 358 | if not file then 359 | error("already finished", 2) 360 | -- Already initialized? 361 | elseif file:seek() == 0 then 362 | error("initialize before finishing", 2) 363 | end 364 | -- Get file size 365 | local file_size = file:seek() 366 | -- Save file size 367 | file:seek("set", 4) 368 | file:write(ntob(file_size - 8, 4)) 369 | -- Save data size 370 | file:seek("set", 40) 371 | file:write(ntob(file_size - 44, 4)) 372 | -- Finalize file for secure reading 373 | file:close() 374 | file = nil 375 | end 376 | } 377 | end 378 | end, 379 | --[[ 380 | Analyses frequencies of audio samples. 381 | 382 | Function 'create_frequency_analyzer' requires 2 arguments: a table with audio samples and the relating sample rate. 383 | A call returns one table with following methods: 384 | - get_frequencies() 385 | - get_frequency_weight(freq) 386 | - get_frequency_range_weight(freq_min, freq_max) 387 | 388 | (FFT: http://www.relisoft.com/science/physics/fft.html) 389 | ]] 390 | create_frequency_analyzer = function(samples, sample_rate) 391 | -- Check function parameters 392 | if type(samples) ~= "table" or 393 | type(sample_rate) ~= "number" or sample_rate ~= math.floor(sample_rate) or sample_rate < 2 then 394 | error("samples table and sample rate expected", 2) 395 | end 396 | local samples_n = #samples 397 | if samples_n ~= math.ceil_pow2(samples_n) then 398 | error("table size has to be a power of two", 2) 399 | end 400 | for _, sample in ipairs(samples) do 401 | if type(sample) ~= "number" then 402 | error("table has only to contain numbers", 2) 403 | elseif sample > 1 or sample < -1 then 404 | error("numbers should be normalized / limited to -1 until 1", 2) 405 | end 406 | end 407 | -- Complex numbers 408 | local complex_t 409 | do 410 | local complex = {} 411 | local function tocomplex(a, b) 412 | if getmetatable(b) ~= complex then return a, {r = b, i = 0} 413 | elseif getmetatable(a) ~= complex then return {r = a, i = 0}, b 414 | else return a, b end 415 | end 416 | complex.__add = function(a, b) 417 | local c1, c2 = tocomplex(a, b) 418 | return setmetatable({r = c1.r + c2.r, i = c1.i + c2.i}, complex) 419 | end 420 | complex.__sub = function(a, b) 421 | local c1, c2 = tocomplex(a, b) 422 | return setmetatable({r = c1.r - c2.r, i = c1.i - c2.i}, complex) 423 | end 424 | complex.__mul = function(a, b) 425 | local c1, c2 = tocomplex(a, b) 426 | return setmetatable({r = c1.r * c2.r - c1.i * c2.i, i = c1.r * c2.i + c1.i * c2.r}, complex) 427 | end 428 | complex.__index = complex 429 | complex_t = function(r, i) 430 | return setmetatable({r = r, i = i}, complex) 431 | end 432 | end 433 | local function polar(theta) 434 | return complex_t(math.cos(theta), math.sin(theta)) 435 | end 436 | local function magnitude(c) 437 | return math.sqrt(c.r^2 + c.i^2) 438 | end 439 | -- Fast Fourier Transform 440 | local function fft(x) 441 | -- Check recursion break 442 | local N = x.n 443 | if N > 1 then 444 | -- Divide 445 | local even, odd = {n = 0}, {n = 0} 446 | for i=1, N, 2 do 447 | even.n = even.n + 1 448 | even[even.n] = x[i] 449 | end 450 | for i=2, N, 2 do 451 | odd.n = odd.n + 1 452 | odd[odd.n] = x[i] 453 | end 454 | -- Conquer 455 | fft(even) 456 | fft(odd) 457 | --Combine 458 | local t 459 | for k = 1, N/2 do 460 | t = polar(-2 * math.pi * (k-1) / N) * odd[k] 461 | x[k] = even[k] + t 462 | x[k+N/2] = even[k] - t 463 | end 464 | end 465 | end 466 | -- Numbers to complex numbers 467 | local data = {n = samples_n} 468 | for i = 1, data.n do 469 | data[i] = complex_t(samples[i], 0) 470 | end 471 | -- Process FFT 472 | fft(data) 473 | -- Complex numbers to numbers 474 | for i = 1, data.n do 475 | data[i] = magnitude(data[i]) 476 | end 477 | -- Calculate ordered frequencies 478 | local frequencies, frequency_sum, sample_rate_half = {n = data.n / 2}, 0, sample_rate / 2 479 | for i=1, frequencies.n do 480 | frequency_sum = frequency_sum + data[i] 481 | end 482 | if frequency_sum > 0 then 483 | for i=1, frequencies.n do 484 | frequencies[i] = {freq = (i-1) / (frequencies.n-1) * sample_rate_half, weight = data[i] / frequency_sum} 485 | end 486 | else 487 | frequencies[1] = {freq = 0, weight = 1} 488 | for i=2, frequencies.n do 489 | frequencies[i] = {freq = (i-1) / (frequencies.n-1) * sample_rate_half, weight = 0} 490 | end 491 | end 492 | -- Return frequencies getter 493 | return { 494 | get_frequencies = function() 495 | local out = {n = frequencies.n} 496 | for i=1, frequencies.n do 497 | out[i] = {freq = frequencies[i].freq, weight = frequencies[i].weight} 498 | end 499 | return out 500 | end, 501 | get_frequency_weight = function(freq) 502 | if type(freq) ~= "number" or freq < 0 or freq > sample_rate_half then 503 | error("valid frequency expected", 2) 504 | end 505 | for i, frequency in ipairs(frequencies) do 506 | if frequency.freq == freq then 507 | return frequency.weight 508 | elseif frequency.freq > freq then 509 | local frequency_last = frequencies[i-1] 510 | return (freq - frequency_last.freq) / (frequency.freq - frequency_last.freq) * (frequency.weight - frequency_last.weight) + frequency_last.weight 511 | end 512 | end 513 | end, 514 | get_frequency_range_weight = function(freq_min, freq_max) 515 | if type(freq_min) ~= "number" or freq_min < 0 or freq_min > sample_rate_half or 516 | type(freq_max) ~= "number" or freq_max < 0 or freq_max > sample_rate_half or 517 | freq_min > freq_max then 518 | error("valid frequencies expected", 2) 519 | end 520 | local weight_sum = 0 521 | for _, frequency in ipairs(frequencies) do 522 | if frequency.freq >= freq_min and frequency.freq <= freq_max then 523 | weight_sum = weight_sum + frequency.weight 524 | end 525 | end 526 | return weight_sum 527 | end 528 | } 529 | end 530 | } 531 | 532 | --[[ 533 | Rounds up number to power of 2. 534 | ]] 535 | function math.ceil_pow2(x) 536 | if type(x) ~= "number" then 537 | error("number expected", 2) 538 | end 539 | local p = 2 540 | while p < x do 541 | p = p * 2 542 | end 543 | return p 544 | end 545 | 546 | --[[ 547 | Rounds down number to power of 2. 548 | ]] 549 | function math.floor_pow2(x) 550 | if type(x) ~= "number" then 551 | error("number expected", 2) 552 | end 553 | local y = math.ceil_pow2(x) 554 | return x == y and x or y / 2 555 | end 556 | 557 | --[[ 558 | Rounds number nearest to power of 2. 559 | ]] 560 | function math.round_pow2(x) 561 | if type(x) ~= "number" then 562 | error("number expected", 2) 563 | end 564 | local min, max = math.floor_pow2(x), math.ceil_pow2(x) 565 | return (x - min) / (max-min) < 0.5 and min or max 566 | end 567 | 568 | --[[ 569 | Converts samples into an ASS (Advanced Substation Alpha) subtitle shape code. 570 | ]] 571 | function audio_to_ass(samples, wave_width, wave_height_scale, wave_thickness) 572 | -- Check function parameters 573 | if type(samples) ~= "table" or not samples[1] or 574 | type(wave_width) ~= "number" or wave_width <= 0 or 575 | type(wave_height_scale) ~= "number" or 576 | type(wave_thickness) ~= "number" or wave_thickness <= 0 then 577 | error("samples table, positive wave width, height scale and thickness expected", 2) 578 | end 579 | for _, sample in ipairs(samples) do 580 | if type(sample) ~= "number" then 581 | error("table has only to contain numbers", 2) 582 | end 583 | end 584 | -- Better fitting forms of known variables for most use 585 | local thick2, samples_n = wave_thickness / 2, #samples 586 | -- Build shape 587 | local shape = string.format("m 0 %d l", samples[1] * wave_height_scale - thick2) 588 | for i=2, samples_n do 589 | shape = string.format("%s %d %d", shape, (i-1) / (samples_n-1) * wave_width, samples[i] * wave_height_scale - thick2) 590 | end 591 | for i=samples_n, 1, -1 do 592 | shape = string.format("%s %d %d", shape, (i-1) / (samples_n-1) * wave_width, samples[i] * wave_height_scale + thick2) 593 | end 594 | return shape 595 | end 596 | --------------------------------------------------------------------------------