int OpusDecoder_init(decoders_OpusDecoder *self, PyObject *args, PyObject *kwds) { char *filename; int error; self->opus_file = NULL; self->audiotools_pcm = NULL; self->closed = 0; if (!PyArg_ParseTuple(args, "s", &filename)) return -1; if ((self->opus_file = op_open_file(filename, &error)) == NULL) { PyErr_SetString(PyExc_ValueError, "error opening Opus file"); return -1; } self->channel_count = op_channel_count(self->opus_file, -1); if ((self->audiotools_pcm = open_audiotools_pcm()) == NULL) return -1; return 0; }
SoundSource::OpenResult SoundSourceOpus::tryOpen(const AudioSourceConfig& /*audioSrcCfg*/) { // From opus/opusfile.h // On Windows, this string must be UTF-8 (to allow access to // files whose names cannot be represented in the current // MBCS code page). // All other systems use the native character encoding. #ifdef _WIN32 QByteArray qBAFilename = getLocalFileName().toUtf8(); #else QByteArray qBAFilename = getLocalFileName().toLocal8Bit(); #endif int errorCode = 0; DEBUG_ASSERT(!m_pOggOpusFile); m_pOggOpusFile = op_open_file(qBAFilename.constData(), &errorCode); if (!m_pOggOpusFile) { qWarning() << "Failed to open OggOpus file:" << getUrlString() << "errorCode" << errorCode; return OpenResult::FAILED; } if (!op_seekable(m_pOggOpusFile)) { qWarning() << "SoundSourceOpus:" << "Stream in" << getUrlString() << "is not seekable"; return OpenResult::UNSUPPORTED_FORMAT; } const int channelCount = op_channel_count(m_pOggOpusFile, kCurrentStreamLink); if (0 < channelCount) { setChannelCount(channelCount); } else { qWarning() << "Failed to read channel configuration of OggOpus file:" << getUrlString(); return OpenResult::FAILED; } const ogg_int64_t pcmTotal = op_pcm_total(m_pOggOpusFile, kEntireStreamLink); if (0 <= pcmTotal) { setFrameCount(pcmTotal); } else { qWarning() << "Failed to read total length of OggOpus file:" << getUrlString(); return OpenResult::FAILED; } const opus_int32 bitrate = op_bitrate(m_pOggOpusFile, kEntireStreamLink); if (0 < bitrate) { setBitrate(bitrate / 1000); } else { qWarning() << "Failed to determine bitrate of OggOpus file:" << getUrlString(); return OpenResult::FAILED; } setSamplingRate(kSamplingRate); m_curFrameIndex = getMinFrameIndex(); return OpenResult::SUCCEEDED; }
opus_file_t OpusDynamicLoader::open_opus_file() { int op_err; opus_file_t op_file{op_open_file(path.c_str(), &op_err), opus_deleter}; if (op_err != 0) { throw util::Error{"Could not open: %s", path.c_str()}; } return std::move(op_file); }
void cmdMusic(const std::string& args) { // terrible hack for testing audio // decode the entire file into a big buffer all at once OggOpusFile* opusFile; if (args.length() == 0) { opusFile = op_open_memory( static_cast<const uint8_t*>(EMBED_DATA(Who_Likes_to_Party_Kevin_MacLeod_incompetech_opus)), EMBED_SIZE(Who_Likes_to_Party_Kevin_MacLeod_incompetech_opus), NULL); } else { opusFile = op_open_file(args.c_str(), NULL); } if (!opusFile) { narf::console->println("Failed to open music file " + args); return; } auto newMusicSize = op_pcm_total(opusFile, -1) * 2; // stereo auto newMusicSamples = new float[newMusicSize]; size_t decoded = 0; while (decoded < newMusicSize) { auto rc = op_read_float_stereo(opusFile, newMusicSamples + decoded, newMusicSize - decoded); if (rc < 0) { narf::console->println("opusfile decode failed"); decoded = 0; break; } decoded += rc * 2; // return code is number of samples per channel, and we are decoding in stereo } if (decoded != newMusicSize) { narf::console->println("opusfile decode returned wrong number of samples (got " + std::to_string(decoded) + ", expected " + std::to_string(newMusicSize) + ")"); delete[] newMusicSamples; newMusicSamples = nullptr; newMusicSize = 0; } SDL_LockMutex(musicMutex); if (musicSamples) { delete[] musicSamples; } musicSamples = newMusicSamples; musicSize = newMusicSize; musicCursor = 0; SDL_UnlockMutex(musicMutex); op_free(opusFile); }
Result SoundSourceOpus::open() { int error = 0; QByteArray qBAFilename = m_qFilename.toLocal8Bit(); m_ptrOpusFile = op_open_file(qBAFilename.constData(), &error); if ( m_ptrOpusFile == NULL ) { qDebug() << "opus: Input does not appear to be an Opus bitstream."; m_lFilelength = 0; return ERR; } // opusfile lib all ways gives you 48000 samplerate and stereo 16 bit sample m_iChannels = 2; this->setBitrate((int)op_bitrate_instant(m_ptrOpusFile)); this->setSampleRate(48000); this->setChannels(m_iChannels); if (m_iChannels > 2) { qDebug() << "opus: No support for more than 2 m_iChannels!"; op_free(m_ptrOpusFile); m_lFilelength = 0; return ERR; } // op_pcm_total returns the total number of frames in the ogg file. The // frame is the channel-independent measure of samples. The total samples in // the file is m_iChannels * ov_pcm_total. rryan 7/2009 I verified this by // hand. a 30 second long 48khz mono ogg and a 48khz stereo ogg both report // 1440000 for op_pcm_total. qint64 ret = op_pcm_total(m_ptrOpusFile, -1) * 2; // qDebug() << m_qFilename << "chan:" << m_iChannels << "sample:" << m_iSampleRate << "LEN:" << ret; if (ret >= 0) { // We pretend that the file is stereo to the rest of the world. m_lFilelength = ret; } else { //error if (ret == OP_EINVAL) { //The file is not seekable. Not sure if any action is needed. qDebug() << "opus: file is not seekable " << m_qFilename; } } return OK; }
pcm_data_t OpusInMemoryLoader::get_resource() { int op_err; // open the opus file opus_file_t op_file{op_open_file(path.c_str(), &op_err), opus_deleter}; if (op_err != 0) { throw util::Error{"Could not open: %s", path.c_str()}; } // determine number of channels and number of pcm samples auto op_channels = op_channel_count(op_file.get(), -1); auto pcm_length = op_pcm_total(op_file.get(), -1); log::dbg("Opus channels=%d, pcm_length=%u", op_channels, static_cast<uint32_t>(pcm_length)); // calculate pcm buffer size depending on the number of channels // if the opus file only had one channel, the pcm buffer size must be // doubled uint32_t length = static_cast<uint32_t>(pcm_length) * 2; pcm_data_t buffer(static_cast<size_t>(length), 0); // read data from opus file int position = 0; while (true) { int samples_read = op_read(op_file.get(), &buffer.front()+position, length-position, nullptr); if (samples_read < 0) { throw util::Error{"Failed to read from opus file: errorcode=%d", samples_read}; } else if(samples_read == 0) { break; } position += samples_read * op_channels; } // convert from mono to stereo if (op_channels == 1) { for(int i = pcm_length-1; i >= 0; i--) { auto value = buffer[i]; buffer[i*2+1] = value; buffer[i*2] = value; } } return std::move(buffer); }
/* * et_opus_open_file: * @filename: Filepath to open * @error: GError or %NULL * * Opens an Opus file. * * Returns: a OggOpusFile on success or %NULL on error. */ OggOpusFile * et_opus_open_file (GFile *gfile, GError **error) { OggOpusFile *file; gchar *path; int error_val; g_return_val_if_fail (error == NULL || *error == NULL, NULL); g_return_val_if_fail (gfile != NULL, NULL); path = g_file_get_path (gfile); /* Opusfile does the UTF-8 to UTF-16 translation on Windows * automatically. */ file = op_open_file (path, &error_val); g_free (path); if (!file) { /* Got error while opening opus file */ switch (error_val) { case OP_EREAD: g_set_error (error, ET_OPUS_ERROR, ET_OPUS_ERROR_READ, "Error reading file"); g_assert (error == NULL || *error != NULL); return NULL; case OP_EFAULT: g_set_error (error, ET_OPUS_ERROR, ET_OPUS_ERROR_FAULT, "Memory allocation failure or internal library error"); g_assert (error == NULL || *error != NULL); return NULL; case OP_EIMPL: g_set_error (error, ET_OPUS_ERROR, ET_OPUS_ERROR_IMPL, "Stream used an unimplemented feature"); g_assert (error == NULL || *error != NULL); return NULL; case OP_EINVAL: g_set_error (error, ET_OPUS_ERROR, ET_OPUS_ERROR_INVAL, "seek () succeeded on this source but tell () did not"); g_assert (error == NULL || *error != NULL); return NULL; case OP_ENOTFORMAT: g_set_error (error, ET_OPUS_ERROR, ET_OPUS_ERROR_NOTFORMAT, "No logical stream found in a link"); g_assert (error == NULL || *error != NULL); return NULL; case OP_EBADHEADER: g_set_error (error, ET_OPUS_ERROR, ET_OPUS_ERROR_BADHEADER, "Corrupted header packet"); g_assert (error == NULL || *error != NULL); return NULL; case OP_EVERSION: g_set_error (error, ET_OPUS_ERROR, ET_OPUS_ERROR_VERSION, "ID header contained an unrecognized version number"); g_assert (error == NULL || *error != NULL); return NULL; case OP_EBADLINK: g_set_error (error, ET_OPUS_ERROR, ET_OPUS_ERROR_BADLINK, "Corrupted link found"); g_assert (error == NULL || *error != NULL); return NULL; case OP_EBADTIMESTAMP: g_set_error (error, ET_OPUS_ERROR, ET_OPUS_ERROR_BADTIMESTAMP, "First/last timestamp in a link failed checks"); g_assert (error == NULL || *error != NULL); return NULL; default: g_assert_not_reached (); break; } } return file; }
/* Parse the the file to get metadata */ Result SoundSourceOpus::parseHeader() { int error = 0; QByteArray qBAFilename = m_qFilename.toLocal8Bit(); OggOpusFile *l_ptrOpusFile = op_open_file(qBAFilename.constData(), &error); this->setBitrate((int)op_bitrate(l_ptrOpusFile, -1) / 1000); this->setSampleRate(48000); this->setChannels(2); qint64 l_lLength = op_pcm_total(l_ptrOpusFile, -1) * 2; this->setDuration(l_lLength / (48000 * 2)); this->setType("opus"); // If we don't have new enough Taglib we use libopusfile parser! #if (TAGLIB_MAJOR_VERSION >= 1) && (TAGLIB_MINOR_VERSION >= 9) TagLib::Ogg::Opus::File f(qBAFilename.constData()); // Takes care of all the default metadata bool result = processTaglibFile(f); TagLib::Ogg::XiphComment *tag = f.tag(); if (tag) { processXiphComment(tag); } #else // From Taglib 1.9.x Opus is supported // Before that we have parse tags by this code int i = 0; const OpusTags *l_ptrOpusTags = op_tags(l_ptrOpusFile, -1); // This is left for debug reasons !! // qDebug() << "opus: We have " << l_ptrOpusTags->comments; for( i = 0; i < l_ptrOpusTags->comments; i ++){ QString l_SWholeTag = QString(l_ptrOpusTags->user_comments[i]); QString l_STag = l_SWholeTag.left(l_SWholeTag.indexOf("=")); QString l_SPayload = l_SWholeTag.right((l_SWholeTag.length() - l_SWholeTag.indexOf("=")) - 1); if (!l_STag.compare("ARTIST") ) { this->setArtist(l_SPayload); } else if (!l_STag.compare("ALBUM")) { this->setAlbum(l_SPayload); } else if (!l_STag.compare("BPM")) { this->setBPM(l_SPayload.toFloat()); } else if (!l_STag.compare("YEAR") || !l_STag.compare("DATE")) { this->setYear(l_SPayload); } else if (!l_STag.compare("GENRE")) { this->setGenre(l_SPayload); } else if (!l_STag.compare("TRACKNUMBER")) { this->setTrackNumber(l_SPayload); } else if (!l_STag.compare("COMPOSER")) { this->setComposer(l_SPayload); } else if (!l_STag.compare("ALBUMARTIST")) { this->setAlbumArtist(l_SPayload); } else if (!l_STag.compare("TITLE")) { this->setTitle(l_SPayload); } else if (!l_STag.compare("REPLAYGAIN_TRACK_PEAK")) { } else if (!l_STag.compare("REPLAYGAIN_TRACK_GAIN")) { this->parseReplayGainString (l_SPayload); } else if (!l_STag.compare("REPLAYGAIN_ALBUM_PEAK")) { } else if (!l_STag.compare("REPLAYGAIN_ALBUM_GAIN")) { } // This is left fot debug reasons!! //qDebug() << "Comment" << i << l_ptrOpusTags->comment_lengths[i] << //" (" << l_ptrOpusTags->user_comments[i] << ")" << l_STag << "*" << l_SPayload; } op_free(l_ptrOpusFile); return OK; #endif #if TAGLIB_MAJOR_VERSION >= 1 && TAGLIB_MINOR_VERSION >= 9 return result ? OK : ERR; #endif }
opus_file_t open_opus_file(const util::Path &path) { if (not path.is_file()) { throw audio::Error{ ERR << "Audio file not found: " << path }; } opus_file_t op_file; int op_err = 0; //asm("int $3"); // check if the file can be opened directly auto native_path = path.resolve_native_path(); if (false && native_path.size() > 0) { op_file.handle = { op_open_file(native_path.c_str(), &op_err), opus_deleter }; } else { // open the file and move the handle to the heap op_file.file = std::make_unique<util::File>(); *op_file.file = path.open_r(); op_file.handle = { op_open_callbacks(op_file.file.get(), &opus_access_funcs, nullptr, 0, &op_err), opus_deleter }; } if (op_err != 0) { const char *reason; switch (op_err) { case OP_EREAD: reason = "read/seek/tell failed or data has changed"; break; case OP_EFAULT: reason = "opus failed to allocate memory " "or something else bad happened internally"; break; case OP_EIMPL: reason = "Stream used an unsupported feature"; break; case OP_EINVAL: reason = "seek() worked, but tell() did not, " "or initial_bytes != start seek pos"; break; case OP_ENOTFORMAT: reason = "Data didn't contain opus stream"; break; case OP_EBADHEADER: reason = "Header packet was invalid or missing"; break; case OP_EVERSION: reason = "ID header has unrecognized version"; break; case OP_EBADLINK: reason = "Data we already saw before seeking not found"; break; case OP_EBADTIMESTAMP: reason = "Validity check for first/last timestamp failed"; break; default: reason = "Unknown other error in opusfile"; break; } throw audio::Error{ ERR << "Could not open opus file: " << path << " = '" << native_path << "': " << reason }; } return op_file; }
Result SoundSourceOpus::parseTrackMetadataAndCoverArt( TrackMetadata* pTrackMetadata, QImage* pCoverArt) const { if (OK == SoundSource::parseTrackMetadataAndCoverArt( pTrackMetadata, pCoverArt)) { // Done if the default implementation in the base class // supports Opus files. return OK; } // Beginning with version 1.9.0 TagLib supports the Opus format. // Until this becomes the minimum version required by Mixxx tags // in .opus files must also be parsed using opusfile. The following // code should removed as soon as it is no longer needed! // // NOTE(uklotzde): The following code has been found in SoundSourceOpus // and will not be improved. We are aware of its shortcomings like // the lack of proper error handling. // From opus/opusfile.h // On Windows, this string must be UTF-8 (to allow access to // files whose names cannot be represented in the current // MBCS code page). // All other systems use the native character encoding. #ifdef _WIN32 QByteArray qBAFilename = getLocalFileName().toUtf8(); #else QByteArray qBAFilename = getLocalFileName().toLocal8Bit(); #endif int error = 0; OggOpusFileOwner l_ptrOpusFile( op_open_file(qBAFilename.constData(), &error)); int i = 0; const OpusTags *l_ptrOpusTags = op_tags(l_ptrOpusFile, -1); pTrackMetadata->setChannels(op_channel_count(l_ptrOpusFile, -1)); pTrackMetadata->setSampleRate(Mixxx::SoundSourceOpus::kSamplingRate); pTrackMetadata->setBitrate(op_bitrate(l_ptrOpusFile, -1) / 1000); pTrackMetadata->setDuration( op_pcm_total(l_ptrOpusFile, -1) / pTrackMetadata->getSampleRate()); bool hasDate = false; for (i = 0; i < l_ptrOpusTags->comments; ++i) { QString l_SWholeTag = QString(l_ptrOpusTags->user_comments[i]); QString l_STag = l_SWholeTag.left(l_SWholeTag.indexOf("=")); QString l_SPayload = l_SWholeTag.right((l_SWholeTag.length() - l_SWholeTag.indexOf("=")) - 1); if (!l_STag.compare("ARTIST")) { pTrackMetadata->setArtist(l_SPayload); } else if (!l_STag.compare("ALBUM")) { pTrackMetadata->setAlbum(l_SPayload); } else if (!l_STag.compare("BPM")) { pTrackMetadata->setBpm(l_SPayload.toFloat()); } else if (!l_STag.compare("DATE")) { // Prefer "DATE" over "YEAR" pTrackMetadata->setYear(l_SPayload.trimmed()); // Avoid to overwrite "DATE" with "YEAR" hasDate |= !pTrackMetadata->getYear().isEmpty(); } else if (!hasDate && !l_STag.compare("YEAR")) { pTrackMetadata->setYear(l_SPayload.trimmed()); } else if (!l_STag.compare("GENRE")) { pTrackMetadata->setGenre(l_SPayload); } else if (!l_STag.compare("TRACKNUMBER")) { pTrackMetadata->setTrackNumber(l_SPayload); } else if (!l_STag.compare("COMPOSER")) { pTrackMetadata->setComposer(l_SPayload); } else if (!l_STag.compare("ALBUMARTIST")) { pTrackMetadata->setAlbumArtist(l_SPayload); } else if (!l_STag.compare("TITLE")) { pTrackMetadata->setTitle(l_SPayload); } else if (!l_STag.compare("REPLAYGAIN_TRACK_GAIN")) { bool trackGainRatioValid = false; double trackGainRatio = ReplayGain::parseGain2Ratio(l_SPayload, &trackGainRatioValid); if (trackGainRatioValid) { ReplayGain trackGain(pTrackMetadata->getReplayGain()); trackGain.setRatio(trackGainRatio); pTrackMetadata->setReplayGain(trackGain); } } // This is left fot debug reasons!! //qDebug() << "Comment" << i << l_ptrOpusTags->comment_lengths[i] << //" (" << l_ptrOpusTags->user_comments[i] << ")" << l_STag << "*" << l_SPayload; } return OK; }
void VoiceMessagesLoader::onLoad(AudioData *audio) { bool started = false; int32 audioindex = -1; Loader *l = 0; Loaders::iterator j = _loaders.end(); { QMutexLocker lock(&voicemsgsMutex); VoiceMessages *voice = audioVoice(); if (!voice) return; for (int32 i = 0; i < AudioVoiceMsgSimultaneously; ++i) { VoiceMessages::Msg &m(voice->_data[i]); if (m.audio != audio || !m.loading) continue; audioindex = i; j = _loaders.find(audio); if (j != _loaders.end() && (j.value()->fname != m.fname || j.value()->data.size() != m.data.size())) { delete j.value(); _loaders.erase(j); j = _loaders.end(); } if (j == _loaders.end()) { l = (j = _loaders.insert(audio, new Loader())).value(); l->fname = m.fname; l->data = m.data; int ret; if (m.data.isEmpty()) { l->file = op_open_file(m.fname.toUtf8().constData(), &ret); } else { l->file = op_open_memory((const unsigned char*)m.data.constData(), m.data.size(), &ret); } if (!l->file) { LOG(("Audio Error: op_open_file failed for '%1', data size '%2', error code %3").arg(m.fname).arg(m.data.size()).arg(ret)); m.state = VoiceMessageStopped; return loadError(j); } ogg_int64_t duration = op_pcm_total(l->file, -1); if (duration < 0) { LOG(("Audio Error: op_pcm_total failed to get full duration for '%1', data size '%2', error code %3").arg(m.fname).arg(m.data.size()).arg(duration)); m.state = VoiceMessageStopped; return loadError(j); } m.duration = duration; m.skipStart = 0; m.skipEnd = duration; m.position = 0; m.started = 0; started = true; } else { if (!m.skipEnd) continue; l = j.value(); } break; } } if (j == _loaders.end()) { LOG(("Audio Error: trying to load part of audio, that is not playing at the moment")); emit error(audio); return; } if (started) { l->pcm_offset = op_pcm_tell(l->file); l->pcm_print_offset = l->pcm_offset - AudioVoiceMsgFrequency; } bool finished = false; DEBUG_LOG(("Audio Info: reading buffer for file '%1', data size '%2', current pcm_offset %3").arg(l->fname).arg(l->data.size()).arg(l->pcm_offset)); QByteArray result; int64 samplesAdded = 0; while (result.size() < AudioVoiceMsgBufferSize) { opus_int16 pcm[AudioVoiceMsgFrequency * AudioVoiceMsgChannels]; int ret = op_read_stereo(l->file, pcm, sizeof(pcm) / sizeof(*pcm)); if (ret < 0) { { QMutexLocker lock(&voicemsgsMutex); VoiceMessages *voice = audioVoice(); if (voice) { VoiceMessages::Msg &m(voice->_data[audioindex]); if (m.audio == audio) { m.state = VoiceMessageStopped; } } } LOG(("Audio Error: op_read_stereo failed, error code %1").arg(ret)); return loadError(j); } int li = op_current_link(l->file); if (li != l->prev_li) { const OpusHead *head = op_head(l->file, li); const OpusTags *tags = op_tags(l->file, li); for (int32 ci = 0; ci < tags->comments; ++ci) { const char *comment = tags->user_comments[ci]; if (opus_tagncompare("METADATA_BLOCK_PICTURE", 22, comment) == 0) { OpusPictureTag pic; int err = opus_picture_tag_parse(&pic, comment); if (err >= 0) { opus_picture_tag_clear(&pic); } } } if (!op_seekable(l->file)) { l->pcm_offset = op_pcm_tell(l->file) - ret; } } if (li != l->prev_li || l->pcm_offset >= l->pcm_print_offset + AudioVoiceMsgFrequency) { l->pcm_print_offset = l->pcm_offset; } l->pcm_offset = op_pcm_tell(l->file); if (!ret) { DEBUG_LOG(("Audio Info: read completed")); finished = true; break; } result.append((const char*)pcm, sizeof(*pcm) * ret * AudioVoiceMsgChannels); l->prev_li = li; samplesAdded += ret; { QMutexLocker lock(&voicemsgsMutex); VoiceMessages *voice = audioVoice(); if (!voice) return; VoiceMessages::Msg &m(voice->_data[audioindex]); if (m.audio != audio || !m.loading || m.fname != l->fname || m.data.size() != l->data.size()) { LOG(("Audio Error: playing changed while loading")); m.state = VoiceMessageStopped; return loadError(j); } } } QMutexLocker lock(&voicemsgsMutex); VoiceMessages *voice = audioVoice(); if (!voice) return; VoiceMessages::Msg &m(voice->_data[audioindex]); if (m.audio != audio || !m.loading || m.fname != l->fname || m.data.size() != l->data.size()) { LOG(("Audio Error: playing changed while loading")); m.state = VoiceMessageStopped; return loadError(j); } if (started) { if (m.source) { alSourceStop(m.source); for (int32 i = 0; i < 3; ++i) { if (m.samplesCount[i]) { alSourceUnqueueBuffers(m.source, 1, m.buffers + i); m.samplesCount[i] = 0; } } m.nextBuffer = 0; } } if (samplesAdded) { if (!m.source) { alGenSources(1, &m.source); alSourcef(m.source, AL_PITCH, 1.f); alSourcef(m.source, AL_GAIN, 1.f); alSource3f(m.source, AL_POSITION, 0, 0, 0); alSource3f(m.source, AL_VELOCITY, 0, 0, 0); alSourcei(m.source, AL_LOOPING, 0); } if (!m.buffers[m.nextBuffer]) alGenBuffers(3, m.buffers); if (!_checkALError()) { m.state = VoiceMessageStopped; return loadError(j); } if (m.samplesCount[m.nextBuffer]) { alSourceUnqueueBuffers(m.source, 1, m.buffers + m.nextBuffer); m.skipStart += m.samplesCount[m.nextBuffer]; } m.samplesCount[m.nextBuffer] = samplesAdded; alBufferData(m.buffers[m.nextBuffer], AL_FORMAT_STEREO16, result.constData(), result.size(), AudioVoiceMsgFrequency); alSourceQueueBuffers(m.source, 1, m.buffers + m.nextBuffer); m.skipEnd -= samplesAdded; m.nextBuffer = (m.nextBuffer + 1) % 3; if (!_checkALError()) { m.state = VoiceMessageStopped; return loadError(j); } } else { finished = true; } if (finished) { m.skipEnd = 0; m.duration = m.skipStart + m.samplesCount[0] + m.samplesCount[1] + m.samplesCount[2]; } m.loading = false; if (m.state == VoiceMessageResuming || m.state == VoiceMessagePlaying || m.state == VoiceMessageStarting) { ALint state = AL_INITIAL; alGetSourcei(m.source, AL_SOURCE_STATE, &state); if (_checkALError()) { if (state != AL_PLAYING) { alSourcePlay(m.source); emit needToCheck(); } } } }