mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2024-12-14 20:20:30 +00:00
Merge branch 'mc-1.16.x' into mc-1.17.x
This commit is contained in:
commit
82a7edee12
14
build.gradle
14
build.gradle
@ -6,6 +6,7 @@ buildscript {
|
||||
}
|
||||
dependencies {
|
||||
classpath 'net.minecraftforge.gradle:ForgeGradle:5.1.24'
|
||||
classpath "org.spongepowered:mixingradle:0.7.+"
|
||||
classpath 'org.parchmentmc:librarian:1.+'
|
||||
}
|
||||
}
|
||||
@ -22,6 +23,7 @@ plugins {
|
||||
}
|
||||
|
||||
apply plugin: 'net.minecraftforge.gradle'
|
||||
apply plugin: "org.spongepowered.mixin"
|
||||
apply plugin: 'org.parchmentmc.librarian.forgegradle'
|
||||
|
||||
version = mod_version
|
||||
@ -71,6 +73,8 @@ minecraft {
|
||||
source sourceSets.main
|
||||
}
|
||||
}
|
||||
|
||||
arg "-mixin.config=computercraft.mixins.json"
|
||||
}
|
||||
|
||||
client {
|
||||
@ -127,6 +131,11 @@ minecraft {
|
||||
accessTransformer file('src/main/resources/META-INF/accesstransformer.cfg')
|
||||
accessTransformer file('src/testMod/resources/META-INF/accesstransformer.cfg')
|
||||
}
|
||||
|
||||
mixin {
|
||||
add sourceSets.main, 'computercraft.mixins.refmap.json'
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
@ -150,6 +159,7 @@ dependencies {
|
||||
checkstyle "com.puppycrawl.tools:checkstyle:8.45"
|
||||
|
||||
minecraft "net.minecraftforge:forge:${mc_version}-${forge_version}"
|
||||
annotationProcessor 'org.spongepowered:mixin:0.8.4:processor'
|
||||
|
||||
compileOnly fg.deobf("mezz.jei:jei-1.17.1:8.0.0.14:api")
|
||||
// compileOnly fg.deobf("commoble.morered:morered-1.16.5:2.1.1.0")
|
||||
@ -168,7 +178,7 @@ dependencies {
|
||||
testModImplementation sourceSets.main.output
|
||||
testModExtra 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.21'
|
||||
|
||||
cctJavadoc 'cc.tweaked:cct-javadoc:1.4.2'
|
||||
cctJavadoc 'cc.tweaked:cct-javadoc:1.4.4'
|
||||
}
|
||||
|
||||
// Compile tasks
|
||||
@ -208,6 +218,8 @@ jar {
|
||||
"Implementation-Version" : "${mod_version}",
|
||||
"Implementation-Vendor" : "SquidDev",
|
||||
"Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ")
|
||||
,
|
||||
"MixinConfigs" : "computercraft.mixins.json",
|
||||
])
|
||||
}
|
||||
|
||||
|
27
doc/events/speaker_audio_empty.md
Normal file
27
doc/events/speaker_audio_empty.md
Normal file
@ -0,0 +1,27 @@
|
||||
---
|
||||
module: [kind=event] speaker_audio_empty
|
||||
see: speaker.playAudio To play audio using the speaker
|
||||
---
|
||||
|
||||
## Return Values
|
||||
1. @{string}: The event name.
|
||||
2. @{string}: The name of the speaker which is available to play more audio.
|
||||
|
||||
|
||||
## Example
|
||||
This uses @{io.lines} to read audio data in blocks of 16KiB from "example_song.dfpwm", and then attempts to play it
|
||||
using @{speaker.playAudio}. If the speaker's buffer is full, it waits for an event and tries again.
|
||||
|
||||
```lua
|
||||
local dfpwm = require("cc.audio.dfpwm")
|
||||
local speaker = peripheral.find("speaker")
|
||||
|
||||
local decoder = dfpwm.make_decoder()
|
||||
for chunk in io.lines("data/example.dfpwm", 16 * 1024) do
|
||||
local buffer = decoder(chunk)
|
||||
|
||||
while not speaker.playAudio(buffer) do
|
||||
os.pullEvent("speaker_audio_empty")
|
||||
end
|
||||
end
|
||||
```
|
200
doc/guides/speaker_audio.md
Normal file
200
doc/guides/speaker_audio.md
Normal file
@ -0,0 +1,200 @@
|
||||
---
|
||||
module: [kind=guide] speaker_audio
|
||||
see: speaker.playAudio Play PCM audio using a speaker.
|
||||
see: cc.audio.dfpwm Provides utilities for encoding and decoding DFPWM files.
|
||||
---
|
||||
|
||||
# Playing audio with speakers
|
||||
CC: Tweaked's speaker peripheral provides a powerful way to play any audio you like with the @{speaker.playAudio}
|
||||
method. However, for people unfamiliar with digital audio, it's not the most intuitive thing to use. This guide provides
|
||||
an introduction to digital audio, demonstrates how to play music with CC: Tweaked's speakers, and then briefly discusses
|
||||
the more complex topic of audio processing.
|
||||
|
||||
## A short introduction to digital audio
|
||||
When sound is recorded it is captured as an analogue signal, effectively the electrical version of a sound
|
||||
wave. However, this signal is continuous, and so can't be used directly by a computer. Instead, we measure (or *sample*)
|
||||
the amplitude of the wave many times a second and then *quantise* that amplitude, rounding it to the nearest
|
||||
representable value.
|
||||
|
||||
This representation of sound - a long, uniformally sampled list of amplitudes is referred to as [Pulse-code
|
||||
Modulation][PCM] (PCM). PCM can be thought of as the "standard" audio format, as it's incredibly easy to work with. For
|
||||
instance, to mix two pieces of audio together, you can just samples from the two tracks together and take the average.
|
||||
|
||||
CC: Tweaked's speakers also work with PCM audio. It plays back 48,000 samples a second, where each sample is an integer
|
||||
between -128 and 127. This is more commonly referred to as 48kHz and an 8-bit resolution.
|
||||
|
||||
Let's now look at a quick example. We're going to generate a [Sine Wave] at 220Hz, which sounds like a low monotonous
|
||||
hum. First we wrap our speaker peripheral, and then we fill a table (also referred to as a *buffer*) with 128×1024
|
||||
samples - this is the maximum number of samples a speaker can accept in one go.
|
||||
|
||||
In order to fill this buffer, we need to do a little maths. We want to play 220 sine waves each second, where each sine
|
||||
wave completes a full oscillation in 2π "units". This means one seconds worth of audio is 2×π×220 "units" long. We then
|
||||
need to split this into 48k samples, basically meaning for each sample we move 2×π×220/48k "along" the sine curve.
|
||||
|
||||
```lua {data-peripheral=speaker}
|
||||
local speaker = peripheral.find("speaker")
|
||||
|
||||
local buffer = {}
|
||||
local t, dt = 0, 2 * math.pi * 220 / 48000
|
||||
for i = 1, 128 * 1024 do
|
||||
buffer[i] = math.floor(math.sin(t) * 127)
|
||||
t = (t + dt) % (math.pi * 2)
|
||||
end
|
||||
|
||||
speaker.playAudio(buffer)
|
||||
```
|
||||
|
||||
## Streaming audio
|
||||
You might notice that the above snippet only generates a short bit of audio - 2.7s seconds to be precise. While we could
|
||||
try increasing the number of loop iterations, we'll get an error when we try to play it through the speaker: the sound
|
||||
buffer is too large for it to handle.
|
||||
|
||||
Our 2.7 seconds of audio is stored in a table with over 130 _thousand_ elements. If we wanted to play a full minute of
|
||||
sine waves (and why wouldn't you?), you'd need a table with almost 3 _million_. Suddenly you find these numbers adding
|
||||
up very quickly, and these tables take up more and more memory.
|
||||
|
||||
Instead of building our entire song (well, sine wave) in one go, we can produce it in small batches, each of which get
|
||||
passed off to @{speaker.playAudio} when the time is right. This allows us to build a _stream_ of audio, where we read
|
||||
chunks of audio one at a time (either from a file or a tone generator like above), do some optional processing to each
|
||||
one, and then play them.
|
||||
|
||||
Let's adapt our example from above to do that instead.
|
||||
|
||||
```lua {data-peripheral=speaker}
|
||||
local speaker = peripheral.find("speaker")
|
||||
|
||||
local t, dt = 0, 2 * math.pi * 220 / 48000
|
||||
while true do
|
||||
local buffer = {}
|
||||
for i = 1, 16 * 1024 * 8 do
|
||||
buffer[i] = math.floor(math.sin(t) * 127)
|
||||
t = (t + dt) % (math.pi * 2)
|
||||
end
|
||||
|
||||
while not speaker.playAudio(buffer) do
|
||||
os.pullEvent("speaker_audio_empty")
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
It looks pretty similar to before, aside from we've wrapped the generation and playing code in a while loop, and added a
|
||||
rather odd loop with @{speaker.playAudio} and @{os.pullEvent}.
|
||||
|
||||
Let's talk about this loop, why do we need to keep calling @{speaker.playAudio}? Remember that what we're trying to do
|
||||
here is avoid keeping too much audio in memory at once. However, if we're generating audio quicker than the speakers can
|
||||
play it, we're not helping at all - all this audio is still hanging around waiting to be played!
|
||||
|
||||
In order to avoid this, the speaker rejects any new chunks of audio if its backlog is too large. When this happens,
|
||||
@{speaker.playAudio} returns false. Once enough audio has played, and the backlog has been reduced, a
|
||||
@{speaker_audio_empty} event is queued, and we can try to play our chunk once more.
|
||||
|
||||
## Storing audio
|
||||
PCM is a fantastic way of representing audio when we want to manipulate it, but it's not very efficient when we want to
|
||||
store it to disk. Compare the size of a WAV file (which uses PCM) to an equivalent MP3, it's often 5 times the size.
|
||||
Instead, we store audio in special formats (or *codecs*) and then convert them to PCM when we need to do processing on
|
||||
them.
|
||||
|
||||
Modern audio codecs use some incredibly impressive techniques to compress the audio as much as possible while preserving
|
||||
sound quality. However, due to CC: Tweaked's limited processing power, it's not really possible to use these from your
|
||||
computer. Instead, we need something much simpler.
|
||||
|
||||
DFPWM (Dynamic Filter Pulse Width Modulation) is the de facto standard audio format of the ComputerCraft (and
|
||||
OpenComputers) world. Originally popularised by the addon mod [Computronics], CC:T now has built-in support for it with
|
||||
the @{cc.audio.dfpwm} module. This allows you to read DFPWM files from disk, decode them to PCM, and then play them
|
||||
using the speaker.
|
||||
|
||||
Let's dive in with an example, and we'll explain things afterwards:
|
||||
|
||||
```lua {data-peripheral=speaker}
|
||||
local dfpwm = require("cc.audio.dfpwm")
|
||||
local speaker = peripheral.find("speaker")
|
||||
|
||||
local decoder = dfpwm.make_decoder()
|
||||
for chunk in io.lines("data/example.dfpwm", 16 * 1024) do
|
||||
local buffer = decoder(chunk)
|
||||
|
||||
while not speaker.playAudio(buffer) do
|
||||
os.pullEvent("speaker_audio_empty")
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Once again, we see the @{speaker.playAudio}/@{speaker_audio_empty} loop. However, the rest of the program is a little
|
||||
different.
|
||||
|
||||
First, we require the dfpwm module and call @{cc.audio.dfpwm.make_decoder} to construct a new decoder. This decoder
|
||||
accepts blocks of DFPWM data and converts it to a list of 8-bit amplitudes, which we can then play with our speaker.
|
||||
|
||||
As mentioned to above, @{speaker.playAudio} accepts at most 128×1024 samples in one go. DFPMW uses a single bit for each
|
||||
sample, which means we want to process our audio in chunks of 16×1024 bytes (16KiB). In order to do this, we use
|
||||
@{io.lines}, which provides a nice way to loop over chunks of a file. You can of course just use @{fs.open} and
|
||||
@{fs.BinaryReadHandle.read} if you prefer.
|
||||
|
||||
## Processing audio
|
||||
As mentioned near the beginning of this guide, PCM audio is pretty easy to work with as it's just a list of amplitudes.
|
||||
You can mix together samples from different streams by adding their amplitudes, change the rate of playback by removing
|
||||
samples, etc...
|
||||
|
||||
Let's put together a small demonstration here. We're going to add a small delay effect to the song above, so that you
|
||||
hear a faint echo about a second later.
|
||||
|
||||
In order to do this, we'll follow a format similar to the previous example, decoding the audio and then playing it.
|
||||
However, we'll also add some new logic between those two steps, which loops over every sample in our chunk of audio, and
|
||||
adds the sample from one second ago to it.
|
||||
|
||||
For this, we'll need to keep track of the last 48k samples - exactly one seconds worth of audio. We can do this using a
|
||||
[Ring Buffer], which helps makes things a little more efficient.
|
||||
|
||||
```lua
|
||||
local dfpwm = require("cc.audio.dfpwm")
|
||||
local speaker = peripheral.find("speaker")
|
||||
|
||||
-- Speakers play at 48kHz, so one second is 48k samples. We first fill our buffer
|
||||
-- with 0s, as there's nothing to echo at the start of the track!
|
||||
local samples_i, samples_n = 1, 48000
|
||||
local samples = {}
|
||||
for i = 1, samples_n do samples[i] = 0 end
|
||||
|
||||
local decoder = dfpwm.make_decoder()
|
||||
for chunk in io.lines("data/example.dfpwm", 16 * 1024) do
|
||||
local buffer = decoder(input)
|
||||
|
||||
for i = 1, #buffer do
|
||||
local original_value = buffer[i]
|
||||
|
||||
-- Replace this sample with its current amplitude plus the amplitude from one second ago.
|
||||
-- We scale both to ensure the resulting value is still between -128 and 127.
|
||||
buffer[i] = original_value * 0.7 + samples[samples_i] * 0.3
|
||||
|
||||
-- Now store the current sample, and move the "head" of our ring buffer forward one place.
|
||||
samples[samples_i] = original_value
|
||||
samples_i = samples_i + 1
|
||||
if samples_i > samples_n then samples_i = 1 end
|
||||
end
|
||||
|
||||
while not speaker.playAudio(buffer) do
|
||||
os.pullEvent("speaker_audio_empty")
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
:::note Confused?
|
||||
Don't worry if you don't understand this example. It's quite advanced, and does use some ideas that this guide doesn't
|
||||
cover. That said, don't be afraid to ask on [Discord] or [IRC] either!
|
||||
:::
|
||||
|
||||
It's worth noting that the examples of audio processing we've mentioned here are about manipulating the _amplitude_ of
|
||||
the wave. If you wanted to modify the _frequency_ (for instance, shifting the pitch), things get rather more complex.
|
||||
For this, you'd need to use the [Fast Fourier transform][FFT] to convert the stream of amplitudes to frequencies,
|
||||
process those, and then convert them back to amplitudes.
|
||||
|
||||
This is, I'm afraid, left as an exercise to the reader.
|
||||
|
||||
[Computronics]: https://github.com/Vexatos/Computronics/ "Computronics on GitHub"
|
||||
[FFT]: https://en.wikipedia.org/wiki/Fast_Fourier_transform "Fast Fourier transform - Wikipedia"
|
||||
[PCM]: https://en.wikipedia.org/wiki/Pulse-code_modulation "Pulse-code Modulation - Wikipedia"
|
||||
[Ring Buffer]: https://en.wikipedia.org/wiki/Circular_buffer "Circular buffer - Wikipedia"
|
||||
[Sine Wave]: https://en.wikipedia.org/wiki/Sine_wave "Sine wave - Wikipedia"
|
||||
|
||||
[Discord]: https://discord.computercraft.cc "The Minecraft Computer Mods Discord"
|
||||
[IRC]: http://webchat.esper.net/?channels=computercraft "IRC webchat on EsperNet"
|
@ -1,8 +1,9 @@
|
||||
; -*- mode: Lisp;-*-
|
||||
|
||||
(sources
|
||||
/doc/stub/
|
||||
/doc/events/
|
||||
/doc/guides/
|
||||
/doc/stub/
|
||||
/build/docs/luaJavadoc/
|
||||
/src/main/resources/*/computercraft/lua/bios.lua
|
||||
/src/main/resources/*/computercraft/lua/rom/
|
||||
@ -27,7 +28,8 @@
|
||||
(module-kinds
|
||||
(peripheral Peripherals)
|
||||
(generic_peripheral "Generic Peripherals")
|
||||
(event Events))
|
||||
(event Events)
|
||||
(guide Guides))
|
||||
|
||||
(library-path
|
||||
/doc/stub/
|
||||
|
@ -23,7 +23,7 @@ public interface GenericPeripheral extends GenericSource
|
||||
* Get the type of the exposed peripheral.
|
||||
*
|
||||
* Unlike normal {@link IPeripheral}s, {@link GenericPeripheral} do not have to have a type. By default, the
|
||||
* resulting peripheral uses the resource name of the wrapped {@link BlockEntity} (for instance {@literal minecraft:chest}).
|
||||
* resulting peripheral uses the resource name of the wrapped {@link BlockEntity} (for instance {@code minecraft:chest}).
|
||||
*
|
||||
* However, in some cases it may be more appropriate to specify a more readable name. Overriding this method allows
|
||||
* you to do so.
|
||||
|
@ -63,7 +63,7 @@ public final class PeripheralType
|
||||
* Create a new non-empty peripheral type with additional traits.
|
||||
*
|
||||
* @param type The name of the type.
|
||||
* @param additionalTypes Additional types, or "traits" of this peripheral. For instance, {@literal "inventory"}.
|
||||
* @param additionalTypes Additional types, or "traits" of this peripheral. For instance, {@code "inventory"}.
|
||||
* @return The constructed peripheral type.
|
||||
*/
|
||||
public static PeripheralType ofType( @Nonnull String type, Collection<String> additionalTypes )
|
||||
@ -76,7 +76,7 @@ public final class PeripheralType
|
||||
* Create a new non-empty peripheral type with additional traits.
|
||||
*
|
||||
* @param type The name of the type.
|
||||
* @param additionalTypes Additional types, or "traits" of this peripheral. For instance, {@literal "inventory"}.
|
||||
* @param additionalTypes Additional types, or "traits" of this peripheral. For instance, {@code "inventory"}.
|
||||
* @return The constructed peripheral type.
|
||||
*/
|
||||
public static PeripheralType ofType( @Nonnull String type, @Nonnull String... additionalTypes )
|
||||
@ -88,7 +88,7 @@ public final class PeripheralType
|
||||
/**
|
||||
* Create a new peripheral type with no primary type but additional traits.
|
||||
*
|
||||
* @param additionalTypes Additional types, or "traits" of this peripheral. For instance, {@literal "inventory"}.
|
||||
* @param additionalTypes Additional types, or "traits" of this peripheral. For instance, {@code "inventory"}.
|
||||
* @return The constructed peripheral type.
|
||||
*/
|
||||
public static PeripheralType ofAdditional( Collection<String> additionalTypes )
|
||||
@ -99,7 +99,7 @@ public final class PeripheralType
|
||||
/**
|
||||
* Create a new peripheral type with no primary type but additional traits.
|
||||
*
|
||||
* @param additionalTypes Additional types, or "traits" of this peripheral. For instance, {@literal "inventory"}.
|
||||
* @param additionalTypes Additional types, or "traits" of this peripheral. For instance, {@code "inventory"}.
|
||||
* @return The constructed peripheral type.
|
||||
*/
|
||||
public static PeripheralType ofAdditional( @Nonnull String... additionalTypes )
|
||||
@ -108,7 +108,7 @@ public final class PeripheralType
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of this peripheral type. This may be {@literal null}.
|
||||
* Get the name of this peripheral type. This may be {@code null}.
|
||||
*
|
||||
* @return The type of this peripheral.
|
||||
*/
|
||||
|
@ -6,6 +6,7 @@
|
||||
package dan200.computercraft.client;
|
||||
|
||||
import dan200.computercraft.ComputerCraft;
|
||||
import dan200.computercraft.client.sound.SpeakerManager;
|
||||
import dan200.computercraft.shared.peripheral.monitor.ClientMonitor;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.client.event.ClientPlayerNetworkEvent;
|
||||
@ -22,7 +23,7 @@ public class ClientHooks
|
||||
if( event.getWorld().isClientSide() )
|
||||
{
|
||||
ClientMonitor.destroyAll();
|
||||
SoundManager.reset();
|
||||
SpeakerManager.reset();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,84 +0,0 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.resources.sounds.AbstractSoundInstance;
|
||||
import net.minecraft.client.resources.sounds.SoundInstance;
|
||||
import net.minecraft.client.resources.sounds.TickableSoundInstance;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public class SoundManager
|
||||
{
|
||||
private static final Map<UUID, MoveableSound> sounds = new HashMap<>();
|
||||
|
||||
public static void playSound( UUID source, Vec3 position, ResourceLocation event, float volume, float pitch )
|
||||
{
|
||||
var soundManager = Minecraft.getInstance().getSoundManager();
|
||||
|
||||
MoveableSound oldSound = sounds.get( source );
|
||||
if( oldSound != null ) soundManager.stop( oldSound );
|
||||
|
||||
MoveableSound newSound = new MoveableSound( event, position, volume, pitch );
|
||||
sounds.put( source, newSound );
|
||||
soundManager.play( newSound );
|
||||
}
|
||||
|
||||
public static void stopSound( UUID source )
|
||||
{
|
||||
SoundInstance sound = sounds.remove( source );
|
||||
if( sound == null ) return;
|
||||
|
||||
Minecraft.getInstance().getSoundManager().stop( sound );
|
||||
}
|
||||
|
||||
public static void moveSound( UUID source, Vec3 position )
|
||||
{
|
||||
MoveableSound sound = sounds.get( source );
|
||||
if( sound != null ) sound.setPosition( position );
|
||||
}
|
||||
|
||||
public static void reset()
|
||||
{
|
||||
sounds.clear();
|
||||
}
|
||||
|
||||
private static class MoveableSound extends AbstractSoundInstance implements TickableSoundInstance
|
||||
{
|
||||
protected MoveableSound( ResourceLocation sound, Vec3 position, float volume, float pitch )
|
||||
{
|
||||
super( sound, SoundSource.RECORDS );
|
||||
setPosition( position );
|
||||
this.volume = volume;
|
||||
this.pitch = pitch;
|
||||
attenuation = Attenuation.LINEAR;
|
||||
}
|
||||
|
||||
void setPosition( Vec3 position )
|
||||
{
|
||||
x = (float) position.x();
|
||||
y = (float) position.y();
|
||||
z = (float) position.z();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStopped()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
125
src/main/java/dan200/computercraft/client/sound/DfpwmStream.java
Normal file
125
src/main/java/dan200/computercraft/client/sound/DfpwmStream.java
Normal file
@ -0,0 +1,125 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.sound;
|
||||
|
||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerPeripheral;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import net.minecraft.client.sounds.AudioStream;
|
||||
import org.lwjgl.BufferUtils;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Queue;
|
||||
|
||||
class DfpwmStream implements AudioStream
|
||||
{
|
||||
public static final int SAMPLE_RATE = SpeakerPeripheral.SAMPLE_RATE;
|
||||
|
||||
private static final int PREC = 10;
|
||||
private static final int LPF_STRENGTH = 140;
|
||||
|
||||
private static final AudioFormat MONO_16 = new AudioFormat( SAMPLE_RATE, 16, 1, true, false );
|
||||
|
||||
private final Queue<ByteBuffer> buffers = new ArrayDeque<>( 2 );
|
||||
|
||||
private int charge = 0; // q
|
||||
private int strength = 0; // s
|
||||
private int lowPassCharge;
|
||||
private boolean previousBit = false;
|
||||
|
||||
DfpwmStream()
|
||||
{
|
||||
}
|
||||
|
||||
void push( @Nonnull ByteBuf input )
|
||||
{
|
||||
int readable = input.readableBytes();
|
||||
ByteBuffer output = ByteBuffer.allocate( readable * 16 ).order( ByteOrder.nativeOrder() );
|
||||
|
||||
for( int i = 0; i < readable; i++ )
|
||||
{
|
||||
byte inputByte = input.readByte();
|
||||
for( int j = 0; j < 8; j++ )
|
||||
{
|
||||
boolean currentBit = (inputByte & 1) != 0;
|
||||
int target = currentBit ? 127 : -128;
|
||||
|
||||
// q' <- q + (s * (t - q) + 128)/256
|
||||
int nextCharge = charge + ((strength * (target - charge) + (1 << (PREC - 1))) >> PREC);
|
||||
if( nextCharge == charge && nextCharge != target ) nextCharge += currentBit ? 1 : -1;
|
||||
|
||||
int z = currentBit == previousBit ? (1 << PREC) - 1 : 0;
|
||||
|
||||
int nextStrength = strength;
|
||||
if( strength != z ) nextStrength += currentBit == previousBit ? 1 : -1;
|
||||
if( nextStrength < 2 << (PREC - 8) ) nextStrength = 2 << (PREC - 8);
|
||||
|
||||
// Apply antijerk
|
||||
int chargeWithAntijerk = currentBit == previousBit
|
||||
? nextCharge
|
||||
: nextCharge + charge + 1 >> 1;
|
||||
|
||||
// And low pass filter: outQ <- outQ + ((expectedOutput - outQ) x 140 / 256)
|
||||
lowPassCharge += ((chargeWithAntijerk - lowPassCharge) * LPF_STRENGTH + 0x80) >> 8;
|
||||
|
||||
charge = nextCharge;
|
||||
strength = nextStrength;
|
||||
previousBit = currentBit;
|
||||
|
||||
// Ideally we'd generate an 8-bit audio buffer. However, as we're piggybacking on top of another
|
||||
// audio stream (which uses 16 bit audio), we need to keep in the same format.
|
||||
output.putShort( (short) ((byte) (lowPassCharge & 0xFF) << 8) );
|
||||
|
||||
inputByte >>= 1;
|
||||
}
|
||||
}
|
||||
|
||||
output.flip();
|
||||
synchronized( this )
|
||||
{
|
||||
buffers.add( output );
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public AudioFormat getFormat()
|
||||
{
|
||||
return MONO_16;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public synchronized ByteBuffer read( int capacity )
|
||||
{
|
||||
ByteBuffer result = BufferUtils.createByteBuffer( capacity );
|
||||
while( result.hasRemaining() )
|
||||
{
|
||||
ByteBuffer head = buffers.peek();
|
||||
if( head == null ) break;
|
||||
|
||||
int toRead = Math.min( head.remaining(), result.remaining() );
|
||||
result.put( head.array(), head.position(), toRead ); // TODO: In 1.17 convert this to a ByteBuffer override
|
||||
head.position( head.position() + toRead );
|
||||
|
||||
if( head.hasRemaining() ) break;
|
||||
buffers.remove();
|
||||
}
|
||||
|
||||
result.flip();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException
|
||||
{
|
||||
buffers.clear();
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.sound;
|
||||
|
||||
import dan200.computercraft.ComputerCraft;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/**
|
||||
* An instance of a speaker, which is either playing a {@link DfpwmStream} stream or a normal sound.
|
||||
*/
|
||||
public class SpeakerInstance
|
||||
{
|
||||
public static final ResourceLocation DFPWM_STREAM = new ResourceLocation( ComputerCraft.MOD_ID, "speaker.dfpwm_fake_audio_should_not_be_played" );
|
||||
|
||||
private DfpwmStream currentStream;
|
||||
private SpeakerSound sound;
|
||||
|
||||
SpeakerInstance()
|
||||
{
|
||||
}
|
||||
|
||||
public synchronized void pushAudio( ByteBuf buffer )
|
||||
{
|
||||
if( currentStream == null ) currentStream = new DfpwmStream();
|
||||
currentStream.push( buffer );
|
||||
}
|
||||
|
||||
public void playAudio( Vec3 position, float volume )
|
||||
{
|
||||
var soundManager = Minecraft.getInstance().getSoundManager();
|
||||
|
||||
if( sound != null && sound.stream != currentStream )
|
||||
{
|
||||
soundManager.stop( sound );
|
||||
sound = null;
|
||||
}
|
||||
|
||||
if( sound != null && !soundManager.isActive( sound ) ) sound = null;
|
||||
|
||||
if( sound == null && currentStream != null )
|
||||
{
|
||||
sound = new SpeakerSound( DFPWM_STREAM, currentStream, position, volume, 1.0f );
|
||||
soundManager.play( sound );
|
||||
}
|
||||
}
|
||||
|
||||
public void playSound( Vec3 position, ResourceLocation location, float volume, float pitch )
|
||||
{
|
||||
var soundManager = Minecraft.getInstance().getSoundManager();
|
||||
currentStream = null;
|
||||
|
||||
if( sound != null )
|
||||
{
|
||||
soundManager.stop( sound );
|
||||
sound = null;
|
||||
}
|
||||
|
||||
sound = new SpeakerSound( location, null, position, volume, pitch );
|
||||
soundManager.play( sound );
|
||||
}
|
||||
|
||||
void setPosition( Vec3 position )
|
||||
{
|
||||
if( sound != null ) sound.setPosition( position );
|
||||
}
|
||||
|
||||
void stop()
|
||||
{
|
||||
if( sound != null ) Minecraft.getInstance().getSoundManager().stop( sound );
|
||||
|
||||
currentStream = null;
|
||||
sound = null;
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.sound;
|
||||
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.client.event.sound.PlayStreamingSourceEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Maps speakers source IDs to a {@link SpeakerInstance}.
|
||||
*/
|
||||
@Mod.EventBusSubscriber( Dist.CLIENT )
|
||||
public class SpeakerManager
|
||||
{
|
||||
private static final Map<UUID, SpeakerInstance> sounds = new ConcurrentHashMap<>();
|
||||
|
||||
@SubscribeEvent
|
||||
public static void playStreaming( PlayStreamingSourceEvent event )
|
||||
{
|
||||
if( !(event.getSound() instanceof SpeakerSound) ) return;
|
||||
SpeakerSound sound = (SpeakerSound) event.getSound();
|
||||
if( sound.stream == null ) return;
|
||||
|
||||
event.getSource().attachBufferStream( sound.stream );
|
||||
event.getSource().play();
|
||||
}
|
||||
|
||||
public static SpeakerInstance getSound( UUID source )
|
||||
{
|
||||
return sounds.computeIfAbsent( source, x -> new SpeakerInstance() );
|
||||
}
|
||||
|
||||
public static void stopSound( UUID source )
|
||||
{
|
||||
SpeakerInstance sound = sounds.remove( source );
|
||||
if( sound != null ) sound.stop();
|
||||
}
|
||||
|
||||
public static void moveSound( UUID source, Vec3 position )
|
||||
{
|
||||
SpeakerInstance sound = sounds.get( source );
|
||||
if( sound != null ) sound.setPosition( position );
|
||||
}
|
||||
|
||||
public static void reset()
|
||||
{
|
||||
sounds.clear();
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.sound;
|
||||
|
||||
import net.minecraft.client.resources.sounds.AbstractSoundInstance;
|
||||
import net.minecraft.client.resources.sounds.TickableSoundInstance;
|
||||
import net.minecraft.client.sounds.AudioStream;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class SpeakerSound extends AbstractSoundInstance implements TickableSoundInstance
|
||||
{
|
||||
DfpwmStream stream;
|
||||
|
||||
SpeakerSound( ResourceLocation sound, DfpwmStream stream, Vec3 position, float volume, float pitch )
|
||||
{
|
||||
super( sound, SoundSource.RECORDS );
|
||||
setPosition( position );
|
||||
this.stream = stream;
|
||||
this.volume = volume;
|
||||
this.pitch = pitch;
|
||||
attenuation = Attenuation.LINEAR;
|
||||
}
|
||||
|
||||
void setPosition( Vec3 position )
|
||||
{
|
||||
x = (float) position.x();
|
||||
y = (float) position.y();
|
||||
z = (float) position.z();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStopped()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick()
|
||||
{
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public AudioStream getStream()
|
||||
{
|
||||
return stream;
|
||||
}
|
||||
}
|
@ -15,7 +15,7 @@ import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketCl
|
||||
import static io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateServerExtensionHandshaker.MAX_WINDOW_SIZE;
|
||||
|
||||
/**
|
||||
* An alternative to {@link WebSocketClientCompressionHandler} which supports the {@literal client_no_context_takeover}
|
||||
* An alternative to {@link WebSocketClientCompressionHandler} which supports the {@code client_no_context_takeover}
|
||||
* extension. Makes CC <em>slightly</em> more flexible.
|
||||
*/
|
||||
@ChannelHandler.Sharable
|
||||
|
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.mixin;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import dan200.computercraft.shared.Registry;
|
||||
import dan200.computercraft.shared.peripheral.modem.wired.BlockCable;
|
||||
import dan200.computercraft.shared.peripheral.modem.wired.CableModemVariant;
|
||||
import dan200.computercraft.shared.peripheral.modem.wired.CableShapes;
|
||||
import dan200.computercraft.shared.util.WorldUtil;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.block.BlockModelShaper;
|
||||
import net.minecraft.client.renderer.block.BlockRenderDispatcher;
|
||||
import net.minecraft.client.renderer.block.ModelBlockRenderer;
|
||||
import net.minecraft.client.renderer.texture.OverlayTexture;
|
||||
import net.minecraft.client.resources.model.BakedModel;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.BlockAndTintGetter;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.HitResult;
|
||||
import net.minecraftforge.client.model.data.IModelData;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Provides custom block breaking progress for modems, so it only applies to the current part.
|
||||
*
|
||||
* @see BlockRenderDispatcher#renderBreakingTexture(BlockState, BlockPos, BlockAndTintGetter, PoseStack, VertexConsumer, IModelData)
|
||||
*/
|
||||
@Mixin( BlockRenderDispatcher.class )
|
||||
public class BlockRenderDispatcherMixin
|
||||
{
|
||||
@Shadow
|
||||
private final Random random;
|
||||
@Shadow
|
||||
private final BlockModelShaper blockModelShaper;
|
||||
@Shadow
|
||||
private final ModelBlockRenderer modelRenderer;
|
||||
|
||||
public BlockRenderDispatcherMixin( Random random, BlockModelShaper blockModelShaper, ModelBlockRenderer modelRenderer )
|
||||
{
|
||||
this.random = random;
|
||||
this.blockModelShaper = blockModelShaper;
|
||||
this.modelRenderer = modelRenderer;
|
||||
}
|
||||
|
||||
@Inject(
|
||||
method = "name=/^renderBreakingTexture/ desc=/IModelData;\\)V$/",
|
||||
at = @At( "HEAD" ),
|
||||
cancellable = true,
|
||||
require = 0 // This isn't critical functionality, so don't worry if we can't apply it.
|
||||
)
|
||||
public void renderBlockDamage(
|
||||
BlockState state, BlockPos pos, BlockAndTintGetter world, PoseStack pose, VertexConsumer buffers, IModelData modelData,
|
||||
CallbackInfo info
|
||||
)
|
||||
{
|
||||
// Only apply to cables which have both a cable and modem
|
||||
if( state.getBlock() != Registry.ModBlocks.CABLE.get()
|
||||
|| !state.getValue( BlockCable.CABLE )
|
||||
|| state.getValue( BlockCable.MODEM ) == CableModemVariant.None
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HitResult hit = Minecraft.getInstance().hitResult;
|
||||
if( hit == null || hit.getType() != HitResult.Type.BLOCK ) return;
|
||||
BlockPos hitPos = ((BlockHitResult) hit).getBlockPos();
|
||||
|
||||
if( !hitPos.equals( pos ) ) return;
|
||||
|
||||
info.cancel();
|
||||
BlockState newState = WorldUtil.isVecInside( CableShapes.getModemShape( state ), hit.getLocation().subtract( pos.getX(), pos.getY(), pos.getZ() ) )
|
||||
? state.getBlock().defaultBlockState().setValue( BlockCable.MODEM, state.getValue( BlockCable.MODEM ) )
|
||||
: state.setValue( BlockCable.MODEM, CableModemVariant.None );
|
||||
|
||||
BakedModel model = blockModelShaper.getBlockModel( newState );
|
||||
long seed = newState.getSeed( pos );
|
||||
modelRenderer.tesselateBlock( world, model, newState, pos, pose, buffers, true, random, seed, OverlayTexture.NO_OVERLAY, modelData );
|
||||
}
|
||||
}
|
@ -57,10 +57,11 @@ public final class NetworkHandler
|
||||
registerMainThread( 13, NetworkDirection.PLAY_TO_CLIENT, ComputerTerminalClientMessage.class, ComputerTerminalClientMessage::new );
|
||||
registerMainThread( 14, NetworkDirection.PLAY_TO_CLIENT, PlayRecordClientMessage.class, PlayRecordClientMessage::new );
|
||||
registerMainThread( 15, NetworkDirection.PLAY_TO_CLIENT, MonitorClientMessage.class, MonitorClientMessage::new );
|
||||
registerMainThread( 16, NetworkDirection.PLAY_TO_CLIENT, SpeakerPlayClientMessage.class, SpeakerPlayClientMessage::new );
|
||||
registerMainThread( 17, NetworkDirection.PLAY_TO_CLIENT, SpeakerStopClientMessage.class, SpeakerStopClientMessage::new );
|
||||
registerMainThread( 18, NetworkDirection.PLAY_TO_CLIENT, SpeakerMoveClientMessage.class, SpeakerMoveClientMessage::new );
|
||||
registerMainThread( 19, NetworkDirection.PLAY_TO_CLIENT, UploadResultMessage.class, UploadResultMessage::new );
|
||||
registerMainThread( 16, NetworkDirection.PLAY_TO_CLIENT, SpeakerAudioClientMessage.class, SpeakerAudioClientMessage::new );
|
||||
registerMainThread( 17, NetworkDirection.PLAY_TO_CLIENT, SpeakerMoveClientMessage.class, SpeakerMoveClientMessage::new );
|
||||
registerMainThread( 18, NetworkDirection.PLAY_TO_CLIENT, SpeakerPlayClientMessage.class, SpeakerPlayClientMessage::new );
|
||||
registerMainThread( 19, NetworkDirection.PLAY_TO_CLIENT, SpeakerStopClientMessage.class, SpeakerStopClientMessage::new );
|
||||
registerMainThread( 20, NetworkDirection.PLAY_TO_CLIENT, UploadResultMessage.class, UploadResultMessage::new );
|
||||
registerMainThread( 20, NetworkDirection.PLAY_TO_CLIENT, UpgradesLoadedMessage.class, UpgradesLoadedMessage::new );
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared.network.client;
|
||||
|
||||
import dan200.computercraft.client.sound.SpeakerManager;
|
||||
import dan200.computercraft.shared.network.NetworkMessage;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
import net.minecraftforge.fmllegacy.network.NetworkEvent;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Starts a sound on the client.
|
||||
*
|
||||
* Used by speakers to play sounds.
|
||||
*
|
||||
* @see dan200.computercraft.shared.peripheral.speaker.TileSpeaker
|
||||
*/
|
||||
public class SpeakerAudioClientMessage implements NetworkMessage
|
||||
{
|
||||
private final UUID source;
|
||||
private final Vec3 pos;
|
||||
private final ByteBuffer content;
|
||||
private final float volume;
|
||||
|
||||
public SpeakerAudioClientMessage( UUID source, Vec3 pos, float volume, ByteBuffer content )
|
||||
{
|
||||
this.source = source;
|
||||
this.pos = pos;
|
||||
this.content = content;
|
||||
this.volume = volume;
|
||||
}
|
||||
|
||||
public SpeakerAudioClientMessage( FriendlyByteBuf buf )
|
||||
{
|
||||
source = buf.readUUID();
|
||||
pos = new Vec3( buf.readDouble(), buf.readDouble(), buf.readDouble() );
|
||||
volume = buf.readFloat();
|
||||
|
||||
SpeakerManager.getSound( source ).pushAudio( buf );
|
||||
content = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toBytes( @Nonnull FriendlyByteBuf buf )
|
||||
{
|
||||
buf.writeUUID( source );
|
||||
buf.writeDouble( pos.x() );
|
||||
buf.writeDouble( pos.y() );
|
||||
buf.writeDouble( pos.z() );
|
||||
buf.writeFloat( volume );
|
||||
buf.writeBytes( content.duplicate() );
|
||||
}
|
||||
|
||||
@Override
|
||||
@OnlyIn( Dist.CLIENT )
|
||||
public void handle( NetworkEvent.Context context )
|
||||
{
|
||||
SpeakerManager.getSound( source ).playAudio( pos, volume );
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@
|
||||
*/
|
||||
package dan200.computercraft.shared.network.client;
|
||||
|
||||
import dan200.computercraft.client.SoundManager;
|
||||
import dan200.computercraft.client.sound.SpeakerManager;
|
||||
import dan200.computercraft.shared.network.NetworkMessage;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
@ -53,6 +53,6 @@ public class SpeakerMoveClientMessage implements NetworkMessage
|
||||
@OnlyIn( Dist.CLIENT )
|
||||
public void handle( NetworkEvent.Context context )
|
||||
{
|
||||
SoundManager.moveSound( source, pos );
|
||||
SpeakerManager.moveSound( source, pos );
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
*/
|
||||
package dan200.computercraft.shared.network.client;
|
||||
|
||||
import dan200.computercraft.client.SoundManager;
|
||||
import dan200.computercraft.client.sound.SpeakerManager;
|
||||
import dan200.computercraft.shared.network.NetworkMessage;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
@ -66,6 +66,6 @@ public class SpeakerPlayClientMessage implements NetworkMessage
|
||||
@OnlyIn( Dist.CLIENT )
|
||||
public void handle( NetworkEvent.Context context )
|
||||
{
|
||||
SoundManager.playSound( source, pos, sound, volume, pitch );
|
||||
SpeakerManager.getSound( source ).playSound( pos, sound, volume, pitch );
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
*/
|
||||
package dan200.computercraft.shared.network.client;
|
||||
|
||||
import dan200.computercraft.client.SoundManager;
|
||||
import dan200.computercraft.client.sound.SpeakerManager;
|
||||
import dan200.computercraft.shared.network.NetworkMessage;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
@ -46,6 +46,6 @@ public class SpeakerStopClientMessage implements NetworkMessage
|
||||
@OnlyIn( Dist.CLIENT )
|
||||
public void handle( NetworkEvent.Context context )
|
||||
{
|
||||
SoundManager.stopSound( source );
|
||||
SpeakerManager.stopSound( source );
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ import static dan200.computercraft.shared.Capabilities.CAPABILITY_PERIPHERAL;
|
||||
/**
|
||||
* This peripheral allows you to interact with command blocks.
|
||||
*
|
||||
* Command blocks are only wrapped as peripherals if the {@literal enable_command_block} option is true within the
|
||||
* Command blocks are only wrapped as peripherals if the {@code enable_command_block} option is true within the
|
||||
* config.
|
||||
*
|
||||
* This API is <em>not</em> the same as the {@link CommandAPI} API, which is exposed on command computers.
|
||||
|
@ -0,0 +1,117 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared.peripheral.speaker;
|
||||
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaTable;
|
||||
import net.minecraft.util.Mth;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static dan200.computercraft.shared.peripheral.speaker.SpeakerPeripheral.SAMPLE_RATE;
|
||||
|
||||
/**
|
||||
* Internal state of the DFPWM decoder and the state of playback.
|
||||
*/
|
||||
class DfpwmState
|
||||
{
|
||||
private static final long SECOND = TimeUnit.SECONDS.toNanos( 1 );
|
||||
|
||||
/**
|
||||
* The minimum size of the client's audio buffer. Once we have less than this on the client, we should send another
|
||||
* batch of audio.
|
||||
*/
|
||||
private static final long CLIENT_BUFFER = (long) (SECOND * 1.5);
|
||||
|
||||
private static final int PREC = 10;
|
||||
|
||||
private int charge = 0; // q
|
||||
private int strength = 0; // s
|
||||
private boolean previousBit = false;
|
||||
|
||||
private boolean unplayed = true;
|
||||
private long clientEndTime = System.nanoTime();
|
||||
private float pendingVolume = 1.0f;
|
||||
private ByteBuffer pendingAudio;
|
||||
|
||||
synchronized boolean pushBuffer( LuaTable<?, ?> table, int size, @Nonnull Optional<Double> volume ) throws LuaException
|
||||
{
|
||||
if( pendingAudio != null ) return false;
|
||||
|
||||
int outSize = size / 8;
|
||||
ByteBuffer buffer = ByteBuffer.allocate( outSize );
|
||||
|
||||
for( int i = 0; i < outSize; i++ )
|
||||
{
|
||||
int thisByte = 0;
|
||||
for( int j = 1; j <= 8; j++ )
|
||||
{
|
||||
int level = table.getInt( i * 8 + j );
|
||||
if( level < -128 || level > 127 )
|
||||
{
|
||||
throw new LuaException( "table item #" + (i * 8 + j) + " must be between -128 and 127" );
|
||||
}
|
||||
|
||||
boolean currentBit = level > charge || (level == charge && charge == 127);
|
||||
|
||||
// Identical to DfpwmStream. Not happy with this, but saves some inheritance.
|
||||
int target = currentBit ? 127 : -128;
|
||||
|
||||
// q' <- q + (s * (t - q) + 128)/256
|
||||
int nextCharge = charge + ((strength * (target - charge) + (1 << (PREC - 1))) >> PREC);
|
||||
if( nextCharge == charge && nextCharge != target ) nextCharge += currentBit ? 1 : -1;
|
||||
|
||||
int z = currentBit == previousBit ? (1 << PREC) - 1 : 0;
|
||||
|
||||
int nextStrength = strength;
|
||||
if( strength != z ) nextStrength += currentBit == previousBit ? 1 : -1;
|
||||
if( nextStrength < 2 << (PREC - 8) ) nextStrength = 2 << (PREC - 8);
|
||||
|
||||
charge = nextCharge;
|
||||
strength = nextStrength;
|
||||
previousBit = currentBit;
|
||||
|
||||
thisByte = (thisByte >> 1) + (currentBit ? 128 : 0);
|
||||
}
|
||||
|
||||
buffer.put( (byte) thisByte );
|
||||
}
|
||||
|
||||
buffer.flip();
|
||||
|
||||
pendingAudio = buffer;
|
||||
pendingVolume = Mth.clamp( volume.orElse( (double) pendingVolume ).floatValue(), 0.0f, 3.0f );
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean shouldSendPending( long now )
|
||||
{
|
||||
return pendingAudio != null && now >= clientEndTime - CLIENT_BUFFER;
|
||||
}
|
||||
|
||||
ByteBuffer pullPending( long now )
|
||||
{
|
||||
ByteBuffer audio = pendingAudio;
|
||||
pendingAudio = null;
|
||||
// Compute when we should consider sending the next packet.
|
||||
clientEndTime = Math.max( now, clientEndTime ) + (audio.remaining() * SECOND * 8 / SAMPLE_RATE);
|
||||
unplayed = false;
|
||||
return audio;
|
||||
}
|
||||
|
||||
boolean isPlaying()
|
||||
{
|
||||
return unplayed || clientEndTime >= System.nanoTime();
|
||||
}
|
||||
|
||||
float getVolume()
|
||||
{
|
||||
return pendingVolume;
|
||||
}
|
||||
}
|
@ -9,77 +9,176 @@ import dan200.computercraft.ComputerCraft;
|
||||
import dan200.computercraft.api.lua.ILuaContext;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.api.lua.LuaTable;
|
||||
import dan200.computercraft.api.peripheral.IComputerAccess;
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
import dan200.computercraft.shared.network.NetworkHandler;
|
||||
import dan200.computercraft.shared.network.client.SpeakerAudioClientMessage;
|
||||
import dan200.computercraft.shared.network.client.SpeakerMoveClientMessage;
|
||||
import dan200.computercraft.shared.network.client.SpeakerPlayClientMessage;
|
||||
import dan200.computercraft.shared.network.client.SpeakerStopClientMessage;
|
||||
import net.minecraft.ResourceLocationException;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.protocol.game.ClientboundCustomSoundPacket;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.state.properties.NoteBlockInstrument;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.*;
|
||||
|
||||
import static dan200.computercraft.api.lua.LuaValues.checkFinite;
|
||||
|
||||
/**
|
||||
* Speakers allow playing notes and other sounds.
|
||||
* The speaker peirpheral allow your computer to play notes and other sounds.
|
||||
*
|
||||
* The speaker can play three kinds of sound, in increasing orders of complexity:
|
||||
* - {@link #playNote} allows you to play noteblock note.
|
||||
* - {@link #playSound} plays any built-in Minecraft sound, such as block sounds or mob noises.
|
||||
* - {@link #playAudio} can play arbitrary audio.
|
||||
*
|
||||
* @cc.module speaker
|
||||
* @cc.since 1.80pr1
|
||||
*/
|
||||
public abstract class SpeakerPeripheral implements IPeripheral
|
||||
{
|
||||
private static final int MIN_TICKS_BETWEEN_SOUNDS = 1;
|
||||
/**
|
||||
* Number of samples/s in a dfpwm1a audio track.
|
||||
*/
|
||||
public static final int SAMPLE_RATE = 48000;
|
||||
|
||||
private final UUID source = UUID.randomUUID();
|
||||
private final Set<IComputerAccess> computers = Collections.newSetFromMap( new HashMap<>() );
|
||||
|
||||
private long clock = 0;
|
||||
private long lastPlayTime = 0;
|
||||
private final AtomicInteger notesThisTick = new AtomicInteger();
|
||||
|
||||
private long lastPositionTime;
|
||||
private Vec3 lastPosition;
|
||||
|
||||
private long lastPlayTime;
|
||||
|
||||
private final List<PendingSound> pendingNotes = new ArrayList<>();
|
||||
|
||||
private final Object lock = new Object();
|
||||
private boolean shouldStop;
|
||||
private PendingSound pendingSound = null;
|
||||
private DfpwmState dfpwmState;
|
||||
|
||||
public void update()
|
||||
{
|
||||
clock++;
|
||||
notesThisTick.set( 0 );
|
||||
|
||||
Vec3 pos = getPosition();
|
||||
Level level = getLevel();
|
||||
if( level == null ) return;
|
||||
MinecraftServer server = level.getServer();
|
||||
|
||||
synchronized( pendingNotes )
|
||||
{
|
||||
for( PendingSound sound : pendingNotes )
|
||||
{
|
||||
lastPlayTime = clock;
|
||||
server.getPlayerList().broadcast(
|
||||
null, pos.x, pos.y, pos.z, sound.volume * 16, level.dimension(),
|
||||
new ClientboundCustomSoundPacket( sound.location, SoundSource.RECORDS, pos, sound.volume, sound.pitch )
|
||||
);
|
||||
}
|
||||
pendingNotes.clear();
|
||||
}
|
||||
|
||||
// The audio dispatch logic here is pretty messy, which I'm not proud of. The general logic here is that we hold
|
||||
// the main "lock" when modifying the dfpwmState/pendingSound variables and no other time.
|
||||
// dfpwmState will only ever transition from having a buffer to not having a buffer on the main thread (so this
|
||||
// method), so we don't need to bother locking that.
|
||||
boolean shouldStop;
|
||||
PendingSound sound;
|
||||
DfpwmState dfpwmState;
|
||||
synchronized( lock )
|
||||
{
|
||||
sound = pendingSound;
|
||||
dfpwmState = this.dfpwmState;
|
||||
pendingSound = null;
|
||||
|
||||
shouldStop = this.shouldStop;
|
||||
if( shouldStop )
|
||||
{
|
||||
dfpwmState = this.dfpwmState = null;
|
||||
sound = null;
|
||||
this.shouldStop = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the speaker and nuke the position, so we don't update it again.
|
||||
if( shouldStop && lastPosition != null )
|
||||
{
|
||||
lastPosition = null;
|
||||
NetworkHandler.sendToAllPlayers( new SpeakerStopClientMessage( getSource() ) );
|
||||
return;
|
||||
}
|
||||
|
||||
long now = System.nanoTime();
|
||||
if( sound != null )
|
||||
{
|
||||
lastPlayTime = clock;
|
||||
NetworkHandler.sendToAllAround(
|
||||
new SpeakerPlayClientMessage( getSource(), pos, sound.location, sound.volume, sound.pitch ),
|
||||
level, pos, sound.volume * 16
|
||||
);
|
||||
syncedPosition( pos );
|
||||
}
|
||||
else if( dfpwmState != null && dfpwmState.shouldSendPending( now ) )
|
||||
{
|
||||
// If clients need to receive another batch of audio, send it and then notify computers our internal buffer is
|
||||
// free again.
|
||||
NetworkHandler.sendToAllTracking(
|
||||
new SpeakerAudioClientMessage( getSource(), pos, dfpwmState.getVolume(), dfpwmState.pullPending( now ) ),
|
||||
getLevel().getChunkAt( new BlockPos( pos ) )
|
||||
);
|
||||
syncedPosition( pos );
|
||||
|
||||
// And notify computers that we have space for more audio.
|
||||
for( IComputerAccess computer : computers )
|
||||
{
|
||||
computer.queueEvent( "speaker_audio_empty", computer.getAttachmentName() );
|
||||
}
|
||||
}
|
||||
|
||||
// Push position updates to any speakers which have ever played a note,
|
||||
// have moved by a non-trivial amount and haven't had a position update
|
||||
// in the last second.
|
||||
if( lastPlayTime > 0 && (clock - lastPositionTime) >= 20 )
|
||||
if( lastPosition != null && (clock - lastPositionTime) >= 20 )
|
||||
{
|
||||
Vec3 position = getPosition();
|
||||
if( lastPosition == null || lastPosition.distanceToSqr( position ) >= 0.1 )
|
||||
if( lastPosition.distanceToSqr( position ) >= 0.1 )
|
||||
{
|
||||
lastPosition = position;
|
||||
lastPositionTime = clock;
|
||||
NetworkHandler.sendToAllTracking(
|
||||
new SpeakerMoveClientMessage( getSource(), position ),
|
||||
getLevel().getChunkAt( new BlockPos( position ) )
|
||||
);
|
||||
syncedPosition( position );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public abstract Level getLevel();
|
||||
|
||||
@Nonnull
|
||||
public abstract Vec3 getPosition();
|
||||
|
||||
protected abstract UUID getSource();
|
||||
|
||||
public boolean madeSound( long ticks )
|
||||
@Nonnull
|
||||
public UUID getSource()
|
||||
{
|
||||
return clock - lastPlayTime <= ticks;
|
||||
return source;
|
||||
}
|
||||
|
||||
public boolean madeSound()
|
||||
{
|
||||
DfpwmState state = dfpwmState;
|
||||
return clock - lastPlayTime <= 20 || (state != null && state.isPlaying());
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@ -90,18 +189,81 @@ public abstract class SpeakerPeripheral implements IPeripheral
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays a sound through the speaker.
|
||||
* Plays a note block note through the speaker.
|
||||
*
|
||||
* This plays sounds similar to the {@code /playsound} command in Minecraft.
|
||||
* It takes the namespaced path of a sound (e.g. {@code minecraft:block.note_block.harp})
|
||||
* with an optional volume and speed multiplier, and plays it through the speaker.
|
||||
* This takes the name of a note to play, as well as optionally the volume
|
||||
* and pitch to play the note at.
|
||||
*
|
||||
* The pitch argument uses semitones as the unit. This directly maps to the
|
||||
* number of clicks on a note block. For reference, 0, 12, and 24 map to F#,
|
||||
* and 6 and 18 map to C.
|
||||
*
|
||||
* A maximum of 8 notes can be played in a single tick. If this limit is hit, this function will return
|
||||
* {@literal false}.
|
||||
*
|
||||
* ### Valid instruments
|
||||
* The speaker supports [all of Minecraft's noteblock instruments](https://minecraft.fandom.com/wiki/Note_Block#Instruments).
|
||||
* These are:
|
||||
*
|
||||
* {@code "harp"}, {@code "basedrum"}, {@code "snare"}, {@code "hat"}, {@code "bass"}, @code "flute"},
|
||||
* {@code "bell"}, {@code "guitar"}, {@code "chime"}, {@code "xylophone"}, {@code "iron_xylophone"},
|
||||
* {@code "cow_bell"}, {@code "didgeridoo"}, {@code "bit"}, {@code "banjo"} and {@code "pling"}.
|
||||
*
|
||||
* @param context The Lua context
|
||||
* @param instrumentA The instrument to use to play this note.
|
||||
* @param volumeA The volume to play the note at, from 0.0 to 3.0. Defaults to 1.0.
|
||||
* @param pitchA The pitch to play the note at in semitones, from 0 to 24. Defaults to 12.
|
||||
* @return Whether the note could be played as the limit was reached.
|
||||
* @throws LuaException If the instrument doesn't exist.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final boolean playNote( ILuaContext context, String instrumentA, Optional<Double> volumeA, Optional<Double> pitchA ) throws LuaException
|
||||
{
|
||||
float volume = (float) checkFinite( 1, volumeA.orElse( 1.0 ) );
|
||||
float pitch = (float) checkFinite( 2, pitchA.orElse( 1.0 ) );
|
||||
|
||||
NoteBlockInstrument instrument = null;
|
||||
for( NoteBlockInstrument testInstrument : NoteBlockInstrument.values() )
|
||||
{
|
||||
if( testInstrument.getSerializedName().equalsIgnoreCase( instrumentA ) )
|
||||
{
|
||||
instrument = testInstrument;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the note exists
|
||||
if( instrument == null ) throw new LuaException( "Invalid instrument, \"" + instrument + "\"!" );
|
||||
|
||||
synchronized( pendingNotes )
|
||||
{
|
||||
if( pendingNotes.size() >= ComputerCraft.maxNotesPerTick ) return false;
|
||||
pendingNotes.add( new PendingSound( instrument.getSoundEvent().getRegistryName(), volume, (float) Math.pow( 2.0, (pitch - 12.0) / 12.0 ) ) );
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays a Minecraft sound through the speaker.
|
||||
*
|
||||
* This takes the [name of a Minecraft sound](https://minecraft.fandom.com/wiki/Sounds.json), such as
|
||||
* {@code "minecraft:block.note_block.harp"}, as well as an optional volume and pitch.
|
||||
*
|
||||
* Only one sound can be played at once. This function will return {@literal false} if another sound was started
|
||||
* this tick, or if some {@link #playAudio audio} is still playing.
|
||||
*
|
||||
* @param context The Lua context
|
||||
* @param name The name of the sound to play.
|
||||
* @param volumeA The volume to play the sound at, from 0.0 to 3.0. Defaults to 1.0.
|
||||
* @param pitchA The speed to play the sound at, from 0.5 to 2.0. Defaults to 1.0.
|
||||
* @return Whether the sound could be played.
|
||||
* @throws LuaException If the sound name couldn't be decoded.
|
||||
* @throws LuaException If the sound name was invalid.
|
||||
* @cc.usage Play a creeper hiss with the speaker.
|
||||
*
|
||||
* <pre>{@code
|
||||
* local speaker = peripheral.find("speaker")
|
||||
* speaker.playSound("entity.creeper.primed")
|
||||
* }</pre>
|
||||
*/
|
||||
@LuaFunction
|
||||
public final boolean playSound( ILuaContext context, String name, Optional<Double> volumeA, Optional<Double> pitchA ) throws LuaException
|
||||
@ -119,89 +281,123 @@ public abstract class SpeakerPeripheral implements IPeripheral
|
||||
throw new LuaException( "Malformed sound name '" + name + "' " );
|
||||
}
|
||||
|
||||
return playSound( context, identifier, volume, pitch, false );
|
||||
synchronized( lock )
|
||||
{
|
||||
if( dfpwmState != null && dfpwmState.isPlaying() ) return false;
|
||||
dfpwmState = null;
|
||||
pendingSound = new PendingSound( identifier, volume, pitch );
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays a note block note through the speaker.
|
||||
* Attempt to stream some audio data to the speaker.
|
||||
*
|
||||
* This takes the name of a note to play, as well as optionally the volume
|
||||
* and pitch to play the note at.
|
||||
* This accepts a list of audio samples as amplitudes between -128 and 127. These are stored in an internal buffer
|
||||
* and played back at 48kHz. If this buffer is full, this function will return {@literal false}. You should wait for
|
||||
* a @{speaker_audio_empty} event before trying again.
|
||||
*
|
||||
* The pitch argument uses semitones as the unit. This directly maps to the
|
||||
* number of clicks on a note block. For reference, 0, 12, and 24 map to F#,
|
||||
* and 6 and 18 map to C.
|
||||
* :::note
|
||||
* The speaker only buffers a single call to {@link #playAudio} at once. This means if you try to play a small
|
||||
* number of samples, you'll have a lot of stutter. You should try to play as many samples in one call as possible
|
||||
* (up to 128×1024), as this reduces the chances of audio stuttering or halting, especially when the server or
|
||||
* computer is lagging.
|
||||
* :::
|
||||
*
|
||||
* @param context The Lua context
|
||||
* @param name The name of the note to play.
|
||||
* @param volumeA The volume to play the note at, from 0.0 to 3.0. Defaults to 1.0.
|
||||
* @param pitchA The pitch to play the note at in semitones, from 0 to 24. Defaults to 12.
|
||||
* @return Whether the note could be played.
|
||||
* @throws LuaException If the instrument doesn't exist.
|
||||
* {@literal @}{speaker_audio} provides a more complete guide in to using speakers
|
||||
*
|
||||
* @param context The Lua context.
|
||||
* @param audio The audio data to play.
|
||||
* @param volume The volume to play this audio at.
|
||||
* @return If there was room to accept this audio data.
|
||||
* @throws LuaException If the audio data is malformed.
|
||||
* @cc.tparam {number...} audio A list of amplitudes.
|
||||
* @cc.tparam [opt] number volume The volume to play this audio at. If not given, defaults to the previous volume
|
||||
* given to {@link #playAudio}.
|
||||
* @cc.since 1.100
|
||||
* @cc.usage Read an audio file, decode it using @{cc.audio.dfpwm}, and play it using the speaker.
|
||||
*
|
||||
* <pre>{@code
|
||||
* local dfpwm = require("cc.audio.dfpwm")
|
||||
* local speaker = peripheral.find("speaker")
|
||||
*
|
||||
* local decoder = dfpwm.make_decoder()
|
||||
* for chunk in io.lines("data/example.dfpwm", 16 * 1024) do
|
||||
* local buffer = decoder(chunk)
|
||||
*
|
||||
* while not speaker.playAudio(buffer) do
|
||||
* os.pullEvent("speaker_audio_empty")
|
||||
* end
|
||||
* end
|
||||
* }</pre>
|
||||
* @cc.see cc.audio.dfpwm Provides utilities for decoding DFPWM audio files into a format which can be played by
|
||||
* the speaker.
|
||||
* @cc.see speaker_audio For a more complete introduction to the {@link #playAudio} function.
|
||||
*/
|
||||
@LuaFunction( unsafe = true )
|
||||
public final boolean playAudio( ILuaContext context, LuaTable<?, ?> audio, Optional<Double> volume ) throws LuaException
|
||||
{
|
||||
checkFinite( 1, volume.orElse( 0.0 ) );
|
||||
|
||||
// TODO: Use ArgumentHelpers instead?
|
||||
int length = audio.length();
|
||||
if( length <= 0 ) throw new LuaException( "Cannot play empty audio" );
|
||||
if( length > 1024 * 16 * 8 ) throw new LuaException( "Audio data is too large" );
|
||||
|
||||
DfpwmState state;
|
||||
synchronized( lock )
|
||||
{
|
||||
if( dfpwmState == null || !dfpwmState.isPlaying() ) dfpwmState = new DfpwmState();
|
||||
state = dfpwmState;
|
||||
|
||||
pendingSound = null;
|
||||
}
|
||||
|
||||
return state.pushBuffer( audio, length, volume );
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all audio being played by this speaker.
|
||||
*
|
||||
* This clears any audio that {@link #playAudio} had queued and stops the latest sound played by {@link #playSound}.
|
||||
*
|
||||
* @cc.since 1.100
|
||||
*/
|
||||
@LuaFunction
|
||||
public final synchronized boolean playNote( ILuaContext context, String name, Optional<Double> volumeA, Optional<Double> pitchA ) throws LuaException
|
||||
public final void stop()
|
||||
{
|
||||
float volume = (float) checkFinite( 1, volumeA.orElse( 1.0 ) );
|
||||
float pitch = (float) checkFinite( 2, pitchA.orElse( 1.0 ) );
|
||||
|
||||
NoteBlockInstrument instrument = null;
|
||||
for( NoteBlockInstrument testInstrument : NoteBlockInstrument.values() )
|
||||
{
|
||||
if( testInstrument.getSerializedName().equalsIgnoreCase( name ) )
|
||||
{
|
||||
instrument = testInstrument;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the note exists
|
||||
if( instrument == null ) throw new LuaException( "Invalid instrument, \"" + name + "\"!" );
|
||||
|
||||
// If the resource location for note block notes changes, this method call will need to be updated
|
||||
boolean success = playSound( context, instrument.getSoundEvent().getRegistryName(), volume, (float) Math.pow( 2.0, (pitch - 12.0) / 12.0 ), true );
|
||||
if( success ) notesThisTick.incrementAndGet();
|
||||
return success;
|
||||
shouldStop = true;
|
||||
}
|
||||
|
||||
private synchronized boolean playSound( ILuaContext context, ResourceLocation name, float volume, float pitch, boolean isNote ) throws LuaException
|
||||
private void syncedPosition( Vec3 position )
|
||||
{
|
||||
if( clock - lastPlayTime < MIN_TICKS_BETWEEN_SOUNDS )
|
||||
lastPosition = position;
|
||||
lastPositionTime = clock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attach( @Nonnull IComputerAccess computer )
|
||||
{
|
||||
computers.add( computer );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void detach( @Nonnull IComputerAccess computer )
|
||||
{
|
||||
computers.remove( computer );
|
||||
}
|
||||
|
||||
private static final class PendingSound
|
||||
{
|
||||
final ResourceLocation location;
|
||||
final float volume;
|
||||
final float pitch;
|
||||
|
||||
private PendingSound( ResourceLocation location, float volume, float pitch )
|
||||
{
|
||||
// Rate limiting occurs when we've already played a sound within the last tick.
|
||||
if( !isNote ) return false;
|
||||
// Or we've played more notes than allowable within the current tick.
|
||||
if( clock - lastPlayTime != 0 || notesThisTick.get() >= ComputerCraft.maxNotesPerTick ) return false;
|
||||
this.location = location;
|
||||
this.volume = volume;
|
||||
this.pitch = pitch;
|
||||
}
|
||||
|
||||
Level world = getLevel();
|
||||
Vec3 pos = getPosition();
|
||||
|
||||
float actualVolume = Mth.clamp( volume, 0.0f, 3.0f );
|
||||
float range = actualVolume * 16;
|
||||
|
||||
context.issueMainThreadTask( () -> {
|
||||
MinecraftServer server = world.getServer();
|
||||
if( server == null ) return null;
|
||||
|
||||
if( isNote )
|
||||
{
|
||||
server.getPlayerList().broadcast(
|
||||
null, pos.x, pos.y, pos.z, range, world.dimension(),
|
||||
new ClientboundCustomSoundPacket( name, SoundSource.RECORDS, pos, actualVolume, pitch )
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
NetworkHandler.sendToAllAround(
|
||||
new SpeakerPlayClientMessage( getSource(), pos, name, actualVolume, pitch ),
|
||||
world, pos, range
|
||||
);
|
||||
}
|
||||
return null;
|
||||
} );
|
||||
|
||||
lastPlayTime = clock;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ import net.minecraftforge.common.util.LazyOptional;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.UUID;
|
||||
|
||||
import static dan200.computercraft.shared.Capabilities.CAPABILITY_PERIPHERAL;
|
||||
|
||||
@ -29,7 +28,6 @@ public class TileSpeaker extends TileGeneric
|
||||
{
|
||||
private final SpeakerPeripheral peripheral;
|
||||
private LazyOptional<IPeripheral> peripheralCap;
|
||||
private final UUID source = UUID.randomUUID();
|
||||
|
||||
public TileSpeaker( BlockEntityType<TileSpeaker> type, BlockPos pos, BlockState state )
|
||||
{
|
||||
@ -48,7 +46,7 @@ public class TileSpeaker extends TileGeneric
|
||||
super.setRemoved();
|
||||
if( level != null && !level.isClientSide )
|
||||
{
|
||||
NetworkHandler.sendToAllPlayers( new SpeakerStopClientMessage( source ) );
|
||||
NetworkHandler.sendToAllPlayers( new SpeakerStopClientMessage( peripheral.getSource() ) );
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,6 +85,7 @@ public class TileSpeaker extends TileGeneric
|
||||
return speaker.getLevel();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Vec3 getPosition()
|
||||
{
|
||||
@ -94,12 +93,6 @@ public class TileSpeaker extends TileGeneric
|
||||
return new Vec3( pos.getX(), pos.getY(), pos.getZ() );
|
||||
}
|
||||
|
||||
@Override
|
||||
protected UUID getSource()
|
||||
{
|
||||
return speaker.source;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals( @Nullable IPeripheral other )
|
||||
{
|
||||
|
@ -9,11 +9,9 @@ import dan200.computercraft.api.peripheral.IComputerAccess;
|
||||
import dan200.computercraft.shared.network.NetworkHandler;
|
||||
import dan200.computercraft.shared.network.client.SpeakerStopClientMessage;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraftforge.fml.LogicalSide;
|
||||
import net.minecraftforge.fmllegacy.LogicalSidedProvider;
|
||||
import net.minecraftforge.fmllegacy.server.ServerLifecycleHooks;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* A speaker peripheral which is used on an upgrade, and so is only attached to one computer.
|
||||
@ -22,21 +20,13 @@ public abstract class UpgradeSpeakerPeripheral extends SpeakerPeripheral
|
||||
{
|
||||
public static final String ADJECTIVE = "upgrade.computercraft.speaker.adjective";
|
||||
|
||||
private final UUID source = UUID.randomUUID();
|
||||
|
||||
@Override
|
||||
protected final UUID getSource()
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void detach( @Nonnull IComputerAccess computer )
|
||||
{
|
||||
// We could be in the process of shutting down the server, so we can't send packets in this case.
|
||||
MinecraftServer server = LogicalSidedProvider.INSTANCE.get( LogicalSide.SERVER );
|
||||
MinecraftServer server = ServerLifecycleHooks.getCurrentServer();
|
||||
if( server == null || server.isStopped() ) return;
|
||||
|
||||
NetworkHandler.sendToAllPlayers( new SpeakerStopClientMessage( source ) );
|
||||
NetworkHandler.sendToAllPlayers( new SpeakerStopClientMessage( getSource() ) );
|
||||
}
|
||||
}
|
||||
|
@ -42,6 +42,6 @@ public class PocketSpeaker extends AbstractPocketUpgrade
|
||||
}
|
||||
|
||||
speaker.update();
|
||||
access.setLight( speaker.madeSound( 20 ) ? 0x3320fc : -1 );
|
||||
access.setLight( speaker.madeSound() ? 0x3320fc : -1 );
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,8 @@ import dan200.computercraft.shared.peripheral.speaker.UpgradeSpeakerPeripheral;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class PocketSpeakerPeripheral extends UpgradeSpeakerPeripheral
|
||||
{
|
||||
private Level world = null;
|
||||
@ -27,6 +29,7 @@ public class PocketSpeakerPeripheral extends UpgradeSpeakerPeripheral
|
||||
return world;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Vec3 getPosition()
|
||||
{
|
||||
|
@ -43,6 +43,7 @@ public class TurtleSpeaker extends AbstractTurtleUpgrade
|
||||
return turtle.getLevel();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Vec3 getPosition()
|
||||
{
|
||||
|
10
src/main/resources/assets/computercraft/sounds.json
Normal file
10
src/main/resources/assets/computercraft/sounds.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"speaker.dfpwm_fake_audio_should_not_be_played": {
|
||||
"sounds": [
|
||||
{
|
||||
"name": "computercraft:empty",
|
||||
"stream": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
BIN
src/main/resources/assets/computercraft/sounds/empty.ogg
Normal file
BIN
src/main/resources/assets/computercraft/sounds/empty.ogg
Normal file
Binary file not shown.
13
src/main/resources/computercraft.mixins.json
Normal file
13
src/main/resources/computercraft.mixins.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"minVersion": "0.8",
|
||||
"required": true,
|
||||
"compatibilityLevel": "JAVA_8",
|
||||
"refmap": "computercraft.mixins.refmap.json",
|
||||
"package": "dan200.computercraft.mixin",
|
||||
"client": [
|
||||
"BlockRenderDispatcherMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
}
|
||||
}
|
@ -29,9 +29,9 @@ CHANNEL_REPEAT = 65533
|
||||
-- greater or equal to this limit wrap around to 0.
|
||||
MAX_ID_CHANNELS = 65500
|
||||
|
||||
local tReceivedMessages = {}
|
||||
local tHostnames = {}
|
||||
local nClearTimer
|
||||
local received_messages = {}
|
||||
local hostnames = {}
|
||||
local prune_received_timer
|
||||
|
||||
local function id_as_channel(id)
|
||||
return (id or os.getComputerID()) % MAX_ID_CHANNELS
|
||||
@ -115,10 +115,10 @@ be @{rednet.open|opened} before sending is possible.
|
||||
Assuming the target was in range and also had a correctly opened modem, it
|
||||
may then use @{rednet.receive} to collect the message.
|
||||
|
||||
@tparam number nRecipient The ID of the receiving computer.
|
||||
@tparam number recipient The ID of the receiving computer.
|
||||
@param message The message to send. This should not contain coroutines or
|
||||
functions, as they will be converted to @{nil}.
|
||||
@tparam[opt] string sProtocol The "protocol" to send this message under. When
|
||||
@tparam[opt] string protocol The "protocol" to send this message under. When
|
||||
using @{rednet.receive} one can filter to only receive messages sent under a
|
||||
particular protocol.
|
||||
@treturn boolean If this message was successfully sent (i.e. if rednet is
|
||||
@ -131,41 +131,41 @@ actually _received_.
|
||||
|
||||
rednet.send(2, "Hello from rednet!")
|
||||
]]
|
||||
function send(nRecipient, message, sProtocol)
|
||||
expect(1, nRecipient, "number")
|
||||
expect(3, sProtocol, "string", "nil")
|
||||
function send(recipient, message, protocol)
|
||||
expect(1, recipient, "number")
|
||||
expect(3, protocol, "string", "nil")
|
||||
-- Generate a (probably) unique message ID
|
||||
-- We could do other things to guarantee uniqueness, but we really don't need to
|
||||
-- Store it to ensure we don't get our own messages back
|
||||
local nMessageID = math.random(1, 2147483647)
|
||||
tReceivedMessages[nMessageID] = os.clock() + 9.5
|
||||
if not nClearTimer then nClearTimer = os.startTimer(10) end
|
||||
local message_id = math.random(1, 2147483647)
|
||||
received_messages[message_id] = os.clock() + 9.5
|
||||
if not prune_received_timer then prune_received_timer = os.startTimer(10) end
|
||||
|
||||
-- Create the message
|
||||
local nReplyChannel = id_as_channel()
|
||||
local tMessage = {
|
||||
nMessageID = nMessageID,
|
||||
nRecipient = nRecipient,
|
||||
local reply_channel = id_as_channel()
|
||||
local message_wrapper = {
|
||||
nMessageID = message_id,
|
||||
nRecipient = recipient,
|
||||
nSender = os.getComputerID(),
|
||||
message = message,
|
||||
sProtocol = sProtocol,
|
||||
sProtocol = protocol,
|
||||
}
|
||||
|
||||
local sent = false
|
||||
if nRecipient == os.getComputerID() then
|
||||
if recipient == os.getComputerID() then
|
||||
-- Loopback to ourselves
|
||||
os.queueEvent("rednet_message", os.getComputerID(), message, sProtocol)
|
||||
os.queueEvent("rednet_message", os.getComputerID(), message_wrapper, protocol)
|
||||
sent = true
|
||||
else
|
||||
-- Send on all open modems, to the target and to repeaters
|
||||
if nRecipient ~= CHANNEL_BROADCAST then
|
||||
nRecipient = id_as_channel(nRecipient)
|
||||
if recipient ~= CHANNEL_BROADCAST then
|
||||
recipient = id_as_channel(recipient)
|
||||
end
|
||||
|
||||
for _, sModem in ipairs(peripheral.getNames()) do
|
||||
if isOpen(sModem) then
|
||||
peripheral.call(sModem, "transmit", nRecipient, nReplyChannel, tMessage)
|
||||
peripheral.call(sModem, "transmit", CHANNEL_REPEAT, nReplyChannel, tMessage)
|
||||
for _, modem in ipairs(peripheral.getNames()) do
|
||||
if isOpen(modem) then
|
||||
peripheral.call(modem, "transmit", recipient, reply_channel, message_wrapper)
|
||||
peripheral.call(modem, "transmit", CHANNEL_REPEAT, reply_channel, message_wrapper)
|
||||
sent = true
|
||||
end
|
||||
end
|
||||
@ -179,23 +179,23 @@ end
|
||||
--
|
||||
-- @param message The message to send. This should not contain coroutines or
|
||||
-- functions, as they will be converted to @{nil}.
|
||||
-- @tparam[opt] string sProtocol The "protocol" to send this message under. When
|
||||
-- @tparam[opt] string protocol The "protocol" to send this message under. When
|
||||
-- using @{rednet.receive} one can filter to only receive messages sent under a
|
||||
-- particular protocol.
|
||||
-- @see rednet.receive
|
||||
-- @changed 1.6 Added protocol parameter.
|
||||
function broadcast(message, sProtocol)
|
||||
expect(2, sProtocol, "string", "nil")
|
||||
send(CHANNEL_BROADCAST, message, sProtocol)
|
||||
function broadcast(message, protocol)
|
||||
expect(2, protocol, "string", "nil")
|
||||
send(CHANNEL_BROADCAST, message, protocol)
|
||||
end
|
||||
|
||||
--[[- Wait for a rednet message to be received, or until `nTimeout` seconds have
|
||||
elapsed.
|
||||
|
||||
@tparam[opt] string sProtocolFilter The protocol the received message must be
|
||||
@tparam[opt] string protocol_filter The protocol the received message must be
|
||||
sent with. If specified, any messages not sent under this protocol will be
|
||||
discarded.
|
||||
@tparam[opt] number nTimeout The number of seconds to wait if no message is
|
||||
@tparam[opt] number timeout The number of seconds to wait if no message is
|
||||
received.
|
||||
@treturn[1] number The computer which sent this message
|
||||
@return[1] The received message
|
||||
@ -227,34 +227,34 @@ received.
|
||||
|
||||
print(message)
|
||||
]]
|
||||
function receive(sProtocolFilter, nTimeout)
|
||||
function receive(protocol_filter, timeout)
|
||||
-- The parameters used to be ( nTimeout ), detect this case for backwards compatibility
|
||||
if type(sProtocolFilter) == "number" and nTimeout == nil then
|
||||
sProtocolFilter, nTimeout = nil, sProtocolFilter
|
||||
if type(protocol_filter) == "number" and timeout == nil then
|
||||
protocol_filter, timeout = nil, protocol_filter
|
||||
end
|
||||
expect(1, sProtocolFilter, "string", "nil")
|
||||
expect(2, nTimeout, "number", "nil")
|
||||
expect(1, protocol_filter, "string", "nil")
|
||||
expect(2, timeout, "number", "nil")
|
||||
|
||||
-- Start the timer
|
||||
local timer = nil
|
||||
local sFilter = nil
|
||||
if nTimeout then
|
||||
timer = os.startTimer(nTimeout)
|
||||
sFilter = nil
|
||||
local event_filter = nil
|
||||
if timeout then
|
||||
timer = os.startTimer(timeout)
|
||||
event_filter = nil
|
||||
else
|
||||
sFilter = "rednet_message"
|
||||
event_filter = "rednet_message"
|
||||
end
|
||||
|
||||
-- Wait for events
|
||||
while true do
|
||||
local sEvent, p1, p2, p3 = os.pullEvent(sFilter)
|
||||
if sEvent == "rednet_message" then
|
||||
local event, p1, p2, p3 = os.pullEvent(event_filter)
|
||||
if event == "rednet_message" then
|
||||
-- Return the first matching rednet_message
|
||||
local nSenderID, message, sProtocol = p1, p2, p3
|
||||
if sProtocolFilter == nil or sProtocol == sProtocolFilter then
|
||||
return nSenderID, message, sProtocol
|
||||
local sender_id, message, protocol = p1, p2, p3
|
||||
if protocol_filter == nil or protocol == protocol_filter then
|
||||
return sender_id, message, protocol
|
||||
end
|
||||
elseif sEvent == "timer" then
|
||||
elseif event == "timer" then
|
||||
-- Return nil if we timeout
|
||||
if p1 == timer then
|
||||
return nil
|
||||
@ -276,34 +276,34 @@ end
|
||||
-- "registering" themselves before doing so (eg while offline or part of a
|
||||
-- different network).
|
||||
--
|
||||
-- @tparam string sProtocol The protocol this computer provides.
|
||||
-- @tparam string sHostname The name this protocol exposes for the given protocol.
|
||||
-- @tparam string protocol The protocol this computer provides.
|
||||
-- @tparam string hostname The name this protocol exposes for the given protocol.
|
||||
-- @throws If trying to register a hostname which is reserved, or currently in use.
|
||||
-- @see rednet.unhost
|
||||
-- @see rednet.lookup
|
||||
-- @since 1.6
|
||||
function host(sProtocol, sHostname)
|
||||
expect(1, sProtocol, "string")
|
||||
expect(2, sHostname, "string")
|
||||
if sHostname == "localhost" then
|
||||
function host(protocol, hostname)
|
||||
expect(1, protocol, "string")
|
||||
expect(2, hostname, "string")
|
||||
if hostname == "localhost" then
|
||||
error("Reserved hostname", 2)
|
||||
end
|
||||
if tHostnames[sProtocol] ~= sHostname then
|
||||
if lookup(sProtocol, sHostname) ~= nil then
|
||||
if hostnames[protocol] ~= hostname then
|
||||
if lookup(protocol, hostname) ~= nil then
|
||||
error("Hostname in use", 2)
|
||||
end
|
||||
tHostnames[sProtocol] = sHostname
|
||||
hostnames[protocol] = hostname
|
||||
end
|
||||
end
|
||||
|
||||
--- Stop @{rednet.host|hosting} a specific protocol, meaning it will no longer
|
||||
-- respond to @{rednet.lookup} requests.
|
||||
--
|
||||
-- @tparam string sProtocol The protocol to unregister your self from.
|
||||
-- @tparam string protocol The protocol to unregister your self from.
|
||||
-- @since 1.6
|
||||
function unhost(sProtocol)
|
||||
expect(1, sProtocol, "string")
|
||||
tHostnames[sProtocol] = nil
|
||||
function unhost(protocol)
|
||||
expect(1, protocol, "string")
|
||||
hostnames[protocol] = nil
|
||||
end
|
||||
|
||||
--- Search the local rednet network for systems @{rednet.host|hosting} the
|
||||
@ -313,36 +313,36 @@ end
|
||||
-- If a hostname is specified, only one ID will be returned (assuming an exact
|
||||
-- match is found).
|
||||
--
|
||||
-- @tparam string sProtocol The protocol to search for.
|
||||
-- @tparam[opt] string sHostname The hostname to search for.
|
||||
-- @tparam string protocol The protocol to search for.
|
||||
-- @tparam[opt] string hostname The hostname to search for.
|
||||
--
|
||||
-- @treturn[1] { number }|nil A list of computer IDs hosting the given
|
||||
-- protocol, or @{nil} if none exist.
|
||||
-- @treturn[2] number|nil The computer ID with the provided hostname and protocol,
|
||||
-- or @{nil} if none exists.
|
||||
-- @since 1.6
|
||||
function lookup(sProtocol, sHostname)
|
||||
expect(1, sProtocol, "string")
|
||||
expect(2, sHostname, "string", "nil")
|
||||
function lookup(protocol, hostname)
|
||||
expect(1, protocol, "string")
|
||||
expect(2, hostname, "string", "nil")
|
||||
|
||||
-- Build list of host IDs
|
||||
local tResults = nil
|
||||
if sHostname == nil then
|
||||
tResults = {}
|
||||
local results = nil
|
||||
if hostname == nil then
|
||||
results = {}
|
||||
end
|
||||
|
||||
-- Check localhost first
|
||||
if tHostnames[sProtocol] then
|
||||
if sHostname == nil then
|
||||
table.insert(tResults, os.getComputerID())
|
||||
elseif sHostname == "localhost" or sHostname == tHostnames[sProtocol] then
|
||||
if hostnames[protocol] then
|
||||
if hostname == nil then
|
||||
table.insert(results, os.getComputerID())
|
||||
elseif hostname == "localhost" or hostname == hostnames[protocol] then
|
||||
return os.getComputerID()
|
||||
end
|
||||
end
|
||||
|
||||
if not isOpen() then
|
||||
if tResults then
|
||||
return table.unpack(tResults)
|
||||
if results then
|
||||
return table.unpack(results)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
@ -350,8 +350,8 @@ function lookup(sProtocol, sHostname)
|
||||
-- Broadcast a lookup packet
|
||||
broadcast({
|
||||
sType = "lookup",
|
||||
sProtocol = sProtocol,
|
||||
sHostname = sHostname,
|
||||
sProtocol = protocol,
|
||||
sHostname = hostname,
|
||||
}, "dns")
|
||||
|
||||
-- Start a timer
|
||||
@ -362,30 +362,28 @@ function lookup(sProtocol, sHostname)
|
||||
local event, p1, p2, p3 = os.pullEvent()
|
||||
if event == "rednet_message" then
|
||||
-- Got a rednet message, check if it's the response to our request
|
||||
local nSenderID, tMessage, sMessageProtocol = p1, p2, p3
|
||||
if sMessageProtocol == "dns" and type(tMessage) == "table" and tMessage.sType == "lookup response" then
|
||||
if tMessage.sProtocol == sProtocol then
|
||||
if sHostname == nil then
|
||||
table.insert(tResults, nSenderID)
|
||||
elseif tMessage.sHostname == sHostname then
|
||||
return nSenderID
|
||||
local sender_id, message, message_protocol = p1, p2, p3
|
||||
if message_protocol == "dns" and type(message) == "table" and message.sType == "lookup response" then
|
||||
if message.sProtocol == protocol then
|
||||
if hostname == nil then
|
||||
table.insert(results, sender_id)
|
||||
elseif message.sHostname == hostname then
|
||||
return sender_id
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
elseif event == "timer" and p1 == timer then
|
||||
-- Got a timer event, check it's the end of our timeout
|
||||
if p1 == timer then
|
||||
break
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
if tResults then
|
||||
return table.unpack(tResults)
|
||||
if results then
|
||||
return table.unpack(results)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local bRunning = false
|
||||
local started = false
|
||||
|
||||
--- Listen for modem messages and converts them into rednet messages, which may
|
||||
-- then be @{receive|received}.
|
||||
@ -393,51 +391,51 @@ local bRunning = false
|
||||
-- This is automatically started in the background on computer startup, and
|
||||
-- should not be called manually.
|
||||
function run()
|
||||
if bRunning then
|
||||
if started then
|
||||
error("rednet is already running", 2)
|
||||
end
|
||||
bRunning = true
|
||||
started = true
|
||||
|
||||
while bRunning do
|
||||
local sEvent, p1, p2, p3, p4 = os.pullEventRaw()
|
||||
if sEvent == "modem_message" then
|
||||
while true do
|
||||
local event, p1, p2, p3, p4 = os.pullEventRaw()
|
||||
if event == "modem_message" then
|
||||
-- Got a modem message, process it and add it to the rednet event queue
|
||||
local sModem, nChannel, nReplyChannel, tMessage = p1, p2, p3, p4
|
||||
if nChannel == id_as_channel() or nChannel == CHANNEL_BROADCAST then
|
||||
if type(tMessage) == "table" and type(tMessage.nMessageID) == "number"
|
||||
and tMessage.nMessageID == tMessage.nMessageID and not tReceivedMessages[tMessage.nMessageID]
|
||||
and ((tMessage.nRecipient and tMessage.nRecipient == os.getComputerID()) or nChannel == CHANNEL_BROADCAST)
|
||||
and isOpen(sModem)
|
||||
local modem, channel, reply_channel, message = p1, p2, p3, p4
|
||||
if channel == id_as_channel() or channel == CHANNEL_BROADCAST then
|
||||
if type(message) == "table" and type(message.nMessageID) == "number"
|
||||
and message.nMessageID == message.nMessageID and not received_messages[message.nMessageID]
|
||||
and ((message.nRecipient and message.nRecipient == os.getComputerID()) or channel == CHANNEL_BROADCAST)
|
||||
and isOpen(modem)
|
||||
then
|
||||
tReceivedMessages[tMessage.nMessageID] = os.clock() + 9.5
|
||||
if not nClearTimer then nClearTimer = os.startTimer(10) end
|
||||
os.queueEvent("rednet_message", tMessage.nSender or nReplyChannel, tMessage.message, tMessage.sProtocol)
|
||||
received_messages[message.nMessageID] = os.clock() + 9.5
|
||||
if not prune_received_timer then prune_received_timer = os.startTimer(10) end
|
||||
os.queueEvent("rednet_message", message.nSender or reply_channel, message.message, message.sProtocol)
|
||||
end
|
||||
end
|
||||
|
||||
elseif sEvent == "rednet_message" then
|
||||
elseif event == "rednet_message" then
|
||||
-- Got a rednet message (queued from above), respond to dns lookup
|
||||
local nSenderID, tMessage, sProtocol = p1, p2, p3
|
||||
if sProtocol == "dns" and type(tMessage) == "table" and tMessage.sType == "lookup" then
|
||||
local sHostname = tHostnames[tMessage.sProtocol]
|
||||
if sHostname ~= nil and (tMessage.sHostname == nil or tMessage.sHostname == sHostname) then
|
||||
rednet.send(nSenderID, {
|
||||
local sender, message, protocol = p1, p2, p3
|
||||
if protocol == "dns" and type(message) == "table" and message.sType == "lookup" then
|
||||
local hostname = hostnames[message.sProtocol]
|
||||
if hostname ~= nil and (message.sHostname == nil or message.sHostname == hostname) then
|
||||
send(sender, {
|
||||
sType = "lookup response",
|
||||
sHostname = sHostname,
|
||||
sProtocol = tMessage.sProtocol,
|
||||
sHostname = hostname,
|
||||
sProtocol = message.sProtocol,
|
||||
}, "dns")
|
||||
end
|
||||
end
|
||||
|
||||
elseif sEvent == "timer" and p1 == nClearTimer then
|
||||
-- Got a timer event, use it to clear the event queue
|
||||
nClearTimer = nil
|
||||
local nNow, bHasMore = os.clock(), nil
|
||||
for nMessageID, nDeadline in pairs(tReceivedMessages) do
|
||||
if nDeadline <= nNow then tReceivedMessages[nMessageID] = nil
|
||||
else bHasMore = true end
|
||||
elseif event == "timer" and p1 == prune_received_timer then
|
||||
-- Got a timer event, use it to prune the set of received messages
|
||||
prune_received_timer = nil
|
||||
local now, has_more = os.clock(), nil
|
||||
for message_id, deadline in pairs(received_messages) do
|
||||
if deadline <= now then received_messages[message_id] = nil
|
||||
else has_more = true end
|
||||
end
|
||||
nClearTimer = bHasMore and os.startTimer(10)
|
||||
prune_received_timer = has_more and os.startTimer(10)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -59,7 +59,8 @@ end
|
||||
-- @treturn Redirect The current terminal redirect
|
||||
-- @since 1.6
|
||||
-- @usage
|
||||
-- Create a new @{window} which draws to the current redirect target
|
||||
-- Create a new @{window} which draws to the current redirect target.
|
||||
--
|
||||
-- window.create(term.current(), 1, 1, 10, 10)
|
||||
term.current = function()
|
||||
return redirectTarget
|
||||
|
@ -0,0 +1,5 @@
|
||||
The speaker program plays audio files using speakers attached to this computer.
|
||||
|
||||
## Examples:
|
||||
- `speaker play example.dfpwm left` plays the "example.dfpwm" audio file using the speaker on the left of the computer.
|
||||
- `speaker stop` stops any currently playing audio.
|
@ -0,0 +1,228 @@
|
||||
--[[-
|
||||
Provides utilities for converting between streams of DFPWM audio data and a list of amplitudes.
|
||||
|
||||
DFPWM (Dynamic Filter Pulse Width Modulation) is an audio codec designed by GreaseMonkey. It's a relatively compact
|
||||
format compared to raw PCM data, only using 1 bit per sample, but is simple enough to simple enough to encode and decode
|
||||
in real time.
|
||||
|
||||
Typically DFPWM audio is read from @{fs.BinaryReadHandle|the filesystem} or a @{http.Response|a web request} as a
|
||||
string, and converted a format suitable for @{speaker.playAudio}.
|
||||
|
||||
## Encoding and decoding files
|
||||
This modules exposes two key functions, @{make_decoder} and @{make_encoder}, which construct a new decoder or encoder.
|
||||
The returned encoder/decoder is itself a function, which converts between the two kinds of data.
|
||||
|
||||
These encoders and decoders have lots of hidden state, so you should be careful to use the same encoder or decoder for
|
||||
a specific audio stream. Typically you will want to create a decoder for each stream of audio you read, and an encoder
|
||||
for each one you write.
|
||||
|
||||
## Converting audio to DFPWM
|
||||
DFPWM is not a popular file format and so standard audio processing tools will not have an option to export to it.
|
||||
Instead, you can convert audio files online using [music.madefor.cc] or with the [LionRay Wav Converter][LionRay] Java
|
||||
application.
|
||||
|
||||
[music.madefor.cc]: https://music.madefor.cc/ "DFPWM audio converter for Computronics and CC: Tweaked"
|
||||
[LionRay]: https://github.com/gamax92/LionRay/ "LionRay Wav Converter "
|
||||
|
||||
@see guide!speaker_audio Gives a more general introduction to audio processing and the speaker.
|
||||
@see speaker.playAudio To play the decoded audio data.
|
||||
@usage Reads "data/example.dfpwm" in chunks, decodes them and then doubles the speed of the audio. The resulting audio
|
||||
is then re-encoded and saved to "speedy.dfpwm". This processed audio can then be played with the `speaker` program.
|
||||
|
||||
```lua
|
||||
local dfpwm = require("cc.audio.dfpwm")
|
||||
|
||||
local encoder = dfpwm.make_encoder()
|
||||
local decoder = dfpwm.make_decoder()
|
||||
|
||||
local out = fs.open("speedy.dfpwm", "wb")
|
||||
for input in io.lines("my_audio_track.dfpwm", 16 * 1024 * 2) do
|
||||
local decoded = decoder(input)
|
||||
local output = {}
|
||||
|
||||
-- Read two samples at once and take the average.
|
||||
for i = 1, #decoded, 2 do
|
||||
local value_1, value_2 = decoded[i], decoded[i + 1]
|
||||
output[(i + 1) / 2] = (value_1 + value_2) / 2
|
||||
end
|
||||
|
||||
out.write(encoder(output))
|
||||
|
||||
sleep(0) -- This program takes a while to run, so we need to make sure we yield.
|
||||
end
|
||||
out.close()
|
||||
```
|
||||
]]
|
||||
|
||||
local expect = require "cc.expect".expect
|
||||
|
||||
local char, byte, floor, band, rshift = string.char, string.byte, math.floor, bit32.band, bit32.arshift
|
||||
|
||||
local PREC = 10
|
||||
local PREC_POW = 2 ^ PREC
|
||||
local PREC_POW_HALF = 2 ^ (PREC - 1)
|
||||
local STRENGTH_MIN = 2 ^ (PREC - 8 + 1)
|
||||
|
||||
local function make_predictor()
|
||||
local charge, strength, previous_bit = 0, 0, false
|
||||
|
||||
return function(current_bit)
|
||||
local target = current_bit and 127 or -128
|
||||
|
||||
local next_charge = charge + floor((strength * (target - charge) + PREC_POW_HALF) / PREC_POW)
|
||||
if next_charge == charge and next_charge ~= target then
|
||||
next_charge = next_charge + (current_bit and 1 or -1)
|
||||
end
|
||||
|
||||
local z = current_bit == previous_bit and PREC_POW - 1 or 0
|
||||
local next_strength = strength
|
||||
if next_strength ~= z then next_strength = next_strength + (current_bit == previous_bit and 1 or -1) end
|
||||
if next_strength < STRENGTH_MIN then next_strength = STRENGTH_MIN end
|
||||
|
||||
charge, strength, previous_bit = next_charge, next_strength, current_bit
|
||||
return charge
|
||||
end
|
||||
end
|
||||
|
||||
--[[- Create a new encoder for converting PCM audio data into DFPWM.
|
||||
|
||||
The returned encoder is itself a function. This function accepts a table of amplitude data between -128 and 127 and
|
||||
returns the encoded DFPWM data.
|
||||
|
||||
:::caution Reusing encoders
|
||||
Encoders have lots of internal state which tracks the state of the current stream. If you reuse an encoder for multiple
|
||||
streams, or use different encoders for the same stream, the resulting audio may not sound correct.
|
||||
:::
|
||||
|
||||
@treturn function(pcm: { number... }):string The encoder function
|
||||
@see encode A helper function for encoding an entire file of audio at once.
|
||||
]]
|
||||
local function make_encoder()
|
||||
local predictor = make_predictor()
|
||||
local previous_charge = 0
|
||||
|
||||
return function(input)
|
||||
expect(1, input, "table")
|
||||
|
||||
local output, output_n = {}, 0
|
||||
for i = 1, #input, 8 do
|
||||
local this_byte = 0
|
||||
for j = 0, 7 do
|
||||
local inp_charge = floor(input[i + j] or 0)
|
||||
if inp_charge > 127 or inp_charge < -128 then
|
||||
error(("Amplitude at position %d was %d, but should be between -128 and 127"):format(i + j, inp_charge), 2)
|
||||
end
|
||||
|
||||
local current_bit = inp_charge > previous_charge or (inp_charge == previous_charge and inp_charge == 127)
|
||||
this_byte = floor(this_byte / 2) + (current_bit and 128 or 0)
|
||||
|
||||
previous_charge = predictor(current_bit)
|
||||
end
|
||||
|
||||
output_n = output_n + 1
|
||||
output[output_n] = char(this_byte)
|
||||
end
|
||||
|
||||
return table.concat(output, "", 1, output_n)
|
||||
end
|
||||
end
|
||||
|
||||
--[[- Create a new decoder for converting DFPWM into PCM audio data.
|
||||
|
||||
The returned decoder is itself a function. This function accepts a string and returns a table of amplitudes, each value
|
||||
between -128 and 127.
|
||||
|
||||
:::caution Reusing decoders
|
||||
Decoders have lots of internal state which tracks the state of the current stream. If you reuse an decoder for multiple
|
||||
streams, or use different decoders for the same stream, the resulting audio may not sound correct.
|
||||
:::
|
||||
|
||||
@treturn function(dfpwm: string):{ number... } The encoder function
|
||||
@see decode A helper function for decoding an entire file of audio at once.
|
||||
|
||||
@usage Reads "data/example.dfpwm" in blocks of 16KiB (the speaker can accept a maximum of 128×1024 samples), decodes
|
||||
them and then plays them through the speaker.
|
||||
|
||||
```lua
|
||||
local dfpwm = require "cc.audio.dfpwm"
|
||||
local speaker = peripheral.find("speaker")
|
||||
|
||||
local decoder = dfpwm.make_decoder()
|
||||
for input in io.lines("data/example.dfpwm", 16 * 1024 * 2) do
|
||||
local decoded = decoder(input)
|
||||
while not speaker.playAudio(output) do
|
||||
os.pullEvent("speaker_audio_empty")
|
||||
end
|
||||
end
|
||||
```
|
||||
]]
|
||||
local function make_decoder()
|
||||
local predictor = make_predictor()
|
||||
local low_pass_charge = 0
|
||||
local previous_charge, previous_bit = 0, false
|
||||
|
||||
return function (input, output)
|
||||
expect(1, input, "string")
|
||||
|
||||
local output, output_n = {}, 0
|
||||
for i = 1, #input do
|
||||
local input_byte = byte(input, i)
|
||||
for _ = 1, 8 do
|
||||
local current_bit = band(input_byte, 1) ~= 0
|
||||
local charge = predictor(current_bit)
|
||||
|
||||
local antijerk = charge
|
||||
if current_bit ~= previous_bit then
|
||||
antijerk = floor((charge + previous_charge + 1) / 2)
|
||||
end
|
||||
|
||||
previous_charge, previous_bit = charge, current_bit
|
||||
|
||||
low_pass_charge = low_pass_charge + floor(((antijerk - low_pass_charge) * 140 + 0x80) / 256)
|
||||
|
||||
output_n = output_n + 1
|
||||
output[output_n] = low_pass_charge
|
||||
|
||||
input_byte = rshift(input_byte, 1)
|
||||
end
|
||||
end
|
||||
|
||||
return output
|
||||
end
|
||||
end
|
||||
|
||||
--[[- A convenience function for decoding a complete file of audio at once.
|
||||
|
||||
This should only be used for short files. For larger files, one should read the file in chunks and process it using
|
||||
@{make_decoder}.
|
||||
|
||||
@tparam string input The DFPWM data to convert.
|
||||
@treturn { number... } The produced amplitude data.
|
||||
@see make_decoder
|
||||
]]
|
||||
local function decode(input)
|
||||
expect(1, input, "string")
|
||||
return make_decoder()(input)
|
||||
end
|
||||
|
||||
--[[- A convenience function for encoding a complete file of audio at once.
|
||||
|
||||
This should only be used for complete pieces of audio. If you are writing writing multiple chunks to the same place,
|
||||
you should use an encoder returned by @{make_encoder} instead.
|
||||
|
||||
@tparam { number... } input The table of amplitude data.
|
||||
@treturn string The encoded DFPWM data.
|
||||
@see make_encoder
|
||||
]]
|
||||
local function encode(input)
|
||||
expect(1, input, "table")
|
||||
return make_encoder()(input)
|
||||
end
|
||||
|
||||
return {
|
||||
make_encoder = make_encoder,
|
||||
encode = encode,
|
||||
|
||||
make_decoder = make_decoder,
|
||||
decode = decode,
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
local function get_speakers(name)
|
||||
if name then
|
||||
local speaker = peripheral.wrap(name)
|
||||
if speaker == nil then
|
||||
error(("Speaker %q does not exist"):format(name), 0)
|
||||
return
|
||||
elseif not peripheral.hasType(name, "speaker") then
|
||||
error(("%q is not a speaker"):format(name), 0)
|
||||
end
|
||||
|
||||
return { speaker }
|
||||
else
|
||||
local speakers = { peripheral.find("speaker") }
|
||||
if #speakers == 0 then
|
||||
error("No speakers attached", 0)
|
||||
end
|
||||
return speakers
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local cmd = ...
|
||||
if cmd == "stop" then
|
||||
local _, name = ...
|
||||
for _, speaker in pairs(get_speakers(name)) do speaker.stop() end
|
||||
elseif cmd == "play" then
|
||||
local _, file, name = ...
|
||||
local speaker = get_speakers(name)[1]
|
||||
|
||||
local handle, err
|
||||
if http and file:match("^https?://") then
|
||||
print("Downloading...")
|
||||
handle, err = http.get{ url = file, binary = true }
|
||||
else
|
||||
handle, err = fs.open(file, "rb")
|
||||
end
|
||||
|
||||
if not handle then
|
||||
printError("Could not play audio:")
|
||||
error(err, 0)
|
||||
end
|
||||
|
||||
print("Playing " .. file)
|
||||
|
||||
local decoder = require "cc.audio.dfpwm".make_decoder()
|
||||
while true do
|
||||
local chunk = handle.read(16 * 1024)
|
||||
if not chunk then break end
|
||||
|
||||
local buffer = decoder(chunk)
|
||||
while not speaker.playAudio(buffer) do
|
||||
os.pullEvent("speaker_audio_empty")
|
||||
end
|
||||
end
|
||||
|
||||
handle.close()
|
||||
else
|
||||
local programName = arg[0] or fs.getName(shell.getRunningProgram())
|
||||
print("Usage:")
|
||||
print(programName .. " play <file or url> [speaker]")
|
||||
print(programName .. " stop [speaker]")
|
||||
end
|
@ -14,10 +14,6 @@ else
|
||||
print(#tModems .. " modems found.")
|
||||
end
|
||||
|
||||
local function idAsChannel(id)
|
||||
return (id or os.getComputerID()) % rednet.MAX_ID_CHANNELS
|
||||
end
|
||||
|
||||
local function open(nChannel)
|
||||
for n = 1, #tModems do
|
||||
local sModem = tModems[n]
|
||||
@ -53,11 +49,16 @@ local ok, error = pcall(function()
|
||||
tReceivedMessages[tMessage.nMessageID] = true
|
||||
tReceivedMessageTimeouts[os.startTimer(30)] = tMessage.nMessageID
|
||||
|
||||
local recipient_channel = tMessage.nRecipient
|
||||
if tMessage.nRecipient ~= rednet.CHANNEL_BROADCAST then
|
||||
recipient_channel = recipient_channel % rednet.MAX_ID_CHANNELS
|
||||
end
|
||||
|
||||
-- Send on all other open modems, to the target and to other repeaters
|
||||
for n = 1, #tModems do
|
||||
local sOtherModem = tModems[n]
|
||||
peripheral.call(sOtherModem, "transmit", rednet.CHANNEL_REPEAT, nReplyChannel, tMessage)
|
||||
peripheral.call(sOtherModem, "transmit", idAsChannel(tMessage.nRecipient), nReplyChannel, tMessage)
|
||||
peripheral.call(sOtherModem, "transmit", recipient_channel, nReplyChannel, tMessage)
|
||||
end
|
||||
|
||||
-- Log the event
|
||||
|
@ -115,6 +115,18 @@ shell.setCompletionFunction("rom/programs/fun/dj.lua", completion.build(
|
||||
{ completion.choice, { "play", "play ", "stop " } },
|
||||
completion.peripheral
|
||||
))
|
||||
shell.setCompletionFunction("rom/programs/fun/speaker.lua", completion.build(
|
||||
{ completion.choice, { "play ", "stop " } },
|
||||
function(shell, text, previous)
|
||||
if previous[2] == "play" then return completion.file(shell, text, previous, true)
|
||||
elseif previous[2] == "stop" then return completion.peripheral(shell, text, previous, false)
|
||||
end
|
||||
end,
|
||||
function(shell, text, previous)
|
||||
if previous[2] == "play" then return completion.peripheral(shell, text, previous, false)
|
||||
end
|
||||
end
|
||||
))
|
||||
shell.setCompletionFunction("rom/programs/fun/advanced/paint.lua", completion.build(completion.file))
|
||||
shell.setCompletionFunction("rom/programs/http/pastebin.lua", completion.build(
|
||||
{ completion.choice, { "put ", "get ", "run " } },
|
||||
|
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared.peripheral.speaker;
|
||||
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.ObjectLuaTable;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
|
||||
class DfpwmStateTest
|
||||
{
|
||||
@Test
|
||||
public void testEncoder() throws LuaException
|
||||
{
|
||||
int[] input = new int[] { 4, 4, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -3, -3, -3, -4, -4, -4, -4, -4, -5, -5, -5, -5, -5, -6, -6, -6, -7, -7, -7, -7, -7, -7, -7, -7, -7, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -7, -7, -7, -7, -7, -7, -7, -7, -7, -6, -6, -6, -6, -6, -6, -6, -6, -6, -5, -5, -5, -5, -5, -5, -5, -4, -4, -4, -4, -4, -3, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -2, -2, -2, -2, -3, -3, -3, -3, -3, -3, -3, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -3, -3, -3, -3, -3, -4, -4, -4, -4, -4, -5, -5, -5, -5, -5, -5, -5, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -7, -7, -7, -7, -7, -7, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -4, -4, -4, -4, -4, -4, -4, -4, -4, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -2, -2, -2, -2, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -3, -3, -3, -3, -3, -4, -4, -4, -4, -4, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -5, -5, -5, -5, -5, -5, -5, -4, -4, -4, -4, -4, -4, -4, -3, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3 };
|
||||
Map<Object, Object> inputTbl = new HashMap<>();
|
||||
for( int i = 0; i < input.length; i++ ) inputTbl.put( (double) (i + 1), input[i] );
|
||||
|
||||
DfpwmState state = new DfpwmState();
|
||||
state.pushBuffer( new ObjectLuaTable( inputTbl ), input.length, Optional.empty() );
|
||||
ByteBuffer result = state.pullPending( 0 );
|
||||
byte[] contents = new byte[result.remaining()];
|
||||
result.get( contents );
|
||||
|
||||
assertArrayEquals(
|
||||
new byte[] { 87, 74, 42, -91, -92, -108, 84, -87, -86, 86, -83, 90, -83, -43, 90, -85, -42, 106, -43, -86, 106, -107, 42, -107, 74, -87, 74, -91, 74, -91, -86, -86, 106, 85, 107, -83, 106, -83, -83, 86, -75, -86, 42, 85, -107, 82, 41, -91, 82, 74, 41, -107, -86, -44, -86, 86, -75, 106, -83, -75, -86, -75, 90, -83, -86, -86, -86, 82, -91, 74, -107, -86, 82, -87, 82, 85, 85, 85, -83, 86, -75, -86, -43, 90, -83, 90, 85, 85, -107, 42, -91, 82, -86, 82, 74, 41, 85, -87, -86, -86, 106, -75, 90, -83, 86, -85, 106, -43, 106, 85, 85, 85, 85, -107, 42, 85, -86, 42, -107, -86, -86, -86, -86, 106, -75, -86, 86, -85 },
|
||||
contents
|
||||
);
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -189,13 +189,18 @@ end
|
||||
local expect_mt = {}
|
||||
expect_mt.__index = expect_mt
|
||||
|
||||
function expect_mt:_fail(message)
|
||||
if self._extra then message = self._extra .. "\n" .. message end
|
||||
fail(message)
|
||||
end
|
||||
|
||||
--- Assert that this expectation has the provided value
|
||||
--
|
||||
-- @param value The value to require this expectation to be equal to
|
||||
-- @throws If the values are not equal
|
||||
function expect_mt:equals(value)
|
||||
if value ~= self.value then
|
||||
fail(("Expected %s\n but got %s"):format(format(value), format(self.value)))
|
||||
self:_fail(("Expected %s\n but got %s"):format(format(value), format(self.value)))
|
||||
end
|
||||
|
||||
return self
|
||||
@ -209,7 +214,7 @@ expect_mt.eq = expect_mt.equals
|
||||
-- @throws If the values are equal
|
||||
function expect_mt:not_equals(value)
|
||||
if value == self.value then
|
||||
fail(("Expected any value but %s"):format(format(value)))
|
||||
self:_fail(("Expected any value but %s"):format(format(value)))
|
||||
end
|
||||
|
||||
return self
|
||||
@ -224,7 +229,7 @@ expect_mt.ne = expect_mt.not_equals
|
||||
function expect_mt:type(exp_type)
|
||||
local actual_type = type(self.value)
|
||||
if exp_type ~= actual_type then
|
||||
fail(("Expected value of type %s\nbut got %s"):format(exp_type, actual_type))
|
||||
self:_fail(("Expected value of type %s\nbut got %s"):format(exp_type, actual_type))
|
||||
end
|
||||
|
||||
return self
|
||||
@ -273,7 +278,7 @@ end
|
||||
-- @throws If they are not equivalent
|
||||
function expect_mt:same(value)
|
||||
if not matches({}, true, self.value, value) then
|
||||
fail(("Expected %s\nbut got %s"):format(format(value), format(self.value)))
|
||||
self:_fail(("Expected %s\nbut got %s"):format(format(value), format(self.value)))
|
||||
end
|
||||
|
||||
return self
|
||||
@ -286,7 +291,7 @@ end
|
||||
-- @throws If this does not match the provided value
|
||||
function expect_mt:matches(value)
|
||||
if not matches({}, false, value, self.value) then
|
||||
fail(("Expected %s\nto match %s"):format(format(self.value), format(value)))
|
||||
self:_fail(("Expected %s\nto match %s"):format(format(self.value), format(value)))
|
||||
end
|
||||
|
||||
return self
|
||||
@ -299,19 +304,19 @@ end
|
||||
-- @throws If this function was not called the expected number of times.
|
||||
function expect_mt:called(times)
|
||||
if getmetatable(self.value) ~= stub_mt or self.value.arguments == nil then
|
||||
fail(("Expected stubbed function, got %s"):format(type(self.value)))
|
||||
self:_fail(("Expected stubbed function, got %s"):format(type(self.value)))
|
||||
end
|
||||
|
||||
local called = #self.value.arguments
|
||||
|
||||
if times == nil then
|
||||
if called == 0 then
|
||||
fail("Expected stub to be called\nbut it was not.")
|
||||
self:_fail("Expected stub to be called\nbut it was not.")
|
||||
end
|
||||
else
|
||||
check('stub', 1, 'number', times)
|
||||
if called ~= times then
|
||||
fail(("Expected stub to be called %d times\nbut was called %d times."):format(times, called))
|
||||
self:_fail(("Expected stub to be called %d times\nbut was called %d times."):format(times, called))
|
||||
end
|
||||
end
|
||||
|
||||
@ -320,7 +325,7 @@ end
|
||||
|
||||
local function called_with_check(eq, self, ...)
|
||||
if getmetatable(self.value) ~= stub_mt or self.value.arguments == nil then
|
||||
fail(("Expected stubbed function, got %s"):format(type(self.value)))
|
||||
self:_fail(("Expected stubbed function, got %s"):format(type(self.value)))
|
||||
end
|
||||
|
||||
local exp_args = table.pack(...)
|
||||
@ -331,14 +336,14 @@ local function called_with_check(eq, self, ...)
|
||||
|
||||
local head = ("Expected stub to be called with %s\nbut was"):format(format(exp_args))
|
||||
if #actual_args == 0 then
|
||||
fail(head .. " not called at all")
|
||||
self:_fail(head .. " not called at all")
|
||||
elseif #actual_args == 1 then
|
||||
fail(("%s called with %s."):format(head, format(actual_args[1])))
|
||||
self:_fail(("%s called with %s."):format(head, format(actual_args[1])))
|
||||
else
|
||||
local lines = { head .. " called with:" }
|
||||
for i = 1, #actual_args do lines[i + 1] = " - " .. format(actual_args[i]) end
|
||||
|
||||
fail(table.concat(lines, "\n"))
|
||||
self:_fail(table.concat(lines, "\n"))
|
||||
end
|
||||
end
|
||||
|
||||
@ -363,15 +368,24 @@ end
|
||||
function expect_mt:str_match(pattern)
|
||||
local actual_type = type(self.value)
|
||||
if actual_type ~= "string" then
|
||||
fail(("Expected value of type string\nbut got %s"):format(actual_type))
|
||||
self:_fail(("Expected value of type string\nbut got %s"):format(actual_type))
|
||||
end
|
||||
if not self.value:find(pattern) then
|
||||
fail(("Expected %q\n to match pattern %q"):format(self.value, pattern))
|
||||
self:_fail(("Expected %q\n to match pattern %q"):format(self.value, pattern))
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Add extra information to this error message.
|
||||
--
|
||||
-- @tparam string message Additional message to prepend in the case of failures.
|
||||
-- @return The current
|
||||
function expect_mt:describe(message)
|
||||
self._extra = tostring(message)
|
||||
return self
|
||||
end
|
||||
|
||||
local expect = {}
|
||||
setmetatable(expect, expect)
|
||||
|
||||
|
@ -88,6 +88,7 @@ describe("The rednet library", function()
|
||||
local fake_computer = require "support.fake_computer"
|
||||
local debugx = require "support.debug_ext"
|
||||
|
||||
local function dawdle() while true do coroutine.yield() end end
|
||||
local function computer_with_rednet(id, fn, options)
|
||||
local computer = fake_computer.make_computer(id, function(env)
|
||||
local fns = { env.rednet.run }
|
||||
@ -105,6 +106,10 @@ describe("The rednet library", function()
|
||||
end
|
||||
end
|
||||
|
||||
if options and options.host then
|
||||
env.rednet.host("some_protocol", "host_" .. id)
|
||||
end
|
||||
|
||||
return parallel.waitForAny(table.unpack(fns))
|
||||
end)
|
||||
local modem = fake_computer.add_modem(computer, "back")
|
||||
@ -203,8 +208,8 @@ describe("The rednet library", function()
|
||||
env.sleep(10)
|
||||
|
||||
-- Ensure our pending message store is empty. Bit ugly to prod internals, but there's no other way.
|
||||
expect(debugx.getupvalue(rednet.run, "tReceivedMessages")):same({})
|
||||
expect(debugx.getupvalue(rednet.run, "nClearTimer")):eq(nil)
|
||||
expect(debugx.getupvalue(rednet.run, "received_messages")):same({})
|
||||
expect(debugx.getupvalue(rednet.run, "prune_received_timer")):eq(nil)
|
||||
end, { open = true })
|
||||
|
||||
local computer_3, modem_3 = computer_with_rednet(3, nil, { open = true, rep = true })
|
||||
@ -222,5 +227,22 @@ describe("The rednet library", function()
|
||||
fake_computer.advance_all(computers, 10)
|
||||
fake_computer.run_all(computers, { computer_1, computer_2 })
|
||||
end)
|
||||
|
||||
it("handles lookups between computers with massive IDs", function()
|
||||
local id_1, id_3 = 24283947, 93428798
|
||||
local computer_1, modem_1 = computer_with_rednet(id_1, function(rednet)
|
||||
local ids = { rednet.lookup("some_protocol") }
|
||||
expect(ids):same { id_3 }
|
||||
end, { open = true })
|
||||
local computer_2, modem_2 = computer_with_rednet(2, nil, { open = true, rep = true })
|
||||
local computer_3, modem_3 = computer_with_rednet(id_3, dawdle, { open = true, host = true })
|
||||
fake_computer.add_modem_edge(modem_1, modem_2)
|
||||
fake_computer.add_modem_edge(modem_2, modem_3)
|
||||
|
||||
local computers = { computer_1, computer_2, computer_3 }
|
||||
fake_computer.run_all(computers, false)
|
||||
fake_computer.advance_all(computers, 3)
|
||||
fake_computer.run_all(computers, { computer_1 })
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
@ -0,0 +1,26 @@
|
||||
describe("cc.audio.dfpwm", function()
|
||||
local dfpwm = require "cc.audio.dfpwm"
|
||||
|
||||
describe("decode", function()
|
||||
it("decodes some test data", function()
|
||||
-- Look, I'm not proud of this.
|
||||
local input = "\43\225\33\44\30\240\171\23\253\201\46\186\68\189\74\160\188\16\94\169\251\87\11\240\19\92\85\185\126\5\172\64\17\250\85\245\255\169\244\1\85\200\33\176\82\104\163\17\126\23\91\226\37\224\117\184\198\11\180\19\148\86\191\246\255\188\231\10\210\85\124\202\15\232\43\162\117\63\220\15\250\88\87\230\173\106\41\13\228\143\246\190\119\169\143\68\201\40\149\62\20\72\3\160\114\169\254\39\152\30\20\42\84\24\47\64\43\61\221\95\191\42\61\42\206\4\247\81"
|
||||
local output = { 1, 2, 2, 2, 2, 2, 2, 1, 1, 1, 0, -1, -2, -2, -1, 0, 1, 0, -1, -3, -5, -5, -5, -7, -9, -11, -11, -9, -9, -9, -9, -10, -12, -12, -10, -8, -6, -6, -8, -10, -12, -14, -16, -18, -17, -15, -12, -9, -6, -3, -2, -2, -2, -2, -2, -2, 0, 3, 6, 7, 7, 7, 4, 1, 1, 1, 1, 3, 5, 7, 9, 12, 15, 15, 12, 12, 12, 9, 9, 11, 12, 12, 14, 16, 17, 17, 17, 14, 11, 11, 11, 10, 12, 14, 14, 13, 13, 10, 9, 9, 7, 5, 4, 4, 4, 4, 4, 6, 8, 10, 10, 10, 10, 10, 10, 10, 9, 8, 8, 8, 7, 6, 4, 2, 0, 0, 0, 0, 0, -1, -1, 0, 1, 3, 3, 3, 3, 2, 0, -2, -2, -2, -3, -5, -7, -7, -5, -3, -1, -1, -1, -1, -1, -1, -2, -2, -1, -1, -1, -1, 0, 1, 1, 1, 2, 3, 4, 5, 6, 7, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 9, 8, 7, 6, 4, 2, 0, 0, 2, 4, 6, 8, 10, 10, 8, 7, 7, 5, 3, 1, -1, 0, 2, 4, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 4, 5, 5, 5, 5, 5, 6, 7, 8, 9, 10, 9, 9, 9, 9, 9, 8, 7, 6, 5, 3, 1, 1, 3, 3, 3, 3, 3, 3, 2, 1, 0, -1, -3, -3, -3, -3, -2, -3, -4, -4, -3, -4, -5, -6, -6, -5, -5, -4, -3, -2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 18, 20, 20, 17, 16, 16, 15, 15, 15, 15, 13, 13, 13, 13, 14, 15, 16, 18, 18, 16, 14, 12, 10, 8, 5, 5, 5, 4, 4, 4, 4, 4, 4, 2, 0, -2, -2, -2, -4, -4, -2, 0, 0, -2, -4, -6, -6, -6, -8, -10, -12, -14, -16, -15, -13, -12, -11, -11, -11, -11, -13, -13, -13, -13, -13, -14, -16, -18, -18, -18, -18, -16, -16, -16, -14, -13, -14, -15, -15, -14, -14, -12, -11, -12, -13, -13, -12, -13, -14, -15, -15, -13, -11, -9, -7, -5, -5, -5, -3, -1, -1, -1, -1, -3, -5, -5, -3, -3, -3, -1, -1, -1, -1, -3, -3, -3, -4, -6, -6, -4, -2, 0, 0, 0, 0, -2, -2, -2, -3, -5, -7, -9, -11, -13, -13, -11, -9, -7, -6, -6, -6, -6, -4, -2, -2, -4, -6, -8, -7, -5, -3, -2, -2, -2, -2, 0, 0, -2, -4, -4, -2, 0, 2, 2, 1, 1, -1, -3, -5, -7, -10, -10, -10, -10, -8, -7, -7, -5, -3, -2, -4, -4, -4, -6, -8, -10, -12, -12, -12, -12, -12, -14, -13, -13, -13, -11, -11, -11, -11, -11, -11, -11, -9, -7, -5, -3, -1, -1, -1, -1, -1, 1, 1, 1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 22, 19, 18, 20, 22, 24, 23, 22, 24, 26, 28, 27, 24, 23, 25, 28, 28, 28, 27, 26, 26, 23, 20, 17, 14, 14, 14, 11, 11, 11, 11, 13, 15, 16, 16, 16, 15, 15, 14, 14, 12, 10, 9, 11, 13, 15, 17, 17, 14, 13, 13, 12, 12, 10, 9, 11, 13, 15, 17, 19, 19, 16, 13, 10, 7, 4, 1, 1, 2, 2, 4, 7, 10, 13, 13, 13, 12, 12, 12, 9, 6, 6, 6, 3, 0, 0, 0, 0, 2, 3, 3, 3, 3, 5, 7, 7, 7, 9, 11, 13, 15, 18, 18, 15, 12, 9, 8, 10, 13, 13, 13, 15, 18, 21, 24, 27, 27, 23, 19, 15, 11, 10, 9, 9, 12, 16, 19, 22, 23, 19, 14, 13, 16, 16, 15, 15, 14, 17, 20, 20, 19, 19, 18, 17, 14, 13, 15, 15, 12, 11, 13, 16, 19, 19, 18, 20, 20, 19, 18, 18, 17, 17, 16, 16, 16, 15, 17, 17, 16, 16, 13, 12, 12, 11, 11, 9, 9, 9, 9, 11, 11, 9, 7, 5, 3, 1, 1, 1, -1, -1, 1, 3, 5, 7, 9, 11, 12, 9, 6, 6, 6, 6, 8, 8, 7, 9, 11, 13, 13, 12, 14, 16, 18, 20, 20, 20, 22, 24, 26, 25, 25, 27, 29, 28, 27, 26, 23, 22, 22, 21, 21, 20, 22, 24, 26, 28, 27, 24, 21, 21, 21, 18, 17, 17, 14, 11, 11, 11, 10, 10, 7, 6, 6, 4, 3, 5, 5, 3, 1, 1, 1, 1, 1, -1, -1, -1, -1, -1, -1, 0, -1, -1, 0, 0, 1, 2, 3, 4, 3, 1, -1, -3, -3, -3, -3, -2, -3, -4, -6, -8, -10, -10, -10, -12, -12, -12, -12, -10, -10, -11, -12, -14, -16, -18, -20, -22, -24, -26, -28, -27, -27, -26, -26, -25, -25, -27, -26, -24, -22, -22, -22, -22, -24, -24, -24, -24, -23, -23, -22, -22, -21, -20, -19, -17, -15, -13, -11, -9, -7, -7, -9, -9, -9, -11, -13, -15, -17, -16, -14, -13, -15, -14, -14, -14, -12, -10, -8, -7, -9, -11, -13, -15, -14, -14, -13, -13, -15, -17, -19, -18, -18, -17, -17, -16, -16, -18, -20, -22, -21, -21, -21, -21, -21, -20, -21, -22, -24, -24, -22, -22, -24, -26, -25, -23, -21, -19, -18, -17, -17, -19, -21, -23, -25, -27, -29, -31, -30, -29, -28, -26, -25, -24, -24, -23, -23, -25, -24, -24, -24, -22, -20, -18, -18, -20, -20, -20, -20, -18, -16, -16, -16, -14, -12, -10, -8, -6, -4, -4, -4, -4, -4, -2, 0, 2, 4, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 3, 3, 3, 3, 4, 5, 6, 5, 3, 1, 1, 1, 1, 1, 1, 1, 0, -1, -1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, -1, -2, -3, -4, -4, -2, 0, 0, 0, 1, 3, 5, 7, 7, 5, 3, 3, 3, 3, 3 }
|
||||
|
||||
local decoded = dfpwm.decode(input)
|
||||
expect(#decoded):describe("The lengths match"):eq(#output)
|
||||
for i = 1, #decoded do expect(decoded[i]):describe("Item at #" .. i):eq(output[i]) end
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("encode", function()
|
||||
it("encodes some data", function()
|
||||
local input = { 4, 4, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -3, -3, -3, -4, -4, -4, -4, -4, -5, -5, -5, -5, -5, -6, -6, -6, -7, -7, -7, -7, -7, -7, -7, -7, -7, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -7, -7, -7, -7, -7, -7, -7, -7, -7, -6, -6, -6, -6, -6, -6, -6, -6, -6, -5, -5, -5, -5, -5, -5, -5, -4, -4, -4, -4, -4, -3, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -2, -2, -2, -2, -3, -3, -3, -3, -3, -3, -3, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -3, -3, -3, -3, -3, -4, -4, -4, -4, -4, -5, -5, -5, -5, -5, -5, -5, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -7, -7, -7, -7, -7, -7, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -4, -4, -4, -4, -4, -4, -4, -4, -4, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -2, -2, -2, -2, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -3, -3, -3, -3, -3, -4, -4, -4, -4, -4, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -5, -5, -5, -5, -5, -5, -5, -4, -4, -4, -4, -4, -4, -4, -3, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3 }
|
||||
local output = { 87, 74, 42, 165, 164, 148, 84, 169, 170, 86, 173, 90, 173, 213, 90, 171, 214, 106, 213, 170, 106, 149, 42, 149, 74, 169, 74, 165, 74, 165, 170, 170, 106, 85, 107, 173, 106, 173, 173, 86, 181, 170, 42, 85, 149, 82, 41, 165, 82, 74, 41, 149, 170, 212, 170, 86, 181, 106, 173, 181, 170, 181, 90, 173, 170, 170, 170, 82, 165, 74, 149, 170, 82, 169, 82, 85, 85, 85, 173, 86, 181, 170, 213, 90, 173, 90, 85, 85, 149, 42, 165, 82, 170, 82, 74, 41, 85, 169, 170, 170, 106, 181, 90, 173, 86, 171, 106, 213, 106, 85, 85, 85, 85, 149, 42, 85, 170, 42, 149, 170, 170, 170, 170, 106, 181, 170, 86, 171 }
|
||||
|
||||
local encoded = dfpwm.encode(input)
|
||||
expect(#encoded):describe("The lengths match"):eq(#output)
|
||||
for i = 1, #encoded do expect(encoded:byte(i)):describe("Item at #" .. i):eq(output[i] % 256) end
|
||||
end)
|
||||
end)
|
||||
end)
|
Loading…
Reference in New Issue
Block a user