iOS OpenAL声音不是位置

时间:2015-05-12 14:01:18

标签: c++ ios objective-c audio openal

好的,这是我的问题。

我正在研究一些帮助类,它为iOS封装了iOS上的OpenAL功能。知道,它工作得很好,这意味着我可以播放多个声音(同时)并配置增益和音高。

我的下一步是添加位置声音。我知道,我必须使用属性作为听众的位置和来源'位置(和速度,但不是那么重要,知道)。

问题是,我在互联网上实现了与教程相对应的所有内容,但声音总是没有衰减,所以它不是位置。

我发现,有时这是由立体声音频文件引起的,所以现在我使用单声道文件,但问题仍然存在。事实上,我从apple下载了这个example project。它适用于Mac OS并且确实有效。所以我从他们的项目中获取了声音文件并在我自己的项目中使用它们。因此声音格式应该没有问题。它仍然没有回放位置。

所以我的问题是: 是否可以在iPhone上播放OpenAl的声音位置? 如果是,那么为实现3D播放应该设置的最小OpenAL属性是什么?也许我只是没有设置一个所需的基本设置。

另外,我认为模拟器是问题,但当我在真正的iPhone上测试我的应用程序时,它也不起作用。

我知道在这里添加我当前的代码,但我认为直接找到错误太多了。我希望有人知道我烦人的问题的标准解决方案(我认为,这将是一个小错误:D)

所以这里是代码:

监听器:

class Listener {
    private:
    vec3    position;
    vec3    velocity;
    vec3    at;
    vec3    up;
public:
    Listener();

    void setOrientation(vec3 at, vec3 up);

    vec3 getPosition();
    void setPosition(vec3 position);

    vec3 getVelocity();
    void setVelocity(vec3 velocity);
};

Listener::Listener() {
    alListenerf(AL_GAIN, 0.5f);
    alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED);
}

void Listener::setOrientation(vec3 at, vec3 up)
{
    printf("Listener Set Orientation\n");
    ALfloat *orientation = (ALfloat*)malloc(6*sizeof(ALfloat));
    orientation[0] = at.x;
    orientation[1] = at.y;
    orientation[2] = at.z;
    orientation[3] = up.x;
    orientation[4] = up.y;
    orientation[5] = up.z;
    alListenerfv(AL_ORIENTATION, orientation);
    this->at = at;
    this->up = up;
    free(orientation);
}

vec3 Listener::getPosition()
{
    return position;
}

void Listener::setPosition(vec3 position)
{
    printf("Listener Set Position\n");
    ALfloat *positionArray = (ALfloat*)malloc(3*sizeof(ALfloat));
    positionArray[0] = position.x;
    positionArray[1] = position.y;
    positionArray[2] = position.z;
    alListenerfv(AL_POSITION, positionArray);
    this->position = position;
    free(positionArray);
}

vec3 Listener::getVelocity()
{
    return velocity;
}

void Listener::setVelocity(vec3 velocity)
{
    printf("Listener Set Velocity\n");
    ALfloat *velArray = (ALfloat*)malloc(3*sizeof(ALfloat));
    velArray[0] = velocity.x;
    velArray[1] = velocity.y;
    velArray[2] = velocity.z;
    alListenerfv(AL_VELOCITY, velArray);
    this->velocity = velocity;
    free(velArray);
}

样品

class Sample {
private:
    UInt32  usageCount;         // Number of Screens using this sound
    const   char *filename;     // The Filename, should be unique!
    ALuint  audioBuffer;        // The sound buffer in which it is stored
public:
    Sample(const char* filename);
    Sample(std::string);

    void    load();
    bool    unload();

    std::string getFilename();
    ALuint  getAudioBuffer();
};

Sample::Sample(const char* filename)
{
    usageCount = 0;
    this->filename = filename;
}

Sample::Sample(std::string filename)
{
    usageCount = 0;
    this->filename = filename.c_str();
}

void Sample::load()
{
    ALenum  format;
    ALvoid* data;
    ALsizei size;
    ALsizei freq;

    if (usageCount == 0) {
        data = SampleLoader::GetOpenALAudioData(filename, &size, &format, &freq);

        // Generate Buffer
        alGenBuffers(1, &audioBuffer);

        // Fill Buffer With Data
        alBufferData(audioBuffer, format, data, size, freq);

        // Free Audio Data
        if (data)
        {
            free(data);
            data = NULL;
        }
    }

    usageCount++;
}

// returns true, if usageCount got zero
bool Sample::unload()
{
    if (usageCount == 1) {
        // Delete Buffer
        alDeleteBuffers(1, &audioBuffer);
        return true;
    }

    usageCount--;
    return false;
}

std::string Sample::getFilename()
{
    std::string strFilename = std::string(filename);
    return strFilename;
}

ALuint Sample::getAudioBuffer()
{
    return audioBuffer;
}

来源

class Source {
    vec3        position;
    vec3        velocity;
    vec3        direction;
    ALfloat     gain;
    ALfloat     pitch;
    ALuint      sourceID;
    Sample      *sample;
public:
    void load();
    void unload();

    ALboolean   isPlaying();

    void play();
    void repeat();
    void stop();

    void updateSample(Sample *sample);
    void updatePosition(vec3 position);
    void updateVelocity(vec3 velocity);
    void updateDirection(vec3 direcation);
    void updateOrienation(vec3 position, vec3 velocity, vec3 direction);
    void updateGain(ALfloat gain);
    void updatePitch(ALfloat pitch);
};

void Source::load()
{
    alGenSources(1, &sourceID);

    position = vec3(0.0f, 0.0f, 0.0f);
    velocity = vec3(0.0f, 0.0f, 0.0f);
    direction = vec3(0.0f, 0.0f, 0.0f);

    alSourcefv(sourceID, AL_POSITION, value_ptr(position));
    alSourcefv(sourceID, AL_VELOCITY, value_ptr(velocity));
    alSourcefv(sourceID, AL_DIRECTION, value_ptr(direction));

    gain = 0.5f;
    pitch = 1.0f;

    alSourcef(sourceID, AL_GAIN, gain);
    alSourcef(sourceID, AL_PITCH, pitch);
}

void Source::unload()
{
    alDeleteSources(1, &sourceID);
}

ALboolean Source::isPlaying()
{
    ALint sourceState;
    alGetSourcei(sourceID, AL_SOURCE_STATE, &sourceState);
    return (sourceState == AL_PLAYING);
}

void Source::play()
{
    alSourcei(sourceID, AL_LOOPING, AL_FALSE);
    alSourcePlay(sourceID);
}

void Source::repeat()
{
    alSourcei(sourceID, AL_LOOPING, AL_TRUE);
    alSourcePlay(sourceID);
}

void Source::stop()
{
    alSourceStop(sourceID);
}

void Source::updateSample(Sample *sample)
{
    alSourcei(sourceID, AL_BUFFER, sample->getAudioBuffer());
}

void Source::updatePosition(vec3 position)
{
    alSource3f(sourceID, AL_POSITION, position.x, position.y, position.z);
}

void Source::updateVelocity(vec3 velocity)
{
    alSourcefv(sourceID, AL_VELOCITY, value_ptr(velocity));
}

void Source::updateDirection(vec3 direction)
{
    alSourcefv(sourceID, AL_DIRECTION, value_ptr(direction));
}

void Source::updateOrienation(vec3 position, vec3 velocity, vec3 direction)
{
    alSourcefv(sourceID, AL_POSITION, value_ptr(position));
    alSourcefv(sourceID, AL_VELOCITY, value_ptr(velocity));
    alSourcefv(sourceID, AL_DIRECTION, value_ptr(direction));
}

void Source::updateGain(ALfloat gain)
{
    alSourcef(sourceID, AL_GAIN, gain);
}

void Source::updatePitch(ALfloat pitch)
{
    alSourcef(sourceID, AL_PITCH, pitch);
}

Sample Loader Function,取自Apple的项目

void* SampleLoader::GetOpenALAudioData(const char* filename, ALsizei *outDataSize, ALenum *outDataFormat, ALsizei*  outSampleRate)
{
    OSStatus                        err = noErr;
    SInt64                          theFileLengthInFrames = 0;
    AudioStreamBasicDescription     theFileFormat;
    UInt32                          thePropertySize = sizeof(theFileFormat);
    ExtAudioFileRef                 extRef = NULL;
    void*                           theData = NULL;
    AudioStreamBasicDescription     theOutputFormat;

    // Create Path
    NSString *filenameString = [NSString stringWithCString:filename encoding:NSUTF8StringEncoding];
    NSArray *components = [filenameString componentsSeparatedByString:@"."];

    NSString *audioFilePath = [[NSBundle mainBundle] pathForResource:[components objectAtIndex:0] ofType:[components objectAtIndex:1]];
    NSURL *audioFileURL = [NSURL fileURLWithPath:audioFilePath];
    CFURLRef inFileURL = (__bridge CFURLRef)audioFileURL;

    // Open a file with ExtAudioFileOpen()
    err = ExtAudioFileOpenURL(inFileURL, &extRef);
    if(err) {
        printf("MyGetOpenALAudioData: ExtAudioFileOpenURL FAILED, Error = %d\n", (int)err);

        // Dispose the ExtAudioFileRef, it is no longer needed
        if (extRef) ExtAudioFileDispose(extRef);
        return theData;
    }

    // Get the audio data format
    err = ExtAudioFileGetProperty(extRef, kExtAudioFileProperty_FileDataFormat, &thePropertySize, &theFileFormat);
    if(err) {
        printf("MyGetOpenALAudioData: ExtAudioFileGetProperty(kExtAudioFileProperty_FileDataFormat) FAILED, Error = %d\n", (int)err);

        // Dispose the ExtAudioFileRef, it is no longer needed
        if (extRef) ExtAudioFileDispose(extRef);
        return theData;
    }
    if (theFileFormat.mChannelsPerFrame > 2)  {
        printf("MyGetOpenALAudioData - Unsupported Format, channel count is greater than stereo\n");

        // Dispose the ExtAudioFileRef, it is no longer needed
        if (extRef) ExtAudioFileDispose(extRef);
        return theData;
    }

    // Set the client format to 16 bit signed integer (native-endian) data
    // Maintain the channel count and sample rate of the original source format
    theOutputFormat.mSampleRate = theFileFormat.mSampleRate;
    theOutputFormat.mChannelsPerFrame = theFileFormat.mChannelsPerFrame;

    theOutputFormat.mFormatID = kAudioFormatLinearPCM;
    theOutputFormat.mBytesPerPacket = 2 * theOutputFormat.mChannelsPerFrame;
    theOutputFormat.mFramesPerPacket = 1;
    theOutputFormat.mBytesPerFrame = 2 * theOutputFormat.mChannelsPerFrame;
    theOutputFormat.mBitsPerChannel = 16;
    theOutputFormat.mFormatFlags = kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked | kAudioFormatFlagIsSignedInteger;

    // Set the desired client (output) data format
    err = ExtAudioFileSetProperty(extRef, kExtAudioFileProperty_ClientDataFormat, sizeof(theOutputFormat), &theOutputFormat);
    if(err) {
        printf("MyGetOpenALAudioData: ExtAudioFileSetProperty(kExtAudioFileProperty_ClientDataFormat) FAILED, Error = %d\n", (int)err);

        // Dispose the ExtAudioFileRef, it is no longer needed
        if (extRef) ExtAudioFileDispose(extRef);
        return theData;
    }

    // Get the total frame count
    thePropertySize = sizeof(theFileLengthInFrames);
    err = ExtAudioFileGetProperty(extRef, kExtAudioFileProperty_FileLengthFrames, &thePropertySize, &theFileLengthInFrames);
    if(err) {
        printf("MyGetOpenALAudioData: ExtAudioFileGetProperty(kExtAudioFileProperty_FileLengthFrames) FAILED, Error = %d\n", (int)err);

        // Dispose the ExtAudioFileRef, it is no longer needed
        if (extRef) ExtAudioFileDispose(extRef);
        return theData;
    }

    // Read all the data into memory
    UInt32 theFramesToRead = (UInt32)theFileLengthInFrames;
    UInt32 dataSize = theFramesToRead * theOutputFormat.mBytesPerFrame;;
    theData = malloc(dataSize);
    if (theData)
    {
        AudioBufferList     theDataBuffer;
        theDataBuffer.mNumberBuffers = 1;
        theDataBuffer.mBuffers[0].mDataByteSize = dataSize;
        theDataBuffer.mBuffers[0].mNumberChannels = theOutputFormat.mChannelsPerFrame;
        theDataBuffer.mBuffers[0].mData = theData;

        // Read the data into an AudioBufferList
        err = ExtAudioFileRead(extRef, &theFramesToRead, &theDataBuffer);
        if(err == noErr)
        {
            // success
            *outDataSize = (ALsizei)dataSize;
            *outDataFormat = (theOutputFormat.mChannelsPerFrame > 1) ? AL_FORMAT_STEREO16 : AL_FORMAT_MONO16;
            *outSampleRate = (ALsizei)theOutputFormat.mSampleRate;
        }
        else
        {
            // failure
            free (theData);
            theData = NULL; // make sure to return NULL
            printf("MyGetOpenALAudioData: ExtAudioFileRead FAILED, Error = %d\n", (int)err);

            // Dispose the ExtAudioFileRef, it is no longer needed
            if (extRef) ExtAudioFileDispose(extRef);
            return theData;
        }       
    }

    // Dispose the ExtAudioFileRef, it is no longer needed
    if (extRef) ExtAudioFileDispose(extRef);
    return theData;
}

SoundManager类

namespace SoundManager {
    extern ALCdevice *openALDevice;
    extern ALCcontext *openALContext;

    extern std::vector<Sample*> samples;

    void initialize();
    void release();

    Sample* manage(const char* filename);
    Sample* manage(std::string filename);
    void remove(Sample *sample);
}

namespace SoundManager {
    ALCdevice *openALDevice;
    ALCcontext *openALContext;

    std::vector<Sample*> samples;
}

void AudioInterruptionListenerCallback(void* user_data, UInt32 interruption_state)
{
    if (kAudioSessionBeginInterruption == interruption_state)
    {
        alcMakeContextCurrent(NULL);
    }
    else if (kAudioSessionEndInterruption == interruption_state)
    {
        AudioSessionSetActive(true);
        alcMakeContextCurrent(SoundManager::openALContext);
    }
}

void SoundManager::initialize()
{
    AudioSessionInitialize(NULL, NULL, AudioInterruptionListenerCallback, NULL);

    UInt32 session_category = kAudioSessionCategory_MediaPlayback;
    AudioSessionSetProperty(kAudioSessionProperty_AudioCategory, sizeof(session_category), &session_category);

    AudioSessionSetActive(true);

    // Open Device
    openALDevice = alcOpenDevice(NULL);

    // Create and activate context
    openALContext = alcCreateContext(openALDevice, NULL);
    alcMakeContextCurrent(openALContext);
}

void SoundManager::release()
{
    // Give up Context and destroy it
    alcMakeContextCurrent(NULL);
    alcDestroyContext(openALContext);

    // Close device
    alcCloseDevice(openALDevice);
}

Sample* SoundManager::manage(const char* filename)
{
    return manage(std::string(filename));
}

Sample* SoundManager::manage(std::string filename)
{
    if (samples.size() >= kMaxNumberOfSamples) {
        Engine::warning("Max number of samples reached! Release one first");
        return NULL;
    }

    // If Sound is already managed, load the managed again
    for (int i=0; i<samples.size(); i++) {
        if (samples[i]->getFilename().compare(filename) == 0) {
            printf("Already loaded\n");
            samples[i]->load();
            return samples[i];
        }
    }

    // Otherwise load the new sound and manage it
    printf("Load new sound\n");
    Sample *sample = new Sample(filename);
    sample->load();
    samples.push_back(sample);
    return sample;
}

void SoundManager::remove(Sample *sample)
{
    for (int i=0; i<samples.size(); i++) {
        if (samples[i]->getFilename().compare(sample->getFilename()) == 0) {
            // If Sound isn't in use from anywhere else, unmanage it
            if (samples[i]->unload()) {
                samples.erase(std::remove(samples.begin(), samples.end(), sample), samples.end());
                return;
            }
        }
    }

    Engine::warning("The sound to be removed wasn't managed!");
}

所以就在这里,这是源,缓冲区和监听器的所有包装类,以及一个管理器,它只是确保一个Sample只加载一次。所有这些都应用于一个屏幕类,它在我的简单引擎中呈现。它是独立的,应该只播放一个从左到右移动的声音。但它没有:

class SoundTestScreen : public Screen
{
private:
    void initialize();
    void load();
    void unload();

    void update(Time &time);
    void draw(Time &time);

private:
    Listener listener;
    Sample *bubbles;
    Source *bubbleSource;

    double totalTime;
    double lastPlayTime;
};

void SoundTestScreen::initialize()
{    
    listener.setPosition(vec3(0, 0, 0));
    listener.setVelocity(vec3(0, 0, 0));
    listener.setOrientation(vec3(0, 0, -1),vec3(0, 1, 0));

    totalTime = 0;
    lastPlayTime = 0;

    // Create Sources to play the samples
    bubbleSource = new Source();
}

void SoundTestScreen::load()
{
    // Only load sample once
    bubbles = SoundManager::manage("sound_electric.wav");

    bubbleSource->load();
    bubbleSource->updateSample(bubbles);
    bubbleSource->updatePosition(vec3(0,0,0));
    bubbleSource->repeat();
}

void SoundTestScreen::unload()
{
    // Only unload sample once
    SoundManager::remove(bubbles);

    bubbleSource->unload();
}

void SoundTestScreen::update(Time &time)
{   
    bubbleSource->updatePosition(vec3(100*sinf(time.ElapsedTime),0,0));
}

void SoundTestScreen::draw(Time &time)
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}

这只是一些基本的实现,但我真的不知道为什么声音不是位置。

我真的希望有人能找到我的错误,这很烦人。

问候

编辑:

它现在正在工作,我现在纠正了一些小错误,但很长时间没找到。 我将这段代码留给那些正在为OpenAL搜索iOS音效系统的人,这在互联网上是罕见的,据我所知。

1 个答案:

答案 0 :(得分:0)

哇,在提供示例代码时,你打倒了雷声! :d

因此,我从中学到的代码就是位置:

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#include <AL\al.h>
#include <AL\alc.h>
#include <AL\alut.h>

// Maximum data buffers we will need.
#define NUM_BUFFERS 3

// Maximum emissions we will need.
#define NUM_SOURCES 3

// These index the buffers and sources.
#define BATTLE      0
#define GUN1        1
#define GUN2        2

// Buffers hold sound data.
ALuint Buffers[NUM_BUFFERS];

// Sources are points of emitting sound.
ALuint Sources[NUM_SOURCES];

// Position of the source sounds.
ALfloat SourcesPos[NUM_SOURCES][3];

// Velocity of the source sounds.
ALfloat SourcesVel[NUM_SOURCES][3];


// Position of the listener.
ALfloat ListenerPos[] = { 0.0, 0.0, 0.0 };

// Velocity of the listener.
ALfloat ListenerVel[] = { 0.0, 0.0, 0.0 };

// Orientation of the listener. (first 3 elements are "at", second 3 are "up")
ALfloat ListenerOri[] = { 0.0, 0.0, -1.0, 0.0, 1.0, 0.0 };


/*
* ALboolean LoadALData()
*
*   This function will load our sample data from the disk using the Alut
*   utility and send the data into OpenAL as a buffer. A source is then
*   also created to play that buffer.
*/
ALboolean LoadALData()
{

    alutInit (NULL, NULL);

    //ALUT allows us to point to a buffer object using an ALuint.
    // furthermore, there are calls that allow you to load from memory.
    Buffers[BATTLE] = alutCreateBufferFromFile("C:\\Users\\Lee\\Desktop\\OpenAL\\OpenALStaticSound\\wavdata\\Battle.wav");
    Buffers[GUN1] = alutCreateBufferFromFile("C:\\Users\\Lee\\Desktop\\OpenAL\\OpenALStaticSound\\wavdata\\Gun1.wav");
    Buffers[GUN2] = alutCreateBufferFromFile("C:\\Users\\Lee\\Desktop\\OpenAL\\OpenALStaticSound\\wavdata\\Gun2.wav");

    // Bind the buffer with the source.
    //alGenSources (1, &Source);
    alGenSources(NUM_SOURCES, Sources);

    //Attach any special modifications to the source
    //alSourcei (Source, AL_BUFFER,   Buffer   );
    //alSourcef (Source, AL_PITCH,    1.0      );
    //alSourcef (Source, AL_GAIN,     1.0      );
    //alSourcefv(Source, AL_POSITION, SourcePos);
    //alSourcefv(Source, AL_VELOCITY, SourceVel);
    //alSourcei (Source, AL_LOOPING,  AL_TRUE  );

    alSourcei (Sources[BATTLE], AL_BUFFER,   Buffers[BATTLE]  );
    alSourcef (Sources[BATTLE], AL_PITCH,    1.0              );
    alSourcef (Sources[BATTLE], AL_GAIN,     1.0              );
    alSourcefv(Sources[BATTLE], AL_POSITION, SourcesPos[BATTLE]);
    alSourcefv(Sources[BATTLE], AL_VELOCITY, SourcesVel[BATTLE]);
    alSourcei (Sources[BATTLE], AL_LOOPING,  AL_TRUE          );

    alSourcei (Sources[GUN1], AL_BUFFER,   Buffers[GUN1]  );
    alSourcef (Sources[GUN1], AL_PITCH,    1.0            );
    alSourcef (Sources[GUN1], AL_GAIN,     1.0            );
    alSourcefv(Sources[GUN1], AL_POSITION, SourcesPos[GUN1]);
    alSourcefv(Sources[GUN1], AL_VELOCITY, SourcesVel[GUN1]);
    alSourcei (Sources[GUN1], AL_LOOPING,  AL_FALSE       );

    alSourcei (Sources[GUN2], AL_BUFFER,   Buffers[GUN2]  );
    alSourcef (Sources[GUN2], AL_PITCH,    1.0            );
    alSourcef (Sources[GUN2], AL_GAIN,     1.0            );
    alSourcefv(Sources[GUN2], AL_POSITION, SourcesPos[GUN2]);
    alSourcefv(Sources[GUN2], AL_VELOCITY, SourcesVel[GUN2]);
    alSourcei (Sources[GUN2], AL_LOOPING,  AL_FALSE       );



    return AL_TRUE;
}

/*
* void SetListenerValues()
*
*   We already defined certain values for the Listener, but we need
*   to tell OpenAL to use that data. This function does just that.
*/
void SetListenerValues()
{
    alListenerfv(AL_POSITION,    ListenerPos);
    alListenerfv(AL_VELOCITY,    ListenerVel);
    alListenerfv(AL_ORIENTATION, ListenerOri);
}

/*
* void KillALData()
*
*   We have allocated memory for our buffers and sources which needs
*   to be returned to the system. This function frees that memory.
*/
void KillALData()
{
    alDeleteBuffers(NUM_BUFFERS, &Buffers[0]);
    alDeleteSources(NUM_SOURCES, &Sources[0]);
    alutExit();
}


int main(){


    printf("Original Tutorial : MindCode's OpenAL Lesson 1: Single Static Source\n\n");
    printf("Updated Tutorial : Lesley A. Gushurst\n");

    alutInit(NULL, 0);
    //alGetError();

    // Load the wav data.

    if(LoadALData() == AL_FALSE)
    {
        printf("Error loading data.");
        return 0;
    } else {
        // Begin the battle sample to play.
        alSourcePlay(Sources[BATTLE]);

        // Go through all the sources and check that they are playing.
        // Skip the first source because it is looping anyway (will always be playing).
        ALint play;

        while (!_kbhit())
        {
            for (int i = 1; i < NUM_SOURCES; i++)
            {
                    alGetSourcei(Sources[i], AL_SOURCE_STATE, &play);

                    if (play != AL_PLAYING)
                    {
                        // Pick a random position around the listener to play the source.

                        double theta = (double) (rand() % 360) * 3.14 / 180.0;

                        SourcesPos[i][0] = -float(cos(theta));
                        SourcesPos[i][1] = -float(rand()%2);
                        SourcesPos[i][2] = -float(sin(theta));

                        alSourcefv(Sources[i], AL_POSITION, SourcesPos[i] );

                        alSourcePlay(Sources[i]);
                    }
            }
        }
        getchar();
    }

    return 0;
}

我尝试使用此代码,如果有效则可能是您的问题&#34;是否可以在iPhone上播放OpenAl的声音位置?&#34;会回答。我不确定,因为我还没有完成移动开发,但我认为这是一个很好的起点。非常尊重MindCode。你必须用其他东西替换wav文件来测试它(而不是像我那样硬编码)。