mirror of
				https://github.com/SquidDev-CC/CC-Tweaked
				synced 2025-10-30 21:23:00 +00:00 
			
		
		
		
	Merge branch 'mc-1.16.x' into mc-1.17.x
This commit is contained in:
		
							
								
								
									
										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; | ||||
|             } | ||||
|         shouldStop = true; | ||||
|     } | ||||
| 
 | ||||
|         // 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; | ||||
|     private void syncedPosition( Vec3 position ) | ||||
|     { | ||||
|         lastPosition = position; | ||||
|         lastPositionTime = clock; | ||||
|     } | ||||
| 
 | ||||
|     private synchronized boolean playSound( ILuaContext context, ResourceLocation name, float volume, float pitch, boolean isNote ) throws LuaException | ||||
|     @Override | ||||
|     public void attach( @Nonnull IComputerAccess computer ) | ||||
|     { | ||||
|         if( clock - lastPlayTime < MIN_TICKS_BETWEEN_SOUNDS ) | ||||
|         { | ||||
|             // 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; | ||||
|         computers.add( computer ); | ||||
|     } | ||||
| 
 | ||||
|         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 ) | ||||
|     @Override | ||||
|     public void detach( @Nonnull IComputerAccess computer ) | ||||
|     { | ||||
|                 server.getPlayerList().broadcast( | ||||
|                     null, pos.x, pos.y, pos.z, range, world.dimension(), | ||||
|                     new ClientboundCustomSoundPacket( name, SoundSource.RECORDS, pos, actualVolume, pitch ) | ||||
|                 ); | ||||
|         computers.remove( computer ); | ||||
|     } | ||||
|             else | ||||
|             { | ||||
|                 NetworkHandler.sendToAllAround( | ||||
|                     new SpeakerPlayClientMessage( getSource(), pos, name, actualVolume, pitch ), | ||||
|                     world, pos, range | ||||
|                 ); | ||||
|             } | ||||
|             return null; | ||||
|         } ); | ||||
| 
 | ||||
|         lastPlayTime = clock; | ||||
|         return true; | ||||
|     private static final class PendingSound | ||||
|     { | ||||
|         final ResourceLocation location; | ||||
|         final float volume; | ||||
|         final float pitch; | ||||
| 
 | ||||
|         private PendingSound( ResourceLocation location, float volume, float pitch ) | ||||
|         { | ||||
|             this.location = location; | ||||
|             this.volume = volume; | ||||
|             this.pitch = pitch; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|     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) | ||||
		Reference in New Issue
	
	Block a user
	 Jonathan Coates
					Jonathan Coates