--[[- Provides utilities for converting between streams of DFPWM audio data and a list of amplitudes. DFPWM (Dynamic Filter Pulse Width Modulation) is an audio codec designed by GreaseMonkey. It's a relatively compact format compared to raw PCM data, only using 1 bit per sample, but is simple enough to simple enough to encode and decode in real time. Typically DFPWM audio is read from @{fs.BinaryReadHandle|the filesystem} or a @{http.Response|a web request} as a string, and converted a format suitable for @{speaker.playAudio}. ## Encoding and decoding files This modules exposes two key functions, @{make_decoder} and @{make_encoder}, which construct a new decoder or encoder. The returned encoder/decoder is itself a function, which converts between the two kinds of data. These encoders and decoders have lots of hidden state, so you should be careful to use the same encoder or decoder for a specific audio stream. Typically you will want to create a decoder for each stream of audio you read, and an encoder for each one you write. ## Converting audio to DFPWM DFPWM is not a popular file format and so standard audio processing tools will not have an option to export to it. Instead, you can convert audio files online using [music.madefor.cc] or with the [LionRay Wav Converter][LionRay] Java application. [music.madefor.cc]: https://music.madefor.cc/ "DFPWM audio converter for Computronics and CC: Tweaked" [LionRay]: https://github.com/gamax92/LionRay/ "LionRay Wav Converter " @see guide!speaker_audio Gives a more general introduction to audio processing and the speaker. @see speaker.playAudio To play the decoded audio data. @usage Reads "data/example.dfpwm" in chunks, decodes them and then doubles the speed of the audio. The resulting audio is then re-encoded and saved to "speedy.dfpwm". This processed audio can then be played with the `speaker` program. ```lua local dfpwm = require("cc.audio.dfpwm") local encoder = dfpwm.make_encoder() local decoder = dfpwm.make_decoder() local out = fs.open("speedy.dfpwm", "wb") for input in io.lines("data/example.dfpwm", 16 * 1024 * 2) do local decoded = decoder(input) local output = {} -- Read two samples at once and take the average. for i = 1, #decoded, 2 do local value_1, value_2 = decoded[i], decoded[i + 1] output[(i + 1) / 2] = (value_1 + value_2) / 2 end out.write(encoder(output)) sleep(0) -- This program takes a while to run, so we need to make sure we yield. end out.close() ``` ]] local expect = require "cc.expect".expect local char, byte, floor, band, rshift = string.char, string.byte, math.floor, bit32.band, bit32.arshift local PREC = 10 local PREC_POW = 2 ^ PREC local PREC_POW_HALF = 2 ^ (PREC - 1) local STRENGTH_MIN = 2 ^ (PREC - 8 + 1) local function make_predictor() local charge, strength, previous_bit = 0, 0, false return function(current_bit) local target = current_bit and 127 or -128 local next_charge = charge + floor((strength * (target - charge) + PREC_POW_HALF) / PREC_POW) if next_charge == charge and next_charge ~= target then next_charge = next_charge + (current_bit and 1 or -1) end local z = current_bit == previous_bit and PREC_POW - 1 or 0 local next_strength = strength if next_strength ~= z then next_strength = next_strength + (current_bit == previous_bit and 1 or -1) end if next_strength < STRENGTH_MIN then next_strength = STRENGTH_MIN end charge, strength, previous_bit = next_charge, next_strength, current_bit return charge end end --[[- Create a new encoder for converting PCM audio data into DFPWM. The returned encoder is itself a function. This function accepts a table of amplitude data between -128 and 127 and returns the encoded DFPWM data. :::caution Reusing encoders Encoders have lots of internal state which tracks the state of the current stream. If you reuse an encoder for multiple streams, or use different encoders for the same stream, the resulting audio may not sound correct. ::: @treturn function(pcm: { number... }):string The encoder function @see encode A helper function for encoding an entire file of audio at once. ]] local function make_encoder() local predictor = make_predictor() local previous_charge = 0 return function(input) expect(1, input, "table") local output, output_n = {}, 0 for i = 1, #input, 8 do local this_byte = 0 for j = 0, 7 do local inp_charge = floor(input[i + j] or 0) if inp_charge > 127 or inp_charge < -128 then error(("Amplitude at position %d was %d, but should be between -128 and 127"):format(i + j, inp_charge), 2) end local current_bit = inp_charge > previous_charge or (inp_charge == previous_charge and inp_charge == 127) this_byte = floor(this_byte / 2) + (current_bit and 128 or 0) previous_charge = predictor(current_bit) end output_n = output_n + 1 output[output_n] = char(this_byte) end return table.concat(output, "", 1, output_n) end end --[[- Create a new decoder for converting DFPWM into PCM audio data. The returned decoder is itself a function. This function accepts a string and returns a table of amplitudes, each value between -128 and 127. :::caution Reusing decoders Decoders have lots of internal state which tracks the state of the current stream. If you reuse an decoder for multiple streams, or use different decoders for the same stream, the resulting audio may not sound correct. ::: @treturn function(dfpwm: string):{ number... } The encoder function @see decode A helper function for decoding an entire file of audio at once. @usage Reads "data/example.dfpwm" in blocks of 16KiB (the speaker can accept a maximum of 128×1024 samples), decodes them and then plays them through the speaker. ```lua {data-peripheral=speaker} local dfpwm = require "cc.audio.dfpwm" local speaker = peripheral.find("speaker") local decoder = dfpwm.make_decoder() for input in io.lines("data/example.dfpwm", 16 * 1024) do local decoded = decoder(input) while not speaker.playAudio(decoded) do os.pullEvent("speaker_audio_empty") end end ``` ]] local function make_decoder() local predictor = make_predictor() local low_pass_charge = 0 local previous_charge, previous_bit = 0, false return function (input, output) expect(1, input, "string") local output, output_n = {}, 0 for i = 1, #input do local input_byte = byte(input, i) for _ = 1, 8 do local current_bit = band(input_byte, 1) ~= 0 local charge = predictor(current_bit) local antijerk = charge if current_bit ~= previous_bit then antijerk = floor((charge + previous_charge + 1) / 2) end previous_charge, previous_bit = charge, current_bit low_pass_charge = low_pass_charge + floor(((antijerk - low_pass_charge) * 140 + 0x80) / 256) output_n = output_n + 1 output[output_n] = low_pass_charge input_byte = rshift(input_byte, 1) end end return output end end --[[- A convenience function for decoding a complete file of audio at once. This should only be used for short files. For larger files, one should read the file in chunks and process it using @{make_decoder}. @tparam string input The DFPWM data to convert. @treturn { number... } The produced amplitude data. @see make_decoder ]] local function decode(input) expect(1, input, "string") return make_decoder()(input) end --[[- A convenience function for encoding a complete file of audio at once. This should only be used for complete pieces of audio. If you are writing writing multiple chunks to the same place, you should use an encoder returned by @{make_encoder} instead. @tparam { number... } input The table of amplitude data. @treturn string The encoded DFPWM data. @see make_encoder ]] local function encode(input) expect(1, input, "table") return make_encoder()(input) end return { make_encoder = make_encoder, encode = encode, make_decoder = make_decoder, decode = decode, }