python-sounddevice icon indicating copy to clipboard operation
python-sounddevice copied to clipboard

how to use channel mapping when in 'Recording with Arbitrary Duration'?

Open huangzhenyu opened this issue 2 years ago • 7 comments

I connect with a fireface sound card. The example using Inputstream object has no mapping as input parameter?

huangzhenyu avatar Aug 03 '21 12:08 huangzhenyu

I suppose stream does not allow to do a more sophisticated mapping. But you can get that with some small modifications, for example (there is certainly nicer ways to code it):

...

rec_channels = [0, 2, 3]  # channel IDs starting at 0

def callback(indata, frames, time, status):
    """This is called (from a separate thread) for each audio block."""
    if status:
        print(status, file=sys.stderr)
    q.put(indata[:, rec_channels].copy())

...

    with sf.SoundFile(args.filename, mode='x', samplerate=args.samplerate,
                      channels=len(rec_channels), subtype=args.subtype) as file:

...

HaHeho avatar Aug 04 '21 13:08 HaHeho

@HaHeho already suggested a plaform-independent method. Something very similar is also used in the implementation of the rec() function.

Depending on your OS and host API you might also be able to use a platform-specific channel mapping, see https://python-sounddevice.readthedocs.io/en/0.4.2/api/platform-specific-settings.html.

mgeier avatar Aug 04 '21 19:08 mgeier

I suppose stream does not allow to do a more sophisticated mapping. But you can get that with some small modifications, for example (there is certainly nicer ways to code it):

...

rec_channels = [0, 2, 3]  # channel IDs starting at 0

def callback(indata, frames, time, status):
    """This is called (from a separate thread) for each audio block."""
    if status:
        print(status, file=sys.stderr)
    q.put(indata[:, rec_channels].copy())

...

    with sf.SoundFile(args.filename, mode='x', samplerate=args.samplerate,
                      channels=len(rec_channels), subtype=args.subtype) as file:

...

thanks for your reply. A question here is when using rec() or playrec() the mapping list can be either channel which could start any number ,eg.[5,6], and this works normally, but this is can not be used in your code when mapping is [5,6] and raise "index 5 is out of bounds for axis 1 with size 2".

huangzhenyu avatar Aug 05 '21 03:08 huangzhenyu

@HaHeho already suggested a plaform-independent method. Something very similar is also used in the implementation of the rec() function.

Depending on your OS and host API you might also be able to use a platform-specific channel mapping, see https://python-sounddevice.readthedocs.io/en/0.4.2/api/platform-specific-settings.html.

thks!it is quite different when i use rec() function and Inputstream() object with extra settings. The former can record normally with dual channel. and the later can only get mono wav.

    channels = len(mapping)
    sd_in    = sd.CoreAudioSettings(channel_map=mapping)
    # sd.default.extra_settings = sd_in
    q = queue.Queue()
    def audio_callback(indata, frames, time, status):
        if status:
            print(status, file=sys.stderr)
        q.put(indata.copy())
    
    try:
        filename_tmp = 'tmp.wav'
        with sf.SoundFile(filename_tmp, mode='x', samplerate=fs, channels=channels) as file:
            with sd.InputStream(samplerate=fs, channels=channels, extra_settings=sd_in,
             callback=audio_callback, device=self.deviceOnlineDetection(soundcard='Fireface', verbose=False)):
                print('#' * 80)
                print('press Ctrl+C to stop the recording')
                print('#' * 80)
                while True:
                    file.write(q.get())

    except KeyboardInterrupt:
        if filename is None:
            filename = '{}.wav'.format(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
        os.rename(filename_tmp, filename)
        # os.remove(filename_tmp)
        print('\nRecording finished: ' + repr(filename))`

huangzhenyu avatar Aug 05 '21 06:08 huangzhenyu

thanks for your reply. A question here is when using rec() or playrec() the mapping list can be either channel which could start any number ,eg.[5,6], and this works normally, but this is can not be used in your code when mapping is [5,6] and raise "index 5 is out of bounds for axis 1 with size 2".

Yes, the provided number of channels for InputStream still has to be max() + 1. I have changed the argparse channels parameter, so this can be used by python rec.py rec.wav -c 0 2 3:

#!/usr/bin/env python3
"""Create a recording with arbitrary duration.

The soundfile module (https://PySoundFile.readthedocs.io/) has to be installed!
"""

import argparse
import tempfile
import queue
import sys

import sounddevice as sd
import soundfile as sf
import numpy  # Make sure NumPy is loaded before it is used in the callback

assert numpy  # avoid "imported but unused" message (W0611)


def int_or_str(text):
    """Helper function for argument parsing."""
    try:
        return int(text)
    except ValueError:
        return text


parser = argparse.ArgumentParser(add_help=False)
parser.add_argument(
    "-l",
    "--list-devices",
    action="store_true",
    help="show list of audio devices and exit",
)
args, remaining = parser.parse_known_args()
if args.list_devices:
    print(sd.query_devices())
    parser.exit(0)
parser = argparse.ArgumentParser(
    description=__doc__,
    formatter_class=argparse.RawDescriptionHelpFormatter,
    parents=[parser],
)
parser.add_argument(
    "filename", nargs="?", metavar="FILENAME", help="audio file to store recording to"
)
parser.add_argument(
    "-d", "--device", type=int_or_str, help="input device (numeric ID or substring)"
)
parser.add_argument("-r", "--samplerate", type=int, help="sampling rate")
parser.add_argument(
    "-c", "--channels", nargs="+", type=int, default=0, help="list of input channels"
)
parser.add_argument(
    "-t", "--subtype", type=str, help='sound file subtype (e.g. "PCM_24")'
)
args = parser.parse_args(remaining)

q = queue.Queue()


def callback(indata, frames, time, status):
    """This is called (from a separate thread) for each audio block."""
    if status:
        print(status, file=sys.stderr)
    q.put(indata[:, args.channels].copy())


try:
    if args.samplerate is None:
        device_info = sd.query_devices(args.device, "input")
        # soundfile expects an int, sounddevice provides a float:
        args.samplerate = int(device_info["default_samplerate"])
    if args.filename is None:
        args.filename = tempfile.mkstemp(
            prefix="delme_rec_unlimited_", suffix=".wav", dir=""
        )

    # Make sure the file is opened before recording anything:
    with sf.SoundFile(
        args.filename,
        mode="x",
        samplerate=args.samplerate,
        channels=len(args.channels),
        subtype=args.subtype,
    ) as file:
        with sd.InputStream(
            samplerate=args.samplerate,
            device=args.device,
            channels=max(args.channels) + 1,
            callback=callback,
        ):
            print("#" * 80)
            print("press Ctrl+C to stop the recording")
            print("#" * 80)
            while True:
                file.write(q.get())
except KeyboardInterrupt:
    print("\nRecording finished: " + repr(args.filename))
    parser.exit(0)
except Exception as e:
    parser.exit(type(e).__name__ + ": " + str(e))

HaHeho avatar Aug 05 '21 09:08 HaHeho

thanks for your reply. A question here is when using rec() or playrec() the mapping list can be either channel which could start any number ,eg.[5,6], and this works normally, but this is can not be used in your code when mapping is [5,6] and raise "index 5 is out of bounds for axis 1 with size 2".

Yes, the provided number of channels for InputStream still has to be max() + 1. I have changed the argparse channels parameter, so this can be used by python rec.py rec.wav -c 0 2 3:

#!/usr/bin/env python3
"""Create a recording with arbitrary duration.

The soundfile module (https://PySoundFile.readthedocs.io/) has to be installed!
"""

import argparse
import tempfile
import queue
import sys

import sounddevice as sd
import soundfile as sf
import numpy  # Make sure NumPy is loaded before it is used in the callback

assert numpy  # avoid "imported but unused" message (W0611)


def int_or_str(text):
    """Helper function for argument parsing."""
    try:
        return int(text)
    except ValueError:
        return text


parser = argparse.ArgumentParser(add_help=False)
parser.add_argument(
    "-l",
    "--list-devices",
    action="store_true",
    help="show list of audio devices and exit",
)
args, remaining = parser.parse_known_args()
if args.list_devices:
    print(sd.query_devices())
    parser.exit(0)
parser = argparse.ArgumentParser(
    description=__doc__,
    formatter_class=argparse.RawDescriptionHelpFormatter,
    parents=[parser],
)
parser.add_argument(
    "filename", nargs="?", metavar="FILENAME", help="audio file to store recording to"
)
parser.add_argument(
    "-d", "--device", type=int_or_str, help="input device (numeric ID or substring)"
)
parser.add_argument("-r", "--samplerate", type=int, help="sampling rate")
parser.add_argument(
    "-c", "--channels", nargs="+", type=int, default=0, help="list of input channels"
)
parser.add_argument(
    "-t", "--subtype", type=str, help='sound file subtype (e.g. "PCM_24")'
)
args = parser.parse_args(remaining)

q = queue.Queue()


def callback(indata, frames, time, status):
    """This is called (from a separate thread) for each audio block."""
    if status:
        print(status, file=sys.stderr)
    q.put(indata[:, args.channels].copy())


try:
    if args.samplerate is None:
        device_info = sd.query_devices(args.device, "input")
        # soundfile expects an int, sounddevice provides a float:
        args.samplerate = int(device_info["default_samplerate"])
    if args.filename is None:
        args.filename = tempfile.mkstemp(
            prefix="delme_rec_unlimited_", suffix=".wav", dir=""
        )

    # Make sure the file is opened before recording anything:
    with sf.SoundFile(
        args.filename,
        mode="x",
        samplerate=args.samplerate,
        channels=len(args.channels),
        subtype=args.subtype,
    ) as file:
        with sd.InputStream(
            samplerate=args.samplerate,
            device=args.device,
            channels=max(args.channels) + 1,
            callback=callback,
        ):
            print("#" * 80)
            print("press Ctrl+C to stop the recording")
            print("#" * 80)
            while True:
                file.write(q.get())
except KeyboardInterrupt:
    print("\nRecording finished: " + repr(args.filename))
    parser.exit(0)
except Exception as e:
    parser.exit(type(e).__name__ + ": " + str(e))

no error raise, but get an empty wav file... I dont know why.

huangzhenyu avatar Aug 05 '21 09:08 huangzhenyu

With no command line parameters I get this error:

TypeError: object of type 'int' has no len()

See plot_input.py for an example how to use a list of channels.

With a command line argument of -c 1 I get this error:

TypeError: No format specified and unable to get format from file extension: (3, '[...]/delme_rec_unlimited_a7j6kaie.wav')

This gives a hint about the problem: mkstemp() returns a tuple.

mgeier avatar Aug 19 '21 19:08 mgeier