4

I'm trying to create a simple app that loads wav files (one for each note of a keyboard) and plays specific ones when a midi note is pressed (or played). So far, I've created a midi input stream using mido and an audio stream using pyaudio in two separate threads. the goal is for the midi stream to update the currently playing notes, and the callback of the pyaudio stream to check for active notes and play those that are. The midi stream works fine, but my audio stream only seems to call the callback once, right when the script is started (print(notes)). Any idea how I can get the audio stream callback to update constantly?

import wave
from io import BytesIO
import os
from mido import MidiFile
import pyaudio
from time import sleep
from threading import Thread
import numpy

# Pipe: active, released
# Rank: many pipes
# Stop: one or more ranks
# Manual: multiple ranks
# Organ: multiple manuals

pipes = []
notes = []
p = pyaudio.PyAudio()


def mapRange(num, inMin, inMax, outMin, outMax):
    return int((num - inMin) * (outMax - outMin) / (inMax - inMin) + outMin)

def callback(in_data, frame_count, time_info, status):
    data = bytes(frame_count)
    print(notes)
    for note in notes:
        pipedata = bytes()
        if len(data) != 0:
            data1 = numpy.fromstring(data, numpy.int16)
            data2 = numpy.fromstring(note['sample'].readframes(frame_count), numpy.int16)
            pipedata = (data1 * 0.5 + data2 * 0.5).astype(numpy.int16)
        else:
            data2 = numpy.fromstring(note['sample'].readframes(frame_count), numpy.int16)
            pipedata = data2.astype(numpy.int16)
        data = pipedata.tostring()
    return (data, pyaudio.paContinue)

stream = p.open(format=pyaudio.paInt24,
                channels=2,
                rate=48000,
                output=True,
                stream_callback=callback,
                start=True)

# start the stream (4)
stream.start_stream()

for root, dirs, files in os.walk("samples"):
    for filename in files:
        file_on_disk = open(os.path.join(root, filename), 'rb')
        pipes.append(
            {"sample": wave.open(BytesIO(file_on_disk.read()), 'rb')})
for msg in MidiFile('test.mid').play():
    if msg.type == "note_on":
        notes.append(pipes[mapRange(msg.note, 36, 96, 0, 56)])
        print("on")
    if msg.type == "note_off":
        #notes[mapRange(msg.note, 36, 96, 0, 56)] = False
        print("off")

# wait for stream to finish (5)
while stream.is_active():
    sleep(0.1)

# stop stream (6)
stream.stop_stream()
stream.close()

# close PyAudio (7)
p.terminate()
John Roper
  • 157
  • 2
  • 14
  • I think you don't need those threads (at least the audio thread) because PyAudio/PortAudio creates a new OS thread itself to call the `callback()` in. Just read the audio files and set up PyAudio in the main thread until the call to `start_stream()`. After that, the `callback()` is already running in the background. Then you can play the MIDI file and append to `notes` (you should probably use a `queue.Queue` instead of a `list`?). Once you are done playing, you can call `stop_stream()`. No additional (Python) thread needed. – Matthias Sep 20 '18 at 05:45
  • I updated for use without threads, but still no difference. The callback function is only called once still. – John Roper Sep 20 '18 at 10:19
  • Hmmm, I don't see a problem except that `notes` is potentially changed during iteration (see my previous comment for a solution) and that the program is running forever because the stream stays "active". Can you please try to reduce the amount of code and make it reproducible? – Matthias Sep 20 '18 at 11:29
  • BTW, to avoid manual conversion to NumPy arrays, you could try the [sounddevice](https://python-sounddevice.readthedocs.io/) and [soundfile](https://pypi.org/project/soundfile/) modules (full disclosure: I'm the author and a heavy contributor, respectively). – Matthias Sep 20 '18 at 11:33
  • Well, queue removes each item after it is retrieved. I need them to stay in the notes until not needed anymore. – John Roper Sep 20 '18 at 16:29

1 Answers1

1

I too faced this issue and found this question in hopes of finding an answer, ended up figuring it out myself.

The data returned on the callback must match the number of frames (frames_per_buffer parameter in p.open). I see you didn't specify one so I think the default is 1024.

The thing is frames_per_buffer does not represent bytes but acrual frames. So since you specify the format as being pyaudio.paInt24 that means that one frames is represented by 3 bytes (24 / 8). So in your callback you should be returning 3072 bytes or the callback will not be called again for some reason.

If you were using blocking mode and not writing those 3072 bytes in stream.write() it would result in a weird effect of slow and crackling audio.

Esser420
  • 780
  • 1
  • 8
  • 19
  • Do you have the code? I'm facing a similar problem, but I can't find any solution. I opened a question here: https://stackoverflow.com/questions/65467212/pyaudio-callback-function-called-only-once Any help would be really appreciated – Mattia Surricchio Dec 27 '20 at 17:21