Browse Source

Initial commit

Yann Weber 10 months ago
commit
d1c8f6c36e
13 changed files with 813 additions and 0 deletions
  1. 28
    0
      COPYING
  2. 38
    0
      README.md
  3. 22
    0
      demo.py
  4. 24
    0
      demo_iter.py
  5. 30
    0
      demo_orig.py
  6. 394
    0
      pyrnnoise.c
  7. 63
    0
      rnnoise_demo.c
  8. 24
    0
      samples/LICENCE
  9. 3
    0
      samples/README
  10. BIN
      samples/bonjour.wav
  11. 21
    0
      setup.py
  12. 55
    0
      test.sh
  13. 111
    0
      test_pyrnnoise.py

+ 28
- 0
COPYING View File

@@ -0,0 +1,28 @@
1
+Copyright (c) 2023, Yann Weber
2
+
3
+Redistribution and use in source and binary forms, with or without
4
+modification, are permitted provided that the following conditions
5
+are met:
6
+
7
+- Redistributions of source code must retain the above copyright
8
+notice, this list of conditions and the following disclaimer.
9
+
10
+- Redistributions in binary form must reproduce the above copyright
11
+notice, this list of conditions and the following disclaimer in the
12
+documentation and/or other materials provided with the distribution.
13
+
14
+- Neither the name of the Xiph.Org Foundation nor the names of its
15
+contributors may be used to endorse or promote products derived from
16
+this software without specific prior written permission.
17
+
18
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19
+``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21
+A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION
22
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 38
- 0
README.md View File

@@ -0,0 +1,38 @@
1
+# pyrnnoise
2
+
3
+Python wrapper for [Xiph.org rnnoise](https://gitlab.xiph.org/xiph/rnnoise)
4
+
5
+## Dependencies
6
+
7
+### librnnoise
8
+
9
+```
10
+git clone 'https://gitlab.xiph.org/xiph/rnnoise/-/tree/master'
11
+cd rnnoise
12
+./autogen.sh
13
+./configure
14
+make
15
+sudo make install
16
+```
17
+
18
+### Build dependencies
19
+
20
+- setuptools
21
+- build
22
+- wheel
23
+
24
+## Building
25
+
26
+`python3 -m build`
27
+
28
+## Run tests
29
+
30
+`bash test.sh`
31
+
32
+## Installing
33
+
34
+`pip install dist/pyrnnoise*.whl`
35
+
36
+## Examples
37
+
38
+See `demo_iter.py` and `demo.py` or run `python3 -c 'import rnnoise; help(rnnoise)'`.

+ 22
- 0
demo.py View File

@@ -0,0 +1,22 @@
1
+import rnnoise
2
+
3
+import sys
4
+import wave
5
+
6
+rnn = rnnoise.RNNoise()
7
+
8
+with wave.open(sys.argv[1], 'rb') as infp:
9
+    with wave.open(sys.argv[2], 'wb') as outfp:
10
+        outfp.setparams(infp.getparams())
11
+        buf = b''
12
+        for i in range(infp.getnframes()):
13
+            dataframe = infp.readframes(10)
14
+            buf += dataframe
15
+            while len(buf) >= (rnn.frame_size*2):
16
+                res = rnn.process_frame(buf[:rnn.frame_size*2])
17
+                outfp.writeframes(res)
18
+                buf = buf[rnn.frame_size*2:]
19
+        if len(buf):
20
+            lenbuf = len(buf)
21
+            buf += bytes(rnn.frame_size*2 - lenbuf)
22
+            outfp.writeframes(rnn.process_frame(buf)[:lenbuf])

+ 24
- 0
demo_iter.py View File

@@ -0,0 +1,24 @@
1
+#!/usr/bin/env python3
2
+import sys
3
+import wave
4
+
5
+import rnnoise
6
+
7
+def in_iter(fname, chunk_sz=4096):
8
+    with wave.open(fname, 'rb') as infp:
9
+        while True:
10
+            data = infp.readframes(chunk_sz)
11
+            if len(data) == 0:
12
+                return
13
+            yield data
14
+
15
+rnn = rnnoise.RNNoise()
16
+
17
+with wave.open(sys.argv[1], 'rb') as infp:
18
+    params = infp.getparams()
19
+
20
+with wave.open(sys.argv[2], 'wb') as outfp:
21
+    outfp.setparams(params)
22
+
23
+    for frames in rnn.iter_on(in_iter(sys.argv[1])):
24
+        outfp.writeframes(frames)

+ 30
- 0
demo_orig.py View File

@@ -0,0 +1,30 @@
1
+import os
2
+import sys
3
+import tempfile
4
+import wave
5
+
6
+wavein=sys.argv[1]
7
+waveout=sys.argv[2]
8
+
9
+datain=tempfile.NamedTemporaryFile()
10
+dataout=tempfile.NamedTemporaryFile()
11
+
12
+with tempfile.NamedTemporaryFile() as dataoutfp:
13
+    with tempfile.NamedTemporaryFile()as datainfp:
14
+        datain_name = datainfp.name
15
+        dataout_name = dataoutfp.name
16
+        with wave.open(wavein, 'rb') as infp:
17
+            params=infp.getparams()
18
+            while True:
19
+                data = infp.readframes(2048)
20
+                if not len(data):
21
+                    break
22
+                datainfp.write(data)
23
+        datainfp.flush()
24
+        cmd = "./rnnoise_demo %r %r" % (datain_name, dataout_name)
25
+        os.system(cmd)
26
+
27
+        with wave.open(waveout, 'wb') as outfp:
28
+            params=outfp.setparams(params)
29
+            outfp.writeframes(dataoutfp.read())
30
+

+ 394
- 0
pyrnnoise.c View File

@@ -0,0 +1,394 @@
1
+/* Copyright (c) 2023 Yann Weber */
2
+/*
3
+   Redistribution and use in source and binary forms, with or without
4
+   modification, are permitted provided that the following conditions
5
+   are met:
6
+
7
+   - Redistributions of source code must retain the above copyright
8
+   notice, this list of conditions and the following disclaimer.
9
+
10
+   - Redistributions in binary form must reproduce the above copyright
11
+   notice, this list of conditions and the following disclaimer in the
12
+   documentation and/or other materials provided with the distribution.
13
+
14
+   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
15
+   ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
16
+   LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
17
+   A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR
18
+   CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
19
+   EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
20
+   PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
21
+   PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
22
+   LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
23
+   NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24
+   SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25
+*/
26
+
27
+#include <errno.h>
28
+#include <limits.h>
29
+#include <poll.h>
30
+#include <stdio.h>
31
+#include <unistd.h>
32
+
33
+#include <rnnoise.h>
34
+
35
+#define PY_SSIZE_T_CLEAN
36
+#include <Python.h>
37
+#include "structmember.h"
38
+
39
+/**The number of samples processed by RNNoise.process_frame()
40
+ * @note There is two samples per frame
41
+ * @note Initialized by PyInit_rnnoise module initialization function
42
+ */
43
+static int frame_size;
44
+
45
+/**@brief Represent a RNNoise python object */
46
+typedef struct
47
+{
48
+	PyObject_VAR_HEAD;
49
+	/** The RNNoise structure */
50
+	DenoiseState *st;
51
+	/** Read only attribute on rnnoise frame size
52
+	 * @note The frame size is in sample (2 bytes per sample)
53
+	 */
54
+	int frame_size;
55
+
56
+	/** When iterating use this iterator as source */
57
+	PyObject *in_iter;
58
+	/** Previous partially processed buffer */
59
+	PyObject *prev_buf_obj;
60
+	/** Pointer on the part of the buffer left to copy */
61
+	char *prev_buf;
62
+	/** Size in bytes of the buffer left to copy */
63
+	Py_ssize_t left_in_buf;
64
+} PyRNNoise_t;
65
+
66
+/**@brief RNNoise.__new__ */
67
+PyObject* pyrnnoise_new(PyTypeObject *subtype, PyObject *args, PyObject* kwds)
68
+{
69
+	PyObject *ret = PyType_GenericNew(subtype, args, kwds);
70
+	if(PyErr_Occurred())
71
+	{
72
+		return NULL;
73
+	}
74
+	return ret;
75
+}
76
+
77
+/**@brief RNNoise.__init__
78
+ * @todo implement model support in argument
79
+ */
80
+int pyrnnoise_init(PyObject *_self, PyObject *args, PyObject *kwds)
81
+{
82
+	PyRNNoise_t *self = (PyRNNoise_t*)_self;
83
+	self->st = rnnoise_create(NULL); // TODO implement model instead of NULL
84
+	self->frame_size = frame_size;
85
+	self->in_iter = NULL;
86
+	self->prev_buf_obj = NULL;
87
+	return 0;
88
+}
89
+
90
+/** @brief RNNoise.__del__ */
91
+void pyrnnoise_del(PyObject *_self)
92
+{
93
+	PyRNNoise_t *self = (PyRNNoise_t*)_self;
94
+	rnnoise_destroy(self->st);
95
+	if(self->prev_buf_obj) { Py_DECREF(self->prev_buf_obj); }
96
+	if(self->in_iter) { Py_DECREF(self->in_iter); }
97
+	return;
98
+}
99
+
100
+/**@brief RNNoise.process_frame(frame:bytes) -> bytes method
101
+ * @param PyObject* Current RNNoise instance
102
+ * @param PyObject** List of arguments (1 expected)
103
+ * @param Py_ssize_t Number of arguments given
104
+ * @return A PyObject* with processed frame (bytes) or NULL (on error)
105
+ */
106
+PyObject *pyrnnoise_process_frame(PyObject *_self, PyObject *const *args, Py_ssize_t nargs)
107
+{
108
+	PyRNNoise_t *self = (PyRNNoise_t*)_self;
109
+	if(nargs != 1)
110
+	{
111
+		PyErr_SetString(PyExc_ValueError, "Expected one bytes argument");
112
+		return NULL;
113
+	}
114
+	if(!PyBytes_Check(args[0]))
115
+	{
116
+		PyErr_SetString(PyExc_TypeError, "Excpected argument to be bytes");
117
+		return NULL;
118
+	}
119
+	Py_ssize_t sz = PyBytes_Size(args[0]);
120
+	if(sz != 2*self->frame_size)
121
+	{
122
+		PyErr_Format(PyExc_ValueError,
123
+				"Expected %ld bytes but got %ld",
124
+				self->frame_size*2, sz);
125
+		return NULL;
126
+	}
127
+
128
+	short *data = (short*)PyBytes_AsString(args[0]);
129
+	short out[self->frame_size];
130
+	float frame[self->frame_size];
131
+
132
+	for(int i=0; i<self->frame_size; i++) { frame[i] = data[i]; }
133
+	rnnoise_process_frame(self->st, frame, frame);
134
+
135
+	for(int i=0; i<self->frame_size; i++) { out[i] = frame[i]; }
136
+	PyObject *ret = PyBytes_FromStringAndSize((char*)out, self->frame_size*2);
137
+	return ret;
138
+}
139
+
140
+/**@brief pyrnnoise_iternext() utility function
141
+ *
142
+ * Fill the short buffer given in argument with previous buffer if set
143
+ * or with next buffer(s) in iterator.
144
+ *
145
+ * @param PyRNNoise_t self
146
+ * @param short* The buffer (with @ref frame_size samples)
147
+ * @return The number of bytes read. Should be @ref frame_size * 2,
148
+ * may be less than this value a last time before falling to 0 on
149
+ * iterator exhauste
150
+ */
151
+static ssize_t _iter_frame(PyRNNoise_t *self, short *buf)
152
+{
153
+	const ssize_t to_read = frame_size * 2;
154
+
155
+	PyObject *inbuf;
156
+	ssize_t readed;
157
+
158
+	if(self->prev_buf_obj)
159
+	{
160
+		Py_ssize_t from_buf = self->left_in_buf > to_read?\
161
+				      to_read:self->left_in_buf;
162
+		memcpy(buf, self->prev_buf, from_buf);
163
+		readed = from_buf;
164
+		if(from_buf == self->left_in_buf)
165
+		{
166
+			Py_DECREF(self->prev_buf_obj);
167
+			self->prev_buf_obj = NULL;
168
+			self->prev_buf = NULL;
169
+		}
170
+		else
171
+		{
172
+			self->left_in_buf -= from_buf;
173
+			self->prev_buf = &self->prev_buf[from_buf];
174
+		}
175
+	}
176
+	else
177
+	{
178
+		readed = 0;
179
+	}
180
+
181
+	while(readed < to_read)
182
+	{
183
+		const ssize_t left = to_read - readed;
184
+		if(!(inbuf = PyIter_Next(self->in_iter)))
185
+		{
186
+			break;
187
+		}
188
+		if(!PyBytes_Check(inbuf))
189
+		{
190
+			PyErr_SetString(PyExc_TypeError, "Expected input iterator to return bytes");
191
+			return -1;
192
+		}
193
+		Py_ssize_t inbuf_sz = PyBytes_Size(inbuf);
194
+		char *inbuf_ptr = PyBytes_AsString(inbuf);
195
+
196
+		memcpy((char*)buf+readed, inbuf_ptr,
197
+				inbuf_sz > left?left:inbuf_sz);
198
+		readed += inbuf_sz > left?left:inbuf_sz;
199
+		if(inbuf_sz > left)
200
+		{
201
+			self->prev_buf = &inbuf_ptr[left];
202
+			self->prev_buf_obj = inbuf;
203
+			self->left_in_buf = inbuf_sz - left;
204
+		}
205
+		else
206
+		{
207
+			Py_DECREF(inbuf);
208
+		}
209
+	}
210
+
211
+	return readed;
212
+}
213
+
214
+/**@brief The tp_iternext method of RNNoise
215
+ * @return Next processed frame from input_iterator (bytes instance)
216
+ */
217
+PyObject *pyrnnoise_iternext(PyObject *_self)
218
+{
219
+	PyRNNoise_t *self = (PyRNNoise_t*)_self;
220
+	if(!self->in_iter)
221
+	{
222
+		PyErr_SetString(PyExc_StopIteration, "No input use the iter_on class method");
223
+		return NULL;
224
+	}
225
+
226
+	float frame[frame_size];
227
+	short res[frame_size];
228
+
229
+	ssize_t readed = _iter_frame(self, res);
230
+	if(readed < 0)
231
+	{
232
+		return NULL;
233
+	}
234
+	else if(!readed)
235
+	{
236
+		Py_DECREF(self->in_iter);
237
+		self->in_iter = NULL;
238
+		PyErr_SetString(PyExc_StopIteration, "Input exhausted");
239
+		return NULL;
240
+	}
241
+	else if(readed  % 2)
242
+	{
243
+		PyErr_SetString(PyExc_ValueError,
244
+			"Iterator exhausted on invalid odd bytes count for as single channel 16bits PCM input");
245
+		return NULL;
246
+	}
247
+
248
+	for(int i=0; i<readed/2; i++) { frame[i] = res[i]; }
249
+	for(int i=readed/2; i<frame_size; i++) { frame[i] = 0; }
250
+
251
+	rnnoise_process_frame(self->st, frame, frame);
252
+
253
+	for(int i=0; i<frame_size; i++) { res[i] = frame[i]; }
254
+	return PyBytes_FromStringAndSize((char*)res, readed);
255
+}
256
+
257
+/**@brief RNNoise.iter_on(iterator) method
258
+ * @return A new reference on self iterator
259
+ */
260
+PyObject* pyrnnoise_iter_on(PyObject *_self, PyObject *iterator)
261
+{
262
+	PyRNNoise_t *self = (PyRNNoise_t*)_self;
263
+	if(!PyIter_Check(iterator))
264
+	{
265
+		PyErr_SetString(PyExc_TypeError, "Expected an iterator");
266
+		return NULL;
267
+	}
268
+	if(self->in_iter)
269
+	{
270
+		Py_DECREF(self->in_iter);
271
+	}
272
+	Py_INCREF(iterator);
273
+	self->in_iter = iterator;
274
+	return PyObject_GetIter(_self);
275
+}
276
+
277
+
278
+/*
279
+ *	RNNoise class definition
280
+ */
281
+
282
+/**@brief RNNoise methods list */
283
+PyMethodDef RNNoise_methods[] = {
284
+	{"process_frame", (PyCFunction)pyrnnoise_process_frame, METH_FASTCALL,
285
+		"Process a frame of data\n\
286
+Arguments :\n\
287
+    - frame : bytes representing a 16bit PCM audio signal (len must be \n\
288
+              rnnoise.frame_size() * rnnoise.bytes_per_sample())\n\
289
+Returns : The processed frame bytes"},
290
+	{"iter_on", (PyCFunction)pyrnnoise_iter_on, METH_O,
291
+		"Consume an iterator and iterate on processed frames\n\
292
+Arguments :\n\
293
+    - iterator : an iterator on PCM 16bit bytes\n\
294
+Returns : an iterator on processed frames"},
295
+	{ NULL } // Sentinel
296
+};
297
+
298
+/**@brief RNNoise attributes list */
299
+PyMemberDef RNNoise_members[] = {
300
+	{"frame_size", T_INT, offsetof(PyRNNoise_t, frame_size), READONLY,
301
+	 "The number of sample processed by RNNoise.process_frame()"},
302
+	{NULL} // Sentinel
303
+};
304
+
305
+/**@brief RNNoise type object */
306
+PyTypeObject RNNoiseType = {
307
+	PyVarObject_HEAD_INIT(NULL, 0)
308
+	.tp_name = "rnnoise.RNNoise",
309
+	.tp_doc= "Xiph RNNoise voice noise reduction wrapper",
310
+	.tp_basicsize = sizeof(PyRNNoise_t),
311
+	.tp_del = pyrnnoise_del,
312
+	.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
313
+	.tp_methods = RNNoise_methods,
314
+	.tp_members = RNNoise_members,
315
+	.tp_iter = PyObject_SelfIter,
316
+	.tp_iternext = pyrnnoise_iternext,
317
+	.tp_init = pyrnnoise_init,
318
+	.tp_new = pyrnnoise_new,
319
+};
320
+
321
+
322
+/*
323
+ *	rnnoise module definition
324
+ */
325
+
326
+/**@brief Module's method without argument, returning the number
327
+ * of samples processed by RNNoise.process_frame()
328
+ */
329
+PyObject* pyrnnoise_frame_size(PyObject *mod, PyObject *nullval)
330
+{
331
+	return PyLong_FromLong(frame_size);
332
+}
333
+
334
+
335
+/**@brief Module's method without argument, returning the number of
336
+ * bytes per sample
337
+ */
338
+PyObject* pyrnnoise_bytes_per_sample(PyObject *mod, PyObject *nullval)
339
+{
340
+	return PyLong_FromLong(2);
341
+}
342
+
343
+/**@brief rnnoise module's methods */
344
+PyMethodDef pyrnnoise_methods[] = {
345
+	{"frame_size", (PyCFunction)pyrnnoise_frame_size, METH_NOARGS,
346
+		"Return the number of samples processed by RNNoise.process_frame() method"},
347
+	{"bytes_per_sample", (PyCFunction)pyrnnoise_bytes_per_sample, METH_NOARGS,
348
+		"Return the number of bytes per sample"},
349
+	{NULL}
350
+};
351
+
352
+/**@brief rnnoise module object */
353
+PyModuleDef pyrnnoise_module = {
354
+	PyModuleDef_HEAD_INIT,
355
+	.m_name = "rnnoise",
356
+	.m_doc = "Python bindings to xiph rnnoise",
357
+	.m_size = -1,
358
+	.m_methods = pyrnnoise_methods,
359
+	.m_slots = NULL,
360
+	.m_traverse = NULL,
361
+	.m_clear = NULL,
362
+	.m_free = NULL,
363
+};
364
+
365
+/**@brief Module initialisation function (kind of main or __init__) */
366
+PyMODINIT_FUNC
367
+PyInit_rnnoise(void)
368
+{
369
+	PyObject *mod;
370
+
371
+	mod = PyModule_Create(&pyrnnoise_module);
372
+	if(mod == NULL) { return NULL; }
373
+
374
+	if(PyType_Ready(&RNNoiseType) < 0)
375
+	{
376
+		goto fail_rnnoisetype_ready;
377
+	}
378
+	Py_INCREF(&RNNoiseType);
379
+	if(PyModule_AddObject(mod, "RNNoise", (PyObject*)&RNNoiseType) < 0)
380
+	{
381
+		goto fail_rnnoisetype_add;
382
+	}
383
+
384
+	frame_size = rnnoise_get_frame_size();
385
+
386
+	return mod;
387
+
388
+fail_rnnoisetype_add:
389
+	Py_DECREF(&RNNoiseType);
390
+fail_rnnoisetype_ready:
391
+	Py_DECREF(mod);
392
+	return NULL;
393
+}
394
+

+ 63
- 0
rnnoise_demo.c View File

@@ -0,0 +1,63 @@
1
+/* Copyright (c) 2018 Gregor Richards
2
+ * Copyright (c) 2017 Mozilla
3
+ * Copyright (c) 2023 Yann Weber */
4
+/*
5
+   Redistribution and use in source and binary forms, with or without
6
+   modification, are permitted provided that the following conditions
7
+   are met:
8
+
9
+   - Redistributions of source code must retain the above copyright
10
+   notice, this list of conditions and the following disclaimer.
11
+
12
+   - Redistributions in binary form must reproduce the above copyright
13
+   notice, this list of conditions and the following disclaimer in the
14
+   documentation and/or other materials provided with the distribution.
15
+
16
+   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
+   ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
+   LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
+   A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR
20
+   CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
21
+   EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
22
+   PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
23
+   PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
24
+   LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
25
+   NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26
+   SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
+*/
28
+
29
+#include <stdio.h>
30
+#include <string.h>
31
+#include <rnnoise.h>
32
+
33
+#define FRAME_SIZE 480
34
+
35
+int main(int argc, char **argv) {
36
+  int i;
37
+  int first = 1;
38
+  float x[FRAME_SIZE];
39
+  FILE *f1, *fout;
40
+  DenoiseState *st;
41
+  st = rnnoise_create(NULL);
42
+  if (argc!=3) {
43
+    fprintf(stderr, "usage: %s <noisy speech> <output denoised>\n", argv[0]);
44
+    return 1;
45
+  }
46
+  f1 = fopen(argv[1], "rb");
47
+  fout = fopen(argv[2], "wb");
48
+  while (1) {
49
+    short tmp[FRAME_SIZE];
50
+    size_t readed = fread(tmp, sizeof(short), FRAME_SIZE, f1);
51
+    bzero(x, sizeof(x));
52
+    for (i=0;i<readed;i++) x[i] = tmp[i];
53
+    rnnoise_process_frame(st, x, x);
54
+    for (i=0;i<readed;i++) tmp[i] = x[i];
55
+    fwrite(tmp, sizeof(short), readed, fout);
56
+    if (feof(f1)) break;
57
+    first = 0;
58
+  }
59
+  rnnoise_destroy(st);
60
+  fclose(f1);
61
+  fclose(fout);
62
+  return 0;
63
+}

+ 24
- 0
samples/LICENCE View File

@@ -0,0 +1,24 @@
1
+Base Audio Libre De Mots Français
2
+-----------------------------------
3
+
4
+La "Base Audio Libre De Mots Français" est une base de données 
5
+d'enregistrements sonores d'une liste d'environ 12000 mots ou 
6
+expressions en langue française. 
7
+
8
+Le contenu de cette base de données est distribué librement sous 
9
+licence "Creative Commons Paternité 2.0". Vous trouverez
10
+plus d'informations sur cette licence sur :
11
+
12
+	http://creativecommons.org/licenses/by/2.0/fr/
13
+
14
+L'ensemble de cette base audio est disponible à l'adresse suivante :
15
+
16
+ 	http://shtooka.net/audio-base/fra/
17
+
18
+L'enregistrement audio a été réalisé à l'aide du programme "Shtooka
19
+Recorder" distribué sous licence GPL et disponible à l'adresse 
20
+suivante :
21
+
22
+	http://shtooka.net/soft/shtooka_recorder/
23
+
24
+"Base Audio Libre De Mots Français" - Copyright (c) 2006-2007 Vion Nicolas

+ 3
- 0
samples/README View File

@@ -0,0 +1,3 @@
1
+Sample generated from shootka.net (see LICENCE file) words using :
2
+
3
+ffmpeg -i input.flac -f wav -acodec pcm_s16le -ac 1 -ar 16000 output.wav

BIN
samples/bonjour.wav View File


+ 21
- 0
setup.py View File

@@ -0,0 +1,21 @@
1
+from setuptools import setup, Extension
2
+
3
+setup(name = 'pyrnnoise',
4
+	version = '0.1',
5
+        test_suite='test_pyrnnoise',
6
+        description="Python wrapper for Xiph.org rnnoise algorithm implementation",
7
+        long_description="file: README.md",
8
+        author = "Yann Weber",
9
+        author_email = "pyrnnoise@yannweb.net",
10
+        url = "https://git.yannweb.net/yannweb/pyrnnoise",
11
+        license = "BSD-3-Clause",
12
+        keywords = ["rnnoise", "noise reduction", "audio processing"],
13
+	ext_modules = [
14
+		Extension(
15
+                        name="rnnoise",
16
+			libraries=['rnnoise'],
17
+                        sources=["pyrnnoise.c"],
18
+                        extra_compile_args=["-Wall", "-Werror"],
19
+		)
20
+	]
21
+);

+ 55
- 0
test.sh View File

@@ -0,0 +1,55 @@
1
+#!/bin/bash
2
+
3
+INFILE=samples/bonjour.wav
4
+
5
+hr() {
6
+ echo "$(printf "%0.s+" $(seq 80))"
7
+}
8
+
9
+hr
10
+echo "Running test_pyrnnoise.py"
11
+hr
12
+/usr/bin/env python3 test_pyrnnoise.py -vv || exit 1
13
+
14
+out_orig=$(mktemp -t out_XXXXXXXX.wav)
15
+out_1=$(mktemp -t out_XXXXXXXX.wav)
16
+out_2=$(mktemp -t out_XXXXXXXX.wav)
17
+
18
+clean_exit() {
19
+	rm $out_orig $out_1 $out_2
20
+	exit $1
21
+}
22
+
23
+
24
+hr
25
+echo "Functionnal test of rnnoise module against Xiph demo program"
26
+hr
27
+
28
+if [ ! -f 'rnnoise_demo' ]
29
+then
30
+	echo "Compiling rnnoise_demo"
31
+	gcc -o rnnoise_demo rnnoise_demo.c -lrnnoise || clean_exit 12
32
+fi
33
+echo "Running Xiph demo programm"
34
+python3 demo_orig.py "$INFILE" "$out_orig" || clean_exit 2
35
+
36
+echo "Running demo.py"
37
+python3 demo.py "$INFILE" "$out_2" || clean_exit 3
38
+if diff -rupN "$out_orig" "$out_2"
39
+then
40
+	echo "demo.py [OK]"
41
+else
42
+	echo "demo.py  [fail]"
43
+fi
44
+
45
+
46
+echo "Running demo_iter.py"
47
+python3 demo_iter.py "$INFILE" "$out_1" || clean_exit 4
48
+if diff -rupN "$out_orig" "$out_1"
49
+then
50
+	echo "demo_iter.py [OK]"
51
+else
52
+	echo "demo_iter.py  [fail]"
53
+fi
54
+
55
+clean_exit 0

+ 111
- 0
test_pyrnnoise.py View File

@@ -0,0 +1,111 @@
1
+#!/usr/bin/env python3
2
+
3
+import unittest
4
+import wave
5
+
6
+import rnnoise
7
+
8
+INPUT="samples/bonjour.wav"
9
+
10
+class TestRNNoiseModule(unittest.TestCase):
11
+
12
+    def test_frame_info(self):
13
+        """ Check rnnoise module's methods """
14
+        fsize = rnnoise.frame_size()
15
+        bps = rnnoise.bytes_per_sample()
16
+        self.assertTrue(isinstance(fsize, int))
17
+        self.assertTrue(isinstance(bps, int))
18
+        self.assertTrue(fsize > 0)
19
+        self.assertTrue(bps > 0)
20
+
21
+class TestRNNoise(unittest.TestCase):
22
+
23
+    def test_process_frame(self):
24
+        rnn = rnnoise.RNNoise()
25
+        with wave.open(INPUT, 'rb') as fp:
26
+            frame = fp.readframes(rnn.frame_size)
27
+            res = rnn.process_frame(frame)
28
+            self.assertTrue(isinstance(res, bytes))
29
+            self.assertEqual(len(res), rnn.frame_size*rnnoise.bytes_per_sample())
30
+
31
+    def test_process_frame_invalid(self):
32
+        rnn = rnnoise.RNNoise()
33
+        bad_frames = (
34
+                bytes(rnn.frame_size),
35
+                bytes((rnn.frame_size-1)*rnnoise.bytes_per_sample()),
36
+                bytes((rnn.frame_size+1)*rnnoise.bytes_per_sample()),
37
+                bytes((rnn.frame_size*rnnoise.bytes_per_sample())-1),
38
+                bytes((rnn.frame_size*rnnoise.bytes_per_sample())+1),
39
+        )
40
+        for bad_frame in bad_frames:
41
+            msg = "Testing with a bad frame"
42
+            with self.subTest(msg, frame_len=len(bad_frame)) as st:
43
+                with self.assertRaises(ValueError):
44
+                    rnn.process_frame(bad_frame)
45
+
46
+class TestRNNoiseIterator(unittest.TestCase):
47
+    """ Testing RNNoise.iter_on() method """
48
+
49
+    def setUp(self):
50
+        rnn = rnnoise.RNNoise()
51
+        self.orig = bytes()
52
+        with wave.open(INPUT, 'rb') as fp:
53
+            needed = rnn.frame_size * rnnoise.bytes_per_sample()
54
+            while True:
55
+                frame = fp.readframes(rnn.frame_size)
56
+                if len(frame) == 0:
57
+                    break
58
+                if len(frame) < needed:
59
+                    frame += bytes(needed-len(frame))
60
+                newframe = rnn.process_frame(frame)
61
+                newframe =  newframe[:len(frame)]
62
+                self.orig += newframe
63
+
64
+    @staticmethod
65
+    def in_iter(fsize):
66
+        """ fsize is a callable returning expected frame size """
67
+        with wave.open(INPUT, 'rb') as fp:
68
+            while True:
69
+                frame = fp.readframes(fsize())
70
+                if len(frame):
71
+                    yield frame
72
+                else:
73
+                    raise StopIteration()
74
+
75
+    def test_iter_big_samples(self):
76
+        """ Testing with iterator on 4096 samples """
77
+        rnn = rnnoise.RNNoise()
78
+        result = bytes()
79
+        for frame in rnn.iter_on(self.in_iter(lambda: 4096)):
80
+            result += frame
81
+        self.assertEqual(result, self.orig)
82
+
83
+    def test_byte_sample(self):
84
+        """ Testing with iterator on 1 byte """
85
+        rnn = rnnoise.RNNoise()
86
+        def _cust_iter():
87
+            for sample in self.in_iter(lambda: 1):
88
+                yield sample[:1]
89
+                yield sample[1:]
90
+
91
+        result = bytes()
92
+        for frame in rnn.iter_on(_cust_iter()):
93
+            result += frame
94
+        self.assertEqual(result, self.orig)
95
+
96
+    def test_invalid(self):
97
+        """ Testing with invalid iterator on odd number of bytes """
98
+        rnn = rnnoise.RNNoise()
99
+        def _cust_iter():
100
+            data = self.in_iter(lambda: 4096)
101
+            yield next(data)[:-1]
102
+            for sample in data:
103
+                yield sample
104
+
105
+        with self.assertRaises(ValueError):
106
+            result = bytes()
107
+            for frame in rnn.iter_on(_cust_iter()):
108
+                result += frame
109
+
110
+if __name__ == '__main__':
111
+    unittest.main()

Loading…
Cancel
Save