├── CHANGELOG.md
├── .idea
├── misc.xml
├── libraries
│ ├── Flutter_Plugins.xml
│ └── Dart_Packages.xml
└── modules.xml
├── lib
├── src
│ ├── chunk.dart
│ ├── byte_helpers.dart
│ ├── data_chunk32.dart
│ ├── data_chunk16.dart
│ ├── generator_function.dart
│ ├── wave_header.dart
│ ├── data_chunk8.dart
│ └── format_chunk.dart
└── wave_generator.dart
├── .gitignore
├── pubspec.yaml
├── .metadata
├── analysis_options.yaml
├── LICENSE
├── README.md
└── test
├── wave_generator_test.dart
├── data_chunk_8_test.dart
└── format_chunk_test.dart
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [0.1.0] - Initial open source version
2 |
3 | * Generate 8 bit audio
4 | * Sin / Square / Triangle waves
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/libraries/Flutter_Plugins.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/lib/src/chunk.dart:
--------------------------------------------------------------------------------
1 | abstract class Chunk {
2 | String get sGroupId;
3 |
4 | int get length;
5 |
6 | Stream bytes();
7 | }
8 |
9 | abstract class DataChunk extends Chunk {
10 | int get bytesPadding;
11 | }
12 |
--------------------------------------------------------------------------------
/lib/src/byte_helpers.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 | import 'dart:typed_data';
3 |
4 | class ByteHelpers {
5 | static Uint8List toBytes(String str) {
6 | var encoder = AsciiEncoder();
7 | return encoder.convert(str);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Files and directories created by pub
2 | .dart_tool/
3 | .packages
4 | # Remove the following pattern if you wish to check in your lock file
5 | pubspec.lock
6 |
7 | # Conventional directory for build outputs
8 | build/
9 |
10 | # Directory created by dartdoc
11 | doc/api/
12 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: wave_generator
2 | description: A dart package to generate audio wave data on the fly
3 | version: 0.1.0
4 | homepage: https://github.com/patchandthat/wave-generator
5 |
6 | environment:
7 | sdk: '>=2.12.0 <3.0.0'
8 |
9 | dev_dependencies:
10 | lints: ^1.0.1
11 | test: ^1.20.2
12 |
--------------------------------------------------------------------------------
/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: 985ccb6d14c6ce5ce74823a4d366df2438eac44f
8 | channel: beta
9 |
10 | project_type: package
11 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # Defines a default set of lint rules enforced for
2 | # projects at Google. For details and rationale,
3 | # see https://github.com/dart-lang/pedantic#enabled-lints.
4 | include: package:lints/recommended.yaml
5 |
6 | # For lint rules and documentation, see http://dart-lang.github.io/linter/lints.
7 | # Uncomment to specify additional rules.
8 | # linter:
9 | # rules:
10 | # - camel_case_types
11 |
12 | analyzer:
13 | # exclude:
14 | # - path/to/excluded/files/**
15 |
--------------------------------------------------------------------------------
/lib/src/data_chunk32.dart:
--------------------------------------------------------------------------------
1 | import 'chunk.dart';
2 |
3 | class DataChunk32 implements DataChunk {
4 | final String _sGroupId = 'data';
5 | // int _dwChunkSize;
6 | // List _data;
7 |
8 | static const double min = -1.0;
9 | static const double max = 1.0;
10 |
11 | @override
12 | Stream bytes() {
13 | throw UnimplementedError();
14 | }
15 |
16 | @override
17 | // TODO: implement length
18 | int get length => throw UnimplementedError();
19 |
20 | @override
21 | // TODO: implement sGroupId
22 | String get sGroupId => _sGroupId;
23 |
24 | @override
25 | // TODO: implement bytesPadding
26 | int get bytesPadding => throw UnimplementedError();
27 | }
28 |
--------------------------------------------------------------------------------
/lib/src/data_chunk16.dart:
--------------------------------------------------------------------------------
1 | import 'chunk.dart';
2 |
3 | class DataChunk16 implements DataChunk {
4 | final String _sGroupId = 'data';
5 | // int _dwChunkSize;
6 | // Int16List _data;
7 |
8 | // nb- stored as two's-complement form
9 |
10 | static const int min = -32760;
11 | static const int max = 32760;
12 |
13 | @override
14 | Stream bytes() {
15 | throw UnimplementedError();
16 | }
17 |
18 | @override
19 | // TODO: implement length
20 | int get length => throw UnimplementedError();
21 |
22 | @override
23 | // TODO: implement sGroupId
24 | String get sGroupId => _sGroupId;
25 |
26 | @override
27 | // TODO: implement bytesPadding
28 | int get bytesPadding => throw UnimplementedError();
29 | }
30 |
--------------------------------------------------------------------------------
/lib/src/generator_function.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math';
2 |
3 | import '../wave_generator.dart';
4 |
5 | abstract class GeneratorFunction {
6 | double generate(double theta);
7 |
8 | static GeneratorFunction create(Waveform type) {
9 | switch (type) {
10 | case Waveform.sine:
11 | return SinGenerator();
12 | case Waveform.triangle:
13 | return TriangleGenerator();
14 | case Waveform.square:
15 | return SquareGenerator();
16 | }
17 | }
18 | }
19 |
20 | class SinGenerator implements GeneratorFunction {
21 | @override
22 | double generate(double theta) {
23 | return sin(theta);
24 | }
25 | }
26 |
27 | class SquareGenerator implements GeneratorFunction {
28 | @override
29 | double generate(double theta) {
30 | return sin(theta) > 0 ? 1.0 : -1.0;
31 | }
32 | }
33 |
34 | class TriangleGenerator implements GeneratorFunction {
35 | @override
36 | double generate(double theta) {
37 | return theta % (2 * pi);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Patrick Allwood
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # wave_generator
2 |
3 | A dart package to generate audio wave data on the fly.
4 |
5 | ## Usage
6 |
7 | To use this plugin, add `wave_generator` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/).
8 |
9 | ### Example
10 |
11 | ``` dart
12 | import 'package:wave_generator/wave_generator.dart';
13 |
14 | () async {
15 |
16 | var generator = WaveGenerator(
17 | /* sample rate */ 44100,
18 | BitDepth.Depth8bit);
19 |
20 | var note = Note(
21 | /* frequency */ 220,
22 | /* msDuration */ 3000,
23 | /* waveform */ Waveform.Triangle,
24 | /* volume */ 0.5);
25 |
26 | var file = new File('output.wav');
27 |
28 | List bytes = List();
29 | await for (int byte in generator.generate(note)) {
30 | bytes.add(byte);
31 | }
32 |
33 | file.writeAsBytes(bytes, mode: FileMode.append);
34 | });
35 | ```
36 |
37 | Or string together a sequence of Notes
38 |
39 | ``` dart
40 |
41 | await for (int byte in generator.generateSequence([note1, note2, note3 /* etc */])) {
42 | // ...
43 | }
44 |
45 | ```
46 |
47 | ### Features
48 |
49 | * Sin, Square, Triangle waves
50 | * 8 Bit depth
--------------------------------------------------------------------------------
/lib/src/wave_header.dart:
--------------------------------------------------------------------------------
1 | import 'dart:typed_data';
2 |
3 | import 'byte_helpers.dart';
4 | import 'chunk.dart';
5 | import 'format_chunk.dart';
6 |
7 | class WaveHeader implements Chunk {
8 | final String _sGroupId = 'RIFF';
9 | final String _sRifType = 'WAVE';
10 |
11 | final FormatChunk formatChunk;
12 | final DataChunk dataChunk;
13 |
14 | const WaveHeader(this.formatChunk, this.dataChunk);
15 |
16 | @override
17 | int get length =>
18 | 4 +
19 | (8 + formatChunk.length) +
20 | (8 + dataChunk.length + dataChunk.bytesPadding);
21 |
22 | @override
23 | String get sGroupId => _sGroupId;
24 |
25 | @override
26 | Stream bytes() async* {
27 | var strBytes = ByteHelpers.toBytes(_sGroupId);
28 | var bytes = strBytes.buffer.asByteData();
29 | for (int i = 0; i < 4; i++) {
30 | yield bytes.getUint8(i);
31 | }
32 |
33 | var byteData = ByteData(4);
34 | byteData.setUint32(0, length, Endian.little);
35 | for (int i = 0; i < 4; i++) {
36 | yield byteData.getUint8(i);
37 | }
38 |
39 | strBytes = ByteHelpers.toBytes(_sRifType);
40 | bytes = strBytes.buffer.asByteData();
41 | for (int i = 0; i < 4; i++) {
42 | yield bytes.getUint8(i);
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/test/wave_generator_test.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:test/test.dart';
4 | import 'package:wave_generator/wave_generator.dart';
5 |
6 | void main() {
7 | test('single-tone', () async {
8 | var generator = WaveGenerator(/* sample rate */ 44100, BitDepth.depth8Bit);
9 |
10 | var note = Note(/* frequency */ 220, /* msDuration */ 3000,
11 | /* waveform */ Waveform.triangle, /* volume */ 0.5);
12 |
13 | var file = File('test_out.wav');
14 |
15 | List bytes = [];
16 | await for (int byte in generator.generate(note)) {
17 | bytes.add(byte);
18 | }
19 |
20 | file.writeAsBytes(bytes, mode: FileMode.append);
21 | });
22 |
23 | test('multi-tones', () async {
24 | var generator = WaveGenerator(44100, BitDepth.depth8Bit);
25 |
26 | int baseTime = 100;
27 | double freq = 440.0;
28 |
29 | int dotDuration = baseTime;
30 | int dashDuration = baseTime * 3;
31 | int symbolGap = baseTime;
32 | int letterGap = baseTime * 3;
33 |
34 | var interSymbolSilence = Note(freq, symbolGap, Waveform.sine, 0.0);
35 | var interLetterSilence = Note(freq, letterGap, Waveform.sine, 0.0);
36 | var dit = Note(freq, dotDuration, Waveform.sine, 0.7);
37 | var dah = Note(freq, dashDuration, Waveform.sine, 0.7);
38 |
39 | var notes = [
40 | dit,
41 | interSymbolSilence,
42 | dit,
43 | interSymbolSilence,
44 | dit,
45 | interLetterSilence,
46 | dah,
47 | interSymbolSilence,
48 | dah,
49 | interSymbolSilence,
50 | dah,
51 | interLetterSilence,
52 | dit,
53 | interSymbolSilence,
54 | dit,
55 | interSymbolSilence,
56 | dit,
57 | interLetterSilence,
58 | ];
59 |
60 | var file = File('s-o-s.wav');
61 |
62 | List bytes = [];
63 | await for (int byte in generator.generateSequence(notes)) {
64 | bytes.add(byte);
65 | }
66 |
67 | file.writeAsBytes(bytes, mode: FileMode.append);
68 | });
69 | }
70 |
--------------------------------------------------------------------------------
/lib/src/data_chunk8.dart:
--------------------------------------------------------------------------------
1 | import 'dart:typed_data';
2 | import 'dart:math';
3 |
4 | import 'byte_helpers.dart';
5 | import 'chunk.dart';
6 | import 'format_chunk.dart';
7 | import 'generator_function.dart';
8 |
9 | import '../wave_generator.dart';
10 |
11 | class DataChunk8 implements DataChunk {
12 | final FormatChunk format;
13 | final List notes;
14 |
15 | final String _sGroupId = 'data';
16 |
17 | // nb. Stored as unsigned bytes in the rage 0 to 255
18 | static const int min = 0;
19 | static const int max = 255;
20 |
21 | int clamp(int byte) {
22 | return byte.clamp(min, max);
23 | }
24 |
25 | const DataChunk8(this.format, this.notes);
26 |
27 | @override
28 | Stream bytes() async* {
29 | // sGroupId
30 | var groupIdBytes = ByteHelpers.toBytes(_sGroupId);
31 | var bytes = groupIdBytes.buffer.asByteData();
32 |
33 | for (int i = 0; i < 4; i++) {
34 | yield bytes.getUint8(i);
35 | }
36 |
37 | // length
38 | var byteData = ByteData(4);
39 | byteData.setUint32(0, length, Endian.little);
40 | for (int i = 0; i < 4; i++) {
41 | yield byteData.getUint8(i);
42 | }
43 |
44 | // Determine when one note ends and the next begins
45 | // Number of samples per note given by sampleRate * note duration
46 | // compare against step count to select the correct note
47 | int noteNumber = 0;
48 | int incrementNoteOnSample =
49 | (notes[noteNumber].msDuration * format.sampleRate) ~/ 1000;
50 |
51 | int sampleMax = totalSamples;
52 | var amplify = (max + 1) / 2;
53 | for (int step = 0; step < sampleMax; step++) {
54 | if (incrementNoteOnSample == step) {
55 | noteNumber += 1;
56 | incrementNoteOnSample +=
57 | (notes[noteNumber].msDuration * format.sampleRate) ~/ 1000;
58 | }
59 |
60 | double theta = notes[noteNumber].frequency * (2 * pi) / format.sampleRate;
61 | GeneratorFunction generator =
62 | GeneratorFunction.create(notes[noteNumber].waveform);
63 |
64 | var y = generator.generate(theta * step);
65 | double volume = (amplify * notes[noteNumber].volume);
66 | double sample = (volume * y) + volume;
67 | int intSampleVal = sample.toInt();
68 | int sampleByte = clamp(intSampleVal);
69 | yield sampleByte;
70 | }
71 |
72 | // If the number of bytes is not word-aligned, ie. number of bytes is odd, we need to pad with additional zero bytes.
73 | // These zero bytes should not appear in the data chunk length header
74 | // but probably do get included for the length bytes in the file header
75 | if (length % 2 != 0) yield 0x00;
76 | }
77 |
78 | @override
79 | int get length => totalSamples * format.blockAlign;
80 |
81 | int get totalSamples {
82 | double secondsDuration =
83 | (notes.map((note) => note.msDuration).reduce((a, b) => a + b) / 1000);
84 | return (format.sampleRate * secondsDuration).toInt();
85 | }
86 |
87 | @override
88 | String get sGroupId => _sGroupId;
89 |
90 | @override
91 | int get bytesPadding => length % 2 == 0 ? 0 : 1;
92 | }
93 |
--------------------------------------------------------------------------------
/lib/src/format_chunk.dart:
--------------------------------------------------------------------------------
1 | import 'dart:typed_data';
2 |
3 | import 'byte_helpers.dart';
4 | import 'chunk.dart';
5 |
6 | import '../wave_generator.dart';
7 |
8 | class FormatChunk implements Chunk {
9 | final int bitsPerByte = 8;
10 |
11 | final String _sGroupId = 'fmt ';
12 | final int _dwChunkSize = 16;
13 | final int _wFormatTag = 1;
14 | final int _wChannels;
15 | final int _dwSamplesPerSecond;
16 | late int _dwAverageBytesPerSecond;
17 | late int _wBlockAlign;
18 | late int _wBitsPerSample;
19 |
20 | FormatChunk(
21 | this._wChannels,
22 | this._dwSamplesPerSecond,
23 | BitDepth bitsPerSample,
24 | ) {
25 | switch (bitsPerSample) {
26 | case BitDepth.depth8Bit:
27 | _wBitsPerSample = 8;
28 | break;
29 | // case BitDepth.Depth16bit:
30 | // _wBitsPerSample = 16;
31 | // break;
32 | // case BitDepth.Depth32bit:
33 | // _wBitsPerSample = 32;
34 | // break;
35 | }
36 |
37 | _wBlockAlign = _wChannels * (_wBitsPerSample ~/ bitsPerByte);
38 | _dwAverageBytesPerSecond = _wBlockAlign * _dwSamplesPerSecond;
39 | }
40 |
41 | @override
42 | int get length => _dwChunkSize;
43 |
44 | int get sampleRate => _dwSamplesPerSecond;
45 |
46 | int get blockAlign => _wBlockAlign;
47 |
48 | int get bytesPerSecond => _dwAverageBytesPerSecond;
49 |
50 | int get bitDepth => _wBitsPerSample;
51 |
52 | @override
53 | String get sGroupId => _sGroupId;
54 |
55 | @override
56 | Stream bytes() async* {
57 | // sGroupId
58 | var groupIdBytes = ByteHelpers.toBytes(_sGroupId);
59 | var bytes = groupIdBytes.buffer.asByteData();
60 |
61 | for (int i = 0; i < 4; i++) {
62 | yield bytes.getUint8(i);
63 | }
64 |
65 | // chunkLength
66 | var byteData = ByteData(4);
67 | byteData.setUint32(0, length, Endian.little);
68 | for (int i = 0; i < 4; i++) {
69 | yield byteData.getUint8(i);
70 | }
71 |
72 | // Audio format
73 | byteData = ByteData(2);
74 | byteData.setUint16(0, _wFormatTag, Endian.little);
75 | for (int i = 0; i < 2; i++) {
76 | yield byteData.getUint8(i);
77 | }
78 |
79 | // Num channels
80 | byteData = ByteData(2);
81 | byteData.setUint16(0, _wChannels, Endian.little);
82 | for (int i = 0; i < 2; i++) {
83 | yield byteData.getUint8(i);
84 | }
85 |
86 | // Sample rate
87 | byteData = ByteData(4);
88 | byteData.setUint32(0, _dwSamplesPerSecond, Endian.little);
89 | for (int i = 0; i < 4; i++) {
90 | yield byteData.getUint8(i);
91 | }
92 |
93 | // Byte rate
94 | byteData = ByteData(4);
95 | byteData.setUint32(0, _dwAverageBytesPerSecond, Endian.little);
96 | for (int i = 0; i < 4; i++) {
97 | yield byteData.getUint8(i);
98 | }
99 |
100 | // Block align
101 | byteData = ByteData(2);
102 | byteData.setUint16(0, _wBlockAlign, Endian.little);
103 | for (int i = 0; i < 2; i++) {
104 | yield byteData.getUint8(i);
105 | }
106 |
107 | // Bits per sample
108 | byteData = ByteData(2);
109 | byteData.setUint16(0, _wBitsPerSample, Endian.little);
110 | for (int i = 0; i < 2; i++) {
111 | yield byteData.getUint8(i);
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/test/data_chunk_8_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:test/test.dart';
2 | import 'package:wave_generator/src/chunk.dart';
3 | import 'package:wave_generator/src/data_chunk8.dart';
4 | import 'package:wave_generator/src/format_chunk.dart';
5 |
6 | import 'package:wave_generator/wave_generator.dart';
7 |
8 | void main() {
9 | group('8 bit data chunk', () {
10 | test('first bytes should be chunk Id "data" big endian', () async {
11 | var sut = createSut();
12 |
13 | var expectedValue = 'data';
14 |
15 | int expectMinimumBytes = 4;
16 | // array of [index, byteValue]
17 | var expectedBytes = [
18 | [00, 0x64],
19 | [01, 0x61],
20 | [02, 0x74],
21 | [03, 0x61]
22 | ];
23 |
24 | int currentByte = 0;
25 |
26 | expect(sut.sGroupId, expectedValue, reason: 'block id is incorrect');
27 |
28 | await for (int byte in sut.bytes()) {
29 | for (List expectedByte in expectedBytes) {
30 | if (currentByte == expectedByte[0]) {
31 | expect(byte, expectedByte[1],
32 | reason:
33 | 'Byte at index $currentByte incorrect. $byte instead of ${expectedByte[1]}');
34 | }
35 | }
36 |
37 | currentByte++;
38 | }
39 |
40 | expect(currentByte, greaterThanOrEqualTo(expectMinimumBytes),
41 | reason: 'Not enough bytes returned');
42 | });
43 |
44 | test(
45 | 'data length bytes should be inferred from format and combined note durations',
46 | () async {
47 | int sampleRate = 44100;
48 | int bytesPerSample = 1;
49 | int channels = 1;
50 | int millisecondsDuration = 100;
51 |
52 | // = total samples * bytes per sample
53 | // = duration * samples per sec * bytes per sample
54 | int expectedDataLengthBytes =
55 | (sampleRate * (millisecondsDuration / 1000)).toInt() *
56 | channels *
57 | bytesPerSample;
58 |
59 | var format = FormatChunk(channels, sampleRate, BitDepth.depth8Bit);
60 | var notes = [Note.a4(millisecondsDuration, 1)];
61 | var sut = createSut(format: format, notes: notes);
62 |
63 | var expectedValue = expectedDataLengthBytes;
64 |
65 | int expectMinimumBytes = 8;
66 | // array of [index, byteValue]
67 | var expectedBytes = [
68 | [04, 0x3A],
69 | [05, 0x11],
70 | [06, 0x00],
71 | [07, 0x00]
72 | ];
73 |
74 | int currentByte = 0;
75 |
76 | expect(sut.length, expectedValue, reason: 'block id is incorrect');
77 |
78 | await for (int byte in sut.bytes()) {
79 | for (List expectedByte in expectedBytes) {
80 | if (currentByte == expectedByte[0]) {
81 | expect(byte, expectedByte[1],
82 | reason:
83 | 'Byte at index $currentByte incorrect. $byte instead of $expectedByte[1]');
84 | }
85 | }
86 |
87 | currentByte++;
88 | }
89 |
90 | expect(currentByte, greaterThanOrEqualTo(expectMinimumBytes),
91 | reason: 'Not enough bytes returned');
92 | });
93 | });
94 | }
95 |
96 | DataChunk createSut({
97 | FormatChunk? format,
98 | List? notes,
99 | }) {
100 | return DataChunk8(
101 | format ??= FormatChunk(1, 44100, BitDepth.depth8Bit),
102 | notes ??= [Note.a4(100, 1)],
103 | );
104 | }
105 |
--------------------------------------------------------------------------------
/lib/wave_generator.dart:
--------------------------------------------------------------------------------
1 | library wave_generator;
2 |
3 | import 'src/chunk.dart';
4 | import 'src/data_chunk8.dart';
5 | import 'src/format_chunk.dart';
6 | import 'src/wave_header.dart';
7 |
8 | /// Bit-depth per sample.
9 | enum BitDepth {
10 | depth8Bit,
11 | // Depth16bit,
12 | // Depth32bit
13 | }
14 |
15 | /// Waveform for a tone.
16 | enum Waveform {
17 | sine,
18 | square,
19 | triangle,
20 | }
21 |
22 | /// Represents a single tone.
23 | class Note {
24 | /// Frequency in Hz.
25 | final double frequency;
26 |
27 | /// Duration in milliseconds.
28 | final int msDuration;
29 |
30 | /// Waveform of the tone.
31 | final Waveform waveform;
32 |
33 | /// Volume in the range 0.0 - 1.0.
34 | final double volume;
35 |
36 | Note(this.frequency, this.msDuration, this.waveform, this.volume) {
37 | if (volume < 0.0 || volume > 1.0) {
38 | throw ArgumentError('Volume should be between 0.0 and 1.0');
39 | }
40 | if (frequency < 0.0) {
41 | throw ArgumentError('Frequency cannot be less than zero');
42 | }
43 | if (msDuration < 0.0) {
44 | throw ArgumentError('Duration cannot be less than zero');
45 | }
46 | }
47 |
48 | factory Note.silent(int duration) {
49 | return Note(1.0, duration, Waveform.sine, 0.0);
50 | }
51 |
52 | factory Note.a4(int duration, double volume) {
53 | return Note(440.0, duration, Waveform.sine, volume);
54 | }
55 |
56 | // Etc, do more
57 | // http://pages.mtu.edu/~suits/notefreqs.html
58 | }
59 |
60 | /// Generates simple waveforms as uncompressed PCM audio data.
61 | class WaveGenerator {
62 | /// Samples generated per second.
63 | final int sampleRate;
64 |
65 | /// Bit-depth of each audio sample.
66 | final BitDepth bitDepth;
67 |
68 | factory WaveGenerator.simple() {
69 | return WaveGenerator(44100, BitDepth.depth8Bit);
70 | }
71 |
72 | const WaveGenerator(this.sampleRate, this.bitDepth);
73 |
74 | /// Generate a byte stream equivalent to a wav file of the Note argument
75 | Stream generate(Note note) async* {
76 | var formatHeader = FormatChunk(1, sampleRate, bitDepth);
77 |
78 | var dataChunk = _getDataChunk(formatHeader, [note]);
79 |
80 | var fileHeader = WaveHeader(formatHeader, dataChunk);
81 |
82 | await for (int data in fileHeader.bytes()) {
83 | yield data;
84 | }
85 |
86 | await for (int data in formatHeader.bytes()) {
87 | yield data;
88 | }
89 |
90 | await for (int data in dataChunk.bytes()) {
91 | yield data;
92 | }
93 | }
94 |
95 | /// Generate a byte stream equivalent to a wav file of the Note list argument, played sequentially
96 | Stream generateSequence(List notes) async* {
97 | var formatHeader = FormatChunk(1, sampleRate, bitDepth);
98 | var dataChunk = _getDataChunk(formatHeader, notes);
99 | var fileHeader = WaveHeader(formatHeader, dataChunk);
100 |
101 | await for (int data in fileHeader.bytes()) {
102 | yield data;
103 | }
104 |
105 | await for (int data in formatHeader.bytes()) {
106 | yield data;
107 | }
108 |
109 | await for (int data in dataChunk.bytes()) {
110 | yield data;
111 | }
112 | }
113 |
114 | DataChunk _getDataChunk(FormatChunk format, List notes) {
115 | switch (bitDepth) {
116 | case BitDepth.depth8Bit:
117 | return DataChunk8(format, notes);
118 | default:
119 | throw UnimplementedError();
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/.idea/libraries/Dart_Packages.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
--------------------------------------------------------------------------------
/test/format_chunk_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:test/test.dart';
2 | import 'package:wave_generator/src/format_chunk.dart';
3 | import 'package:wave_generator/wave_generator.dart';
4 |
5 | void main() {
6 | group('format chunk', () {
7 | test('sGroupId should be "fmt "', () {
8 | var sut = createSut();
9 |
10 | expect(sut.sGroupId, 'fmt ');
11 | });
12 |
13 | test('first bytes should be "fmt " big endian', () async {
14 | var sut = createSut();
15 |
16 | int count = 0;
17 | var expectedFirstBytes = [0x66, 0x6D, 0x74, 0x20];
18 | await for (int byte in sut.bytes()) {
19 | expect(byte, expectedFirstBytes[count],
20 | reason:
21 | "byte $count should be ${expectedFirstBytes[count]} but was $byte");
22 | count += 1;
23 | if (count > 3) break;
24 | }
25 | });
26 |
27 | test('PCM data length should be 16 bytes', () {
28 | var sut = createSut();
29 |
30 | expect(sut.length, 16);
31 | });
32 |
33 | test('PCM data length bytes should be 16 little endian', () async {
34 | var sut = createSut();
35 |
36 | int count = -1;
37 | var expectedBytes = [0x10, 0x00, 0x00, 0x00]; // 16
38 | await for (int byte in sut.bytes()) {
39 | count++;
40 | if (count < 4) continue;
41 | if (count >= 8) break;
42 |
43 | expect(byte, expectedBytes[count - 4],
44 | reason:
45 | "byte $count should be ${expectedBytes[count - 4]} but was $byte");
46 | }
47 | expect(count, greaterThanOrEqualTo(7),
48 | reason: 'Not enough bytes returned');
49 | });
50 |
51 | test('Audio format bytes should be 1 (PCM)', () async {
52 | var sut = createSut();
53 |
54 | int expectMinimumBytes = 10;
55 | // array of [index, byteValue]
56 | var expectedBytes = [
57 | [8, 0x01],
58 | [9, 0x00]
59 | ];
60 |
61 | int currentByte = 0;
62 |
63 | await for (int byte in sut.bytes()) {
64 | for (List expectedByte in expectedBytes) {
65 | if (currentByte == expectedByte[0]) {
66 | expect(byte, expectedByte[1],
67 | reason:
68 | 'Byte at index $currentByte incorrect. $byte instead of ${expectedByte[1]}');
69 | }
70 | }
71 |
72 | currentByte++;
73 | }
74 |
75 | expect(currentByte, greaterThanOrEqualTo(expectMinimumBytes),
76 | reason: 'Not enough bytes returned');
77 | });
78 |
79 | test('Num channels bytes should be 1', () async {
80 | var sut = createSut();
81 |
82 | int expectMinimumBytes = 12;
83 | // array of [index, byteValue]
84 | var expectedBytes = [
85 | [10, 0x01],
86 | [11, 0x00]
87 | ];
88 |
89 | int currentByte = 0;
90 |
91 | await for (int byte in sut.bytes()) {
92 | for (List expectedByte in expectedBytes) {
93 | if (currentByte == expectedByte[0]) {
94 | expect(byte, expectedByte[1],
95 | reason:
96 | 'Byte at index $currentByte incorrect. $byte instead of ${expectedByte[1]}');
97 | }
98 | }
99 |
100 | currentByte++;
101 | }
102 |
103 | expect(currentByte, greaterThanOrEqualTo(expectMinimumBytes),
104 | reason: 'Not enough bytes returned');
105 | });
106 |
107 | test('Num channels bytes should be correct', () async {
108 | var sut = createSut(channels: 300);
109 |
110 | int expectMinimumBytes = 12;
111 | // array of [index, byteValue]
112 | var expectedBytes = [
113 | [10, 0x2C],
114 | [11, 0x01]
115 | ];
116 |
117 | int currentByte = 0;
118 |
119 | await for (int byte in sut.bytes()) {
120 | for (List expectedByte in expectedBytes) {
121 | if (currentByte == expectedByte[0]) {
122 | expect(byte, expectedByte[1],
123 | reason:
124 | 'Byte at index $currentByte incorrect. $byte instead of ${expectedByte[1]}');
125 | }
126 | }
127 |
128 | currentByte++;
129 | }
130 |
131 | expect(currentByte, greaterThanOrEqualTo(expectMinimumBytes),
132 | reason: 'Not enough bytes returned');
133 | });
134 |
135 | test('default sample rate should be 44100', () async {
136 | var sut = createSut();
137 |
138 | int expectMinimumBytes = 16;
139 | // array of [index, byteValue]
140 | var expectedBytes = [
141 | [12, 0x44],
142 | [13, 0xAC],
143 | [14, 0x00],
144 | [15, 0x00]
145 | ];
146 |
147 | int currentByte = 0;
148 |
149 | await for (int byte in sut.bytes()) {
150 | for (List expectedByte in expectedBytes) {
151 | if (currentByte == expectedByte[0]) {
152 | expect(byte, expectedByte[1],
153 | reason:
154 | 'Byte at index $currentByte incorrect. $byte instead of ${expectedByte[1]}');
155 | }
156 | }
157 |
158 | currentByte++;
159 | }
160 |
161 | expect(currentByte, greaterThanOrEqualTo(expectMinimumBytes),
162 | reason: 'Not enough bytes returned');
163 | });
164 |
165 | test('when sample rate is not default, sample rate bytes should be correct',
166 | () async {
167 | var sut = createSut(sampleRate: 196000);
168 |
169 | int expectMinimumBytes = 16;
170 | // array of [index, byteValue]
171 | var expectedBytes = [
172 | [12, 0xA0],
173 | [13, 0xFD],
174 | [14, 0x02],
175 | [15, 0x00]
176 | ];
177 |
178 | int currentByte = 0;
179 |
180 | await for (int byte in sut.bytes()) {
181 | for (List expectedByte in expectedBytes) {
182 | if (currentByte == expectedByte[0]) {
183 | expect(byte, expectedByte[1],
184 | reason:
185 | 'Byte at index $currentByte incorrect. $byte instead of ${expectedByte[1]}');
186 | }
187 | }
188 |
189 | currentByte++;
190 | }
191 |
192 | expect(currentByte, greaterThanOrEqualTo(expectMinimumBytes),
193 | reason: 'Not enough bytes returned');
194 | });
195 |
196 | test(
197 | 'byte rate should be 4 byte little endian equal to sample rate * channels * bytes per sample',
198 | () async {
199 | var sut =
200 | createSut(sampleRate: 44100, channels: 2, depth: BitDepth.depth8Bit);
201 |
202 | int expectedValue = (44100 * 2 * 1).toInt();
203 |
204 | int expectMinimumBytes = 20;
205 | // array of [index, byteValue]
206 | var expectedBytes = [
207 | [16, 0x88],
208 | [17, 0x58],
209 | [18, 0x01],
210 | [19, 0x00]
211 | ];
212 |
213 | int currentByte = 0;
214 |
215 | expect(sut.bytesPerSecond, expectedValue,
216 | reason: 'Byte rate is incorrect');
217 |
218 | await for (int byte in sut.bytes()) {
219 | for (List expectedByte in expectedBytes) {
220 | if (currentByte == expectedByte[0]) {
221 | expect(byte, expectedByte[1],
222 | reason:
223 | 'Byte at index ${currentByte} incorrect. ${byte} instead of ${expectedByte[1]}');
224 | }
225 | }
226 |
227 | currentByte++;
228 | }
229 |
230 | expect(currentByte, greaterThanOrEqualTo(expectMinimumBytes),
231 | reason: 'Not enough bytes returned');
232 | });
233 |
234 | test(
235 | 'Block align should be 2 bytes little endian equal to channels * bytes per sample, ie. frame size',
236 | () async {
237 | var sut =
238 | createSut(sampleRate: 44100, channels: 5, depth: BitDepth.depth8Bit);
239 |
240 | int expectedValue = (5 * (8 / 8)).toInt();
241 |
242 | int expectMinimumBytes = 22;
243 | // array of [index, byteValue]
244 | var expectedBytes = [
245 | [20, 0x05],
246 | [21, 0x00]
247 | ];
248 |
249 | int currentByte = 0;
250 |
251 | expect(sut.blockAlign, expectedValue, reason: 'Block align is incorrect');
252 |
253 | await for (int byte in sut.bytes()) {
254 | for (List expectedByte in expectedBytes) {
255 | if (currentByte == expectedByte[0]) {
256 | expect(byte, expectedByte[1],
257 | reason:
258 | 'Byte at index $currentByte incorrect. $byte instead of ${expectedByte[1]}');
259 | }
260 | }
261 |
262 | currentByte++;
263 | }
264 |
265 | expect(currentByte, greaterThanOrEqualTo(expectMinimumBytes),
266 | reason: 'Not enough bytes returned');
267 | });
268 |
269 | test('bits per sample should be set correctly', () async {
270 | var sut =
271 | createSut(sampleRate: 44100, channels: 2, depth: BitDepth.depth8Bit);
272 |
273 | int expectedValue = 8;
274 |
275 | int expectMinimumBytes = 24;
276 | // array of [index, byteValue]
277 | var expectedBytes = [
278 | [22, 0x08],
279 | [23, 0x00]
280 | ];
281 |
282 | int currentByte = 0;
283 |
284 | expect(sut.bitDepth, expectedValue,
285 | reason: 'Bits per sample is incorrect');
286 |
287 | await for (int byte in sut.bytes()) {
288 | for (List expectedByte in expectedBytes) {
289 | if (currentByte == expectedByte[0]) {
290 | expect(byte, expectedByte[1],
291 | reason:
292 | 'Byte at index ${currentByte} incorrect. ${byte} instead of ${expectedByte[1]}');
293 | }
294 | }
295 |
296 | currentByte++;
297 | }
298 |
299 | expect(currentByte, greaterThanOrEqualTo(expectMinimumBytes),
300 | reason: 'Not enough bytes returned');
301 | });
302 | });
303 | }
304 |
305 | FormatChunk createSut(
306 | {int channels = 1,
307 | int sampleRate = 44100,
308 | BitDepth depth = BitDepth.depth8Bit}) {
309 | return FormatChunk(channels, sampleRate, depth);
310 | }
311 |
--------------------------------------------------------------------------------