├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── constants.v ├── header.v ├── io.v ├── reader.v ├── v.mod ├── vave.v ├── vpkg.json └── writer.v /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: thecodrr 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vave/ 2 | vave_test -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Abdullah Atta 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 |
2 |

🌊 Vave

3 |

4 | A crazy simple library for reading/writing WAV files written in V! 5 |

6 |

7 | 8 |

9 |
10 | 11 | ## Installation: 12 | 13 | Install using `vpkg` 14 | 15 | ```bash 16 | vpkg get https://github.com/thecodrr/vave 17 | ``` 18 | 19 | Install using `V`'s builtin `vpm` (you will need to import the module with: `import thecodrr.vave` with this method of installation): 20 | 21 | ```shell 22 | v install thecodrr.vave 23 | ``` 24 | 25 | Install using `git`: 26 | 27 | ```bash 28 | cd path/to/your/project 29 | git clone https://github.com/thecodrr/vave 30 | ``` 31 | 32 | Then in the wherever you want to use it: 33 | 34 | ```javascript 35 | import thecodrr.vave //OR simply vave depending on how you installed 36 | ``` 37 | 38 | And that's it! 39 | 40 | ## Usage 41 | 42 | _This library is in use in the [vspeech](https://github.com/thecodrr/vspeech) (V Bindings for DeepSpeech) utility that uses [Mozilla's DeepSpeech](https://github.com/mozilla/DeepSpeech) for Speech-to-Text. Do check that out as well._ 43 | 44 | ### vave.open(`path`,`mode`) 45 | 46 | Open a new WAV file in the specified mode. All mode supported by `C.fopen` are supported (e.g. `r`, `rb` etc.) 47 | 48 | ```javascript 49 | mut wav := vave.open("/path/to/vave/file", "r") //open for reading 50 | ``` 51 | 52 | **NOTES:** The data is read into a `byteptr` and needs to be manually freed each and everytime or it will cause a huge memory leak. This library has been tested with `valgrind` and after freeing there is no other memory leak (if you find any, do report). I haven't implemented writing samples yet due to lack of time but its in my future plans. 53 | 54 | ### WavFile `struct` 55 | 56 | `WavFile` struct is used for reading/writing samples and other metadata. It is returned by `vave.open`. 57 | 58 | #### Read: 59 | 60 | #### WavFile.read_raw() 61 | 62 | Read all the samples from the file in their raw form. 63 | 64 | #### WaveFile.read_samples(`count`) 65 | 66 | Read a specific amount of samples from the file. 67 | 68 | #### WaveFile.read_sample() 69 | 70 | Read only one sample from the file. 71 | 72 | #### Write: 73 | 74 | **TODO** 75 | 76 | #### WaveFile.close() 77 | 78 | Close the file and free all associated resources. 79 | 80 | ### Metadata Methods: 81 | 82 | #### WaveFile.total_samples() 83 | 84 | Get total number of audio samples in the file. 85 | 86 | #### WaveFile.duration() 87 | 88 | Get total duration of the audio file. 89 | 90 | #### WaveFile.data_len() 91 | 92 | Get the total length of sample bytes in the file. 93 | 94 | #### WaveFile.bytes_per_sample() 95 | 96 | Get total bytes per each sample. 97 | 98 | #### WaveFile.sample_rate() 99 | 100 | Get the sample rate (samples per second). 101 | 102 | #### WaveFile.sample_size() 103 | 104 | Get the size of one sample() 105 | 106 | #### WaveFile.format() 107 | 108 | Get the format of the WAV audio. Either `PCM`, `IEEE`, `ALAW`,`MULAW` or `EXTENSIBLE`. 109 | 110 | #### WaveFile.num_channels() 111 | 112 | Get the total number of channels in the audio stream. 113 | 114 | #### WaveFile.valid_bits_per_sample() 115 | 116 | Get the bits per each sample. 117 | 118 | ## Supported Formats: 119 | 120 | Currently only the following formats are supported: 121 | 122 | 1. PCM 123 | 124 | 2. IEEE 125 | 126 | 3. ALAW 127 | 128 | 4. MULAW 129 | 130 | 5. EXTENSIBLE 131 | 132 | ### Find this library useful? :heart: 133 | 134 | Support it by joining **[stargazers](https://github.com/thecodrr/vave/stargazers)** for this repository. :star:or [buy me a cup of coffee](https://ko-fi.com/thecodrr) 135 | And **[follow](https://github.com/thecodrr)** me for my next creations! 🤩 136 | 137 | # License 138 | 139 | ```xml 140 | MIT License 141 | 142 | Copyright (c) 2019 Abdullah Atta 143 | 144 | Permission is hereby granted, free of charge, to any person obtaining a copy 145 | of this software and associated documentation files (the "Software"), to deal 146 | in the Software without restriction, including without limitation the rights 147 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 148 | copies of the Software, and to permit persons to whom the Software is 149 | furnished to do so, subject to the following conditions: 150 | 151 | The above copyright notice and this permission notice shall be included in all 152 | copies or substantial portions of the Software. 153 | 154 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 155 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 156 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 157 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 158 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 159 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 160 | SOFTWARE. 161 | ``` 162 | -------------------------------------------------------------------------------- /constants.v: -------------------------------------------------------------------------------- 1 | module vave 2 | 3 | const ( 4 | WAV_RIFF_CHUNK_ID = 'RIFF'.str 5 | WAV_FORMAT_CHUNK_ID = 'fmt '.str 6 | WAV_FACT_CHUNK_ID = 'fact'.str 7 | WAV_DATA_CHUNK_ID = 'data'.str 8 | WAVE_ID = 'WAVE'.str 9 | WAV_RIFF_HEADER_SIZE = u32(8) 10 | DEFAULT_SUB_FORMAT = [0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71] 11 | ) 12 | 13 | pub enum Formats { 14 | pcm = 0x0001, 15 | ieee = 0x0003, 16 | alaw = 0x0006, 17 | mulaw = 0x0007, 18 | extensible = 0xfffe 19 | } -------------------------------------------------------------------------------- /header.v: -------------------------------------------------------------------------------- 1 | module vave 2 | 3 | struct WavMasterChunk { 4 | mut: 5 | /* RIFF header */ 6 | id u32 7 | size u32 8 | wave_id u32 9 | format_chunk WavFormatChunk 10 | fact_chunk WavFactChunk 11 | data_chunk WavDataChunk 12 | } 13 | 14 | struct WavFormatChunk { 15 | mut: 16 | /* RIFF header */ 17 | id u32 18 | size u32 19 | format_tag u16 20 | n_channels u16 21 | sample_rate u32 22 | avg_bytes_per_sec u32 23 | block_align u16 24 | bits_per_sample u16 25 | ext_size u16 26 | valid_bits_per_sample u16 27 | channel_mask u32 28 | sub_format SubFormat 29 | } 30 | 31 | struct SubFormat { 32 | format_code u16 33 | fixed_str [14]byte 34 | } 35 | 36 | struct WavFactChunk { 37 | mut: 38 | /* RIFF header */ 39 | id u32 40 | size u32 41 | sample_length u32 42 | } 43 | 44 | struct WavDataChunk{ 45 | mut: 46 | /* RIFF header */ 47 | id u32 48 | size u32 49 | } 50 | 51 | fn (w &WavFile) parse_format_chunk() WavFormatChunk { 52 | chunk := WavFormatChunk{} 53 | 54 | if !w.parse_chunk_header(&chunk) && !compare(&chunk.id, WAV_FORMAT_CHUNK_ID) { 55 | panic("Couldn't find the format chunk.") 56 | } 57 | 58 | if chunk.size > 40 { 59 | panic("Size of format chunk is too big. Must be less than or equal to 40.") 60 | } 61 | 62 | if !w.parse_chunk_body(&chunk, chunk.size) { 63 | panic("Failed to read the format chunk body.") 64 | } 65 | 66 | if chunk.n_channels > 2 && chunk.format_tag != u16(Formats.extensible) { 67 | panic("This wav file has more than 2 channels but isn't WAV_FORMAT_EXTENSIBLE.") 68 | } 69 | 70 | if !(Formats(chunk.format_tag) in [.pcm, .ieee, .alaw, .mulaw]) { 71 | if chunk.ext_size != 0 { 72 | if !(Formats(chunk.sub_format.format_code) in [.pcm, .ieee, .alaw, .mulaw]) { 73 | panic("Only PCM, IEEE float and log-PCM log files are accepted.") 74 | } 75 | } else { 76 | panic("Only PCM, IEEE float and log-PCM log files are accepted.") 77 | } 78 | } 79 | 80 | return chunk 81 | } 82 | 83 | fn (w &WavFile) parse_master_chunk() WavMasterChunk { 84 | chunk := WavMasterChunk{} 85 | if !w.parse_chunk_header(&chunk) || !compare(&chunk.id, WAV_RIFF_CHUNK_ID) || !w.read_u32(&chunk.wave_id) || !compare(&chunk.wave_id, WAVE_ID) { 86 | panic("Couldn't find the RIFF chunk. This is probably not a WAVE file.") 87 | } 88 | return chunk 89 | } 90 | 91 | fn (w &WavFile) parse_fact_chunk() WavFactChunk { 92 | chunk := WavFactChunk{} 93 | if w.parse_chunk_header(&chunk) && compare(&chunk.id, WAV_FACT_CHUNK_ID) && w.parse_chunk_body(&chunk, chunk.size) { 94 | return chunk 95 | } 96 | return chunk 97 | } 98 | 99 | fn (w mut WavFile) parse_header() bool { 100 | w.chunk = w.parse_master_chunk() 101 | w.chunk.format_chunk = w.parse_format_chunk() 102 | w.chunk.fact_chunk = w.parse_fact_chunk() 103 | if compare(&w.chunk.fact_chunk.id, WAV_DATA_CHUNK_ID) { 104 | w.chunk.data_chunk.id = w.chunk.fact_chunk.id 105 | w.chunk.data_chunk.size = w.chunk.fact_chunk.size 106 | w.chunk.fact_chunk.size = 0 107 | } else { 108 | for !compare(&w.chunk.data_chunk.id, WAV_DATA_CHUNK_ID) && !w.eof() { 109 | w.read_u32(&w.chunk.data_chunk.id) 110 | } 111 | if w.eof() { 112 | panic("Couldn't find data chunk.") 113 | } 114 | w.read_u32(&w.chunk.data_chunk.size) 115 | return true 116 | } 117 | return false 118 | } 119 | 120 | fn (w &WavFile) parse_chunk_header(chunk voidptr) bool { 121 | return w.read_into_struct(chunk, 0, u32(8)) 122 | } 123 | 124 | fn (w &WavFile) parse_chunk_body(chunk voidptr, size u32) bool { 125 | // a quick way to read everything after the header into the struct 126 | // NOTE: the RIFF header is 8 bytes long 127 | return w.read_into_struct(chunk, 8, size) 128 | } 129 | 130 | fn compare(a voidptr, b byteptr) bool { 131 | data := byteptr(a) 132 | for i in 0..4 { 133 | if byte(data[i]) != b[i] {return false} 134 | } 135 | return true 136 | } -------------------------------------------------------------------------------- /io.v: -------------------------------------------------------------------------------- 1 | module vave 2 | 3 | fn (w &WavFile) read_bytes(buf voidptr, len int) bool { 4 | return C.fread(buf, len, 1, w.fp) == 1 5 | } 6 | 7 | fn (w &WavFile) read_u32(buf voidptr) bool { 8 | return w.read_bytes(buf, sizeof(u32)) 9 | } 10 | 11 | fn (w &WavFile) read_u16(buf voidptr) bool { 12 | return w.read_bytes(buf, sizeof(u16)) 13 | } 14 | 15 | fn (w &WavFile) read_into_struct(c voidptr, skip int, size u32) bool { 16 | return w.read_bytes(*byte(c) + skip, int(size)) 17 | } 18 | 19 | /* fn (w &WavFile) pos() i64 { 20 | pos := ftell(w.fp) 21 | if (pos == i64(-1)) { 22 | panic("Failed to get file position.") 23 | } else { 24 | header_size := i64(w.get_header_size()) 25 | println("header size:" + header_size.str()) 26 | return (pos - header_size) / i64(w.chunk.format_chunk.block_align) 27 | } 28 | } */ 29 | 30 | fn (w &WavFile) eof() bool { 31 | return feof(w.fp) > -1 || ftell(w.fp) == int(w.get_header_size() + w.chunk.data_chunk.size) 32 | } -------------------------------------------------------------------------------- /reader.v: -------------------------------------------------------------------------------- 1 | module vave 2 | 3 | //TODO implement different reading functions like mono, stereo etc. 4 | 5 | //read_sample reads one sample from the audio stream 6 | //it can be used for streaming etc. The returned data 7 | //must be freed manually or it will cause a memory leak. 8 | pub fn (w &WavFile) read_sample() byteptr { 9 | return w.read_samples(1) 10 | } 11 | 12 | //read_raw reads all the audio data from the audio stream 13 | //the data is put into w.data instead of returning it 14 | //and automatically frees it on w.close() 15 | pub fn (w &WavFile) read_raw() byteptr { 16 | return w.read_samples(int(w.total_samples())) 17 | } 18 | 19 | //read_samples reads multiple samples from the audio stream 20 | //the returned data must be freed manually or it will cause a memory leak. 21 | pub fn (w &WavFile) read_samples(count int) byteptr { 22 | if w.mode in ['wb', 'wbx', 'ab'] { 23 | panic("File was opened in wrong mode.") 24 | } 25 | if w.chunk.format_chunk.format_tag == u16(Formats.extensible) { 26 | println("warn: EXTENSIBLE format is not supported.") 27 | } 28 | total_samples := int(w.num_channels()) * count * int(w.sample_size()) 29 | data := malloc(total_samples) 30 | C.fread(data, w.sample_size(), int(w.num_channels()) * count, w.fp) 31 | if ferror(w.fp) > 0 { 32 | panic("Failed to read the data chunk.") 33 | } 34 | return data 35 | } -------------------------------------------------------------------------------- /v.mod: -------------------------------------------------------------------------------- 1 | Module { 2 | name: 'vave' 3 | version: '0.0.2' 4 | deps: [] 5 | } -------------------------------------------------------------------------------- /vave.v: -------------------------------------------------------------------------------- 1 | module vave 2 | 3 | import os 4 | 5 | struct C.FILE 6 | fn C.feof(f &FILE) int 7 | fn C.ferror() int 8 | 9 | struct WavFile { 10 | filename string 11 | mut: 12 | chunk WavMasterChunk 13 | fp &C.FILE 14 | mode string 15 | } 16 | 17 | //open opens a WAV file for read/write in the specified mode 18 | pub fn open(path, mode string) &WavFile { 19 | os.tmpdir() //hack to include os import 20 | 21 | mut wav := &WavFile{ 22 | fp: C.NULL 23 | } 24 | match mode { 25 | "rb", "r" {wav.mode = "rb"} 26 | "r+", "rb+", "r+b" {wav.mode = "rb+"} 27 | "w", "wb" {wav.mode = "wb"} 28 | "w+", "wb+", "w+b" {wav.mode = "wb+"} 29 | "wx", "wbx" {wav.mode = "wbx"} 30 | "w+x", "wb+x", "w+bx" {wav.mode = "wb+x"} 31 | "a", "ab" {wav.mode = "ab"} 32 | "a+","ab+","ab+b" {wav.mode = "ab+"} 33 | else { 34 | panic("init: wrong 'mode' given.") 35 | } 36 | } 37 | 38 | $if windows { 39 | wav.fp = C._wfopen(path.replace('/', '\\').to_wide(), wav.mode.to_wide()) 40 | } $if linux { 41 | wav.fp = C.fopen(path.replace('\\', '/').str, wav.mode.str) 42 | } 43 | 44 | if wav.fp == C.NULL { 45 | panic("Couldn't open file: ${path}. Make sure it exists.") 46 | } 47 | 48 | if wav.mode[0] == `r` { 49 | wav.parse_header() 50 | } else if wav.mode[0] == `a` { 51 | if !wav.parse_header() { 52 | C.rewind(wav.fp) 53 | } 54 | } 55 | return wav 56 | } 57 | 58 | pub fn (w mut WavFile) close() int { 59 | if !w.finalize() { 60 | panic("Failed to close the file.") 61 | } 62 | unsafe { 63 | free(w) 64 | } 65 | return 0 66 | } 67 | 68 | pub fn (w &WavFile) bytes_per_sample() u16 { 69 | return w.chunk.format_chunk.bits_per_sample / u16(8) 70 | } 71 | 72 | pub fn (w &WavFile) total_samples() u32 { 73 | return w.chunk.data_chunk.size / u32(w.chunk.format_chunk.block_align) 74 | } 75 | 76 | pub fn (w &WavFile) sample_rate() u32 { 77 | return w.chunk.format_chunk.sample_rate 78 | } 79 | 80 | pub fn (w &WavFile) channel_mask() u32 { 81 | return w.chunk.format_chunk.channel_mask 82 | } 83 | 84 | pub fn (w &WavFile) sub_format() u16 { 85 | return w.chunk.format_chunk.sub_format.format_code 86 | } 87 | 88 | pub fn (w &WavFile) sample_size() u16 { 89 | return w.chunk.format_chunk.block_align / w.chunk.format_chunk.n_channels 90 | } 91 | 92 | pub fn (w &WavFile) format() Formats { 93 | return Formats(w.chunk.format_chunk.format_tag) 94 | } 95 | 96 | pub fn (w &WavFile) num_channels() u16 { 97 | return w.chunk.format_chunk.n_channels 98 | } 99 | 100 | pub fn (w &WavFile) valid_bits_per_sample() u16 { 101 | if w.chunk.format_chunk.format_tag != u16(Formats.extensible) { 102 | return w.chunk.format_chunk.bits_per_sample 103 | } else { 104 | return w.chunk.format_chunk.valid_bits_per_sample 105 | } 106 | } 107 | 108 | pub fn (w &WavFile) duration() u32 { 109 | return w.total_samples() / w.sample_rate() 110 | } 111 | 112 | pub fn (w &WavFile) data_len() int { 113 | return int(w.chunk.data_chunk.size) 114 | } 115 | 116 | // Private 117 | 118 | fn (w mut WavFile) finalize () bool { 119 | if w.fp == C.NULL { 120 | return false 121 | } 122 | 123 | ret := C.fclose(w.fp) 124 | if (ret != 0) { 125 | panic("Couldn't close the file properly.") 126 | } 127 | return true 128 | } 129 | 130 | /* fn (w mut WavFile) reopen(path, mode string) &WavFile { 131 | w.finalize() 132 | return init(path, mode) 133 | } */ 134 | 135 | fn (w &WavFile) get_header_size() u32 { 136 | mut header_size := WAV_RIFF_HEADER_SIZE + u32(4) + 137 | WAV_RIFF_HEADER_SIZE + w.chunk.format_chunk.size + 138 | WAV_RIFF_HEADER_SIZE 139 | 140 | if compare(&w.chunk.fact_chunk.id, WAV_FACT_CHUNK_ID) { 141 | header_size += WAV_RIFF_HEADER_SIZE + w.chunk.fact_chunk.size 142 | } 143 | 144 | return header_size 145 | } -------------------------------------------------------------------------------- /vpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vave", 3 | "version": "0.0.2", 4 | "author": ["thecodrr "], 5 | "repo": "https://github.com/thecodrr/vave", 6 | "sources": ["https://v-pkg.github.io/registry/"], 7 | "dependencies": [] 8 | } 9 | -------------------------------------------------------------------------------- /writer.v: -------------------------------------------------------------------------------- 1 | module vave 2 | 3 | //TODO --------------------------------------------------------------------------------