| Разработка и ромхакинг > Ромхакинг и программирование |
| конвертация аудио сэмплов и планы на треккер |
| (1/6) > >> |
| SeregaZ:
в своем редакторе использовал bass.dll для ресэмплинга из высококачественных вавок и ogg в 10.2khz для использования в играх. однако итог получается очень тихий. на форуме дали код по увеличению громкости - прикрутил. качество так себе. больше искажений привносит, чем громкости добавляет. тут меня просветили что нужна нормализация. ну нужна так нужна - на форуме bass.dll чот посоветовали... однако у меня ж руки крюки, так что не выходит. в дискорде посоветовали использовать программу SoX. конвертирует вроде неплохо. в смысле при использования флага нормализации. результат явно погромче, чем тот мой старый из bass.dll. однако эта программа, хоть и консольная - то есть можно прилепить костыль и использовать, но требует тыщу дллок в своей папке. это меня просто убивает. конечно я могу сделать распаковку из редактора, уж тем более все равно такая распаковка происходит... это как бы сразу и приложение и установщик в одном флаконе. однако хотелось бы больше юзерфрендливости. если в случае с bass.dll я могу сконвертировать результат прям в памяти и тут-же из памяти его проиграть, то с SoX такого не получится. там сначала надо будет родить файл результата конвертирования и потом его загрузить в редактор и проиграть. это ну вабще не айс :) да, работать будет, но это такой костыль блин... посему у меня два вопроса: 1. может ли кто посодействовать с перепиливанием кода оригинала SoX, чтобы оно из приложения превратилось в одиночную дллку, и получается могла работать с памятью? типа подсунул образ файла оригинала из памяти - оно результат родило тут-же память. тут-же из памяти проиграл, послушал, устроило - сохранил, создался файл. не понравилось - закрыл, память просто очистилась и все как раньше было, без всяких промежуточных сохранений на жесткий диск. https://sourceforge.net/p/sox/code/ci/master/tree/ понятия не имею что это за язык. 2. о самой нормализации - по идее, в самом начале я родил алгоритм ресэмплинга из высосокачественной вавки в мои целевые 10.2к, правда я весьма посредственно представлял что там должно было происходить... и результат был, как бы мне сойдет, но я не уверен что это был правильный результат :) посему забил, и вернулся к bass.dll. так я к чему - может быть есть какой-то алгоритм, какие-то правила для этой самой нормализации... то есть моя вавка - это мульон значений от -127 до +127. может мне просто в цикле нужно производить какие-то операции с этими значениями и в итоге этот самый SoX нафиг не нужен будет? ну и в каком месте следует производить эту нормализацию? с оригинальной высококачественной вавкой и после конвертировать в 10.2к, или можно сначала конвертировать в 10.2к и уже после производить эту нормализацию? просто с 10.2к данных будет в разы меньше :) а высококачественная вавка - там надо будет сначала определить в каком формате данные, стерео или моно... гемороя больше. не говоря что это может быть ogg файл. в случае bass.dll ему все фиолетово для конвертации что wav что 16 бит, что 24, что ogg - результат мне предоставит 8bit mono PCM 10.2. проще связываться с уже конверченным результатом и что-то там нормализировать, но правильнее думаю все-таки с высококачественной вавкой творить свои дела, и уже потом конвертировать - ведь там данных больше. так уж получилось что хочу всего и сразу :) честно говоря в своем редакторе уже сам начинаю путаться где и что... посему для вставки сэмплов в игру хотелось бы прикрутить сии прибамбасы, чтоб сэмплы стали красивше, а не так как сейчас. и причем чтоб все однокликово, хотя я понимаю что однокликово не получится, что лучше заранее подготовить сэмплы в крутых аудио редакторах, и только потом импортировать... но просто такими редакторами пользоваться умеют не все и посему для таких людей, которые не знают всякие монстры аудиоредактирования - хотелось бы подстелить соломки, создав такой однокликовый функционал. |
| Sharpnull:
--- Цитата: SeregaZ от 19 Декабрь 2019, 14:59:50 ---1. может ли кто посодействовать с перепиливанием кода оригинала SoX, чтобы оно из приложения превратилось в одиночную дллку, и получается могла работать с памятью? --- Конец цитаты --- http://sox.sourceforge.net/Docs/FAQ: --- Цитата ---8. Is SoX's functionality also available as a function library? Yes, the source code for SoX contains both SoX, the application, and libSoX. LibSoX is a static library that can be used in other applications. It contains routines such as sox_open_read(), sox_open_write(), sox_read(), sox_write(), and sox_close() to allow reading and writing audio files. Further information can be found in the libSoX manual page (though it is somewhat out of date) and in the examples supplied in the SoX source-code distribution. --- Конец цитаты --- Хотя там написано про статическую компиляцию, по докам понятно, что компиляция как динамическая библиотека возможна. Различаться громкость между bass.dll и SoX может быть из-за параметров, в SoX можно указать dB-level (кажется тоже самое, что опция "Нормализовать пик амплитуд до" в нормализации в Audacity). В Audacity ещё есть опция "Убрать смещения по оси амплитуды и выровнять по отметке 0.0". Лучше вам конечно разобраться как работает нормализация и понять почему такой разный результат. -------- Лучше конечно поискать алгоритм, если он не очень сложный. Целую библиотеку подключать только из-за нормализация слишком жирно. |
| SeregaZ:
так никто и не знает :) отмалчиваются как партизаны... так то знать бы чего куда прибавлять\убавлять и фиг бы с ним. но с этим SoX все-же лучше. там в командной строке надо добавлять параметр norm и нормализует так сказать. а громкость это gain - её я не трогаю. что интересно после нормализации если были "заскоки" - как они там clips по научному? он пишет в консоли об этом, дескать может потом убавите громкость чуть чуть? мне впрочем и с этими клипсами пойдет. что интересно в случае с bass.dll там маленько съедает звук С при конвертации - становится шипение вместо чистой С, как если бы по тарелке на барабанах стучать hi-hat которая. в случае с SoX такого вроде нет. звук "С" остается большее нормальным, чем басовский "Ш". |
| Sharpnull:
--- Цитата: SeregaZ от 19 Декабрь 2019, 17:55:43 ---так никто и не знает :) отмалчиваются как партизаны --- Конец цитаты --- Чего не знают? Как добавить библиотеку SoX в программу я указал. По 2-му вопросу, убрав всё воду: вы хотите алгоритм нормализации. Ну так курите код SoX или других библиотек. За вас никто не будет гадать почему там тихо, а там нет. Сделать хорошо по умолчанию всё равно не получится. Придётся добавить опцию "нормализация" и по-хорошему другие настройки. |
| SeregaZ:
алгоритм не знают и молчат. а по поводу шерстения кода - так этож совсем не мой язык так сказать. я даж не в курсе в какой среде это нужно собирать. это то еще проклятье чужой код пытаться завести... как сто тыщь ошибок вылезет. то синтаксис не тот, то файла какого не хватает... знаю я какой это геморой. эх... печаль тоска и безобразие. пойду погуглю чем статическая от динамической библиотеки отличается. Добавлено позже: похоже дллкой не получится. там прицелом тыщу других длл. получается они все в корневой папке болтаться должны. в смысле рядом с ехе файлом. так как перенеся в другую папку при попытке импорта этой длл - он ругается, что подружек не видит. хотя они рядышком лежат. отдельная же ехешка такой проблемы иметь не будет. лежит себе в отдельной папке и лежит. но вот неудобство будет писать во временный файл. при работе в памяти было бы солиднее, и только итоговый результат чтоб сохранять как юзверь благославит, то есть ему понравилось. сейчас будет сохранять в любом случае. печалька. |
| SeregaZ:
вобщем теперь из редактора весь суп набор SoX будет распаковываться в папку рядом. потом по требованию сувать туда сэмпл, SoX его конвертит и ложит рядышком. редактор ждет, пока конвертер закончит, берет этот файл, загружает и дальше по накатанной. результат главно позитивный, а вот метод гавно. я доволен :) |
| megavolt85:
--- Цитата: SeregaZ от 19 Декабрь 2019, 18:54:45 ---похоже дллкой не получится. там прицелом тыщу других длл. получается они все в корневой папке болтаться должны. в смысле рядом с ехе файлом. --- Конец цитаты --- ох батенька, да вы винды не знаете :) она гадкая ищет DLL либо в папке с EXE'хой, либо в system32 --- Цитата: SeregaZ от 19 Декабрь 2019, 18:54:45 ---так этож совсем не мой язык так сказать. --- Конец цитаты --- пардон, а на каком языке пишешь что православный СИ не узнаёшь? |
| SeregaZ:
у меня древнеславянский - PureBasic. от этой идеи по использованию dll пришлось отказаться. во первых синтаксис как правильно импортировать функции, а так-же порядок их применения. а второе - главный минус в том, что эта библиотека требует тыщу еще других. получается если я её буду в своем ехе файле вызывать, то надо обязательно чтоб они рядышком валялись. это не преемлимо. итак я там уже насорил так сказать... а тут еще десяток лепить, да еще со всякими матерными названиями. поплевался, да делать нечего... засунул их всех в ехешку, а когда моя ехе запускается она сразу и установщик и само приложение - если нет библиотек рядышком в отдельной папке - то создает и распаковывает туда. после уже оттуда ехе файл в скрытом режиме запускается и конвертит. потом моя программа забирает файл и дальше использует. в своей же программе запилил меню с тремя вариантами загрузки: старый с bass.dll, потом этот новый с SoX как есть, и третий SoX с нормализацией. и сэмплы стали получше :) мои старые ваще ничерта не слыхать и приходилось FM звук делать потише, чтобы сэмплы не тонули. а с этой нормализацией прям нормально так вышло. результатом доволен, а вот метод, повторюсь - гавно :) |
| Sharpnull:
SeregaZ, жаль, что C не знаете. Параметр --norm равносилен gain -n. В исходном коде gain в src/gain.c, там же "эмулируется" старый --norm. Чтобы вычленить алгоритм нужно убирать условия типа if (p->do_balance || p->do_balance_no_clip) {, которые не используются и задаются другими параметрами, переместить макросы, понять когда вызываются create, start, flow, drain, stop функции и т. п. Здесь даются ссылки на библиотеки (тоже C) нормализации с реализацией вроде попроще и там же пример совсем простой пиковой нормализации в пару строк, которая не учитывает человеческий слух, но и проблем с клиппингом нет. Возможно её вы и пробовали сами реализовать. В sox/src/synth.c есть другая реализация такой нормализации: --- Код: ---double min, max; int j; for (j = 0, min = max = 0; j < buffer_len; ++j) { min = min(min, buffer[j]); max = max(max, buffer[j]); } for (j = 0; j < buffer_len; ++j) { buffer[j] = (2 * buffer[j] - max - min) / (max - min); } --- Конец кода --- На вход buffer с положительными вещественными значениями, а на выходе от -1.0 до 1.0. Для 16-bit WAV (или сырых данных) значения допустим от 0 до 65535 (или [-32767(8), 32767] приводите к этому добавив 32767 или 32768), после [-1.0, 1.0] умножаете на 32767 или (value + 1.0) / 2.0 * 65535. Про граничные значения не знаю. В общем, тема непростая, как и говорил выше, если вы только добавите опцию "нормализация" без настроек, то результат не всегда будет желаемым и только для совсем ленивых пользователей вашей программы. |
| SeregaZ:
это то понятно, что даже голос одной и той-же мадамы в разных сэмплах будет отнормализирован по разному. ведь там разные фразы, разные топовые пики, следовательно громкость будет разной. по идее все фразы надо чтоб были в одном сэмпле, а после нормализации резать. тогда все будут одинаковые по громкости. функционал по резке я добавлял. а вот длительность у меня маленькая - максимум 10 секунд. туда могут не уместится все фразы за раз. ну пока так как есть. дальше посмотрю когда дойдет дело. пока спал додумал что сделал несколько не верно :) всегда пишет в 10.2к, но ведь исходные сэмплы могут быть меньше. если для Дюны пофиг и можно все поголовно 10.2 фигачить, то для Зомбей там максимум 7+ сколько-то, так как вроде драйвер там старый используется. этот момент надо будет предусмотреть. |
| Segaman:
почитал тему. непонял почему неиспользовать goldwave например. все эти танцы с бубном больше времени вашего потратят, чем если пользоваться тем, что уже до вас написано и хорошо отлажено. хотя дело может быть в том, что я не понял сути |
| SeregaZ:
объяснение зачем :) с примерами! |
| Sharpnull:
SeregaZ, про goldwave можно было просто написать, что вам нужно программно делать. Почему вы пишите везде 10.2 КГц, а в программе 10400 Гц написано? |
| SeregaZ:
а... эм... чото вот втемяшилось что 10.2... даааааа... надо исправить на 10.4 :) ;10.4/8.7/7.3/6.5/5.8/5.2 - frequency FLAGS ; 5 /6 /7 /8 /9 /A |
| SeregaZ:
тем временем я окончательно вынес мозг на форуме bass.dll товарищу Ian и в итоге теперь нормализацию (увеличение громкости) редактор сэмплов может делать и с помощью bass. вот теперь дилемма - откручивать ли обратно SoX? :) из-за него редактор раздулся до 10 мегабайт... из-за него будет создаваться дополнительная папка в папке с редактором, и из-за него будут создаваться временные файлы конвертации на жестком диске в количестве 1 штука. пока думаю хрен с ним. прикрутил же уже... тем более тут рекомендуют делать регулируемое увеличение громкости, я же сделал что сделал. посмотрим кароче как пойдет. пока ручная регулировка усиления не предусмотрена. |
| Sharpnull:
--- Цитата: SeregaZ от 27 Декабрь 2019, 18:08:44 ---вот теперь дилемма - откручивать ли обратно SoX? --- Конец цитаты --- Конечно убирать, вы делаете для ленивых, им всё равно, а кому надо будет разбираться в параметрах специальных программ. Лучше бы написали, как добились этого: правильный параметр/готовка или своя реализация? С примером кода. |
| SeregaZ:
с использованием функционала bass.dll. тот буржуйский товарищ все разжувал, и все неправильные мои места поправил :) а так-то я конечно согласен, что работая с этими данными в сыром виде, по всяким там формулам - было бы гораздо лучше, да и размером меньше. но там есть один нюанс: различные форматы входящего аудио. если PCM WAV как бы все понятно, то всякие прочие форматы - я там вобще не представляю что происходит :) всякие OGG и прочее. а bass удослужливо все это сам переваривает и я прихожу на все готовое и забираю результат. Добавлено позже: --- Код: ---Procedure EncodeCallback(handle.i, channel.a, *buffer, length.i, *user) Define Rec=#True CopyMemory(*buffer, *MemStore + EncSampleSize, length) ; *MemStore - тоже глобальный кусочек памяти EncSampleSize + length ProcedureReturn Rec EndProcedure ; global value EncSampleSize = 0 ;// Create a decoding channel from the source file ; *SoundData - образ входного файла в памяти, SampSize - размер этого сэмпла в памяти Decoder = BASS_StreamCreateFile(#True, *SoundData, 0, SampSize, #BASS_STREAM_DECODE) BASS_ChannelSetAttribute(Decoder, #BASS_ATTRIB_SRC, 6); If SEMenuEvent = #WavImportLoad4 ; в моем случае это другой пункт меню. если без нормализации просто пропустить ; Normalization requires an additional pass to get the current level. ; You can use BASS_ChannelGetLevelEx in that. SElevel.f SEpeak.f Repeat result = BASS_ChannelGetLevelEx(Decoder, @SElevel, 1, #BASS_LEVEL_MONO); If SElevel ; if any value exist If (SEpeak < SElevel) : SEpeak = SElevel : EndIf EndIf Until result = 0 ; repeat until SElevel = 0. end of playing? ; You can then use the BASS_FX_VOLUME effect To normalize the level ; based on that "peak" reading, like this: SEvolfx = BASS_ChannelSetFX(Decoder, #BASS_FX_VOLUME, 0) SEparam.BASS_FX_VOLUME_PARAM SEparam\fTarget = 1.0 / SEpeak SEparam\fTime = 0; BASS_FXSetParameters(SEvolfx, SEparam); ; Then rewind the decoder (BASS_ChannelSetPosition With pos=0) And do your conversion. BASS_ChannelSetPosition(Decoder, 0, #BASS_POS_BYTE) EndIf ;// Create a mixer To change the sample rate To 10.4 kHz RecOutputFrequency Mixer = BASS_Mixer_StreamCreate(RecOutputFrequency, 1, #BASS_STREAM_DECODE|#BASS_SAMPLE_8BITS|#BASS_MIXER_END) ;// Add the decoder To the mixer As a channel BASS_Mixer_StreamAddChannel(Mixer, Decoder, #BASS_MIXER_DOWNMIX); ; memory for 10 sec sample *MemStore = AllocateMemory(104000) ;// Set a WAV writer on it |#BASS_ENCODE_NOHEAD BASS_Encode_Start(Mixer, #Null, #BASS_ENCODE_PCM|#BASS_ENCODE_NOHEAD, @EncodeCallback(), #NUL) ; wait, until convert happen: While (BASS_ChannelIsActive(Decoder) = #BASS_ACTIVE_PLAYING) BASS_ChannelGetData(Mixer, @Buf, SizeOf(Buf)); Wend --- Конец кода --- |
| SeregaZ:
как известно я люблю что-то начинать ваять... и забрасывать на половине. вот сейчас хочу начать делать недоGEMS треккер, чтобы можно было визуально править какие-то нюансы. то есть абы какой конвертер из Дефлемаска в ГЕМС я запилил, и с мелодиями без извращений результат вполне себе... однако весь затык с использованием как раз извращений в Дефлемаске - всяких слайдов для нот. сии приблуды в конвертере весьма поверхностны и неточны. и поэтому думаю надо сделать что-то типа дополнительного редактора для правки подобных случаев. использовать буду не GEMS напрямую как оно есть - то есть 4 файла банков, а распакованный вариант .code файл с кучей всяких .raw файлов и прочих, что рожает r57shell'овский комбайн при распаковке банков GEMS. что-то типа такого: --- Код: --- SECTION CODE channel_0: loop $7F delay 0 nop mastervolume 4 tempo 120 delay 192 pitch $0000 delay 27 patch patch_0C delay 0 volume 12 duration 3 delay 24 note $52 note $51 delay 48 ... --- Конец кода --- с синтаксисом что есть что в этих строчках все понятно и даже у меня что-то там умеет проигрывать (опять таки не очень точно). весь затык в том, как это все удобно организовать :) хотелось бы конечно как в дефлемаске, но тут есть несколько глобальных различий. если дефлемаск, скажем в первом канале имеет 100 строчек, то и во втором 100. и в третьем 100 и в 10 тоже 100. то в GEMS каждый канал может иметь свое количество строчек (строчка это я имею ввиду одну единицу делея - задержки между событиями), а во вторых в GEMS может быть до 16 каналов - в дефлемаске только 10, и то строго первые 6 FM, потом 3 PSG тональный и 1 PSG шумовой. в GEMS любой вариант типа инструмента FM или PSG может быть в любой дорожке. предварительно набросал примерно так: то есть в одной строчке будет целая куча параметров, хотя в тексте тот-же bpm tempo например - используется обычно только в одном месте, и получается эта графа bpm везде у меня будет пустая. в дефлемаске сделали этот момент путем добавления слотов под параметры. мне сделать подобное будет сложновато. проще все-таки сделать целую линию с пятком параметров, чем динамическое изменение количества параметров, которое должно случится в одно событие. с этим как бы все понятно. но главный затык заключается в том, что в одной дорожке GEMS могут быть аккорды. это встречается редко, но оно бывает и в мою схему это дело невписывается ну вобще никак :) мало того аккорды - разные ноты, но и это могут быть разные инструменты. вобщем дичная дичь... тут бы может помогло бы как раз это динамическое изменение количества эффектов для события, как в Дефлемаске - но это я не рожу. сложновато будет. как бы выкрутится? |
| SeregaZ:
внезапно понадобилось это самое С. но я, как известно, в нем ничерта не понимаю... честно по пионерски пытался конвертировать код из С в свой недоязык, но некоторые конструкции меня вгоняют в дикий ужос. например: --- Код: ---For ( ; VTblPtr[VTBLFLAGS] != 0xFF; VTblPtr += 7) --- Конец кода --- что там происходит - черт знает :) я понимаю если у For есть переменная, которая изменяется. есть стартовое значение для переменной - например 0 и есть конечное - скажем 5. есть параметр Step - шаг. типа если поставить шаг 2, то цикл с 0 до 5 будет крутится так: 0, 2, 4, конец. но здеееесь? For, и первым параметром точка с запятой... шта?!?!? а после еще != это что? неравно? или вот здесь: --- Код: ---PSGVTBLTG3(VTBLFLAGS) &= ~0x20 --- Конец кода --- амперсанд равно и значок герца. буржуи "объяснили" it is a binary and of the inverted $20. мне как-то помогло слабо... хоть и честно скопипастил оттуда уже переведенную конструкцию. я хоть и изобрел свой любимый метод: если не понимаешь код, то просто верь - но здесь уже перебор. вобщем я к чему веду... тут у нас на форуме тыщу специалистов по С :) может хоть один сможет помочь в одном геройском деле по спасению вселенной? есть исходник GEMS проигрывателя от товарища ValleyBell'a - я его пытался конвертировать в свой язык. но не выходит... перенес структуры, глобальные переменные, начал функции и утоп в непонятках. так вот может ли кто поглядеть и пересобрать этот исходник, вместо консольного ехешника сделать одиночную dll, которую можно импортировать в проект и где будут три "внешние" команды: посылка 4 поинтеров памяти в библиотеку (это адреса памяти GEMS банков - сэмплы, инструменты, модуляции и ноты мелодий), плей и стоп. предполагается что посылаться для проигрывания всегда будет одна мелодия (или спецэффект). впрочем наверное надо еще некий GEMSinit еще вывести. типа чтоб очередность была: GEMSinit (для инициализации. видимо оно нужно будет перед каждым запуском новой мелодии. для сброса так сказать. и это еще большой вопрос предусмотрен ли сброс данных в этом коде. ведь в оригинале консольная программа запускается один раз с одним комплектом банков GEMS) послать 4 указателя (и видимо размеры памяти этих указателей. что-то типа GEMSSetSamples(pointer*, size), GEMSSetEnvelopes(pointer*, size) и так далее всего 4 банка) GEMSPlay - всегда первая мелодия играет (нулевая видимо, так как с 0 начинается) GEMSStop в оригинале это ехе файл, которому подсовываешь 4 файла банков GEMS и после еще надо нажать ентыр, чтобы заиграло. в архиве пример приложен и батник для запуска с подсовыванием этих банков. однако предполагается что таких запусков будет тыща - то есть кликая по списку в редакторе запускаешь каждый и слушаешь что там есть, тыкаешь следующий... это что получается - создавать временные 4 файла только с этим спецэффектом и мелодией, после запускать сторонний ехе и эмулировать нажатие ентыра в это окно? а выключать путем убийства процесса? как-то это не очень сорить на жестком диске пользователя временными файлами (каюсь, я уже сорю, когда происходит оптимизация вставленной графики. однако графики вставляется куда меньше, чем ежели каждый сэмпл или спецэффект проигрывать путем клика мышкой) посему я хотел бы собирать из этих текстовых "распакованных" файлов 4 образа GEMS банков в памяти редактора и после посылать их в библиотеку. тогда никаких временных файлов создано не будет. чисто и комфортно и жесткий диск зазря не насилуем. так что есливчто - оставляю ссыль на архив: https://www.dropbox.com/s/zt86m73g6a65dn4/SEGA.zip?dl=1 |
| Sharpnull:
SeregaZ, --- Код: ---for ( ; VTblPtr[VTBLFLAGS] != 0xFF; VTblPtr += 7) --- Конец кода --- Си-шный оператор for простой. for (начальное_действие; условие_продолжения; выполнить_после_каждой_итерации). Каждое из 3 выражений можно опустить вплоть до бесконечного цикла (for (;;)), выражения могут быть любыми. Это вы могли бы легко изучить. В данном примере цикл выполняется пока байт из массива не равен 0xFF, а VTblPtr += 7 - переход на следующие 7 байтов. Было бы попроще написать: for (int i = 0; VTblPtr[i + VTBLFLAGS] != 0xFF; i += 7). --- Код: ---PSGVTBLTG3(VTBLFLAGS) &= ~0x20 --- Конец кода --- В коде написано: --- Код: ---PSGVTBLTG3[VTBLFLAGS] &= ~0x20; // yes - clear locked bit in TG3 --- Конец кода --- Там уже объяснение, что снимается бит. Действительно, обычно так и снимают биты. Можно записать как unsigned char n = 0; n = n & (~0x20); где ~ инвертирует все биты (~0x20 = 0xDF (для однобайтовой переменной)), а & - побитовое И. Посмотрел код, там есть инициализация и очищение. Всё ли очищает - трудно сказать. Запутано из-за использования разных слоёв абстракций (init от одного, воспроизведение от другого) и есть лишняя команда gems_loop(), которая вызывается до инициализации, что меня удивило (если убрать, всё работает, но может она нужна). |
| Навигация |
| Главная страница сообщений |
| Следующая страница |