mirror of
https://github.com/CCLeonOS/LeonOS.git
synced 2026-03-03 15:17:01 +00:00
229 lines
8.2 KiB
Lua
229 lines
8.2 KiB
Lua
|
|
--[[-
|
|||
|
|
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,
|
|||
|
|
}
|