Skip to content

Commit

Permalink
Prepare to publish (#15)
Browse files Browse the repository at this point in the history
* Documentation and privitization

* Adding util tests

* Finish testing and moving things around

* Fix analysis
  • Loading branch information
liamappelbe authored Aug 19, 2023
1 parent 79f9c7d commit 7acd05f
Show file tree
Hide file tree
Showing 16 changed files with 378 additions and 250 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 1.3.0

- Support raw audio files, which are essentially Wav files without their format
header (ie just the samples).
- Expose some of the internal utils, which may be useful for other serialization
tasks (eg BytesReader and BytesWriter).

## 1.2.0

- Added a duration method to Wav.
Expand Down
24 changes: 15 additions & 9 deletions test/wav_util_test.dart → example/info.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2022 The wav authors
// Copyright 2023 The wav authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -12,13 +12,19 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import 'package:test/test.dart';
import 'package:wav/wav_utils.dart';
import 'package:wav/wav.dart';

void main() async {
test('clamp returns value within range', () {
expect(WavUtils.clamp(-1, 10), 0);
expect(WavUtils.clamp(5, 10), 5);
expect(WavUtils.clamp(15, 10), 10);
});
void main(List<String> argv) async {
if (argv.length != 1) {
print('Wrong number of args. Usage:');
print(' dart run info.dart input.wav');
return;
}

final wav = await Wav.readFile(argv[0]);
print(argv[0]);
print('Format: ${wav.format}');
print('Channels: ${wav.channels.length}');
print('Sample rate: ${wav.samplesPerSecond} Hz');
print('Duration: ${wav.duration.toStringAsFixed(3)} sec');
}
103 changes: 103 additions & 0 deletions lib/bytes_reader.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright 2022 The wav authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import 'dart:typed_data';

import 'util.dart';
import 'wav_format.dart';

/// Utility class to incrementally read through a series of bytes, interpreting
/// byte combinations as little endian ints and floats etc. Every read operation
/// moves the read head forward by the corresponding number of bytes.
class BytesReader {
final Uint8List _bytes;
int _p = 0;

/// Constructs a [BytesReader].
BytesReader(this._bytes);

/// Skip forward [n] bytes.
void skip(int n) {
_p += n;
if (_p > _bytes.length) {
throw FormatException('WAV is corrupted, or not a WAV file.');
}
}

ByteData _read(int n) {
final p0 = _p;
skip(n);
return ByteData.sublistView(_bytes, p0, _p);
}

/// Reads a Uint8 from the buffer.
int readUint8() => _read(1).getUint8(0);

/// Reads a Uint16 from the buffer.
int readUint16() => _read(2).getUint16(0, Endian.little);

/// Reads a Uint24 from the buffer.
int readUint24() => readUint8() + 0x100 * readUint16();

/// Reads a Uint32 from the buffer.
int readUint32() => _read(4).getUint32(0, Endian.little);

bool _checkString(String s) =>
s ==
String.fromCharCodes(
Uint8List.sublistView(_read(s.length)),
);

/// Reads a string of the same length as [s], then checks that the read string
/// matches [s]. Throws a [FormatException] if they don't match. [s] must be
/// ASCII only.
void assertString(String s) {
if (!_checkString(s)) {
throw FormatException('WAV is corrupted, or not a WAV file.');
}
}

/// Reads RIFF chunks until one is found that has the given [identifier]. When
/// this function returns, the read head will either be just after the
/// [identifier] (about to read the size), or at the end of the buffer.
void findChunk(String identifier) {
while (!_checkString(identifier)) {
final size = readUint32();
skip(roundUpToEven(size));
}
}

double _readSample8bit() => intToSample(readUint8(), 8);
double _readSample16Bit() => intToSample(fold(readUint16(), 16), 16);
double _readSample24Bit() => intToSample(fold(readUint24(), 24), 24);
double _readSample32Bit() => intToSample(fold(readUint32(), 32), 32);
double _readSampleFloat32() => _read(4).getFloat32(0, Endian.little);
double _readSampleFloat64() => _read(8).getFloat64(0, Endian.little);

/// Returns a closure that reads samples of the given [format] from this
/// buffer. Calling these closures advances the read head of this buffer.
SampleReader getSampleReader(WavFormat format) {
return [
_readSample8bit,
_readSample16Bit,
_readSample24Bit,
_readSample32Bit,
_readSampleFloat32,
_readSampleFloat64,
][format.index];
}
}

/// Reads a sample and returns it as a double, usually in the range [-1, 1].
typedef SampleReader = double Function();
82 changes: 82 additions & 0 deletions lib/bytes_writer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright 2022 The wav authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import 'dart:typed_data';

import 'util.dart';
import 'wav_format.dart';

/// Utility class to construct a byte buffer by writing little endian ints and
/// floats etc. Every write operation appends to the end of the buffer.
class BytesWriter {
final _bytes = BytesBuilder();

/// Writes a Uint8 to the buffer.
void writeUint8(int x) => _bytes.addByte(x);

/// Writes a Uint16 to the buffer.
void writeUint16(int x) {
writeUint8(x);
writeUint8(x >> 8);
}

/// Writes a Uint24 to the buffer.
void writeUint24(int x) {
writeUint16(x);
writeUint8(x >> 16);
}

/// Writes a Uint32 to the buffer.
void writeUint32(int x) {
writeUint24(x);
writeUint8(x >> 24);
}

void _writeSample8Bit(double x) => writeUint8(sampleToInt(x, 8));
void _writeSample16Bit(double x) => writeUint16(fold(sampleToInt(x, 16), 16));
void _writeSample24Bit(double x) => writeUint24(fold(sampleToInt(x, 24), 24));
void _writeSample32Bit(double x) => writeUint32(fold(sampleToInt(x, 32), 32));

void _writeBytes(ByteData b, int n) => _bytes.add(b.buffer.asUint8List(0, n));

static final _fbuf = ByteData(8);
void _writeSampleFloat32(double x) =>
_writeBytes(_fbuf..setFloat32(0, x, Endian.little), 4);
void _writeSampleFloat64(double x) =>
_writeBytes(_fbuf..setFloat64(0, x, Endian.little), 8);

/// Writes string [s] to the buffer. [s] must be ASCII only.
void writeString(String s) {
for (int c in s.codeUnits) {
_bytes.addByte(c);
}
}

/// Returns a closure that reads samples of the given [format] from this
/// buffer. Calling these closures advances the read head of this buffer.
SampleWriter getSampleWriter(WavFormat format) => [
_writeSample8Bit,
_writeSample16Bit,
_writeSample24Bit,
_writeSample32Bit,
_writeSampleFloat32,
_writeSampleFloat64,
][format.index];

/// Takes the byte buffer from [this] and clears [this].
Uint8List takeBytes() => _bytes.takeBytes();
}

/// Writes a sample, usually clamping it to the range [-1, 1].
typedef SampleWriter = void Function(double);
22 changes: 13 additions & 9 deletions lib/raw_file.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@

import 'dart:typed_data';

import 'wav_bytes_reader.dart';
import 'wav_bytes_writer.dart';
import 'bytes_reader.dart';
import 'bytes_writer.dart';
import 'wav_format.dart';
import 'wav_no_io.dart' if (dart.library.io) 'wav_io.dart';
import 'wav_types.dart';

/// Reads raw audio data from a file.
///
Expand All @@ -31,8 +31,11 @@ Future<List<Float64List>> readRawAudioFile(

/// Reads raw audio samples from a byte buffer.
///
/// As the buffer won't contain metadata, the format and number of channels
/// must be must be known/specified as arguments.
/// Raw audio files are essentially Wav files without a format header. The
/// format header tells the reader important metadata like the number of
/// channels and the sample format. As the input won't contain that metadata,
/// the format and number of channels must be must be known by the user, and
/// specified as arguments.
List<Float64List> readRawAudio(
Uint8List bytes,
int numChannels,
Expand All @@ -56,7 +59,7 @@ List<Float64List> readRawAudio(
}

// Read samples.
final byteReader = WavBytesReader(bytes);
final byteReader = BytesReader(bytes);
final readSample = byteReader.getSampleReader(format);
for (int i = 0; i < numSamples; ++i) {
for (int j = 0; j < numChannels; ++j) {
Expand All @@ -78,8 +81,9 @@ Future<void> writeRawAudioFile(

/// Writes raw audio samples to a byte buffer.
///
/// This will not write any meta-data to the buffer (bits per sample,
/// number of channels or sample rate).
/// Raw audio files are essentially Wav files without a format header. So this
/// function will not write any metadata to the buffer (bits per sample, number
/// of channels or sample rate).
Uint8List writeRawAudio(List<Float64List> channels, WavFormat format) {
// Calculate sizes etc.
final numChannels = channels.length;
Expand All @@ -89,7 +93,7 @@ Uint8List writeRawAudio(List<Float64List> channels, WavFormat format) {
}

// Write samples.
final bytes = WavBytesWriter();
final bytes = BytesWriter();
final writeSample = bytes.getSampleWriter(format);
for (int i = 0; i < numSamples; ++i) {
for (int j = 0; j < numChannels; ++j) {
Expand Down
40 changes: 40 additions & 0 deletions lib/util.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright 2022 The wav authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/// Returns the closest number to [x] int the range [0, y].
int clamp(int x, int y) => x < 0
? 0
: x > y
? y
: x;

/// Shifts int [x] of bit width [bits] up by half the total range, then wraps
/// any overflowing values around to maintain the bit width. This is used to
/// convert between signed and unsigned PCM.
int fold(int x, int bits) => (x + (1 << (bits - 1))) % (1 << bits);

/// Rounds [x] up to the nearest even number.
int roundUpToEven(int x) => x + (x % 2);

double _writeScale(int bits) => (1 << (bits - 1)) * 1.0;
double _readScale(int bits) => _writeScale(bits) - 0.5;

/// Converts an audio sample [x] in the range [-1, 1] to an unsigned integer of
/// bit width [bits].
int sampleToInt(double x, int bits) =>
clamp(((x + 1) * _writeScale(bits)).floor(), (1 << bits) - 1);

/// Converts an int [x] of bit width [bits] to an audio sample in the range
/// [-1, 1].
double intToSample(int x, int bits) => (x / _readScale(bits)) - 1;
2 changes: 1 addition & 1 deletion lib/wav.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@
// limitations under the License.

export 'wav_file.dart';
export 'wav_types.dart';
export 'wav_format.dart';
Loading

0 comments on commit 7acd05f

Please sign in to comment.