Can One Copy a Sound?

Is there a means to copy an AudioSound object? (Other than just loading the sound multiple times.)

To explain:

I have a class that handles a sound-object–but which doesn’t load that sound-object. (The sound-object is passed in as a parameter.)

As part of the usage of this class, I want to be able to make copies of its instances.

At the moment, the sound-related aspect of this copying simply involves passing the parent-instance’s sound-object into the copied instance. Which is fine… until I want to give the sounds in different instances different sound-attributes. (e.g. Play-rate, volume, etc.)

At this point I start to want separate sound-objects per class-instance.

Looking into this, AudioSound is apparently an abstract-class, meaning that I can’t just copy-construct the sound-object. A quick search of the API didn’t turn up anything that looks like it’ll copy a sound. And I don’t want to modify my sound-managing class to load the sound itself.

So, is this possible…?

(I do have a workaround for now, but being able to copy the sound-object would, I feel, make for a cleaner solution–if feasible…)

So its kinda sound instancing
i am not sure but i think i saw something similar in fmod

Hmm… Well, even if I were using FMod, would that interface be exposed by Panda?

But in any case, I’m pretty sure that I’m on the OpenAL backend.

[edit]
Doing a bit more searching, I did find the “getSound” method of the AudioManager class.

However, I get the distinct impression that it’s not really intended to be used in this way.

And in any case, unless I’m messing up the other parameters somehow, it doesn’t seem to accept an AudioSound as a parameter–it wants a “MovieAudio” object.

I also see now that, if I print a loaded sound, it indicates that the object is an “OpenALAudioSound”, specifically. However, I’m not sure that I can import that class within Python–if it can be done, I’m not sure of where to import it from. (“panda3d.core” doesn’t seem to have it, and a few attempts at other paths likewise failed.)

I must warn you, I haven’t fully figured it out. But the easiest way is to use copies of audio samples and create your own UserDataAudio object, which you can copy multiple times.

However, this is only theoretical, since I don’t have time to figure out how to create sound programmatically.

Thanks to the extensive forum, I found an example.

It remains to figure out how to get the data from the audio sample buffer from your uploaded file.

Hmm, I see… Although I’m hesitant about how fast such an approach might be, since I want this for realtime usage…

Still, I thank you, as it is an interesting approach!

And I doubt your doubts. I don’t see any problems, you load audio samples into memory once and produce them at your discretion, I can’t figure out where there might be a performance loss.

from direct.showbase.ShowBase import ShowBase

from panda3d.core import UserDataAudio
import wave

def copy_audio(data):
    source = UserDataAudio(44100, 2)
    source.append(data)
    source.done()
    return(base.sfxManagerList[0].get_sound(source)  )

class MyApp(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

        audio_data = False
        with wave.open('fx.wav', 'rb') as wav_file:
            num_frames = wav_file.getnframes()
            frames = wav_file.readframes(num_frames)
            audio_data = True

        if audio_data:
            sound_1 = copy_audio(frames)
            sound_1.play()

            sound_2 = copy_audio(frames)
            sound_2.set_play_rate(0.05)
            sound_2.play()
  
app = MyApp()
app.run()

However, for some reason the sound does not loop, it seems that this problem has not been solved yet.

An example of a pseudo mini piano.

from direct.showbase.ShowBase import ShowBase

from panda3d.core import UserDataAudio
import wave

def copy_audio(data):
    source = UserDataAudio(44100, 2)
    source.append(data)
    source.done()
    
    return(base.sfxManagerList[0].get_sound(source))

class MyApp(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

        self.audio_data = False
        with wave.open('fx.wav', 'rb') as wav_file:
            num_frames = wav_file.getnframes()
            self.frames = wav_file.readframes(num_frames)
            self.audio_data = True

        for i in range(1, 10):
            base.accept(str(i), self.piano, [i])
 
    def piano(self, note):
        if self.audio_data:
            sound = copy_audio(self.frames)
            sound.set_play_rate(note)
            sound.play()

app = MyApp()
app.run()

I don’t hear any delays or stuttering.

Ah, I think that I was going by the example in the thread to which you linked, which appeared to be working with data that had to be “manually” packed, and that over quite a few iterations!

But what you posted does indeed look rather more promising!

Trying it out, however, I run into the issue that it looks like that “wave” module is expecting, well, wav-files–while I’m using ogg-files!

Even trying it with a wav-file that I have lying around, the module can’t open said file, reporting that it’s of an unknown format. :/

It looks like Panda’s audio-manager classes do have a method that retrieves sound-data–but it’s private. Likewise, it looks like the AudioSound classes store their data–but don’t make it accessible.

All right, I think at this point that I’m going to just go with a workaround for now.

Thank you for your help!

I think you need to take into account that your wav file may have differences in the form of discrediting frequency and number of channels, this needs to be taken into account.

channels = wav_file.getnchannels()
frame_rate = wav_file.getframerate()

In my example, the frequency is 44100 and there are two channels.

The ffmpeg library is included with Panda, you can convert your ogg file to wav.

The thing is, the error is happening before I get to any of that–it’s happening on the call to “wave.open”.

I would rather stick to ogg-files: they tend to be smaller.

I understand what your problem is, it looks like you are using the wav format 32 bits floating point.

You need to select Signed 16 bit PCM when exporting or saving. I have an Audacity program, the file created in it opens without problems in the Wave module, while the size is several times smaller compared to 32-bit.

So, a quick comparison:

If I take an original wav-file, and convert it to both a 16-bit wav-file and an ogg-file (using Audacity, which as it happens is my choice of audio-editor too), I get the following sizes:

Original: ~7 MB
16-bit wav: ~3.5 MB
Ogg: ~490 kB

So the ogg-files are still much smaller.

Plus, with quite a lot of audio-files already in the project, I don’t want to convert them all.

And, yes, I could convert just the ones that I want to copy… but then I have to remember do that every time I decide that I want to copy an audio-file, which seems like a nuisance.

And as I said, I’ve settled on using a workaround for my current purposes.

In any case, to my mind the better solution might be to request (or make a pull-request providing) the implementation of a copy-function in AudioSound, on the C++ side. It looks like the data is there–it just needs to be used in this way. (I might think about trying such a modification sometime soon…)

There is no make_copy() on AudioSound. That would be a useful feature request!

However, you can just pass the same MovieAudio object or Filename into another call to AudioManager::get_sound (which is the underlying method used by loader.loadSound and friends).

What I suggest is the two-step loading: first loading the MovieAudio object using MovieAudio.get(filename), and then passing that to loader.loadSound() (or whatever call you are using to load it) instead of a path, then you’re sure that it doesn’t reload it from disk.

Using UserDataAudio and your own loading logic sounds absolutely overkill since Panda3D already implements loading sound files from .wav, and other formats like .ogg. If you wanted to access the raw sound data, you could just use the MovieAudio API.

Hmm… The thing is, the class in which I wanted this currently doesn’t have access to the audio-loading methods, and it seems cleaner to me that it continue to not have such access. Especially for nothing more than the task of making a copy of an object!

(By comparison, when a related class copies a model, it doesn’t have to have access to the model-loading methods. It just has to have access to NodePath’s “copyTo” method.)

For now I’ve worked around the issue–at least for the purpose to which I’m so far putting it–by keeping two instances of the class in question, each of which has been passed its own sound-object.

However, I’ll file a feature request over on GitHub shortly, I intend!

[edit] Okay, I’ve made a feature-request, and it should be visible here:

Maybe you can instead pass around the MovieAudio object and create the AudioSound on-demand from it?

Hmm… But would that not incur the same issue as constructing from filename: that the class that handles the AudioSound-object would have to have access to audio-loading methods from the AudioManager classes…?

If I’m understanding correctly, the main difference would be that I would be calling “AudioManager.getSound” rather than “AudioManager.loadSfx”.