Перейти к содержимому

Лидеры


Популярный контент

Показан контент с высокой репутацией 22.04.2017 в Записи блога

  1. 2 балла
    Перед тем, как я начну, хочу сразу обратиться к забугорным ребятам, читающим эту запись. Hey! If you are reading this, please tell Vexatos to document the sound card. The in-game manual page is a meaningless piece of text that explains nothing. Documentation is written to help others to get an idea of how something works. The sound card is a complex thing that definitely needs a how-to guide to be used in programs. So if he wants the sound card to be used in programs, he should write the proper documentation with the detailed description of each concept, feature, and component method. The following is the result of efforts put to understand how the sound card works. It took me several months until I realized how to make a simple beep. How am I supposed to create more complex waves? >_> Ну да ладно. Хей! Разбирался я недавно, как работает звуковая карта из CX, и поэтому хочу предоставить результаты своих трудов. Как и написано сверху, доков нет, автор мода бездействует, везде уныние и отчаяние, поэтому пришлось действовать по-хардкорному. То есть чтением исходников и научнейшим тыком. Итак, есть компьютер со звуковой картой. Нужен звук. Звучит просто, м? Звуковая карта даёт нам замечательные функции вроде установки ADSR-огибающей, типа волны, шума простого или из LFSR, амплитудной и частотной модуляции. Но мы тут решили заняться простым звуком, поэтому всякие LFSR, AM, FM безжалостно выкидываем. Получаем такой план: Открыть канал. Поставить тип волны. Задать частоту волны. Установить громкость. Обработать. 1. Открыть канал Э, извините, а что за каналы? ... В звуковой карте звук генерируется с помощью инструкций, которые выполняются друг за другом. Почти все работают с определённым каналом. Если бы канал был один, мы бы смогли играть в один момент времени только одну, условно, ноту. Чтобы параллельно шло сразу несколько звуковых волн, нужно использовать несколько каналов. На каждом можно задать свои тип и частоту волны, громкость, ADSR-огибающую. По умолчанию таких каналов в звуковой карте 8 штук. С каналами всё довольно просто: открытый канал генерирует звук, закрытый канал не генерирует звук (правда, нужно будет здесь кое-что поправить, когда будет изучать ADSR). Открыть канал можно с помощью функции sound.open(channel: number). Закрыть: sound.close(channel: number). 2. Поставить тип волны Всего типов звуковая карта поддерживает пять: шум (noise), меандр (square), синусоида (sine), треугольная (triangle) и пилообразная (sawtooth). Waveforms ru [CC BY-SA 3.0 (http://creativecommons.org/licenses/by-sa/3.0) или GFDL (http://www.gnu.org/copyleft/fdl.html)], автор Omegatron (original work) Kirill Borisenko (Russian translation; see File:Waveforms for Cyrillic Segment.png) Incnis Mrsi (ultimate work), с Викисклада Очевидно, что разные типы волн звучат по-разному. Установить тип волны можно с помощью функции sound.setWave(channel: number, type: number). Видно, что тип волны должен быть числом. Соответствующее типу волны число можно получить с помощью таблицы sound.modes. Например, sound.modes.sine или sound.modes.noise. 3. Задать частоту волны Чем больше частота, тем выше звук. Частота 440 Hz соответствует ноте ля в первой октаве. Другие частоты можно найти по табличке на Википедии. Используем функцию sound.setFrequency(channel: number, freq: number). 4. Установить громкость Громкость может быть в пределах от 0 до 1 включительно. Половина максимальной громкости - 0.5. И так далее. Функция: sound.setVolume(channel: number, volume: number). 5. Обработать Чтобы обработать все инструкции, нужно вызвать sound.process(). Но если вы просто возьмёте и вызовете функцию, то ничего не услышите. Дело в том, что, достигнув конца инструкций, звуковая карта прекращает генерировать звук. Поэтому нужно выставить некоторую задержку в конце всех других инструкций, чтобы звуковая карта подождала перед прекращением воспроизведения звука. Для этого служит sound.delay(delay: number). Задержка указывается в миллисекундах. Например, sound.delay(2500). При этом эта функция тоже является инструкцией, поэтому её можно выставить несколько раз. Однако если задержка превышает 5000 (5 секунд), то звуковая карта выдаст ошибку. Проиграв несколько типов волн, мы всё равно останемся неудовлетворёнными. Ну где это видано, что звук мгновенно нарастает и затухает? Чтобы как-то это контроллировать, мы воспользуемся ADSR-огибающей. На Википедии довольно хорошо написано, в чём смысл этого. Скопируем сюда оттуда пикчу: И вкратце распишем суть. Если вы хоть когда-либо слышали музыкальные инструменты, вы могли заметить, что громкость звука со временем меняется. У гитары громкость мгновенно нарастает и постепенно затухает, а у флейты нарастает не сразу, например. Чтобы очень-очень грубо моделировать эти инструменты, мы используем ADSR. Attack - это время нарастания громкости звука до максимальной (последняя устанавливается через sound.volume). Decay - время затухания сигнала сразу после достижения максимальной отметки до следующего параметра. Sustain - отметка громкости, к которой стремится Decay. Значение 1 - это максимальная громкость звука на канале (установленная с помощью sound.setVolume), 0 - отсутствие звука вовсе. При достижении отметки Sustain звук остаётся на этом уровне до "отпускания клавиши" (т.е. закрытия канала). Release - время затухания сигнала после закрытия канала, от отметки Sustain до нуля. Время везде в миллисекундах. Попробуем? Вызываем функцию sound.setADSR(channel: number, attack: number, decay: number, sustain: number, release: number) до пятого шага. Например sound.setADSR(1, 1000, 500, 0.33, 1000). И замечаем, что громкость меняется со временем. Yay! На этом счастливом моменте повествование обрывается. Итоговый код, который можно запустить и послушать примитивный бип: local sound = require("component").sound sound.close(1) sound.close(2) sound.open(1) sound.setWave(1, sound.modes.sine) sound.setFrequency(1, 440) sound.setVolume(1, 1) sound.setADSR(1, 1000, 500, 0.33, 1000) sound.delay(1000) sound.open(2) sound.setWave(2, sound.modes.noise) sound.setFrequency(2, 440) sound.setVolume(2, 0.6) sound.setADSR(2, 1, 250, 0, 1) sound.delay(1500) sound.process() К остальному, надеюсь, я ещё вернусь в другой записи. Сначала нужно самому понять, как оно работает в версии для CX. Поэтому наслаждаться пока приходится только бибикалкой.
  2. 1 балл
    Немногие знают, как работают палитры в OpenComputers. Расскажу здесь, как избавиться от необходимости прописывать гектары цветов в палитре, покажу, как упаковываются цвета в OpenComputers и дам пару алгоритмов для работы с индексами. Сразу условимся, что индексы палитр у нас будут начинаться с нуля. На каждой из трёх уровней видеокарт и мониторов своя поддерживаемая палитра цветов. Будем двигаться снизу вверх. Первый уровень Палитра состоит из двух цветов: чёрного и заданного в конфиге (по умолчанию белого). Конвертация цвета в индекс палитры тривиальна: цвет нулевой — и индекс нулевой (чёрный цвет); цвет ненулевой — индекс единичный. Цвет в индекс (deflate) и обратно (inflate) превращать — одно удовольствие: local palette = { 0x000000, CONFIG.monochromeColor } local function t1deflate(index) if index == 0 then return 0 else return 1 end end local function t1inflate(index) return palette[index + 1] end Как и говорил. Второй уровень В палитре второго уровня имеется 16 закреплённых цветов: local palette = {0xFFFFFF, 0xFFCC33, 0xCC66CC, 0x6699FF, 0xFFFF33, 0x33CC33, 0xFF6699, 0x333333, 0xCCCCCC, 0x336699, 0x9933CC, 0x333399, 0x663300, 0x336600, 0xFF3333, 0x000000} При конвертации цвета в индекс палитры вернётся ближайший к данному цвет из палитры. Насколько цвета друг к другу близки, рассчитывается по специальной формуле, которая учитывает, что человеческий глаз лучше воспринимает зелёный, нежели красный и синий. В коде этим занимается функция delta. Вот как она выглядит (вместе с функций extract, выделяющей из числа вида 0xABCDEF числа 0xAB, 0xCD, 0xEF): local function extract(color) color = color % 0x1000000 local r = math.floor(color / 0x10000) local g = math.floor((color - r * 0x10000) / 0x100) local b = color - r * 0x10000 - g * 0x100 return r, g, b end local function delta(color1, color2) local r1, g1, b1 = extract(color1) local r2, g2, b2 = extract(color2) local dr = r1 - r2 local dg = g1 - g2 local db = b1 - b2 return (0.2126 * dr^2 + 0.7152 * dg^2 + 0.0722 * db^2) end Теперь можно конвертировать цвет в индекс палитры. Суть такова: выбираем из двух цветов ближайший и возвращаем его. local function t2deflate(color) -- Сначала проверяем, совпадает ли данный цвет -- с каким-либо из палитры for idx, v in pairs(palette) do if v == color then return idx end end -- Составляем таблицу разниц между цветами local deltas = {} for idx, v in pairs(palette) do table.append(deltas, {idx, delta(v, color)}) end -- Сортируем по увеличению разницы table.sort(deltas, function(a, b) return a[2] < b[2] end) -- Первый элемент будет с наименьшей разницей, -- то есть искомый. Возвращаем индекс. return deltas[1][1] - 1 end Обратная же процедура — превращение индекса палитры в цвет — неизменна. local t2inflate = t1inflate Третий уровень Палитра третьего уровня содержит уже 256 цветов: первые 16 цветов изменяются, а остальные соответствуют цветам палитры RGB-6-8-5. Это означает, что можно смешивать 6 оттенков красного, 8 оттенков зелёного и 5 оттенков синего. В общем-то, довольно очевидна причина такого выбора: человеческий глаз лучше всего различает оттенки зелёного и хуже всего — оттенки синего. В любом случае, здесь алгоритмец будет посложнее. Сначала нужно сгенерировать палитру. Начнём с первых 16 цветов. Они не включаются в палитру RGB-6-8-5, поэтому их заполнять нужно отдельно. В OpenComputers по умолчанию они содержат оттенки серого. Так как чёрный и белый уже включены в основную, зафиксированную палитру, то заново их дублировать смысла нет. local palette = {} -- grayscale for i = 1, 16, 1 do palette[i] = 0xFF * i / (16 + 1) * 0x10101 end Таким образом в таблице получаются следующие оттенки серого: 0x0F, 0x1E, 0x2D, 0x3C, 0x4B, 0x5A, 0x69, 0x78, 0x87, 0x96, 0xA5, 0xB4, 0xC3, 0xD2, 0xE1, 0xF0 Эти цвета мы записываем в индексы от 0 до 15. Теперь нужно сгенерировать остальные цвета — они не изменяются. Здесь будет посложнее. Посмотрим на картинку с палитрой: В OpenComputers левая верхняя ячейка палитры (0x000000) имеет индекс 16, а правая нижняя (0xFFFFFF) имеет индекс 255. Индексы распределяются слева направо, сверху вниз. То есть правая верхняя ячейка (0x00FFFF) имеет индекс 55, а вторая сверху и левая (0x330000) — это номер 56. Отсюда вытекает следующий алгоритм нахождения цвета: сначала найти индексы отдельно по R, G, B, затем для каждого из этих трёх индексов найти соответствующий ему оттенок цвета, а затем всё сложить. for idx = 16, 255, 1 do local i = idx - 16 local iB = i % 5 local iG = (i / 5) % 8 local iR = (i / 5 / 8) % 6 local r = math.floor(iR * 0xFF / (6 - 1) + 0.5) local g = math.floor(iG * 0xFF / (8 - 1) + 0.5) local b = math.floor(iB * 0xFF / (5 - 1) + 0.5) palette[idx + 1] = r * 0x10000 + g * 0x100 + b end Идея следующая. Каждый из трёх каналов принимает значение от 0 до 255 (0xFF). Разбиваем их на определённое число ступеней (по-умному — квантуем): 6 для красного, 8 для зелёного и 5 для синего. Например, синий канал мы разобьём так: 0/4 · 255 = 0 1/4 · 255 = 63.75 2/4 · 255 = 127.5 3/4 · 255 = 191.25 4/4 · 255 = 255 В знаменателе тут 4, а не 5, потому что считаем с нуля. Затем округляем до ближайшего целого конструкцией math.floor(x + 0.5). Перебрав все комбинации, мы получим все 6 × 5 × 8 = 240 цветов неизменяемой части палитры. Всё. Палитра есть, теперь можно, наконец-то, конвертировать индексы между цветами. Из индексов получить цвет довольно просто. Достаточно использовать ту же функцию, что и для предыдущих уровней: t3inflate = t2inflate С обратной же конвертацией всё несколько сложнее. Функция, используемая в OC, подбирает ближайший цвет хитрым алгоритмом, который я привожу ниже. local function t3deflate(color) local paletteIndex = t2deflate(color) -- Если цвет из палитры, то используем значение выше for k, v in pairs(palette) do if v == color then return paletteIndex end end -- Иначе используем хитромудрый код local r, g, b = extract(color) local idxR = math.floor(r * (6 - 1) / 0xFF + 0.5) local idxG = math.floor(g * (8 - 1) / 0xFF + 0.5) local idxB = math.floor(b * (5 - 1) / 0xFF + 0.5) local deflated = 16 + idxR * 8 * 5 + idxG * 5 + idxB if (delta(t3inflate(deflated % 0x100), color) < delta(t3inflate(paletteIndex & 0x100), color)) then return deflated else return paletteIndex end end Хитромудрость здесь не сильно сложная, на самом деле. Мы сначала находим индекс самого близкого цвета из изменяемой части палитры (paletteIndex). Дальше высчитываем индекс цвета из неизменяемой части (deflated), для чего в каждом канале отдельно ищем номер ближайшей ступени, на которые ранее квантовали. Последний if сравнивает 2 варианта и возвращает самый похожий (с точки зрения человека) на исходный. В общем-то, это всё. Показал портированный со Scala на Lua код, который используется в OpenComputers. С помощью этого можно оптимизировать операции с экраном, выбирая поддерживаемые монитором цвета. И заодно избавиться от таблиц цветов, которые некоторые буквально берут и копипастят в файл, даже не задумываясь об изменяемых цветах палитры. Особенно это важно, когда берётся значение цвета через gpu.get, потому что следующий код всегда вернёт false: local gpu = require("component").gpu gpu.setForeground(0x20AFFF) gpu.setBackground(0x20AFFF) gpu.set(1, 1, "HI") return select(2, gpu.get(1, 1)) == 0x20AFFF И всё потому, что gpu.get возвращает уже приведённый к индексу из палитры цвет. А 0x20AFFF в палитре, если не менять первые 16 цветов, не имеется. Enjoy :P
Эта таблица лидеров рассчитана в Москва/GMT+03:00
×
×
  • Создать...