├── 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 | --------------------------------------------------------------------------------