feat: buffered decoding API

This commit is contained in:
2024-06-19 04:08:54 +02:00
parent ae6ad428ab
commit c7a634eeb3
10 changed files with 1985 additions and 305 deletions
+2 -5
View File
@@ -1,7 +1,4 @@
.vscode/
ols.json
odinfmt.json
example/
example/!*.odin
tests/test-files/LPC_example.flac
tests/test-files/fixed_example.flac
tests/test-files/verbatim_example.flac
tests/*.flac
+16 -8
View File
@@ -2,11 +2,14 @@ Files:-
27 -> This one uses the old format of specifying a variable block strategy. Look into it.
The way to know the blocking strategy for this old format FLACs is to see if min_block_size and max_block_size are different,
if they are, then the blocking strategy is VARIABLE, if not, then it's FIXED.
44 -> High memory and CPU usage. needs optimization.
uncommon/01 -> This one decodes fine but the expected MD5 is 0x00 * 16 which doesn't match the calculated MD5. not sure what to do about this.
uncommon/02 -> No MD5
uncommon/03 -> No MD5
Playback issues in the example program that may relate to the decoder:-
05 - CRC fails for a frame. The gotten CRC is different each time, expected CRC is also different sometimes.
06 - Same as above
23 - Crunchy loud noise mixed with the actual audio data
25 - Same CRC problem
It's worth noting that if we get rid of the CRC checks the files play just fine.
uncommon/04 -> We get a bits per sample mismatch
uncommon/10 -> We error with Invalid Signature. I assume for these two I should look for the frame sync bits. But maybe my decoder should reject these. idk.
uncommon/11 -> We error with Invalid Signature
@@ -14,19 +17,24 @@ uncommon/11 -> We error with Invalid Signature
faulty/01 -> This one works when it shouldn't (Wrong max blocksize)
faulty/02 -> This one works when it shouldn't (Wrong max framesize)
faulty/04 -> This one works when it shouldn't. This one has a wrong number of channels (whatever that means)
Each frame has to be played by itself, otherwise plays sped up.
faulty/05 -> This one works when it shouldn't. (Wrong number of samples)
This should be fine to decode and play.
faulty/08 -> This one works when it shouldn't. (Blocksize 65536, exceeds the allowed max of 65535)
faulty/09 -> This one works when it shouldn't. (Blocksize 1, below the allowed min of 16 (last frame(subframe?) is allowed to have less than 16))
We should error here. We segfault currently.
faulty/09 -> This one works when it shouldn't. (Blocksize 1, below the allowed min of 16 (only the last frame is allowed to have less than 16))
We should error here. We play correctly currently.
faulty/11 -> We return an error now, but we should just warn and decode the audio fine.
Things:-
[ ] Options
- [ ] Vorbis comment data
- [x] Vorbis comment data
- [ ] Cuesheet ?
- [ ] Seektable ?
- [x] Picture data
[ ] File streaming API? io.Stream??
[x] File streaming API? io.Stream??
[ ] Lots of checks (look at the TODOs in flac.odin)
[ ] Memory optimizations
[x] Memory optimizations
[ ] Speed optimizations
[ ] Seeking
[ ] Fuzzing to test the stability of the code
-36
View File
@@ -1,36 +0,0 @@
package flac_example
import "core:fmt"
import "core:mem"
import "core:mem/virtual"
import "core:os"
import "core:time"
import "../"
main :: proc() {
if len(os.args) < 2 {
fmt.eprintln("Please provide a path to a flac file.")
os.exit(1)
}
arena: virtual.Arena
arena_err := virtual.arena_init_static(&arena, mem.Megabyte * 8)
if arena_err != nil {
fmt.eprintln("Failed to alloc memory")
return
}
allocator := virtual.arena_allocator(&arena)
start := time.now()
flac_data, err := flac.load_from_file(os.args[1], allocator = allocator)
if err != nil {
fmt.eprintln(err)
os.exit(1)
}
end := time.now()
elapsed := time.diff(start, end)
fmt.println("Decoding took", elapsed)
}
+189
View File
@@ -0,0 +1,189 @@
package flac_example
import "core:fmt"
import "core:mem"
import "core:mem/virtual"
import "core:crypto/legacy/md5"
import "core:os"
import "core:time"
import "shared:flac"
import "shared:alsa"
DEVICE :: "default"
main :: proc() {
if len(os.args) < 2 {
fmt.eprintln("Please provide a path to a flac file.")
os.exit(1)
}
arena: virtual.Arena
handle: ^alsa.pcm_t
alsa_err := alsa.pcm_open(&handle, DEVICE, .PLAYBACK, .BLOCK)
if alsa_err < 0 {
fmt.eprintln("Failed to initialize audio device: ", alsa.strerror(alsa_err))
os.exit(1)
}
arena_err := virtual.arena_init_growing(&arena, mem.Megabyte * 8)
if arena_err != nil {
fmt.eprintln("Failed to alloc memory")
return
}
allocator := virtual.arena_allocator(&arena)
start := time.now()
flac_data, reader, err := flac.load_from_file_buffered(os.args[1], allocator)
if err != nil {
fmt.println("Error while reading flac file:", err)
os.exit(1)
}
defer flac.destroy(flac_data, reader, allocator)
fmt.println(flac_data)
alsa_err = alsa.pcm_set_params(handle, .S16_LE, .RW_INTERLEAVED, u32(flac_data.metadata.channels), flac_data.metadata.sample_rate, 1, 500000)
if alsa_err < 0 {
fmt.eprintln("Playback open error:", alsa.strerror(alsa_err))
os.exit(1)
}
sample_rate := flac_data.metadata.sample_rate
channels := flac_data.metadata.channels
//md5_ctx: md5.Context
//md5.init(&md5_ctx)
for {
frame, err := flac.read_next_frame(reader, flac_data)
defer delete(frame.samples)
if err != nil {
if err == .EOF {
break
} else {
fmt.println("Error while decoding frame:", err)
os.exit(1)
}
}
//if frame.sample_rate != sample_rate || frame.channels != channels {
// sample_rate = frame.sample_rate
// channels = frame.channels
// alsa_err = alsa.pcm_set_params(handle, .S16_LE, .RW_INTERLEAVED, u32(channels), sample_rate, 1, 0)
// if alsa_err < 0 {
// fmt.eprintln("Playback open error:", alsa.strerror(alsa_err))
// os.exit(1)
// }
//}
//converted_samples := make([]u8, len(samples))
//defer delete(converted_samples)
//for sample, i in samples {
// converted_samples[i] = u8(sample & 0xFF)
//}
converted_samples := make([]i16, len(frame.samples))
defer delete(converted_samples)
for sample, i in frame.samples {
new_sample := sample & 0xFF
new_sample |= ((sample >> 8) & 0xFF) << 8
converted_samples[i] = i16(new_sample)
}
frames := alsa.pcm_writei(handle, raw_data(converted_samples), u64(len(frame.samples)) / u64(frame.channels))
if frames < 0 {
fmt.eprintln("Error writing to PCM device:", alsa.strerror(auto_cast frames))
break
}
fmt.printfln("Played %d frames", frames)
//flac.md5hash(&md5_ctx, flac_data.metadata.bits_per_sample, samples)
}
end := time.now()
//err = flac.md5sum(&md5_ctx, flac_data)
//if err == .MD5_Mismatch {
// fmt.println("Decoded audio data is not correct.")
// os.exit(1)
//}
alsa_err = alsa.pcm_drain(handle)
if alsa_err < 0 {
fmt.eprintln("pcm_drain failed with:", alsa.strerror(alsa_err))
}
alsa.pcm_close(handle)
elapsed := time.diff(start, end)
fmt.println("Decoding took", elapsed)
}
old_main :: proc() {
if len(os.args) < 2 {
fmt.eprintln("Please provide a path to a flac file.")
os.exit(1)
}
handle: ^alsa.pcm_t
arena: virtual.Arena
arena_err := virtual.arena_init_growing(&arena, mem.Megabyte * 8)
if arena_err != nil {
fmt.eprintln("Failed to alloc memory")
return
}
allocator := virtual.arena_allocator(&arena)
alsa_err := alsa.pcm_open(&handle, DEVICE, .PLAYBACK, .BLOCK)
if alsa_err < 0 {
fmt.eprintln("Failed to initialize audio device: ", alsa.strerror(alsa_err))
os.exit(1)
}
start := time.now()
flac_data, err := flac.load_from_file(os.args[1], allocator = allocator)
if err != nil {
fmt.eprintln(err)
if err == .MD5_Mismatch {
fmt.printfln("Expected: %v, got: %v", flac_data.metadata.expected_md5, flac_data.metadata.calculated_md5)
}
os.exit(1)
}
defer flac.destroy(flac_data, allocator = allocator)
end := time.now()
fmt.println(flac_data.metadata)
alsa_err = alsa.pcm_set_params(handle, .S16_LE, .RW_INTERLEAVED, u32(flac_data.metadata.channels), flac_data.metadata.sample_rate, 1, 500000)
if alsa_err < 0 {
fmt.eprintln("Playback open error:", alsa.strerror(alsa_err))
os.exit(1)
}
converted_samples := make([]i16, len(flac_data.samples))
for sample, i in flac_data.samples {
new_sample := sample & 0xFF
new_sample |= ((sample >> 8) & 0xFF) << 8
converted_samples[i] = i16(new_sample)
}
for {
frames := alsa.pcm_writei(handle, raw_data(converted_samples), u64(len(flac_data.samples)) / u64(flac_data.metadata.channels))
if frames < 0 {
fmt.eprintln("Error writing to PCM device:", alsa.strerror(auto_cast frames))
break
}
fmt.printfln("Played %d frames", frames)
}
alsa_err = alsa.pcm_drain(handle)
if alsa_err < 0 {
fmt.eprintln("pcm_drain failed with:", alsa.strerror(alsa_err))
}
alsa.pcm_close(handle)
elapsed := time.diff(start, end)
fmt.println("Decoding took", elapsed)
}
+81
View File
@@ -0,0 +1,81 @@
package main
import "core:os"
import "core:fmt"
import "core:mem"
import "core:mem/virtual"
import "core:time"
import "shared:alsa"
import "shared:flac"
DEVICE :: "default"
main :: proc() {
if len(os.args) < 2 {
fmt.eprintln("Please provide a path to a flac file.")
os.exit(1)
}
handle: ^alsa.pcm_t
arena: virtual.Arena
arena_err := virtual.arena_init_growing(&arena, mem.Megabyte * 8)
if arena_err != nil {
fmt.eprintln("Failed to alloc memory")
return
}
allocator := virtual.arena_allocator(&arena)
alsa_err := alsa.pcm_open(&handle, DEVICE, .PLAYBACK, .BLOCK)
if alsa_err < 0 {
fmt.eprintln("Failed to initialize audio device: ", alsa.strerror(alsa_err))
os.exit(1)
}
start := time.now()
flac_data, err := flac.load_from_file(os.args[1], allocator = allocator)
if err != nil {
fmt.eprintln(err)
if err == .MD5_Mismatch {
fmt.printfln("Expected: %v, got: %v", flac_data.metadata.expected_md5, flac_data.metadata.calculated_md5)
}
os.exit(1)
}
defer flac.destroy(flac_data, allocator = allocator)
end := time.now()
fmt.println(flac_data.metadata)
alsa_err = alsa.pcm_set_params(handle, .S16_LE, .RW_INTERLEAVED, u32(flac_data.metadata.channels), flac_data.metadata.sample_rate, 1, 500000)
if alsa_err < 0 {
fmt.eprintln("Playback open error:", alsa.strerror(alsa_err))
os.exit(1)
}
converted_samples := make([]i16, len(flac_data.samples))
for sample, i in flac_data.samples {
new_sample := sample & 0xFF
new_sample |= ((sample >> 8) & 0xFF) << 8
converted_samples[i] = i16(new_sample)
}
for {
frames := alsa.pcm_writei(handle, raw_data(converted_samples), u64(len(flac_data.samples)) / u64(flac_data.metadata.channels))
if frames < 0 {
fmt.eprintln("Error writing to PCM device:", alsa.strerror(auto_cast frames))
break
}
fmt.printfln("Played %d frames", frames)
}
alsa_err = alsa.pcm_drain(handle)
if alsa_err < 0 {
fmt.eprintln("pcm_drain failed with:", alsa.strerror(alsa_err))
}
alsa.pcm_close(handle)
elapsed := time.diff(start, end)
fmt.println("Decoding took", elapsed)
}
+517 -154
View File
@@ -1,6 +1,7 @@
package flac
import "core:bytes"
import "core:bufio"
import "core:crypto/legacy/md5"
import "core:fmt"
import "core:io"
@@ -20,7 +21,6 @@ FlacError :: enum {
Missing_StreamInfo,
Missing_Lead_Out_Track,
Invalid_Coefficient_Precision,
Metadata_Block_Length_Mismatch,
CRC_Mismatch,
MD5_Mismatch,
Unsupported_BPS, // Remove when implemented
@@ -36,17 +36,12 @@ Option:
`.skip_md5_check`
Skips checking the MD5 hash of the decoded sample data.
Offers a slight performance increase but will not error if the data is incorrect.
`.only_return_picture_data`
Returns the embedded picture metadata and encoded pixel data (if available)
`.only_return_metadata` includes this.
`.only_return_metadata`
Returns all metadata of a FLAC stream without decoding the audio data.
*/
// TODO: Implement these
Option :: enum {
skip_md5_check,
only_return_picture_data,
only_return_metadata,
}
@@ -77,9 +72,8 @@ PictureType :: enum u32 {
}
Flac :: struct {
// TODO: Other stuff
metadata: FlacMetadata,
samples: []i32,
samples: [dynamic]i32,
}
FlacMetadata :: struct {
@@ -90,6 +84,8 @@ FlacMetadata :: struct {
expected_md5: [16]byte,
calculated_md5: [16]byte,
pictures: []Picture,
vendor_name: string,
vorbis_comments: []string,
}
Picture :: struct {
@@ -103,14 +99,17 @@ Picture :: struct {
data: []byte,
}
Frame :: struct {
channels: u8,
sample_rate: u32,
samples: []i32,
}
@(private)
decode_subframe :: proc(r: ^Reader, bps: u8, block_size: u16) -> (samples: []i32, err: Error) #no_bounds_check {
// TODO: temp allocator?? so we copy the slice of data into the _actual_ sample buffer
// Maybe even a static arena with a size of 32MB by default (and configurable using #config) or something
// that we then use to allocate from (better perf is the reason because allocating on the heap for every subframe
// is probably noticibly slower)
subframe_samples := make([]i32, block_size)
data := read_byte(r) or_return
data := read_bits(r, 8) or_return
subframe_type := (data & 0x7E) >> 1
low_3_bits := subframe_type & 7
@@ -118,7 +117,7 @@ decode_subframe :: proc(r: ^Reader, bps: u8, block_size: u16) -> (samples: []i32
wasted_bits := 0
if has_wasted_bits {
for (read_bit(r) or_return) != 1 {
for (read_bits(r, 1) or_return) != 1 {
wasted_bits += 1
}
wasted_bits += 1
@@ -251,7 +250,7 @@ decode_subframe :: proc(r: ^Reader, bps: u8, block_size: u16) -> (samples: []i32
for s in 0..<num_samples {
quotient := 0
// Read bits until we encounter a 1 (also called unary encoding). That's our quotient.
for (read_bit(r) or_return) != 1 {
for (read_bits(r, 1) or_return) != 1 {
quotient += 1
}
remainder := int(read_bits(r, uint(rice_parameter)) or_return)
@@ -352,7 +351,7 @@ decode_subframe :: proc(r: ^Reader, bps: u8, block_size: u16) -> (samples: []i32
for s in 0..<num_samples {
quotient := 0
// Read bits until we encounter a 1 (also called unary encoding). That's our quotient.
for (read_bit(r) or_return) != 1 {
for (read_bits(r, 1) or_return) != 1 {
quotient += 1
}
remainder := int(read_bits(r, uint(rice_parameter)) or_return)
@@ -395,22 +394,29 @@ decode_subframe :: proc(r: ^Reader, bps: u8, block_size: u16) -> (samples: []i32
return subframe_samples, nil
}
// TODO: remove
frame_num := 0
@(private)
decode_frame :: proc(
r: ^Reader,
flac: ^Flac,
md5_ctx: ^md5.Context,
options: Options,
) -> (
err: Error,
) #no_bounds_check {
//
// Frame Header
//
frame_beginning := r.i
//fmt.println("STARTING FRAME", frame_num)
//fmt.println("FRAME START AT OFFSET:", r.i)
tee_r: io.Tee_Reader
crc_buffer: bytes.Buffer
bytes.buffer_init_allocator(&crc_buffer, 0, 128)
defer bytes.buffer_destroy(&crc_buffer)
crc := bytes.buffer_to_stream(&crc_buffer)
r := &Reader{
r = io.tee_reader_init(&tee_r, r, crc),
buf = r.buf,
x = r.x,
n = r.n,
}
data := read_data(r, u16be) or_return
@@ -451,7 +457,7 @@ decode_frame :: proc(
case 2..=5:
actual_block_size = 576 * auto_cast (pow2(block_size - 2))
case 6:
actual_block_size = auto_cast (read_byte(r) or_return) + 1
actual_block_size = auto_cast (read_bits(r, 8) or_return) + 1
case 7:
actual_block_size = (read_data(r, u16be) or_return) + 1
case 8..=15:
@@ -491,7 +497,7 @@ decode_frame :: proc(
case ._96kHz:
actual_sample_rate_in_Hz = 96000
case .USE_8_BITS_IN_kHz_FROM_HEADER_END:
actual_sample_rate_in_Hz = auto_cast (read_byte(r) or_return) * 1000
actual_sample_rate_in_Hz = auto_cast (read_bits(r, 8) or_return) * 1000
case .USE_16_BITS_IN_Hz_FROM_HEADER_END:
actual_sample_rate_in_Hz = auto_cast (read_data(r, u16be) or_return)
case .USE_16_BITS_IN_TENS_OF_Hz_FROM_HEADER_END:
@@ -504,16 +510,9 @@ decode_frame :: proc(
// TODO: sample rate MUST NOT be 0 if the subframe contains audio.
// a sample rate of 0 MAY be used when non-audio is represented.
frame_header_end := r.i
frame_header_crc := read_byte(r) or_return
calculated_header_crc := calculate_crc8(r.s[frame_beginning:frame_header_end])
//fmt.printfln("block size bits: 0b%b", block_size)
//fmt.printfln("actual block size: %d sample(s)", actual_block_size)
//fmt.println("sample rate bits:", sample_rate)
//fmt.printfln("actual sample rate: %dHz", actual_sample_rate_in_Hz)
//fmt.println("channel assignment:", channel_assignment)
crc8_bytes := bytes.buffer_to_bytes(&crc_buffer)
frame_header_crc := u8(read_bits(r, 8) or_return)
calculated_header_crc := calculate_crc8(crc8_bytes)
if frame_header_crc != calculated_header_crc {
fmt.printfln("Expected frame header CRC: 0x%X, got: 0x%X", frame_header_crc, calculated_header_crc)
@@ -576,7 +575,6 @@ decode_frame :: proc(
// so many questions...
}
// TODO: remember to clean this up
subframes := make([][]i32, frame_channels)
defer delete(subframes)
for i in 0..<frame_channels {
@@ -594,6 +592,59 @@ decode_frame :: proc(
align_to_byte(r)
correlate(subframes, channel_assignment)
// Write the samples interleaved.
for i in 0..<len(subframes[0]) {
for subframe, sf in subframes {
sample := subframe[i]
append(&flac.samples, sample)
if .skip_md5_check not_in options {
switch bps {
case 1..=8:
md5.update(md5_ctx, {u8(sample)})
case 9..=16:
first_byte := u8(sample & 0xFF)
second_byte := u8((sample >> 8) & 0xFF)
md5.update(md5_ctx, {first_byte, second_byte})
case 17..=24:
first_byte := u8(sample & 0xFF)
second_byte := u8((sample >> 8) & 0xFF)
third_byte := u8((sample >> 16) & 0xFF)
md5.update(md5_ctx, {first_byte, second_byte, third_byte})
case 25..=32:
data := transmute([4]u8)sample
md5.update(md5_ctx, data[:])
case:
fmt.println("INVALID BPS")
os.exit(1)
}
}
}
}
for subframe in subframes {
delete(subframe)
}
//
// Frame Footer
//
crc16_bytes := bytes.buffer_to_bytes(&crc_buffer)
frame_crc := u16(read_data(r, u16be) or_return)
calculated_frame_crc := calculate_crc16(crc16_bytes)
if frame_crc != calculated_frame_crc {
fmt.eprintfln("Expected frame CRC 0x%X, got 0x%X", frame_crc, calculated_frame_crc)
return .CRC_Mismatch
}
return nil
}
@(private)
correlate :: proc(subframes: [][]i32, channel_assignment: ChannelAssignment) {
// Undo Stereo Decorrelation
#partial switch channel_assignment {
case .STEREO_MID_SIDE:
@@ -625,67 +676,11 @@ decode_frame :: proc(
subframes[1][i] = left - side
}
}
// TODO: Maybe this is slow if we have a lot of samples??
// The cases are ranges because streaminfo can define unusual bits per sample
for i in 0..<len(subframes[0]) {
for subframe in subframes {
sample := subframe[i]
switch bps {
case 1..=8:
md5.update(&md5_ctx, {u8(sample)})
case 9..=16:
first_byte := u8(sample & 0xFF)
second_byte := u8((sample >> 8) & 0xFF)
md5.update(&md5_ctx, {first_byte, second_byte})
case 17..=24:
first_byte := u8(sample & 0xFF)
second_byte := u8((sample >> 8) & 0xFF)
third_byte := u8((sample >> 16) & 0xFF)
md5.update(&md5_ctx, {first_byte, second_byte, third_byte})
case 25..=32:
data := transmute([4]u8)sample
md5.update(&md5_ctx, data[:])
case:
fmt.println("INVALID BPS")
os.exit(1)
}
}
}
// TODO: We need a way to copy (or better yet just point to) the decoded sample data
// We'll probably either copy each subframe's samples somehow, or have a dynamic array and append to that.
// Both are slow-ish but I can't think of a faster way that just works.
// This doesn't work btw, just for my future self to not think this is fine.
for subframe in subframes {
copy(flac.samples, subframe)
}
//
// Frame Footer
//
frame_end := r.i
frame_crc := read_data(r, u16be) or_return
calculated_frame_crc := calculate_crc16(r.s[frame_beginning:frame_end])
if auto_cast frame_crc != calculated_frame_crc {
fmt.eprintfln("Expected frame CRC 0x%X, got 0x%X", frame_crc, calculated_frame_crc)
return .CRC_Mismatch
}
frame_num += 1
return nil
}
load_from_bytes :: proc(data: []byte, options := Options{}, allocator := context.allocator) -> (flac: ^Flac, err: Error) {
r: Reader
bytes.reader_init(&r, data)
md5.init(&md5_ctx)
defer md5.reset(&md5_ctx)
header := read_data(&r, FlacHeader) or_return
@(private)
read_metadata :: proc(r: ^Reader, buffered := false, allocator := context.allocator) -> (flac: ^Flac, err: Error) {
header := read_data(r, FlacHeader) or_return
if header.magic != FLAC_MAGIC {
return nil, .Invalid_Signature
}
@@ -718,7 +713,7 @@ load_from_bytes :: proc(data: []byte, options := Options{}, allocator := context
pictures := make([dynamic]Picture, 0, 2, allocator)
last_block := streaminfo_header.last_block
for !last_block {
md_block_hdr := read_data(&r, u32be) or_return
md_block_hdr := read_data(r, u32be) or_return
metadata_block_header := MetadataBlockHeader {
last_block = bool(md_block_hdr >> 31),
type = BlockType((md_block_hdr >> 24) & 0x7F),
@@ -731,70 +726,95 @@ load_from_bytes :: proc(data: []byte, options := Options{}, allocator := context
case .STREAMINFO:
// Do nothing, we already parse the mandatory streaminfo metadata
case .PADDING:
//fmt.printfln("Skipping %d bytes of padding data", metadata_block_header.length)
bytes.reader_seek(&r, auto_cast metadata_block_header.length, .Current)
if buffered {
reader := cast(^bufio.Reader)r.data
skipped := bufio.reader_discard(reader, int(metadata_block_header.length)) or_return
} else {
reader := cast(^bytes.Reader)r.data
skipped := bytes.reader_seek(reader, i64(metadata_block_header.length), .Current) or_return
}
case .APPLICATION:
//app_id := read_slice(&r, 4) or_return
//fmt.println("Found application with ID:", string(app_id))
//fmt.printfln("Skipping %d bytes of application metadata block", metadata_block_header.length)
bytes.reader_seek(&r, auto_cast metadata_block_header.length, .Current)
// TODO: handle the application data
if buffered {
reader := cast(^bufio.Reader)r.data
skipped := bufio.reader_discard(reader, int(metadata_block_header.length)) or_return
} else {
reader := cast(^bytes.Reader)r.data
skipped := bytes.reader_seek(reader, i64(metadata_block_header.length), .Current) or_return
}
case .SEEKTABLE:
// TODO: check for size mismatch?
seekpoints := metadata_block_header.length / 18
//fmt.printfln("we have %d seekpoints", seekpoints)
actual_size := 0
for i in 0..<seekpoints {
_ = read_data(&r, Seekpoint) or_return
_ = read_data(r, Seekpoint) or_return
actual_size += size_of(Seekpoint)
//fmt.println(seekpoint)
}
if metadata_block_header.length != u32(actual_size) {
fmt.eprintln("Seektable metadata header length mismatch!")
fmt.eprintfln("Expected size: %d, Got %d", metadata_block_header.length, actual_size)
}
case .VORBIS_COMMENT:
// TODO: save this info to a struct
vendor_name_len := read_data(&r, u32le) or_return
vendor_name := string(read_slice(&r, auto_cast vendor_name_len) or_return)
user_comment_list_len := read_data(&r, u32le) or_return
vendor_name_len := read_data(r, u32le) or_return
vendor_name_slice := make([]byte, vendor_name_len)
read_slice(r, vendor_name_slice) or_return
vendor_name := string(vendor_name_slice)
user_comment_list_len := read_data(r, u32le) or_return
actual_size := 4 + vendor_name_len + 4
comments := make([]string, user_comment_list_len)
for i in 0..<user_comment_list_len {
comment_len := read_data(&r, u32le) or_return
comment := string(read_slice(&r, auto_cast comment_len) or_return)
//fmt.println(comment)
comment_len := read_data(r, u32le) or_return
comment_slice := make([]byte, comment_len)
read_slice(r, comment_slice) or_return
comment := string(comment_slice)
actual_size += 4 + comment_len
comments[i] = comment
}
flac.metadata.vorbis_comments = comments
flac.metadata.vendor_name = vendor_name
if metadata_block_header.length != u32(actual_size) {
// TODO: should this be a warning instead of an error that halts the decoding?
// I think so, the comment isn't crucial to the decoding process.
//fmt.println("Metadata header length mismatch!")
//fmt.printfln("Expected size: %d, Got %d", metadata_block_header.length, actual_size)
return flac, .Metadata_Block_Length_Mismatch
fmt.eprintln("Vorbis metadata header length mismatch!")
fmt.eprintfln("Expected size: %d, Got %d", metadata_block_header.length, actual_size)
}
// TODO: framing bit??
//fmt.println(vendor_name)
case .CUESHEET:
// TODO: save this to a struct
// + there's some CD-DA stuff that we need to consider I think
cuesheet := read_data(&r, CueSheet) or_return
cuesheet := read_data(r, CueSheet) or_return
if cuesheet.num_tracks < 1 {
// TODO: should this be a warning instead of an error that halts the decoding?
return flac, .Missing_Lead_Out_Track
}
//fmt.println(cuesheet)
for i in 0..<cuesheet.num_tracks {
track := read_data(&r, CueSheetTrack) or_return
track := read_data(r, CueSheetTrack) or_return
for i in 0..<track.num_track_index_points {
_ = read_data(&r, CueSheetTrackIndex) or_return
_ = read_data(r, CueSheetTrackIndex) or_return
//fmt.println(track_index)
}
//fmt.println(track)
}
case .PICTURE:
pic_type := PictureType(read_data(&r, u32be) or_return)
mime_len := read_data(&r, u32be) or_return
mimetype := string(read_slice(&r, auto_cast mime_len) or_return)
desc_len := read_data(&r, u32be) or_return
desc := string(read_slice(&r, auto_cast desc_len) or_return)
metadata := read_data(&r, PictureMetadata) or_return
data := read_slice(&r, auto_cast metadata.data_len) or_return
pic_type := PictureType(read_data(r, u32be) or_return)
mime_len := read_data(r, u32be) or_return
mimetype_slice := make([]byte, mime_len)
read_slice(r, mimetype_slice) or_return
mimetype := string(mimetype_slice)
desc_len := read_data(r, u32be) or_return
desc_slice := make([]byte, desc_len)
read_slice(r, desc_slice) or_return
desc := string(desc_slice)
metadata := read_data(r, PictureMetadata) or_return
data := make([]byte, metadata.data_len)
read_slice(r, data) or_return
picture := Picture{
type = pic_type,
mimetype = mimetype,
@@ -808,40 +828,334 @@ load_from_bytes :: proc(data: []byte, options := Options{}, allocator := context
append(&pictures, picture)
case .INVALID:
fmt.eprintln("Invalid block type")
bytes.reader_seek(&r, auto_cast metadata_block_header.length, .Current)
if buffered {
reader := cast(^bufio.Reader)r.data
skipped := bufio.reader_discard(reader, int(metadata_block_header.length)) or_return
} else {
reader := cast(^bytes.Reader)r.data
skipped := bytes.reader_seek(reader, i64(metadata_block_header.length), .Current) or_return
}
case:
fmt.printfln("Skipping %d bytes of Unknown metadata block", metadata_block_header.length)
bytes.reader_seek(&r, auto_cast metadata_block_header.length, .Current)
if buffered {
reader := cast(^bufio.Reader)r.data
skipped := bufio.reader_discard(reader, int(metadata_block_header.length)) or_return
} else {
reader := cast(^bytes.Reader)r.data
skipped := bytes.reader_seek(reader, i64(metadata_block_header.length), .Current) or_return
}
}
// Yucky
err = decode_frame(&r, flac)
for err == nil {
err = decode_frame(&r, flac)
}
flac.metadata.pictures = pictures[:]
return flac, nil
}
read_next_frame :: proc(r: ^Reader, flac: ^Flac) -> (frame: Frame, err: Error) {
//
// Frame Header
//
tee_r: io.Tee_Reader
crc_buffer: bytes.Buffer
bytes.buffer_init_allocator(&crc_buffer, 0, 128)
defer bytes.buffer_destroy(&crc_buffer)
crc := bytes.buffer_to_stream(&crc_buffer)
r := &Reader{
r = io.tee_reader_init(&tee_r, r, crc),
buf = r.buf,
x = r.x,
n = r.n,
}
data := read_data(r, u16be) or_return
sync_code := data >> 2
blocking_strategy := BlockingStrategy(data & 1)
data = read_data(r, u16be) or_return
block_size := data >> 12
sample_rate := SampleRate((data >> 8) & 0xF)
channel_assignment := ChannelAssignment((data >> 4) & 0xF)
sample_size := SampleSize((data >> 1) & 7)
coded_num := decode_extended_utf8(r) or_return
if blocking_strategy == .VARIABLE {
// Sample number of first sample
// MUST NOT be larger than 36 bits unencoded or 7 bytes encoded.
// MUST be equal to the number of samples preceding the current frame. Otherwise seeking is not possible.
// TODO: also write a check for the number of samples so we'll probably keep track of how many samples we decoded
// maybe the number of decoded samples can be inferred by the number of frames ?? or just use the length of
// the decoded_samples slice.
if coded_num > 0x1000000000 {
// TODO: Sample number can't be bigger than 36 bits
}
} else {
// Frame number
// MUST NOT be larger than 31 bits unencoded or 6 bytes encoded.
// MUST be equal to the number of frames preceding the current frame. Otherwise seeking is not possible.
// TODO: also write a check for the number of frames so we'll probably keep track of how many frames we decoded.
if coded_num > 0x80000000 {
// TODO: Frame number cannot be bigger than 31 bits
}
}
actual_block_size: u16be
switch block_size {
case 1:
actual_block_size = 192
case 2..=5:
actual_block_size = 576 * auto_cast (pow2(block_size - 2))
case 6:
actual_block_size = auto_cast (read_bits(r, 8) or_return) + 1
case 7:
actual_block_size = (read_data(r, u16be) or_return) + 1
case 8..=15:
actual_block_size = 256 * auto_cast (pow2(block_size - 8))
case:
return {}, .Invalid_Block_Size
}
//if actual_block_size < 15 && !last_frame {
// TODO: block sizes less than 16 are only valid for the last frame and MUST NOT be used for any other frame.
//}
actual_sample_rate_in_Hz: u32
switch sample_rate {
case .USE_STREAMINFO:
actual_sample_rate_in_Hz = flac.metadata.sample_rate
case ._88_2kHz:
actual_sample_rate_in_Hz = 88200
case ._176_4kHz:
actual_sample_rate_in_Hz = 176400
case ._192kHz:
actual_sample_rate_in_Hz = 192000
case ._8kHz:
actual_sample_rate_in_Hz = 8000
case ._16kHz:
actual_sample_rate_in_Hz = 16000
case ._22_05kHz:
actual_sample_rate_in_Hz = 22005
case ._24kHz:
actual_sample_rate_in_Hz = 24000
case ._32kHz:
actual_sample_rate_in_Hz = 32000
case ._44_1kHz:
actual_sample_rate_in_Hz = 44100
case ._48kHz:
actual_sample_rate_in_Hz = 48000
case ._96kHz:
actual_sample_rate_in_Hz = 96000
case .USE_8_BITS_IN_kHz_FROM_HEADER_END:
actual_sample_rate_in_Hz = auto_cast (read_bits(r, 8) or_return) * 1000
case .USE_16_BITS_IN_Hz_FROM_HEADER_END:
actual_sample_rate_in_Hz = auto_cast (read_data(r, u16be) or_return)
case .USE_16_BITS_IN_TENS_OF_Hz_FROM_HEADER_END:
actual_sample_rate_in_Hz = auto_cast (read_data(r, u16be) or_return) * 10
case .INVALID: fallthrough
case:
return {}, .Invalid_Sample_Rate
}
// TODO: sample rate MUST NOT be 0 if the subframe contains audio.
// a sample rate of 0 MAY be used when non-audio is represented.
crc8_bytes := bytes.buffer_to_bytes(&crc_buffer)
frame_header_crc := u8(read_bits(r, 8) or_return)
calculated_header_crc := calculate_crc8(crc8_bytes)
if frame_header_crc != calculated_header_crc {
fmt.printfln("Expected frame header CRC: 0x%X, got: 0x%X", frame_header_crc, calculated_header_crc)
return {}, .CRC_Mismatch
}
//
// Subframes
//
bps: u8
switch sample_size {
case .USE_STREAMINFO:
bps = flac.metadata.bits_per_sample
case ._8BPS:
bps = 8
case ._12BPS:
bps = 12
case ._16BPS:
bps = 16
case ._20BPS:
bps = 20
case ._24BPS:
bps = 24
case ._32BPS:
bps = 32
}
if bps != flac.metadata.bits_per_sample {
return {}, .Bits_Per_Second_Mismatch
}
frame_channels := flac.metadata.channels
switch channel_assignment {
case .MONO:
frame_channels = 1
case ._3CHANNELS:
frame_channels = 3
case ._4CHANNELS:
frame_channels = 4
case ._5CHANNELS:
frame_channels = 5
case ._6CHANNELS:
frame_channels = 6
case ._7CHANNELS:
frame_channels = 7
case ._8CHANNELS:
frame_channels = 8
case ._2CHANNELS: fallthrough
case .STEREO_LEFT_SIDE: fallthrough
case .STEREO_SIDE_RIGHT: fallthrough
case .STEREO_MID_SIDE:
frame_channels = 2
}
if frame_channels != flac.metadata.channels {
// TODO: warning or error?
// we have a faulty file that says it has 5 channels but subframes all say they got 1
// we also have an two uncommon files with increasing and decreasing number of channels
// uncommon are supposed to be unusual but still valid FLAC files. why is that first one faulty then???
// so many questions...
}
subframes := make([][]i32, frame_channels)
defer delete(subframes)
for i in 0..<frame_channels {
frame_bps := bps
// For side channels we increase the bps by 1
// Left, mid and right channels don't need an extra bit.
// ref: https://github.com/ietf-wg-cellar/flac-specification/blob/master/rfc_backmatter.md#first-audio-frame
if (channel_assignment == .STEREO_SIDE_RIGHT && i == 0) ||
((channel_assignment == .STEREO_LEFT_SIDE || channel_assignment == .STEREO_MID_SIDE) && i == 1) {
frame_bps += 1
}
subframes[i] = decode_subframe(r, frame_bps, auto_cast actual_block_size) or_return
}
align_to_byte(r)
correlate(subframes, channel_assignment)
samples_arr := make([dynamic]i32, 0, block_size * 2)
// Write the samples interleaved.
for i in 0..<len(subframes[0]) {
for subframe, sf in subframes {
sample := subframe[i]
append(&samples_arr, sample)
}
}
for subframe in subframes {
delete(subframe)
}
frame.samples = samples_arr[:]
frame.channels = frame_channels
frame.sample_rate = actual_sample_rate_in_Hz
//
// Frame Footer
//
crc16_bytes := bytes.buffer_to_bytes(&crc_buffer)
frame_crc := u16(read_data(r, u16be) or_return)
calculated_frame_crc := calculate_crc16(crc16_bytes)
if frame_crc != calculated_frame_crc {
fmt.eprintfln("Expected frame CRC 0x%X, got 0x%X", frame_crc, calculated_frame_crc)
return {}, .CRC_Mismatch
}
return frame, nil
}
md5hash :: proc(md5_ctx: ^md5.Context, bps: u8, samples: []i32) {
for sample in samples {
switch bps {
case 1..=8:
md5.update(md5_ctx, {u8(sample)})
case 9..=16:
first_byte := u8(sample & 0xFF)
second_byte := u8((sample >> 8) & 0xFF)
md5.update(md5_ctx, {first_byte, second_byte})
case 17..=24:
first_byte := u8(sample & 0xFF)
second_byte := u8((sample >> 8) & 0xFF)
third_byte := u8((sample >> 16) & 0xFF)
md5.update(md5_ctx, {first_byte, second_byte, third_byte})
case 25..=32:
data := transmute([4]u8)sample
md5.update(md5_ctx, data[:])
case:
fmt.println("INVALID BPS")
os.exit(1)
}
}
}
md5sum :: proc(md5_ctx: ^md5.Context, flac: ^Flac) -> Error {
calculated_md5: [16]byte
md5.final(md5_ctx, calculated_md5[:])
flac.metadata.calculated_md5 = calculated_md5
if mem.compare(calculated_md5[:], flac.metadata.expected_md5[:]) != 0 {
return .MD5_Mismatch
}
return nil
}
// `load_from_bytes` takes a byte slice and initializes a bitreader for it.
// The data is expected to be a full flac stream.
load_from_bytes :: proc(data: []byte, options := Options{}, allocator := context.allocator) -> (flac: ^Flac, err: Error) {
context.allocator = allocator
byte_r: bytes.Reader
bytes.reader_init(&byte_r, data)
r := Reader{bytes.reader_to_stream(&byte_r), 0, 0, 0}
flac = read_metadata(&r) or_return
if .only_return_metadata in options {
return flac, nil
}
md5_ctx: md5.Context
md5.init(&md5_ctx)
defer md5.reset(&md5_ctx)
for {
err = decode_frame(&r, flac, &md5_ctx, options)
if err != nil {
if err == .EOF {
break
} else {
return flac, err
}
}
}
if err != .EOF {
return flac, err
}
//
// Validate the decoded audio data's md5
//
calculated_md5: [16]byte
md5.final(&md5_ctx, calculated_md5[:])
flac.metadata.calculated_md5 = calculated_md5
if mem.compare(calculated_md5[:], header.streaminfo.md5[:]) != 0 {
return flac, .MD5_Mismatch
if .skip_md5_check not_in options {
md5sum(&md5_ctx, flac) or_return
}
return flac, nil
}
// `load_from_file` reads the entire file to memory and operates on the returned byte slice.
// Only use this with small files.
load_from_file :: proc(filename: string, options := Options{}, allocator := context.allocator) -> (flac: ^Flac, err: Error) {
context.allocator = allocator
@@ -849,22 +1163,71 @@ load_from_file :: proc(filename: string, options := Options{}, allocator := cont
defer delete(data)
if ok {
return load_from_bytes(data)
return load_from_bytes(data, options)
} else {
return nil, .Unable_To_Read_File
}
}
destroy :: proc(flac: ^Flac) {
// TODO: clean up the allocations
delete(flac.samples)
// TODO: I think this data gets deleted when the data from os.read_entire_file is deleted.
// Double check that and if it's true then make sure to copy this data.
// Copying small strings is fine but copying the image pixel data might be slow.
//for picture in flac.metadata.pictures {
// picture.mimetype
// picture.data
// picture.description
//}
delete(flac.metadata.pictures)
// `load_from_file_buffered` initializes a buffered reader for the file and reads all the metadata blocks.
// Calls to `read_next_frame()` MUST be made to decode the audio data.
// This is the recommended way to read most FLAC files as it has a minimal memory overhead.
load_from_file_buffered :: proc(filename: string, allocator := context.allocator) -> (flac: ^Flac, r: ^Reader, err: Error) {
context.allocator = allocator
file, open_err := os.open(filename)
if open_err != os.ERROR_NONE {
fmt.eprintln(open_err)
return nil, {}, .Unable_To_Read_File
}
br := new(bufio.Reader)
bufio.reader_init(br, os.stream_from_handle(file))
r = new(Reader)
r.r = bufio.reader_to_stream(br)
r.buf = 0
r.x = 0
r.n = 0
flac = read_metadata(r, true) or_return
return flac, r, nil
}
/* `destroy` cleans up and frees the memory allocated by all 3 loading functions.
The returned reader from `load_from_file_buffered` MUST be passed to `destroy` in order to close the file handle.
`load_from_file` and `load_from_bytes` may pass nil as the reader.
---
The allocator that was used when loading the flac file MUST be passed to `destroy` in order to properly free
the memory, otherwise a bad free may occur and crash the program.
*/
destroy :: proc(flac: ^Flac, r: ^Reader = nil, allocator := context.allocator) {
context.allocator = allocator
delete(flac.samples)
for picture in flac.metadata.pictures {
delete(picture.mimetype)
delete(picture.data)
delete(picture.description)
}
delete(flac.metadata.pictures)
for comment in flac.metadata.vorbis_comments {
delete(comment)
}
delete(flac.metadata.vorbis_comments)
delete(flac.metadata.vendor_name)
free(flac)
if r != nil {
br := cast(^bufio.Reader)r.data
closer, ok := io.to_closer(br.rd)
io.close(closer)
bufio.reader_destroy(br)
free(br)
free(r)
}
}
+2 -7
View File
@@ -79,11 +79,6 @@ crc16_lookup := [256]u16 {
fixed_coefficients := [4][]i32{{1}, {2, -1}, {3, -3, 1}, {4, -6, 4, -1}}
// TODO: no idea if I want to keep this global and a thread_local
// or if I should just init it in load_from_bytes
@(thread_local)
md5_ctx: md5.Context
BlockType :: enum u16be {
STREAMINFO,
PADDING,
@@ -253,7 +248,7 @@ decode_extended_utf8 :: proc(r: ^Reader) -> (decoded_num: u64, err: Error) {
decoded_num = u64(0)
first_byte := read_byte(r) or_return
first_byte := read_bits(r, 8) or_return
utf8_sequence_len := 0
for i: uint = 7; i > 0; i -= 1 {
@@ -269,7 +264,7 @@ decode_extended_utf8 :: proc(r: ^Reader) -> (decoded_num: u64, err: Error) {
octets := make([dynamic]byte, 0, 8)
defer delete(octets)
for i := utf8_sequence_len - 1; i > 0; i -= 1 {
append(&octets, read_byte(r) or_return)
append(&octets, byte(read_bits(r, 8) or_return))
}
switch utf8_sequence_len {
+58 -92
View File
@@ -8,119 +8,85 @@ import "core:io"
Uses core:bytes reader under the hood with a few useful procedures for reading structs and other types.
*/
Reader :: struct {
using _: bytes.Reader,
bits_read: u8,
using r: io.Stream,
// Temporary read buffer.
buf: [8]u8,
// Between 0 and 7 buffered bits since previous read operations.
x: u8,
// The number of buffered bits in x.
n: uint,
}
read_slice :: #force_inline proc(r: ^Reader, size: int) -> (res: []u8, err: io.Error) {
#no_bounds_check {
if r.i + i64(size) <= i64(len(r.s)) {
res = r.s[r.i:r.i + i64(size)]
r.i += i64(size)
return res, .None
}
}
read_slice :: #force_inline proc(r: ^Reader, buf: []u8) -> io.Error {
io.read_full(r^, buf) or_return
if i64(len(r.s)) == r.i {
return []u8{}, .EOF
} else {
return []u8{}, .Short_Buffer
}
return nil
}
read_data :: #force_inline proc(r: ^Reader, $T: typeid) -> (res: T, err: io.Error) {
b := read_slice(r, size_of(T)) or_return
b: [size_of(T)]byte
read_slice(r, b[:]) or_return
return (^T)(&b[0])^, nil
}
read_bit :: proc(r: ^Reader) -> (bit: byte, err: Error) #no_bounds_check {
if r.i >= i64(len(r.s)) {
return 0, .EOF
// Read reads and returns the next n bits, at most 64. It buffers bits up to the
// next byte boundary.
// Borrowed from https://github.com/mewkiz/flac
read_bits :: proc(r: ^Reader, n: uint) -> (res: u64, err: io.Error) #no_bounds_check {
if n == 0 {
return 0, nil
}
bits := 8 - (r.bits_read + 1)
b := (r.s[r.i] >> bits) & 1
r.bits_read += 1
n := n
if r.bits_read == 8 {
r.i += 1
r.bits_read = 0
}
return b, nil
}
read_bits :: proc(r: ^Reader, n: uint) -> (bits: u64, err: Error) #no_bounds_check {
res: u64
// Fast path: if we can read all bits within the current byte.
if r.bits_read + u8(n) <= 8 {
res = (u64(r.s[r.i]) >> (8 - r.bits_read - u8(n))) & ((1 << n) - 1)
r.bits_read += u8(n)
if r.bits_read == 8 {
r.bits_read = 0
r.i += 1
}
// Read buffered bits.
if r.n > 0 {
switch {
case r.n == n:
r.n = 0
return u64(r.x), nil
case r.n > n:
r.n -= n
mask := ~u8(0) << r.n
res = u64(r.x&mask) >> r.n
r.x &~= mask
return res, nil
}
// General case: reading across multiple bytes.
rem_bits := n
if r.bits_read > 0 {
// Read the remaining bits in the current byte.
bits_in_byte := 8 - r.bits_read
res = u64(r.s[r.i]) & ((1 << bits_in_byte) - 1)
rem_bits -= uint(bits_in_byte)
r.bits_read = 0
r.i += 1
n -= r.n
res = u64(r.x)
r.n = 0
}
// Read full bytes.
for rem_bits >= 8 {
res = (res << 8) | u64(r.s[r.i])
rem_bits -= 8
r.i += 1
// Fill the temporary buffer.
bytes := n / 8
bits := n % 8
if bits > 0 {
bytes += 1
}
io.read_full(r, r.buf[:bytes]) or_return
// Read remaining bits in the final byte.
if rem_bits > 0 {
res = (res << rem_bits) | (u64(r.s[r.i]) >> (8 - rem_bits))
r.bits_read += u8(rem_bits)
// Read bits from the temporary buffer.
for b in r.buf[:bytes-1] {
res <<= 8
res |= u64(b)
}
return res, nil
}
read_byte :: proc(r: ^Reader) -> (b: byte, err: Error) #no_bounds_check {
if r.i >= i64(len(r.s)) {
return 0, .EOF
}
// Continue reading from the current byte if it has unread bits.
if r.bits_read != 0 {
cur_byte := r.s[r.i]
next_byte := r.s[r.i + 1]
rem_bits := 8 - r.bits_read
rem_bits_mask: u8 = (1 << rem_bits) - 1
b = ((cur_byte & rem_bits_mask) << r.bits_read) | (next_byte >> rem_bits)
r.i += 1
b := r.buf[bytes-1]
if bits > 0 {
res <<= bits
r.n = 8 - bits
mask := ~u8(0) << r.n
res |= u64(b&mask) >> r.n
r.x = b & ~mask
} else {
b = r.s[r.i]
r.i += 1
res <<= 8
res |= u64(b)
}
return b, nil
return res, nil
}
align_to_byte :: proc(r: ^Reader) -> Error {
if r.bits_read == 0 {
return nil
}
if r.i >= i64(len(r.s)) {
return .EOF
}
// Align by skipping the remaining bits in the byte
r.bits_read = 0
r.i += 1
return nil
align_to_byte :: proc(r: ^Reader) {
r.x = 0
r.n = 0
}
File diff suppressed because it is too large Load Diff