-
Notifications
You must be signed in to change notification settings - Fork 212
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add cc.audio.wav
module
#1928
base: mc-1.20.x
Are you sure you want to change the base?
Add cc.audio.wav
module
#1928
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR!
It is a bit of a shame that the interface is so different to the DFPWM module. It might be possible to add a more reader-based approach to DFPWM to match readWAV
here, will have a think.
I think it would be useful to add some additional comments throughout the code here. As it stands, this is currently ~100 LOC of pretty dense parsing code, and having some references back to the original spec would be useful for people coming back to this code in the future.
I am a little concerned about error handling. There's a lot of cases right now where string.unpack
will blow up if the input is truncated in any way, and not quite sure what to do about that.
expect(1, data, "string") | ||
local bitDepth, dataType, blockAlign | ||
local temp, pos = str_unpack("c4", data) | ||
if temp ~= "RIFF" then error("bad argument #1 (not a WAV file)", 2) end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
local function readWAVFile(path) | ||
expect(1, path, "string") | ||
local file = assert(fs.open(path, "rb")) | ||
local data = file.readAll() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice if we could avoid reading the whole file into memory at once. I wonder if readWAV
could take a "reader" function (like function(bytes: number): string|nil
), so you can just do readWAV(file.read)
directly.
This would require some changes to parsing the INFO
section (which uses string.unpack("s")
), but I think otherwise should be possible?
I guess this would then mean there needs to be a close
method on the WAV handle, to clean up the underlying file handle. Ughr.
elseif dataType == "signed" then | ||
if bitDepth == 16 then function transform(n) return math_floor(n / 0x100) end | ||
elseif bitDepth == 24 then function transform(n) return math_floor(n / 0x10000) end | ||
elseif bitDepth == 32 then function transform(n) return math_floor(n / 0x1000000) end end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should there be an error here for an invalid bit depth?
end | ||
function retval.read(samples) | ||
if pos > #data then return nil end | ||
local chunk = { ("<" .. format:rep(math.min(samples * channels, (#data - pos + 1) / (bitDepth / 8)))):unpack(data, pos) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I don't feel good about this line (the extra format string allocation, and then putting it into a table), but some quick benchmarks do show it's the quickest approach :(. The crimes we must commit sometimes!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I didn't like it either, but I found the same results after a comparison in AUKit. It's gross, but it's for the best.
This PR adds a new
cc.audio.wav
module, which assists in decoding WAV files. Derived from my WAV code in AUKit, it's able to properly read an entire WAV file, including metadata (which is now used to show the file's title and artist inspeaker play
if present). This module makes it much easier to work with WAV audio files, and I hope it encourages more adoption of WAV over raw DFPWM.The module consists of a main function,
readWAV
, which reads full WAV file data and returns a table with the contents of the file; andreadWAVFile
, which is a simple wrapper that reads from a file instead. The table contains the following elements:codec
: A string with information about the codec used in the file (one ofu8
,s16
,s24
,s32
,f32
,dfpwm
)sampleRate
: The sample rate of the audio in Hz. If this is not 48000, the file will need to be resampled to play correctly.channels
: The number of channels in the file (1 = mono, 2 = stereo).length
: The number of samples in the file. Divide by sample rate to get seconds.metadata
: If the WAV file containsINFO
metadata, this table contains the metadata.Known keys are converted to friendly names like
artist
,album
, andtrack
, while unknown keys are kept the same.Otherwise, this table is empty.
read(length: number): number[]...
: This is a function that reads the audio data in chunks.It takes the number of samples to read, and returns each channel chunk as multiple return values.
Channel data is in the same format as
speaker.playAudio
takes: 8-bit signed numbers.The reader supports 8/16/24/32-bit PCM, 32-bit float, and DFPWM codecs. It does not perform any resampling if the file isn't 48kHz. Any further file support is left to 3rd-party libraries (such as AUKit). The WAV parser in
speaker.lua
has been replaced withcc.audio.wav
, which makes WAV parsing a bit more robust.I also updated some docs in
cc.audio.dfpwm
for the most recent information about FFmpeg.