mirror of
https://github.com/CCLeonOS/LeonOS.git
synced 2026-03-03 06:47:00 +00:00
添加 LeonOS 的基本实现,包括: - 核心 API 模块(colors, disk, gps, keys, multishell, parallel, rednet, redstone, settings, vector) - 命令行程序(about, alias, bg, clear, copy, delete, edit, fg, help, list, lua, mkdir, move, paint, peripherals, programs, reboot, set, shutdown, threads) - 系统启动脚本和包管理 - 文档(README.md, LICENSE) - 开发工具(devbin)和更新程序 实现功能: - 完整的线程管理系统 - 兼容 ComputerCraft 的 API 设计 - 改进的 shell 和命令补全系统 - 多标签终端支持 - 设置管理系统
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,
|
||
}
|