2012-07-02 77 views
5

我已經使用AudioRecord編寫了用於錄製音頻文件的代碼,並且在SD卡上寫入文件時我製作了兩個版本。帶有增益調整的AudioRecord在三星設備上不起作用

版本1 錄製的文件保存在SD卡上。

版本2 我在記錄文件上應用增益功能並保存在SD卡上。

這對索尼愛立信手機真棒。音頻音量也在很大程度上提升。

但我努力使它在三星設備上工作。

當我播放錄製的文件聽起來像是會說話的湯姆:P

起初我還以爲Samusung設備不喜歡我都用來創建 AudioRecorder組合。

因此,我使用下面的方法,其中我循環到可用的配置並使用最佳配置來初始化AudioRecord。

public AudioRecord findAudioRecord() { 
    for (int rate: mSampleRates) { 
     for (short audioFormat: new short[] { 
      AudioFormat.ENCODING_PCM_8BIT, AudioFormat.ENCODING_PCM_16BIT 
     }) { 
      for (short channelConfig: new short[] { 
       AudioFormat.CHANNEL_IN_MONO, AudioFormat.CHANNEL_IN_STEREO 
      }) { 
       try { 
        Log.i("vipul", "Attempting rate " + rate + "Hz, bits: " + audioFormat + ", channel: " + channelConfig); 
        int bufferSize = AudioRecord.getMinBufferSize(rate, channelConfig, audioFormat); 

        if (bufferSize != AudioRecord.ERROR_BAD_VALUE) { 
         // check if we can instantiate and have a success 
         AudioRecord recorder = new AudioRecord(
         AudioSource.DEFAULT, rate, channelConfig, audioFormat, bufferSize); 

         if (recorder.getState() == AudioRecord.STATE_INITIALIZED) return recorder; 
        } 
       } catch (Exception e) { 
        e.printStackTrace(); 
       } 
      } 
     } 
    } 
    return null; 
} 

下面是在Sony手機上運行良好的代碼。但努力工作在三星設備上。

public class EnvironmentRecorder extends Activity implements OnClickListener { 

    private static final int RECORDER_BPP = 16; 
    private static final String AUDIO_RECORDER_FILE_EXT_WAV = ".wav"; 
    private static final String AUDIO_RECORDER_FOLDER = "MyRecorder"; 
    private static final String AUDIO_RECORDER_TEMP_FILE = "record_temp.raw"; 
    private static final int RECORDER_SAMPLERATE = 44100; 
    private static final int RECORDER_CHANNELS = AudioFormat.CHANNEL_IN_STEREO; 
    private static final int RECORDER_AUDIO_ENCODING = AudioFormat.ENCODING_PCM_16BIT; 
    private Button start, stop; 
    private AudioRecord recorder = null; 
    private int bufferSize = 0; 
    private Thread recordingThread = null; 
    private boolean isRecording = false; 
    private static int[] mSampleRates = new int[] { 
     8000, 11025, 22050, 44100 
    }; 

    @Override 
    protected void onCreate(Bundle savedInstanceState) { 
     super.onCreate(savedInstanceState); 
     setContentView(R.layout.main); 
     start = (Button) findViewById(R.id.start); 
     stop = (Button) findViewById(R.id.stop); 
     start.setOnClickListener(this); 
     stop.setOnClickListener(this); 
    } 

    @Override 
    public void onClick(View view) { 
     switch (view.getId()) { 
     case R.id.start: 
      startRecord(); 
      break; 
     case R.id.stop: 
      stopRecording(); 
      break; 
     } 
    } 

    public EnvironmentRecorder() { 

     try { 
      bufferSize = AudioRecord.getMinBufferSize(RECORDER_SAMPLERATE, RECORDER_CHANNELS, RECORDER_AUDIO_ENCODING); 
     } catch (Exception e) { 
      e.printStackTrace(); 
     } 

    } 

    private String getFilename1() { 
     String filepath = Environment.getExternalStorageDirectory().getPath(); 
     File file = new File(filepath, AUDIO_RECORDER_FOLDER); 

     if (!file.exists()) { 
      file.mkdirs(); 
     } 

     return (file.getAbsolutePath() + "/" + "NotGained" + AUDIO_RECORDER_FILE_EXT_WAV); 
    } 

    private String getFilename2() { 
     String filepath = Environment.getExternalStorageDirectory().getPath(); 
     File file = new File(filepath, AUDIO_RECORDER_FOLDER); 

     if (!file.exists()) { 
      file.mkdirs(); 
     } 

     return (file.getAbsolutePath() + "/" + "Gained" + AUDIO_RECORDER_FILE_EXT_WAV); 
    } 

    private String getTempFilename() { 
     String filepath = Environment.getExternalStorageDirectory().getPath(); 
     File file = new File(filepath, AUDIO_RECORDER_FOLDER); 

     if (!file.exists()) { 
      file.mkdirs(); 
     } 

     File tempFile = new File(filepath, AUDIO_RECORDER_TEMP_FILE); 

     if (tempFile.exists()) tempFile.delete(); 

     return (file.getAbsolutePath() + "/" + AUDIO_RECORDER_TEMP_FILE); 
    } 

    public AudioRecord findAudioRecord() { 
     for (int rate: mSampleRates) { 
      for (short audioFormat: new short[] { 
       AudioFormat.ENCODING_PCM_8BIT, AudioFormat.ENCODING_PCM_16BIT 
      }) { 
       for (short channelConfig: new short[] { 
        AudioFormat.CHANNEL_IN_MONO, AudioFormat.CHANNEL_IN_STEREO 
       }) { 
        try { 
         Log.v("vipul", "Attempting rate " + rate + "Hz, bits: " + audioFormat + ", channel: " + channelConfig); 
         int bufferSize = AudioRecord.getMinBufferSize(rate, channelConfig, audioFormat); 

         if (bufferSize != AudioRecord.ERROR_BAD_VALUE) { 
          // check if we can instantiate and have a success 
          AudioRecord recorder = new AudioRecord(
          AudioSource.DEFAULT, rate, channelConfig, audioFormat, bufferSize); 

          if (recorder.getState() == AudioRecord.STATE_INITIALIZED) return recorder; 
         } 
        } catch (Exception e) { 
         e.printStackTrace(); 
        } 
       } 
      } 
     } 
     return null; 
    } 

    public void startRecord() { 
     /* 
     * recorder = new AudioRecord(MediaRecorder.AudioSource.MIC, 
     * RECORDER_SAMPLERATE, RECORDER_CHANNELS, RECORDER_AUDIO_ENCODING, 
     * bufferSize); 
     */ 

     recorder = findAudioRecord(); 

     recorder.startRecording(); 

     isRecording = true; 

     recordingThread = new Thread(new Runnable() { 

      @Override 
      public void run() { 
       writeAudioDataToFile(); 
      } 
     }, "AudioRecorder Thread"); 

     recordingThread.start(); 
    } 

    private void writeAudioDataToFile() { 
     byte data[] = new byte[bufferSize]; 
     String filename = getTempFilename(); 
     FileOutputStream os = null; 

     try { 
      os = new FileOutputStream(filename); 
     } catch (FileNotFoundException e) { 
      e.printStackTrace(); 
     } 

     int read = 0; 

     if (null != os) { 
      while (isRecording) { 
       read = recorder.read(data, 0, bufferSize); 

       if (AudioRecord.ERROR_INVALID_OPERATION != read) { 
        try { 
         os.write(data); 
        } catch (IOException e) { 
         e.printStackTrace(); 
        } 
       } 
      } 

      try { 
       os.close(); 
      } catch (IOException e) { 
       e.printStackTrace(); 
      } 
     } 
    } 

    public void stopRecording() { 
     if (null != recorder) { 
      isRecording = false; 

      recorder.stop(); 
      recorder.release(); 

      recorder = null; 
      recordingThread = null; 
      copyWaveFile(getTempFilename(), getFilename1(), getFilename2()); 
      deleteTempFile(); 
     } 

    } 

    private void deleteTempFile() { 
     File file = new File(getTempFilename()); 

     file.delete(); 
    } 

    private void copyWaveFile(String inFilename, String outFileName1, String outFileName2) { 
     FileInputStream in = null; 
     FileOutputStream out1 = null, out2 = null; 
     long totalAudioLen = 0; 
     long totalDataLen = totalAudioLen + 36; 
     long longSampleRate = RECORDER_SAMPLERATE; 
     int channels = 2; 
     long byteRate = RECORDER_BPP * RECORDER_SAMPLERATE * channels/8; 

     byte[] data = new byte[bufferSize]; 

     try { in = new FileInputStream(inFilename); 
      out1 = new FileOutputStream(outFileName1); 
      out2 = new FileOutputStream(outFileName2); 
      totalAudioLen = in .getChannel().size(); 
      totalDataLen = totalAudioLen + 36; 

      WriteWaveFileHeader(out1, totalAudioLen, totalDataLen, longSampleRate, channels, byteRate); 
      WriteWaveFileHeader(out2, totalAudioLen, totalDataLen, longSampleRate, channels, byteRate); 

      while (in .read(data) != -1) { 

       out1.write(data); // Writing Non-Gained Data 

       float rGain = 2.5f; 
       for (int i = 0; i < data.length/2; i++) { 

        short curSample = getShort(data[i * 2], data[i * 2 + 1]); 
        if (rGain != 1) { 
         // apply gain 
         curSample *= rGain; 
         // convert back from short sample that was "gained" to 
         // byte data 
         byte[] a = getByteFromShort(curSample); 
         // modify buffer to contain the gained sample 
         data[i * 2] = a[0]; 
         data[i * 2 + 1] = a[1]; 
        } 

       } 

       out2.write(data); // Writing Gained Data 
      } 
      out1.close(); 
      out2.close(); in .close(); 

      Toast.makeText(this, "Done!!", Toast.LENGTH_LONG).show(); 
     } catch (FileNotFoundException e) { 
      e.printStackTrace(); 
     } catch (IOException e) { 
      e.printStackTrace(); 
     } 
    } 

    private short getShort(byte argB1, byte argB2) { 
     return (short)((argB1 & 0xff) | (argB2 << 8)); 

    } 

    private byte[] getByteFromShort(short x) { 
     // variant 1 - noise 
     byte[] a = new byte[2]; 
     a[0] = (byte)(x & 0xff); 
     a[1] = (byte)((x >> 8) & 0xff); 

     // variant 2 - noise and almost broke my ears - very loud 
     // ByteBuffer buffer = ByteBuffer.allocate(2); 
     // buffer.putShort(x); 
     // buffer.flip(); 

     return a; 
    } 

    private void WriteWaveFileHeader(FileOutputStream out, long totalAudioLen, long totalDataLen, long longSampleRate, int channels, long byteRate) 
    throws IOException { 

     byte[] header = new byte[44]; 

     header[0] = 'R'; 
     header[1] = 'I'; 
     header[2] = 'F'; 
     header[3] = 'F'; 
     header[4] = (byte)(totalDataLen & 0xff); 
     header[5] = (byte)((totalDataLen >> 8) & 0xff); 
     header[6] = (byte)((totalDataLen >> 16) & 0xff); 
     header[7] = (byte)((totalDataLen >> 24) & 0xff); 
     header[8] = 'W'; 
     header[9] = 'A'; 
     header[10] = 'V'; 
     header[11] = 'E'; 
     header[12] = 'f'; 
     header[13] = 'm'; 
     header[14] = 't'; 
     header[15] = ' '; 
     header[16] = 16; 
     header[17] = 0; 
     header[18] = 0; 
     header[19] = 0; 
     header[20] = 1; 
     header[21] = 0; 
     header[22] = (byte) channels; 
     header[23] = 0; 
     header[24] = (byte)(longSampleRate & 0xff); 
     header[25] = (byte)((longSampleRate >> 8) & 0xff); 
     header[26] = (byte)((longSampleRate >> 16) & 0xff); 
     header[27] = (byte)((longSampleRate >> 24) & 0xff); 
     header[28] = (byte)(byteRate & 0xff); 
     header[29] = (byte)((byteRate >> 8) & 0xff); 
     header[30] = (byte)((byteRate >> 16) & 0xff); 
     header[31] = (byte)((byteRate >> 24) & 0xff); 
     header[32] = (byte)(2 * 16/8); 
     header[33] = 0; 
     header[34] = RECORDER_BPP; 
     header[35] = 0; 
     header[36] = 'd'; 
     header[37] = 'a'; 
     header[38] = 't'; 
     header[39] = 'a'; 
     header[40] = (byte)(totalAudioLen & 0xff); 
     header[41] = (byte)((totalAudioLen >> 8) & 0xff); 
     header[42] = (byte)((totalAudioLen >> 16) & 0xff); 
     header[43] = (byte)((totalAudioLen >> 24) & 0xff); 

     out.write(header, 0, 44); 
    } 

} 

我想知道是否需要添加任何額外的loc以使我的AudioRecord適合三星設備。

+0

您好Vipul,您是否找到了解決方案?我們在Galaxy S3上遇到了同樣的問題,我們相信它就像您的一樣。 – Giuseppe

回答

6

我們也在一些三星Android設備上錄音。不幸的是,它似乎非常糟糕,因爲即使是同一個手機型號的不同版本在相同的代碼庫中的行爲也不同。

這裏是我目前的研究結果,希望你找到一些有用的東西:

1.破碎初始化:

不幸的是,你正在使用,以查詢有效的錄製配置的策略會失敗至少在三星運行Android 2.3的Galaxy Young和Ace模型 問題是,一些無效的AudioRecord配置而不是簡單的失敗,如果嘗試,將會完全阻塞音頻捕獲子系統。您需要重置手機才能從此狀態恢復。

2.不一致的採樣率沿着相同的手機型號

版本在舊的銀河王牌手機,記錄@ 11025Hz支持,16位單聲道會成功。在更新的Ace版本中,此AudioRecord配置將被視爲有效,但錄製的結果將會失真,並具有「花栗鼠」效應。一個非常流行的吉他調諧器應用程序硬編碼這個採樣率是沒有給這些手機正確的調整讀數正是因爲這個問題!

3.某些配置的音量極低。

在Galaxy Young和Galaxy Ace中,從麥克風或默認音源@ 44,100Hz(假定所有應該都正常工作的規範速率)進行錄音會產生未失真但極低音量的錄音。我還沒有找到一種方法來解決這個問題,而不是軟件放大(這相當於放大一個非常低分辨率的圖像,結果的推理「jageddnes」)。

4.未能在每個音頻捕捉源上支持標準的44,100Hz採樣率。

在Galaxy Young和Galaxy Ace中,從攝像機源錄製失敗@ 44,100Hz。 (同樣,配置將被接受爲有效),產生完整的垃圾。然而,錄音@ 8,000Hz,16,000Hz和48,000Hz工作正常,併產生具有非常可接受的音量水平的錄音。令人沮喪的是,根據Android文檔,44,100Hz是所有設備應支持的採樣率。

5. OpenSL沒有解決任何報告的問題。

使用NDK和OpenSL產生相同的描述結果。看來AudioRecorder類只是簡單地將調用封裝到OpenSL中,而問題要麼是基於硬件的問題,要麼是埋在內核代碼中的較低層級。

這種情況確實很不幸,因爲這些模式正變得非常流行 - 至少在墨西哥。

祝你好運 - 並請報告,如果你有更好的運氣與這些手機工作。 =)

+0

看來你對這個問題有相當的瞭解。我現在面臨同樣的問題。你也許知道三星是否有修正?我問,因爲我沒有設備,我需要44100Hz,我不知道現在該做什麼。 – stefple

+0

...對不起,忘了說:現在我的應用程序沒有工作,現在的用戶有問題。我現在看到的唯一解決方案是,我排除這些設備。 – stefple

+0

嗨@stefple,不,我不知道是否有修補程序。目前,我們正在使用MIC音頻源處理Ace和Young機型的44,100個,並應用軟件放大(將樣本乘以一個常數並在發生溢出時削減值)來補償低電平。質量受到影響,但這足以滿足我們DSP的目的。使用CAMCORDER進行錄製可在48000Hz下以非常好的音量播放。如果你可以負擔增加一個採樣率轉換程序,你可以從48000Hz到44100Hz,幾乎沒有質量損失。 – 2012-11-21 14:48:29

1

音頻增益控制要增加音頻的幅度,需要計算增益因子並將計算的增益因子與每個捕獲的採樣相乘。下面的代碼就是這樣做的。附:忽略不相關的代碼

public class MainActivity extends Activity { 

public static final int SAMPLE_RATE = 16000; 

private AudioRecord mRecorder; 
private File mRecording; 
private short[] mBuffer; 
private final String startRecordingLabel = "Start recording"; 
private final String stopRecordingLabel = "Stop recording"; 
private boolean mIsRecording = false; 
private ProgressBar mProgressBar; 
float iGain = 1.0f; 
CheckBox gain; 

protected int bitsPerSamples = 16; 

@Override 
public void onCreate(final Bundle savedInstanceState) { 
    super.onCreate(savedInstanceState); 
    setContentView(R.layout.fragment_main); 

    initRecorder(); 

    Button bluetooth = (Button)findViewById(R.id.blue); 
    gain = (CheckBox) findViewById(R.id.checkBox1); 
    mProgressBar = (ProgressBar) findViewById(R.id.progressBar); 

    final Button button = (Button) findViewById(R.id.start); 
    button.setText(startRecordingLabel); 

    bluetooth.setOnClickListener(new View.OnClickListener() { 

     @Override 
     public void onClick(View v) { 
      // TODO Auto-generated method stub 
      Intent i = new Intent(""); 
     } 
    }); 
    gain.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { 

     @Override 
     public void onCheckedChanged(CompoundButton buttonView, 
       boolean isChecked) { 

      if (gain.isChecked()) { 
       iGain = 5.0f; 

      } else { 
       iGain = 2.0f; 
      } 
     } 
    }); 

    button.setOnClickListener(new View.OnClickListener() { 
     @Override 
     public void onClick(final View v) { 
      if (!mIsRecording) { 
       button.setText(stopRecordingLabel); 
       mIsRecording = true; 
       mRecorder.startRecording(); 
       mRecording = getFile("raw"); 
       startBufferedWrite(mRecording); 
      } else { 
       button.setText(startRecordingLabel); 
       mIsRecording = false; 
       mRecorder.stop(); 
       File waveFile = getFile("wav"); 
       try { 
        rawToWave(mRecording, waveFile); 
       } catch (IOException e) { 
        Toast.makeText(MainActivity.this, e.getMessage(), 
          Toast.LENGTH_SHORT).show(); 
       } 
       Toast.makeText(MainActivity.this, 
         "Recorded to " + waveFile.getName(), 
         Toast.LENGTH_SHORT).show(); 
      } 
     } 
    }); 
} 

@Override 
public void onDestroy() { 
    mRecorder.release(); 
    super.onDestroy(); 
} 

private void initRecorder() { 
    int bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, 
      AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); 
    mBuffer = new short[bufferSize]; 
    mRecorder = new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE, 
      AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, 
      bufferSize); 
} 

private void startBufferedWrite(final File file) { 
    new Thread(new Runnable() { 
     @Override 
     public void run() { 
      DataOutputStream output = null; 
      try { 
       output = new DataOutputStream(new BufferedOutputStream(
         new FileOutputStream(file))); 
       while (mIsRecording) { 
        double sum = 0; 

        int readSize = mRecorder.read(mBuffer, 0, 
          mBuffer.length); 

        final int bytesPerSample = bitsPerSamples/8; 
        final int emptySpace = 64 - bitsPerSamples; 
        int byteIndex = 0; 
        int byteIndex2 = 0; 
        int temp = 0; 
        int mLeftTemp = 0; 
        int mRightTemp = 0; 
        int a = 0; 
        int x = 0; 

        for (int frameIndex = 0; frameIndex < readSize; frameIndex++) { 

         for (int c = 0; c < 1; c++) { 

          if (iGain != 1) { 

           long accumulator = 0; 
           for (int b = 0; b < bytesPerSample; b++) { 

            accumulator += ((long) (mBuffer[byteIndex++] & 0xFF)) << (b * 8 + emptySpace); 
           } 

           double sample = ((double) accumulator/(double) Long.MAX_VALUE); 
           sample *= iGain; 
           int intValue = (int) ((double) sample * (double) Integer.MAX_VALUE); 

           for (int i = 0; i < bytesPerSample; i++) { 
            mBuffer[i + byteIndex2] = (byte) (intValue >>> ((i + 2) * 8) & 0xff); 
           } 
           byteIndex2 += bytesPerSample; 

          } 
         }// end for(channel) 

         // mBuffer[frameIndex] *=iGain; 
         if (mBuffer[frameIndex] > 32765) { 
          mBuffer[frameIndex] = 32767; 

         } else if (mBuffer[frameIndex] < -32767) { 
          mBuffer[frameIndex] = -32767; 
         } 
         output.writeShort(mBuffer[frameIndex]); 
         sum += mBuffer[frameIndex] * mBuffer[frameIndex]; 

        } 

        if (readSize > 0) { 
         final double amplitude = sum/readSize; 
         mProgressBar.setProgress((int) Math.sqrt(amplitude)); 
        } 
       } 
      } catch (IOException e) { 
       Toast.makeText(MainActivity.this, e.getMessage(), 
         Toast.LENGTH_SHORT).show(); 
      } finally { 
       mProgressBar.setProgress(0); 
       if (output != null) { 
        try { 
         output.flush(); 
        } catch (IOException e) { 
         Toast.makeText(MainActivity.this, e.getMessage(), 
           Toast.LENGTH_SHORT).show(); 
        } finally { 
         try { 
          output.close(); 
         } catch (IOException e) { 
          Toast.makeText(MainActivity.this, e.getMessage(), 
            Toast.LENGTH_SHORT).show(); 
         } 
        } 
       } 
      } 
     } 
    }).start(); 
} 

private void rawToWave(final File rawFile, final File waveFile) 
     throws IOException { 

    byte[] rawData = new byte[(int) rawFile.length()]; 
    DataInputStream input = null; 
    try { 

     input = new DataInputStream(new FileInputStream(rawFile)); 
     input.read(rawData); 
    } finally { 
     if (input != null) { 
      input.close(); 
     } 
    } 

    DataOutputStream output = null; 
    try { 
     output = new DataOutputStream(new FileOutputStream(waveFile)); 
     // WAVE header 
     // see http://ccrma.stanford.edu/courses/422/projects/WaveFormat/ 
     writeString(output, "RIFF"); // chunk id 
     writeInt(output, 36 + rawData.length); // chunk size 
     writeString(output, "WAVE"); // format 
     writeString(output, "fmt "); // subchunk 1 id 
     writeInt(output, 16); // subchunk 1 size 
     writeShort(output, (short) 1); // audio format (1 = PCM) 
     writeShort(output, (short) 1); // number of channels 
     writeInt(output, SAMPLE_RATE); // sample rate 
     writeInt(output, SAMPLE_RATE * 2); // byte rate 
     writeShort(output, (short) 2); // block align 
     writeShort(output, (short) 16); // bits per sample 
     writeString(output, "data"); // subchunk 2 id 
     writeInt(output, rawData.length); // subchunk 2 size 
     // Audio data (conversion big endian -> little endian) 
     short[] shorts = new short[rawData.length/2]; 
     ByteBuffer.wrap(rawData).order(ByteOrder.LITTLE_ENDIAN) 
       .asShortBuffer().get(shorts); 
     ByteBuffer bytes = ByteBuffer.allocate(shorts.length * 2); 

     for (short s : shorts) { 

      // Apply Gain 
      /* 
      * s *= iGain; if(s>32767) { s=32767; } else if(s<-32768) { 
      * s=-32768; } 
      */ 
      bytes.putShort(s); 
     } 
     output.write(bytes.array()); 
    } finally { 
     if (output != null) { 
      output.close(); 
     } 
    } 
} 

private File getFile(final String suffix) { 
    Time time = new Time(); 
    time.setToNow(); 
    return new File(Environment.getExternalStorageDirectory(), 
      time.format("%Y%m%d%H%M%S") + "." + suffix); 
} 





private void writeInt(final DataOutputStream output, final int value) 
     throws IOException { 
    output.write(value >> 0); 
    output.write(value >> 8); 
    output.write(value >> 16); 
    output.write(value >> 24); 
} 

private void writeShort(final DataOutputStream output, final short value) 
     throws IOException { 
    output.write(value >> 0); 
    output.write(value >> 8); 
} 

private void writeString(final DataOutputStream output, final String value) 
     throws IOException { 
    for (int i = 0; i < value.length(); i++) { 
     output.write(value.charAt(i)); 
    } 
} 
} 
相關問題