pyalsaaudio
pyalsaaudio copied to clipboard
Missing sample(s) at the end of 'chunks'
The problem reported in a previous issue remains. I've done quite a few tests, capturing samples and reading them in several different ways:
- Tight loops
- Threads
- Poll/Epoll
Each time, samples are lost at chunk boundaries. I've changed sample frequencies from 12000 to 48000 Hz. Change chunksize to different values. Nothing seems to solve the problem. I did a C version of the 'tight loop', which seems to work flawlessly. I can't really imagine that Python wouldn't be able to handle 12000 samples/s, so I don't know if there is some logic mine which is failing.
I'd appreciate pointers to continue experimenting!
Notes on the following program:
- The input/outputs are set at the top. In my case they correspond to Front Mic and Speaker output. I've connected them together with a cable to test. It's not necessary to do that, but probably it'd good to connect something to the input to see the missing samples.
- Two methods are in the Generator class. One for Poll, the other uses threads. Select the desired one near the bottom by (un)commenting (in
testcalls()). - The program generates a 1 second 1000 Hz tone and plots the result using matplotlib.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# lms_sines.py
#
# Copyright 2017 John Coppens <[email protected]>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.
#
#
import alsaaudio
import pylab as plt
import math as m
import numpy as np
import scipy as sp
import cmath
import select
import struct
from threading import Thread
import sys, time
import pdb
debug = False
MIC_INPUT = 2
SPKR_OUTPUT = 8
LOG_FNAME = "test.dat" # None
class In_Thread(Thread):
def __init__(self, in_pcm, in_frames, channels):
super(In_Thread, self).__init__()
self.in_pcm = in_pcm
self.in_data = []
self.in_frames = in_frames
self.channels = channels
def run(self):
in_frame = 0
while in_frame < self.in_frames:
l, chunk = self.in_pcm.read()
if l > 0:
d = list(struct.unpack("<" + "h" * (l * self.channels), chunk))
self.in_data += d
in_frame += l
return
class Out_Thread(Thread):
def __init__(self, out_pcm, out_data, out_frames, chunksize):
super(Out_Thread, self).__init__()
self.out_data = out_data
self.out_frames = out_frames
self.out_pcm = out_pcm
self.chunksize = chunksize
def run(self):
out_frame = 0
while out_frame < self.out_frames*2:
if self.out_pcm.write(self.out_data[out_frame:
out_frame + self.chunksize]) != 0:
out_frame += self.chunksize
print(out_frame)
class Generator():
""" Generate and record sounds
Constructor arguments:
fs Sample frequency [48000]
chunksize Chunk size in frames [32]
(1 frame is a sample on each channel)
samplesize Sample size in bytes [2]
"""
def __init__(self, fs = 48000, chunksize = 32, samplesize = 2):
#self.show_host_apis()
self.samplerate = fs
self.samplesize = samplesize
self.chunksize = chunksize
def selfcapture_threads(self, f = 1000, in_frames = 48000, channels = 1):
# Set up recorder
in_pcm = alsaaudio.PCM(alsaaudio.PCM_CAPTURE, mode = alsaaudio.PCM_NORMAL)
in_pcm.setchannels(channels)
in_pcm.setrate(self.samplerate)
in_pcm.setformat(alsaaudio.PCM_FORMAT_S16_LE)
in_pcm.setperiodsize(self.chunksize) # Period size in FRAMES
inpcm_fd, inpcm_emask = in_pcm.polldescriptors()[0]
# Set up generator
out_pcm = alsaaudio.PCM(alsaaudio.PCM_PLAYBACK, mode = alsaaudio.PCM_NORMAL)
out_pcm.setchannels(1)
out_pcm.setrate(self.samplerate)
out_pcm.setformat(alsaaudio.PCM_FORMAT_S16_LE)
out_pcm.setperiodsize(self.chunksize)
outpcm_fd, outpcm_emask = out_pcm.polldescriptors()[0]
# Generate the entire waveform (as signed 16-bit, little endians)
omega = 2.0 * m.pi * f / self.samplerate
out_frames = in_frames
out_data = b''
for t in range(out_frames):
s = int(32760 * m.sin(omega * t))
out_data += struct.pack("<h", s)
in_thread = In_Thread(in_pcm, in_frames, channels)
out_thread = Out_Thread(out_pcm, out_data, out_frames, self.chunksize)
in_thread.start()
out_thread.start()
in_thread.join()
out_thread.join()
return in_thread.in_data
def selfcapture_poll(self, f = 1000, in_frames = 48000, channels = 1):
# Set up recorder
in_pcm = alsaaudio.PCM(alsaaudio.PCM_CAPTURE, mode = alsaaudio.PCM_NONBLOCK)
in_pcm.setchannels(channels)
in_pcm.setrate(self.samplerate)
in_pcm.setformat(alsaaudio.PCM_FORMAT_S16_LE)
in_pcm.setperiodsize(self.chunksize) # Period size in FRAMES
inpcm_fd, inpcm_emask = in_pcm.polldescriptors()[0]
# Set up generator
out_pcm = alsaaudio.PCM(alsaaudio.PCM_PLAYBACK, mode = alsaaudio.PCM_NONBLOCK)
out_pcm.setchannels(1)
out_pcm.setrate(self.samplerate)
out_pcm.setformat(alsaaudio.PCM_FORMAT_S16_LE)
out_pcm.setperiodsize(self.chunksize)
outpcm_fd, outpcm_emask = out_pcm.polldescriptors()[0]
# Generate the entire waveform (as signed 16-bit, little endians)
omega = 2.0 * m.pi * f / self.samplerate
out_frame = 0
out_frames = in_frames
out_data = b''
for t in range(out_frames):
s = int(32760 * m.sin(omega * t))
out_data += struct.pack("<h", s)
in_data = []
poller = select.poll()
if debug:
print("Descriptors: In: %s, Out: %s" % (in_pcm.polldescriptors(),
out_pcm.polldescriptors()))
poller.register(inpcm_fd, inpcm_emask)
poller.register(outpcm_fd, outpcm_emask)
l, chunk = in_pcm.read()
in_frame = 0
# in_pcm.start()
while True:
ready = poller.poll(30)
if len(ready) == 0:
if debug: print("End events")
break
for fd, event in ready:
if fd == inpcm_fd:
if in_frame < in_frames:
l, chunk = in_pcm.read()
if l > 0:
d = list(struct.unpack("<" + "h" * (l * channels), chunk))
in_data += d
in_frame += l
else:
if debug: print("End input")
poller.unregister(fd)
elif fd == outpcm_fd:
if out_frame < out_frames*2:
if out_pcm.write(out_data[out_frame:
out_frame + self.chunksize]) != 0:
out_frame += self.chunksize
else:
if debug: print("End output")
poller.unregister(fd)
return in_data
def plot_data(self, data, channels = 1, plot_time = True):
""" Data are the data samples (16-bit only for now),
interleaved Left/Right
"""
xlen = len(data) // channels # xlen = 96000 = 48000
data = data[:xlen*channels]
x = []
for i in range(xlen):
if plot_time:
x.append(i/self.samplerate)
else:
x.append(i)
try:
if channels == 1:
plt.plot(x, data)
elif channels == 2:
plt.plot(x, data[0::2])
plt.plot(x, data[1::2])
except:
print("len(x): %d len(y): %d" % (len(x), len(data)))
plt.xlabel("Time (%s)" % ("s" if plot_time else "samples"))
plt.ylabel("Signal (16bit signed)")
plt.grid(True)
plt.show()
def print_data(self, data):
for y in data:
print("%6d " % y, end = '')
print()
def test_calls():
gen = Generator(chunksize = 64, fs = 48000)
#data = gen.selfcapture_threads(1000, 48000, channels = 2)
data = gen.selfcapture_poll(1000, 48000, channels = 2)
gen.plot_data(data, channels = 2, plot_time = False)
def main(args):
test_calls()
return 0
if __name__ == '__main__':
import sys
sys.exit(main(sys.argv))
In the code above, lots of code is duplicated because of the two methods (threads and polling) implemented.
Here are some screenshots of the output. These is the generated sine wave, reproduced and (using a cable) re-introduced (to the left and right mic inputs, plotted as blue and orange - not easy to distinguish), recorded and plotted:
(This is a single lost sample, the value from 'read' was 0 for both channels)
(An example of a double error)
To be sure the sine was was generated correctly, I connected the output to another machine, and recorded it there using Audacity.
is this still reproducible?
The code posted by the OP claims to be python3 code, so we can expect it to work. Have you tried?
There is a small chance that the fix I provided for #51 also fixes this. But I would have to spend more time to see whether these problems are the same.
With the changes below the OP code runs again. But the figures don't reproduce on my system, this might require some alsa configuration to work. (Commit used: 6317d9ad on main)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# lms_sines.py
#
# Copyright 2017 John Coppens <[email protected]>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.
#
#
import alsaaudio
import pylab as plt
import math as m
import numpy as np
import scipy as sp
import cmath
import select
import struct
from threading import Thread
import sys, time
import pdb
debug = True
MIC_INPUT = 2
SPKR_OUTPUT = 8
LOG_FNAME = "test.dat" # None
class In_Thread(Thread):
def __init__(self, in_pcm, in_frames, channels):
super(In_Thread, self).__init__()
self.in_pcm = in_pcm
self.in_data = []
self.in_frames = in_frames
self.channels = channels
def run(self):
in_frame = 0
while in_frame < self.in_frames:
l, chunk = self.in_pcm.read()
if l > 0:
d = list(struct.unpack("<" + "h" * (l * self.channels), chunk))
self.in_data += d
in_frame += l
return
class Out_Thread(Thread):
def __init__(self, out_pcm, out_data, out_frames, chunksize):
super(Out_Thread, self).__init__()
self.out_data = out_data
self.out_frames = out_frames
self.out_pcm = out_pcm
self.chunksize = chunksize
def run(self):
out_frame = 0
while out_frame < self.out_frames*2:
if self.out_pcm.write(self.out_data[out_frame:
out_frame + self.chunksize]) != 0:
out_frame += self.chunksize
print(out_frame)
class Generator():
""" Generate and record sounds
Constructor arguments:
fs Sample frequency [48000]
chunksize Chunk size in frames [32]
(1 frame is a sample on each channel)
samplesize Sample size in bytes [2]
"""
def __init__(self, fs = 48000, chunksize = 32, samplesize = 2):
#self.show_host_apis()
self.samplerate = fs
self.samplesize = samplesize
self.chunksize = chunksize
def selfcapture_threads(self, f = 1000, in_frames = 48000, channels = 1):
# Set up recorder
in_pcm = alsaaudio.PCM(alsaaudio.PCM_CAPTURE,
mode = alsaaudio.PCM_NORMAL,
channels = channels,
rate = self.samplerate,
format = alsaaudio.PCM_FORMAT_S16_LE,
periodsize = self.chunksize,
)
inpcm_fd, inpcm_emask = in_pcm.polldescriptors()[0]
# Set up generator
out_pcm = alsaaudio.PCM(alsaaudio.PCM_PLAYBACK,
mode = alsaaudio.PCM_NORMAL,
channels = 1,
rate = self.samplerate,
format = alsaaudio.PCM_FORMAT_S16_LE,
periodsize = self.chunksize,)
outpcm_fd, outpcm_emask = out_pcm.polldescriptors()[0]
# Generate the entire waveform (as signed 16-bit, little endians)
omega = 2.0 * m.pi * f / self.samplerate
out_frames = in_frames
out_data = b''
for t in range(out_frames):
s = int(32760 * m.sin(omega * t))
out_data += struct.pack("<h", s)
in_thread = In_Thread(in_pcm, in_frames, channels)
out_thread = Out_Thread(out_pcm, out_data, out_frames, self.chunksize)
in_thread.start()
out_thread.start()
in_thread.join()
out_thread.join()
return in_thread.in_data
def selfcapture_poll(self, f = 1000, in_frames = 48000, channels = 1):
# Set up recorder
in_pcm = alsaaudio.PCM(alsaaudio.PCM_CAPTURE,
mode = alsaaudio.PCM_NONBLOCK,
channels = channels,
rate = self.samplerate,
format = alsaaudio.PCM_FORMAT_S16_LE,
periodsize = self.chunksize,)
inpcm_fd, inpcm_emask = in_pcm.polldescriptors()[0]
# Set up generator
out_pcm = alsaaudio.PCM(alsaaudio.PCM_PLAYBACK,
mode = alsaaudio.PCM_NONBLOCK,
channels = 1,
rate = self.samplerate,
format = alsaaudio.PCM_FORMAT_S16_LE,
periodsize = self.chunksize,)
outpcm_fd, outpcm_emask = out_pcm.polldescriptors()[0]
# Generate the entire waveform (as signed 16-bit, little endians)
omega = 2.0 * m.pi * f / self.samplerate
out_frame = 0
out_frames = in_frames
out_data = b''
for t in range(out_frames):
s = int(32760 * m.sin(omega * t))
out_data += struct.pack("<h", s)
in_data = []
poller = select.poll()
if debug:
print("Descriptors: In: %s, Out: %s" % (in_pcm.polldescriptors(),
out_pcm.polldescriptors()))
poller.register(inpcm_fd, inpcm_emask)
poller.register(outpcm_fd, outpcm_emask)
l, chunk = in_pcm.read()
in_frame = 0
# in_pcm.start()
while True:
ready = poller.poll(30)
if len(ready) == 0:
if debug: print("End events")
break
for fd, event in ready:
if fd == inpcm_fd:
if in_frame < in_frames:
l, chunk = in_pcm.read()
if l > 0:
d = list(struct.unpack("<" + "h" * (l * channels), chunk))
in_data += d
in_frame += l
else:
if debug: print("End input")
poller.unregister(fd)
elif fd == outpcm_fd:
if out_frame < out_frames*2:
if out_pcm.write(out_data[out_frame:
out_frame + self.chunksize]) != 0:
out_frame += self.chunksize
else:
if debug: print("End output")
poller.unregister(fd)
return in_data
def plot_data(self, data, channels = 1, plot_time = True):
""" Data are the data samples (16-bit only for now),
interleaved Left/Right
"""
xlen = len(data) // channels # xlen = 96000 = 48000
data = data[:xlen*channels]
x = []
for i in range(xlen):
if plot_time:
x.append(i/self.samplerate)
else:
x.append(i)
try:
if channels == 1:
plt.plot(x, data)
elif channels == 2:
plt.plot(x, data[0::2])
plt.plot(x, data[1::2])
except:
print("len(x): %d len(y): %d" % (len(x), len(data)))
plt.xlabel("Time (%s)" % ("s" if plot_time else "samples"))
plt.ylabel("Signal (16bit signed)")
plt.grid(True)
plt.show()
def print_data(self, data):
for y in data:
print("%6d " % y, end = '')
print()
def test_calls():
gen = Generator(chunksize = 64, fs = 48000)
#data = gen.selfcapture_threads(1000, 48000, channels = 2)
data = gen.selfcapture_poll(1000, 48000, channels = 2)
gen.plot_data(data, channels = 2, plot_time = False)
def main(args):
test_calls()
return 0
if __name__ == '__main__':
import sys
sys.exit(main(sys.argv))
The previous issue mentioned is probably: #32
as OP apparently lost interest, i'm closing this.
i didn't study the test code in detail, because it's way too complex. - if there is an actual problem in the library, it can be reproduced much more easily. if the complexity seems necessary for reproduction, then either the bug is in the user code, or the real trigger is most likely system load of some kind (cpu, memory, device activity (interrupts), etc.).
the pattern in the plots does not indicate that samples are lost or added, but rather that the buffer is overwritten or not written in the first place. i suspect a bug in a lower layer (hardware, driver, libalsa), and hope it was fixed or worked around in the right place meanwhile.