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