Программирование музыки🎵

⚠️ Программирование музыки в кодах — занятие неблагодарное. Лучше это делать с помощью какого-нибудь музыкального редактора. Но редактор может не удовлетворить все Ваши потребности, так что освоить программирование в кодах все же желательно.

🎹 Принцип тот же — последовательно читаются данные, и вызывается процедура воспроизведения. При этом необходимо предусмотреть прерывание исполнения (чаще всего в этих целях контролируется нажатие какой-нибудь клавиши). Например:
Assembler Z80:
10   START LD HL,60000 ; HL=адрес данных
20   NEXT  LD E,(HL)   ; DE=
30        INC HL       ; дли-
40        LD D,(HL)    ; тель
50        INC HL       ; ность
60        LD A,D       ; DE
70        CP 255       ; =
80        JR NZ,CONT   ; 65535
90        LD A,E       ; ?
100       CP 255       ; если да,
110       JR Z,START   ; то мелодия сначала
120  CONT LD A,D       ; DE=
130       OR E         ; 0 ?
140       JR NZ,BEEP   ; если нет, то перейти на BEEP
150       LD E,(HL)    ; DE=
160       INC HL       ; длитель-
170       LD D,(HL)    ; ность
180       INC HL       ; паузы
190  PAUSE HALT        ; ожидание прерывания
200       DEC DE       ; DE=DE-1
210       LD A,D       ; DE=
220       OR E         ; 0 ?
230       JR NZ,PAUSE  ; если нет, то цикл
240       JR NEXT      ; переход на обработку след. данных
250  BEEP LD C,(HL)    ; BC=
260       INC HL       ; час-
270       LD B,(HL)    ; то-
280       INC HL       ; та
290       PUSH HL      ; сохранение HL
300       PUSH BC      ; поместить BC в стек
310       POP HL       ; поместить значение из стека в HL
320       CALL 949     ; вызов подпрограммы воспроизведения
330       CALL 654     ; контроль клавиатуры
340       POP HL       ; восстановление HL
350       LD A,E       ; A=E
360       CP 255       ; если нажата клавиша,
370       RET NZ       ; то возврат в бейсик
380       JR NEXT      ; переход на обработку след. данных

👉 При вызове этой подпрограммы из машинных кодов обратите внимание, что прерывания должны быть разрешены! Иначе, если в данных первой будет стоять пауза, компьютер «зависнет».

📦 Перед использованием этой довольно страшненькой подпрограммы необходимо создать для нее массив данных (это и есть самое веселое). В данном случае он должен располагаться с адреса 60000 (#ЕА60), но Вы можете легко изменить его местонахождение, поменяв число в первой строке этой подпрограммы.

✅ Данные для каждой ноты должны быть следующими:
  • Сначала два байта длительности (первый — младший)
  • Затем два байта частоты
  • Если вместо длительности поместить два нуля, то следующая пара байт будет рассматриваться как длина паузы
  • Чтобы отметить конец мелодии, вставьте в данные вместо очередной длительности два байта 255
⏱️ Длительность паузы измеряется в пятидесятых долях секунды, а в чем измеряется длительность ноты не знает, наверное, никто, хотя рассчитать необходимое значение все же можно.

🚫 Теперь о недостатках, которых, честно говоря, хоть отбавляй. Два из них уже были упомянуты выше:
  1. Трудность составления мелодии
  2. Довольно странное измерение длительности нот
➕ Кроме того, длительность зависит от частоты. Например, если Вы введете такие данные: 0,1,100,0,0,1,0,5,255,255, то не услышите две разные ноты одинаковой длины, как ожидали.

❌ Ещё один недостаток — возможность прервать исполнение только в промежутке между нотами. Впрочем, это можно легко исправить:
Assembler Z80:
10   START LD HL,60000 ; HL=адрес данных
20   NEXT  LD E,(HL)   ; DE=
30        INC HL       ; дли-
40        LD D,(HL)    ; тель
50        INC HL       ; ность
60        LD A,D       ; DE
70        CP 255       ; =
80        JR NZ,CONT   ; 65535
90        LD A,E       ; ?
100       CP 255       ; если да,
110       JR Z,START   ; то мелодия сначала
120  CONT LD A,D       ; DE=
130       OR E         ; 0 ?
140       JR NZ,BEEP   ; если нет, то перейти на BEEP
150       LD E,(HL)    ; DE=
160       INC HL       ; длитель-
170       LD D,(HL)    ; ность
180       INC HL       ; паузы
190  PAUSE XOR A       ; A=0 (контроль всей клавиатуры)
200       IN A,(254)   ; опрос клавиш
210       CPL          ; инвертирование A
220       AND 31       ; сброс лишних битов
230       RET NZ       ; если нажата клавиша, то возврат
240       HALT         ; ожидание прерывания
250       DEC DE       ; DE=DE-1
260       LD A,D       ; DE=
270       OR E         ; 0 ?
280       JR NZ,PAUSE  ; если нет, то цикл
290       JR NEXT      ; переход на обработку след. данных
300  BEEP LD C,(HL)    ; BC=
310       INC HL       ; час-
320       LD B,(HL)    ; то-
330       INC HL       ; та
340       PUSH HL      ; сохранение HL
350       PUSH BC      ; поместить BC в стек
360       POP HL       ; поместить значение из стека в HL
370       CALL PLAY    ; вызов подпрограммы воспроизведения
380       POP HL       ; восстановление HL
390       RET NZ       ; то возврат в бейсик
400       JR NEXT      ; переход на обработку след. данных
410  PLAY DI           ; запрет прерываний
420       LD A,(23624) ; A=
430       SRL A        ; цвет
440       SRL A        ; бор-
450       SRL A        ; дюра
460  LOOP1 XOR 16      ; инвертирование бита D4
470       OUT (254),A  ; вывод A в порт 254
480       LD C,A       ; сохранение A
490       PUSH HL      ; сохранение HL
500  LOOP2 XOR A       ; A=0 (контроль всей клавиатуры)
510       IN A,(254)   ; опрос клавиш
520       CPL          ; инвертирование A
530       AND 31       ; сброс лишних битов
540       JR Z,CONT2   ; если клавиша не нажата, то продолжить
550       POP HL       ; снять значение со стека
560       EI           ; разрешение прерываний
570       RET          ; возврат
580  CONT2 DEC HL      ; HL=HL-1
590       LD A,H       ; HL=
600       OR L         ; 0 ?
610       JR NZ,LOOP2  ; если нет, то цикл
620       POP HL       ; восстановление HL
630       DEC DE       ; DE=DE-1
640       LD A,D       ; DE=
650       OR E         ; 0 ?
660       LD A,C       ; восстановление A
670       JR NZ,LOOP1  ; если DE<>0, то цикл
680       EI           ; разрешение прерываний
690       RET          ; возврат

⚠️ Обратите внимание на то, что одинаковые данные будут воспроизводиться по-разному в этой и предыдущей подпрограммами.

🎯 И, наконец, самый очевидный недостаток — музыка, которая получается с помощью этих подпрограмм слишком примитивна. На ассемблере можно сделать что-нибудь и посерьёзнее.

💡 Надеюсь, что убедил Вас не пытаться программировать музыку напрямую в кодах, а вместо этого приобрести хороший музыкальный редактор.

✨ Все подпрограммы, приведенные в этой главе, можно использовать и для создания эффектов. Для этого достаточно вместо нот закодировать отдельные ступени эффекта. Желательно, чтобы каждая ступень была как можно короче, тогда эффект будет звучать плавно.

Многоголосые мелодии 🎶🎶

Хотя в предыдущей главе я старательно доказывал Вам тщетность попыток программирования музыки на ассемблере, Вы все-таки должны знать основные принципы этого занятия.

🎹 Сейчас я опишу создание многоголосой музыки, то есть музыки, в которой может звучать две или больше нот одновременно.

🎵 Первый пример: имитация двухголосого звучания

Assembler Z80:
10   DI          ; запрет прерываний
20   LD D,200    ; D=частота 1
30   LD E,50     ; E=частота 2
40   LD H,150    ; H=длительность
50   XOR A       ; A=бордюр для голоса 1 (0)
60   EX AF,AF'   ; смена регистров A и F на альтернативные
70   XOR A       ; A=бордюр для голоса 2 (0)
80   LD C,E      ; C=счетчик 1
90   LD B,D      ; B=счетчик 2
100  BEEP EX AF,AF' ; смена регистров A и F (смена голоса)
110       DEC C     ; C=C-1
120       JR NZ,CONT ; если C<>0, топерейти на CONT
130       LD C,E    ; восстановить счетчик 1
140       XOR 16    ; инвертировать бит D4 голоса 1
150  CONT OUT (254),A ; вывести A в порт 254
160       EX AF,AF' ; смена регистров A и F (смена голоса)
170       DEC B     ; B=B-1
180       JR NZ,CONT2 ; если B<>0, то перейти на CONT2
190       LD B,D    ; восстановить счетчик 2
200       XOR 16    ; инвертировать бит D4 голоса 2
210  CONT2 OUT (254),A ; вывести A в порт 254
220       INC L     ; L=L+1
230       JR NZ,BEEP ; если L<>0, то перейти на BEEP
240       NOP       ; резерв
250       NOP       ; резерв
260       DEC H     ; H=H-1
270       JR NZ,BEEP ; если H<>0, то перейти на BEEP
280  EI    ; разрешение прерываний
290  RET   ; возврат

📝 Теперь поясню некоторые детали:
  • Два альтернативных регистра А используются первым и вторым голосами для хранения цвета бордюра и состояния динамика.
  • Странная операция с регистром L перед завершением цикла предназначена для «растягивания» длительности. Если её убрать, то звучание этой подпрограммы станет настолько кратковременным, что Вы можете его даже не услышать.
  • Два байта резерва (операции NOP) Вы можете использовать для изменения частоты обоих голосов во время воспроизведения. Для этого вместо «NOP» нужно вставить INC D или DEC D для первого голоса, и INC Е или DEC E для второго.
💻 Эту подпрограмму можно использовать на ассемблере (см. предыдущую главу) или на бейсике (в данном случае параметры следует заносить оператором РОКЕ в следующие ячейки: ADDR+2 - частота 1, ADDR+4 - частота 2, ADDR+6 - длительность, где ADDR - адрес расположения подпрограммы в памяти).

🎼 Многоголосые подпрограммы с таблицей частот

Следующие две многоголосые подпрограммы очень похожи друг на друга, но первая звучит значительно тише и несколько чище второй.

🔊 Подпрограмма 1:
Assembler Z80:
10   DI          ; запрет прерываний
20   LD HL,2000  ; HL=длительность
30  LOOP1 LD IX,FRQS ; IX=адрес таблицы частот
40        LD B,5     ; B=число голосов
50  LOOP2 DEC (IX+0) ; байт по адресу IX+0 уменьшить на 1
60        JR NZ,NOSIGN ; если не 0, то перейти на NOSIGN
70        LD A,24    ; A=цвет бордюра + 24
80        OUT (254),A ; вывод A в порт 254
90        LD A,(IX+5) ; обновление счетчика
100       LD (IX+0),A ; текущего канала
110       XOR A      ; A=цвет бордюра (0)
120       OUT (254),A ; вывод A в порт 254
130  NOSIGN INC IX   ; IX=IX+1
140       DJNZ LOOP2 ; цикл
150       DEC HL     ; HL=HL-1
160       LD A,H     ; HL=
170       OR L       ; 0?
180       JR NZ,LOOP1 ; если нет, то цикл
190  EI    ; разрешение прерываний
200  RET   ; возврат
210  FRQS DEFB 1,1,1,1,1 ; счетчики для 5 каналов
220       DEFB 10,20,30 ; частоты для
230       DEFB 40,50    ; 5 каналов

🔊 Подпрограмма 2:
Assembler Z80:
10   DI          ; запрет прерываний
20   LD HL,2000  ; HL=длительность
30  LOOP1 LD IX,FRQS ; IX=адрес таблицы частот
40        LD B,5     ; B=число голосов
50  LOOP2 DEC (IX+0) ; байт по адресу IX+0 уменьшить на 1
60        JR NZ,NOSIGN ; если не 0, то перейти на NOSIGN
70        LD A,(IX+5) ; A=содержимое IX+5
80        XOR 1     ; инвертирование бита D0
90        LD (IX+5),A ; содержимое IX+5=A
100       LD A,0    ; A=цвет бордюра
110       JR Z,NOSIGN ; если D0 содер. IX+5=0,то перейти на NOSIGN
120       OR 24     ; установить в 1 биты D3 и D4 регистра A
130       OUT (254),A ; вывод A в порт 254
140       LD A,(IX+10) ; обновление счетчика
150       LD (IX+0),A ; текущего канала
160  NOSIGN INC IX  ; IX=IX+1
170       DJNZ LOOP2 ; цикл
180       DEC HL    ; HL=HL-1
190       LD A,H    ; HL=
200       OR L      ; 0?
210       JR NZ,LOOP1 ; если нет, то цикл
220  EI    ; разрешение прерываний
230  RET   ; возврат
240  FRQS DEFB 1,1,1,1,1 ; счетчики для 5 каналов
250       DEFB 0,0,0,0,0 ; буферы для 5 каналов
260       DEFB 10,20,30 ; частоты для
270       DEFB 40,50    ; 5 каналов

📋 Эти подпрограммы заводят счетчики для каждого из каналов и последовательно их уменьшают. Если какой-либо из счетчиков становится равным 0, на динамик выводится сигнал, а в данный счетчик заносится частота из таблицы.

🎛️ В данных примерах подпрограммы воспроизводят по пять голосов, но Вы можете легко изменить их число, модифицировав значения:
  • Для первой подпрограммы: строки 40 и 90
  • Для второй подпрограммы: строки 40, 70, 90 и 140 (в строку 140 надо вставить число голосов, умноженное на 2)
📊 При этом надо также скорректировать таблицы частот.

⚠️ Хотя голосов может быть довольно много (до 85), увлекаться их количеством не стоит, так как чем их больше, тем хуже качество. Впрочем, вряд ли найдется гений, способный написать мелодию на 85 голосов.

💡 Если Вы хотите использовать многоголосие, но в конкретный момент времени Вам надо воспроизвести меньшее число голосов, чем задано, то неиспользуемым голосам надо присвоить частоты, совпадающие с частотами используемых голосов.

Обработка внешних сигналов 🎤

ZX-Spectrum предоставляет программистам довольно интересную возможность — считывать и сохранять в памяти звуковые сигналы, подаваемые на его магнитофонный вход. Эти фрагменты можно воспроизводить с различной скоростью, что, несомненно, очень интересно. Правда, качество полученного таким образом звука оставляет желать лучшего, а его максимальная длительность ограничивается несколькими десятками секунд (просто-напросто кончаются 48К ОЗУ!).

📝 Подпрограмма записи звука в память (способ 1)

Assembler Z80:
10   DI          ; запрет прерываний
20   LD HL,25000 ; HL=адрес сохранения звука
30   LD DE,35000 ; DE=длительность звука
40  LOOP1 LD B,8 ; B=счетчик битов
50  LOOP2 SLA (HL) ; скроллинг значения в памяти
60        IN A,(254) ; ввести значение из порта 254
70        BIT 6,A  ; проверить бит магнитофона
80        JR Z,NOSIGN ; если 0, то перейти на NOSIGN
90        SET 0,(HL) ; установить бит D0 в памяти
100  NOSIGN LD C,3 ; C=задержка
110       PAUSE DEC C ; C=C-1
120            JR NZ,PAUSE ; если C<>0, то цикл
130       DJNZ LOOP2 ; продолжить цикл обработки байта
140       INC HL  ; HL=HL+1
150       DEC DE  ; DE=DE-1
160       LD A,D  ; DE=
170       OR E    ; 0?
180       JR NZ,LOOP1 ; если нет, то цикл
190  EI    ; разрешение прерываний
200  RET   ; возврат

▶️ Подпрограмма воспроизведения (способ 1)

Assembler Z80:
10   DI          ; запрет прерываний
20   LD HL,25000 ; HL=адрес сохраненного звука
30   LD DE,35000 ; DE=длительность звука
40        NOP    ; резерв
50        XOR A  ; A=цвет бордюра (0)
60  LOOP3 LD B,8 ; B=счетчик битов
70  LOOP4 AND 239 ; сброс бита D4 регистра A
80        RLC (HL) ; скроллинг данных через флаг CY
90        JR NC,NOSGN ; если флаг CY=0, то перейти на NOSGN
100       OR 16   ; установка бита D4 регистра A
110  NOSGN OUT (254),A ; вывод A в порт 254
120       LD C,3  ; C=задержка
130  PAUS2 DEC C  ; C=C-1
140            JR NZ,PAUS2 ; если C<>0, то цикл
150       DJNZ LOOP4 ; продолжить цикл обработки байта
160       INC HL  ; HL=HL+1
170       LD C,A  ; сохранение A
180       DEC DE  ; DE=DE-1
190       LD A,D  ; DE=
200       OR E    ; 0?
210       LD A,C  ; восстановление A
220       JR NZ,LOOP3 ; если DE<>0, то цикл
230  EI    ; разрешение прерываний
240  RET   ; возврат

⚠️ Адреса данных и их длина должны совпадать в этих двух подпрограммах. Обратите также внимание на то, что они используют 35Кб памяти с адреса 25000. Поэтому в этой области ОЗУ не должно быть никаких данных. Сами подпрограммы тоже должны располагаться где-нибудь в другом месте.

🔧 Оптимальным вариантом будет такой:
  • Перенумеруйте строки второй подпрограммы, начиная с 210 и с шагом 10
  • Соедините то, что получилось с первой подпрограммой
  • Введите следующие строки:
Assembler Z80:
7    ORG 24900
20   LD HL,BUFFER
30   LD DE,LENGTH
220  LD HL,BUFFER
230  LD DE,LENGTH
450  LENGTH EQU 35000 ; LENGTH=35000
460  BUFFER NOP ; здесь располагается буфер

🚨 Проследите также, чтобы ассемблер располагался приблизительно с адреса 40000. В любом случае, он будет стёрт из памяти при вызове подпрограммы ввода звука. Поэтому, не стоит вызывать её из обреченного ассемблера.

⚠️ Ещё одно важное замечание: при использовании этих подпрограмм следите, чтобы выражение BUFFER+LENGTH не было больше 65535, иначе часть звуковых данных просто не запишется, а вместо них Вы услышите шум.

⏱️ В строке 100 первой подпрограммы и в строке 120 второй, в регистр С заносится задержка между выборками данных. Если эти два значения равны, то звук будет воспроизводиться с реальной скоростью. Если второе число меньше первого, то звук будет ускорен, а если больше — замедлён. Причем, чем меньше задержка в подпрограмме ввода, тем выше качество вводимого сигнала, но меньше его длительность.

🔄 Байт резерва в подпрограмме воспроизведения предназначен для переворачивания звука, то есть проигрывания его с конца на начало. Для включения этого эффекта надо заменить операцию NOP в этом байте на ADD HL,DE, a INC HL в строке 160 на DEC HL.

🎛️ Подпрограмма записи звука (способ 2)

Существует и другой способ записи звука. Он основан на подсчете звуковых импульсов. Перед первым способом он не имеет никаких особых преимуществ, за исключением возможности незначительного понижения уровня шума.
Assembler Z80:
10   DI          ; запрет прерываний
20   LD HL,25000 ; HL=адрес сохранения звука
30   LD DE,35000 ; DE=длительность звука
40   LD BC,511   ; C - буфер, B - счетчик
50  LOOP1 IN A,(254) ; ввод значения из порта 254
60        EXX     ; смена набора регистров
70        LD B,1  ; B=задержка
80        PAUSE DJNZ PAUSE ; задержка
90        EXX     ; смена набора регистров
100       BIT 6,A ; проверить бит магнитофона
110       LD A,0  ; A=0
120       JR NZ,SIGN ; если бит D6=1, то перейти на SIGN
130       DEC A   ; A=255
140  SIGN CP C    ; A=C (предыдущему значению)?
150       JR NZ,WRITE ; если нет, то перейти на WRITE
160       INC B   ; B=B+1 (счетчик импульсов)
170       JR NZ,LOOP1 ; если B<>0, то цикл
180  WRITE DEC B  ; B=B-1
190       JR NZ,POKE ; если B<>0, то перейти на POKE
200       LD B,2  ; B=2
210       CPL     ; инвертирование A
220       LD C,A  ; C=A
230       JR LOOP1 ; перейти на LOOP1
240  POKE INC B   ; B=B+1
250       LD (HL),B ; запись значения в память
260       LD B,1  ; B=1
270       LD C,A  ; C=A
280       INC HL  ; HL=HL+1
290       DEC DE  ; DE=DE-1
300       LD A,D  ; DE=
310       OR E    ; 0?
320       JR NZ,LOOP1 ; если нет, то цикл
330  EI    ; разрешение прерываний
340  RET   ; возврат

📊 В строке 20 задается адрес сохранения звука, в строке 30 — длительность вводимого звука, в строке 70 — задержка между выборками сигнала.

🔇 Строки 180 — 240 обеспечивают шумопонижение.

📈 В регистре С сохраняется значение предыдущей выборки, а регистр В служит счетчиком одинаковых выборок.

▶️ Подпрограмма воспроизведения (способ 2)

Assembler Z80:
10   DI          ; запрет прерываний
20   LD HL,25000 ; HL=адрес сохраненного звука
30   LD DE,35000 ; DE=длительность звука
40        NOP    ; резерв
50        XOR A  ; A=цвет бордюра (0)
60  LOOP2 LD B,(HL) ; B=очередная длительность
70  LOOP3 OUT (254),A ; вывод A в порт 254
80        EXX     ; смена набора регистров
90        LD B,1  ; B=задержка
100       PAUS2 DJNZ PAUS2 ; задержка
110       EXX     ; смена набора регистров
120       CALL 124 ; компенсационная
130       BIT 0,(HL) ; задержка
140       DJNZ LOOP3 ; цикл
150       LD B,(HL) ; B=текущая длительность
160       DEC B   ; B=
170       INC B   ; 0?
180       JR Z,CONT ; если да, то перейти на CONT
190       XOR 16  ; инвертирование бита D4
200  CONT INC HL  ; HL=HL+1
210       DEC DE  ; DE=DE-1
220       LD B,A  ; сохранение A
230       LD A,D  ; DE=
240       OR E    ; 0?
250       LD A,B  ; восстановление A
260       JR NZ,LOOP2 ; если DE<>0, то цикл
270  EI    ; разрешение прерываний
280  RET   ; возврат

📊 В строке 20 задается адрес сохраненного звука, в строке 30 — его длительность, а в строке 90 — задержка между выборками (она же — скорость воспроизведения).

🔄 Байт резерва, как и раньше, предназначен для обратного воспроизведения. Для включения этого эффекта надо заменить операцию NOP в этом байте на ADD HL,DE, а INC HL в строке 200 на DEC HL.

⏱️ Строки 120 и 130 нужны только для временной синхронизации с подпрограммой записи звука (по адресу 124 находится команда RET).

📝 Всё сказанное относительно задержки между выборками и распределения памяти для подпрограмм, использующих первый способ записи, справедливо и для подпрограмм, использующих второй способ.

🎵 Особенностью подпрограмм второго типа является то, что длительность записываемого звука зависит не только от объёма выделенной памяти, но и от самого звука. Чем меньше перепадов уровней в записываемом звуке (иными словами — чем меньше его средняя частота), тем более продолжительный звук можно записать. Например, если записывать одну «тишину», то памяти хватит на несколько минут!

⚠️ Вы можете произвольно менять адрес и длину запоминаемого звука, но при этом необходимо быть предельно осторожным и не испортить жизненно важные области ОЗУ. Такие, как стек или системные переменные бейсика.

💾 Звук, записанный этими подпрограммами можно сохранить на ленте или диске с помощью обычных команд записи, а затем загружать по мере надобности.

🗜️ Сжатие звуковых данных

Так как даже не очень длинный звуковой фрагмент занимает в памяти много места, возникает естественное желание как-нибудь его сократить. Спешу Вас обрадовать — это возможно! Звуковые данные очень хорошо компрессируются по любому алгоритму. Для этого можно использовать даже экранные компрессоры.

💡 Неплохо было бы их немного модифицировать для такого использования, но, если Вы не сможете это сделать, дам несколько советов:
  1. Без изменения компрессора можно сжимать только те файлы, размер которых меньше или равен 6912 байтам (если какой-либо компрессор отказывается принимать файлы с длиной отличной от 6912 байт, то дополните их до данного размера нулями).
  2. Обычно компрессоры при распаковке картинки помещают её прямо в область экрана. Поэтому, придется данные перемещать оттуда в их «родной» адрес. Это можно сделать с помощью следующей подпрограммы:
Assembler Z80:
10   LD HL,16384 ;
20   LD DE,16385 ; очистка
30   LD BC,6911  ;
40   LD (HL),L   ; экрана
50   LDIR        ;
60   CALL DECOMP ; вызов декомпрессора
70   LD HL,16384 ; HL=откуда перемещать
80   LD DE,ADDR  ; DE=куда перемещать
90   LD BC,LENGTH ; BC=сколько перемещать
100  LDIR        ; перемещение данных
110  CALL 3435   ; вызов подпрограммы CLS
120  RET         ; возврат

⚡ Данные будут на экране такое короткое время, что Вы, скорее всего, этого и не заметите (при достаточно быстрой процедуре декомпрессии).

🔄 Программа на бейсике для загрузки и воспроизведения звука

Бейсик ZX:
10  CLEAR VAL "24899": PRINT "Wait please…"
20  FOR A=VAL "24900" TO VAL "24972": READ D: POKE A,D: NEXT A
30  CLS: PRINT "Press any key, then speak…": PAUSE NOT PI
40  CLS: RANDOMIZE USR VAL "24900"
50  CLS: PRINT "Press any key to replay…": PAUSE NOT PI
60  CLS: RANDOMIZE USR VAL "24934": GOTO VAL "30"
70  DATA 243, 33, 141, 97, 17, 114, 158, 6, 8, 203, 38, 219, 254, 203, 119, 40, 2, 203, 198, 14, 1, 13, 32, 253, 16, 239, 35, 27, 122, 179, 32, 231, 251, 201
80  DATA 243, 33, 141, 97, 17, 114, 158, 0, 62, 7, 6, 8, 230, 239, 203, 6, 48, 2, 246, 16, 211, 254, 14, 1, 13, 32, 253, 16, 239, 35, 27, 79, 122, 179, 121, 32, 229, 251, 201

🔄 Если Вы захотите включить воспроизведение «наоборот», вставьте в эту программу следующую строку:
Бейсик ZX:
45 POKE VAL "24941",VAL "25": POKE VAL "24963",VAL "43"

🚫 Для отключения этого эффекта удалите данную строку и перезапустите программу.

💾 Непривычная запись чисел в этой программе приводит к экономии памяти. Так, выражение «24900» занимает 11 байт, a «VAL "24900"» — только 8 («NOT PI» равняется нулю).

Реверберация 🔊

Говоря человеческим языком, реверберация — это эхо.

🔄 Программа, реализующая этот интересный эффект по существу является воссоединением обоих фрагментов первого типа из предыдущей главы. То есть, эта программа одновременно воспроизводит звук и записывает на его место новый.

🔁 Подпрограмма реверберации

Assembler Z80:
10   DI          ; запрет прерываний
20  LOOP1 LD HL,25000 ; HL=адрес буфера
30        LD DE,2560  ; DE=задержка эха
40  LOOP2 LD B,8      ; B=счетчик битов
50  LOOP3 XOR A       ; A=цвет бордюра (0)
60        RRC (HL)    ; скроллинг данных через флаг CY
70        JR NC,NOOUT ; если CY=0, то перейти на NOOUT
80        OR 16       ; установка бита D4 регистра A
90  NOOUT OUT (254),A ; вывод A в порт 254
100       RES 7,(HL)  ; сброс бита D7 в памяти
110       XOR A       ; A=0 (опрос всей клавиатуры)
120       IN A,(254)  ; ввод значения из порта 254
130       BIT 6,A     ; проверить бит магнитофона
140       JR Z,NOINP  ; если 0, то перейти на NOINP
150       SET 7,(HL)  ; установить бит D7 в памяти
160  NOINP CPL        ; клавиша
170       AND 31      ; нажата?
180       JR NZ,EXIT  ; если да, то перейти на EXIT
190       LD C,2      ; C=задержка
200  PAUSE DEC C      ; C=C-1
210            JR NZ,PAUSE ; если C<>0, то цикл
220       DJNZ LOOP3  ; продолжить цикл обработки байта
230       INC HL      ; HL=HL+1
240       DEC DE      ; DE=DE-1
250       LD A,D      ; DE=
260       OR E        ; 0?
270       JR NZ,LOOP2 ; если нет, то перейти на LOOP2
280       JR LOOP1    ; перейти на LOOP1
290  EXIT EI          ; разрешение прерываний
300       RET         ; возврат

⏯️ Эта подпрограмма будет работать до тех пор, пока Вы не нажмете какую-либо клавишу. А так как она очень чувствительна к подобного рода действиям, вызывать её лучше с помощью маленькой программки на бейсике:
Бейсик ZX:
10 FOR I=1 TO 10: NEXT I
20 RANDOMIZE USR A

📌 — где А — адрес подпрограммы ревербератора.

💡 Если Вы хотите, чтобы независимо от местоположения подпрограммы буфер находился сразу за ней, то введите следующие строки:
Assembler Z80:
20   LD HL,BUFFER
310  BUFFER NOP ; здесь располагается буфер

🎛️ В строке 190 содержится значение задержки между выборками сигнала. Меняя, его Вы можете настраивать соотношение «качество звука — объём памяти».

⏱️ Изменяя значение в строке 30 можно менять задержку эха.

⚠️ И последнее: если Вы будете вводить звук с выхода какого-либо устройства, то эхо будет звучать один раз. Если же — с помощью микрофона, то Вы можете установить количество повторений, регулируя громкость воспроизводимого звука и уровень записи магнитофона. Если хотя бы один из этих параметров будет ниже необходимого уровня, то эхо прозвучит один раз. Чем выше эти параметры, тем большее число повторений Вы услышите (их громкость будет, как в жизни от раза к разу уменьшаться).

🚨 Однако не следует увлекаться. Если Вы переусердствуете, то программа самовозбудится и все что она будет воспроизводить, впоследствии будет звучать на фоне ужасно противного шума. Если это произошло, выключите громкость или микрофон на несколько секунд, и все вернется на свои места.

🎶 Удачи в экспериментах со звуком на вашем ZX Spectrum! 👾