Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion DMCompiler/DMStandard/Types/Client.dm
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@
return TRUE

proc/SoundQuery()
set opendream_unimplemented = TRUE
proc/MeasureText(text, style, width=0)
set opendream_unimplemented = TRUE

Expand Down
12 changes: 11 additions & 1 deletion OpenDreamClient/Audio/DreamSoundChannel.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
using OpenDreamShared.Network.Messages;
using Robust.Client.Audio;
using Robust.Shared.Audio.Components;

namespace OpenDreamClient.Audio;

public sealed class DreamSoundChannel(AudioSystem audioSystem, (EntityUid Entity, AudioComponent Component) source) {
public sealed class DreamSoundChannel(AudioSystem audioSystem, (EntityUid Entity, AudioComponent Component) source, SoundData soundData) {
public readonly (EntityUid Entity, AudioComponent Component) Source = source;
private SoundData _soundData = soundData;

public SoundData SoundData {
get {
_soundData.Offset = Source.Component.PlaybackPosition;
_soundData.Length = (float)audioSystem.GetAudioLength(Source.Component.FileName).TotalSeconds;

Check warning

Code scanning / InspectCode

Use of obsolete symbol Warning

CS0618: Operator 'implicit Robust.Shared.Audio.ResolvedSoundSpecifier.operator ResolvedSoundSpecifier(string)' is obsolete: 'String literals for sounds are deprecated, use a SoundSpecifier or ResolvedSoundSpecifier as appropriate instead'
Copy link
Collaborator Author

@ike709 ike709 Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Electing to ignore this as converting a bunch of audio code to use sound specifiers is out of scope and GetAudioLength(SoundSpecifier) just calls this GetAudioLength(string) overload currently anyways.

return _soundData;
}
}

public void Stop() {
audioSystem.Stop(Source.Entity, Source.Component);
Expand Down
24 changes: 18 additions & 6 deletions OpenDreamClient/Audio/DreamSoundEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ public void Initialize() {
_netManager.Disconnect += DisconnectedFromServer;
}

public void PlaySound(int channel, MsgSound.FormatType format, ResourceSound sound, float volume, float offset) {
public void PlaySound(SoundData soundData, MsgSound.FormatType format, ResourceSound sound) {
if (_audioSystem == null)
_entitySystemManager.Resolve(ref _audioSystem);

var channel = (int)soundData.Channel;

if (channel == 0) {
//First available channel
for (int i = 0; i < _channels.Length; i++) {
Expand All @@ -58,14 +60,14 @@ public void PlaySound(int channel, MsgSound.FormatType format, ResourceSound sou
return;
}

var db = 20 * MathF.Log10(volume); // convert from DM volume (0-100) to OpenAL volume (db)
var source = _audioSystem.PlayGlobal(stream, null, AudioParams.Default.WithVolume(db).WithPlayOffset(offset)); // TODO: Positional audio.
var db = 20 * MathF.Log10(soundData.Volume / 100.0f); // convert from DM volume (0-100) to OpenAL volume (db)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The / 100.0f has been moved from the only PlaySound() method call to in the method itself. Not sure if that's the right move or not but it simplifies things.

var source = _audioSystem.PlayGlobal(stream, null, AudioParams.Default.WithVolume(db).WithPlayOffset(soundData.Offset).WithLoop(soundData.Repeat != 0)); // TODO: Positional audio.
if (source == null) {
_sawmill.Error($"Failed to play audio ${sound}");
return;
}

_channels[channel - 1] = new DreamSoundChannel(_audioSystem, source.Value);
_channels[channel - 1] = new DreamSoundChannel(_audioSystem, source.Value, soundData);
}

public void StopChannel(int channel) {
Expand All @@ -85,12 +87,22 @@ public void StopAllChannels() {
private void RxSound(MsgSound msg) {
if (msg.ResourceId.HasValue) {
_resourceManager.LoadResourceAsync<ResourceSound>(msg.ResourceId.Value,
sound => PlaySound(msg.Channel, msg.Format!.Value, sound, msg.Volume / 100.0f, msg.Offset));
sound => PlaySound(msg.SoundData, msg.Format!.Value, sound));
} else {
StopChannel(msg.Channel);
StopChannel(msg.SoundData.Channel);
}
}

public List<SoundData> GetSoundQuery() {
List<SoundData> result = new List<SoundData>();
foreach (var channel in _channels) {
if(channel is null) continue;
result.Add(channel.SoundData);
}

return result;
}

private void DisconnectedFromServer(object? sender, NetDisconnectedArgs e) {
StopAllChannels();
}
Expand Down
4 changes: 3 additions & 1 deletion OpenDreamClient/Audio/IDreamSoundEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ namespace OpenDreamClient.Audio;

public interface IDreamSoundEngine {
void Initialize();
void PlaySound(int channel, MsgSound.FormatType format, ResourceSound sound, float volume, float offset);
void PlaySound(SoundData soundData, MsgSound.FormatType format, ResourceSound sound);
void StopChannel(int channel);
void StopAllChannels();

List<SoundData>? GetSoundQuery();
}
13 changes: 13 additions & 0 deletions OpenDreamClient/Interface/DreamInterfaceManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
using Robust.Shared.Utility;
using SixLabors.ImageSharp;
using System.Linq;
using OpenDreamClient.Audio;
using Robust.Shared.Map;

namespace OpenDreamClient.Interface;
Expand All @@ -44,6 +45,7 @@ internal sealed class DreamInterfaceManager : IDreamInterfaceManager {
[Dependency] private readonly ITimerManager _timerManager = default!;
[Dependency] private readonly IUriOpener _uriOpener = default!;
[Dependency] private readonly IGameController _gameController = default!;
[Dependency] private readonly IDreamSoundEngine _dreamSoundEngine = default!;

private readonly ISawmill _sawmill = Logger.GetSawmill("opendream.interface");

Expand Down Expand Up @@ -120,6 +122,8 @@ public void Initialize() {
_netManager.RegisterNetMessage<MsgPrompt>(RxPrompt);
_netManager.RegisterNetMessage<MsgPromptList>(RxPromptList);
_netManager.RegisterNetMessage<MsgPromptResponse>();
_netManager.RegisterNetMessage<MsgSoundQuery>(RxSoundQuery);
_netManager.RegisterNetMessage<MsgSoundQueryResponse>();
_netManager.RegisterNetMessage<MsgBrowse>(RxBrowse);
_netManager.RegisterNetMessage<MsgTopic>();
_netManager.RegisterNetMessage<MsgWinSet>(RxWinSet);
Expand Down Expand Up @@ -186,6 +190,15 @@ private void RxPromptList(MsgPromptList pPromptList) {
ShowPrompt(prompt);
}

private void RxSoundQuery(MsgSoundQuery soundQuery) {
var allSounds = _dreamSoundEngine.GetSoundQuery();
var response = new MsgSoundQueryResponse {
PromptId = soundQuery.PromptId,
Sounds = allSounds,
};
_netManager.ClientSendMessage(response);
}

private void RxBrowse(MsgBrowse pBrowse) {
var referencedElement = (pBrowse.Window != null) ? FindElementWithId(pBrowse.Window) : DefaultWindow;

Expand Down
55 changes: 50 additions & 5 deletions OpenDreamRuntime/DreamConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,34 @@ public void HandleMsgPromptResponse(MsgPromptResponse message) {
_promptEvents.Remove(message.PromptId);
}

public void HandleMsgSoundQueryResponse(MsgSoundQueryResponse message) {
// PARITY NOTE: BYOND excludes certain sound datum vars like "volume" for no better reason than "it isn't tracked"
// Well we track it so we send those vars too under the assumption that more info won't break anything

if (!_promptEvents.TryGetValue(message.PromptId, out var promptEvent)) {
_sawmill.Warning($"{message.MsgChannel}: Received MsgSoundQueryResponse for prompt {message.PromptId} which does not exist.");
return;
}

DreamList allSounds = new DreamList(_objectTree.List.ObjectDefinition, message.Sounds?.Count ?? 0);
if (message.Sounds is not null) {
foreach (var soundData in message.Sounds) {
var sound = new DreamObjectSound(_objectTree.GetObjectDefinition(_objectTree.Sound.Id));
sound.SetVariableValue("channel", new DreamValue(soundData.Channel));
sound.SetVariableValue("offset", new DreamValue(soundData.Offset));
sound.SetVariableValue("volume", new DreamValue(soundData.Volume));
sound.SetVariableValue("len", new DreamValue(soundData.Length));
sound.SetVariableValue("repeat", new DreamValue(soundData.Repeat));
sound.SetVariableValue("file", string.IsNullOrEmpty(soundData.File) ? DreamValue.Null : new DreamValue(soundData.File));

allSounds.AddValue(new DreamValue(sound));
}
}

promptEvent.Invoke(new DreamValue(allSounds));
_promptEvents.Remove(message.PromptId);
}

public void HandleMsgTopic(MsgTopic pTopic) {
DreamList hrefList = DreamProcNativeRoot.Params2List(_objectTree, HttpUtility.UrlDecode(pTopic.Query));
DreamValue srcRefValue = hrefList.GetValue(new DreamValue("src"));
Expand All @@ -219,12 +247,16 @@ public void OutputDreamValue(DreamValue value) {
ushort channel = (ushort)outputObject.GetVariable("channel").GetValueAsInteger();
ushort volume = (ushort)outputObject.GetVariable("volume").GetValueAsInteger();
float offset = outputObject.GetVariable("offset").UnsafeGetValueAsFloat();
byte repeat = (byte)Math.Clamp(outputObject.GetVariable("repeat").UnsafeGetValueAsFloat(), 0, 2);
DreamValue file = outputObject.GetVariable("file");

var msg = new MsgSound() {
Channel = channel,
Volume = volume,
Offset = offset
var msg = new MsgSound {
SoundData = new SoundData {
Channel = channel,
Volume = volume,
Offset = offset,
Repeat = repeat
}
};

if (!file.TryGetValueAsDreamResource(out var soundResource)) {
Expand All @@ -236,7 +268,8 @@ public void OutputDreamValue(DreamValue value) {
}

msg.ResourceId = soundResource?.Id;
if (soundResource?.ResourcePath is { } resourcePath) {
var resourcePath = soundResource?.ResourcePath;
if (resourcePath != null) {
if (resourcePath.EndsWith(".ogg"))
msg.Format = MsgSound.FormatType.Ogg;
else if (resourcePath.EndsWith(".wav"))
Expand All @@ -245,6 +278,8 @@ public void OutputDreamValue(DreamValue value) {
throw new Exception($"Sound {resourcePath} is not a supported file type");
}

msg.SoundData.File = resourcePath ?? string.Empty;

Session?.Channel.SendMessage(msg);
return;
}
Expand Down Expand Up @@ -275,6 +310,16 @@ public Task<DreamValue> Prompt(DreamValueType types, string title, string messag
return task;
}

public Task<DreamValue> SoundQuery() {
var task = MakePromptTask(out var promptId);
var msg = new MsgSoundQuery {
PromptId = promptId,
};

Session?.Channel.SendMessage(msg);
return task;
}

public async Task<DreamValue> PromptList(DreamValueType types, IDreamList list, string title, string message, DreamValue defaultValue) {
DreamValue[] listValues = list.CopyToArray();

Expand Down
7 changes: 7 additions & 0 deletions OpenDreamRuntime/DreamManager.Connections.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ private void InitializeConnectionManager() {
_netManager.RegisterNetMessage<MsgPrompt>();
_netManager.RegisterNetMessage<MsgPromptList>();
_netManager.RegisterNetMessage<MsgPromptResponse>(RxPromptResponse);
_netManager.RegisterNetMessage<MsgSoundQuery>();
_netManager.RegisterNetMessage<MsgSoundQueryResponse>(RxSoundQueryResponse);
_netManager.RegisterNetMessage<MsgBrowseResource>();
_netManager.RegisterNetMessage<MsgBrowseResourceRequest>(RxBrowseResourceRequest);
_netManager.RegisterNetMessage<MsgBrowseResourceResponse>();
Expand Down Expand Up @@ -222,6 +224,11 @@ private void RxPromptResponse(MsgPromptResponse message) {
connection.HandleMsgPromptResponse(message);
}

private void RxSoundQueryResponse(MsgSoundQueryResponse message) {
var connection = ConnectionForChannel(message.MsgChannel);
connection.HandleMsgSoundQueryResponse(message);
}

private void RxTopic(MsgTopic message) {
var connection = ConnectionForChannel(message.MsgChannel);
connection.HandleMsgTopic(message);
Expand Down
2 changes: 2 additions & 0 deletions OpenDreamRuntime/Procs/Native/DreamProcNative.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ public static void SetupNativeProcs(DreamObjectTree objectTree) {
objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_winget);
objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_winset);

objectTree.SetNativeProc(objectTree.Client, DreamProcNativeClient.NativeProc_SoundQuery);

objectTree.SetNativeProc(objectTree.List, DreamProcNativeList.NativeProc_Add);
objectTree.SetNativeProc(objectTree.List, DreamProcNativeList.NativeProc_Copy);
objectTree.SetNativeProc(objectTree.List, DreamProcNativeList.NativeProc_Cut);
Expand Down
12 changes: 12 additions & 0 deletions OpenDreamRuntime/Procs/Native/DreamProcNativeClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Threading.Tasks;
using OpenDreamRuntime.Objects.Types;

namespace OpenDreamRuntime.Procs.Native;

internal static class DreamProcNativeClient {
[DreamProc("SoundQuery")]
public static async Task<DreamValue> NativeProc_SoundQuery(AsyncNativeProc.State state) {
var client = (DreamObjectClient)state.Src!;
return await client.Connection.SoundQuery();
}
}
68 changes: 59 additions & 9 deletions OpenDreamShared/Network/Messages/MsgSound.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,13 @@ public enum FormatType : byte {

public override MsgGroups MsgGroup => MsgGroups.EntityEvent;

public ushort Channel;
public ushort Volume;
public float Offset;
public SoundData SoundData;
public int? ResourceId;
public FormatType? Format; // TODO: This should probably be sent along with the sound resource instead somehow
//TODO: Frequency and friends

public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) {
Channel = buffer.ReadUInt16();
Volume = buffer.ReadUInt16();
Offset = buffer.ReadFloat();
SoundData = new SoundData(buffer);

if (buffer.ReadBoolean()) {
ResourceId = buffer.ReadInt32();
Expand All @@ -31,9 +27,7 @@ public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer
}

public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) {
buffer.Write(Channel);
buffer.Write(Volume);
buffer.Write(Offset);
SoundData.WriteToBuffer(buffer);

buffer.Write(ResourceId != null);
if (ResourceId != null) {
Expand All @@ -45,4 +39,60 @@ public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer
}
}
}

public struct SoundData {
/// <summary>
/// The DreamSoundChannel channel (out of 1024) that the sound is set to play on
/// </summary>
public ushort Channel;

/// <summary>
/// Volume as a percentage
/// </summary>
public ushort Volume;

/// <summary>
/// Current playback position in seconds
/// </summary>
public float Offset;

/// <summary>
/// Total playtime of the song in seconds, adjusted for frequency
/// TODO: adjust for freq
/// </summary>
public float Length;

/// <summary>
/// Set to 0 to not repeat, 1 to repeat indefinitely, or 2 to repeat forwards and backwards
/// TODO: Implement repeat=2
/// </summary>
public byte Repeat;

/// <summary>
/// Filepath to the resource, if present
/// </summary>
public string File = string.Empty;

public SoundData(NetIncomingMessage buffer) {
ReadFromBuffer(buffer);
}

private void ReadFromBuffer(NetIncomingMessage buffer) {
Channel = buffer.ReadUInt16();
Volume = buffer.ReadUInt16();
Offset = buffer.ReadFloat();
Length = buffer.ReadFloat();
Repeat = buffer.ReadByte();
File = buffer.ReadString();
}

public void WriteToBuffer(NetOutgoingMessage buffer) {
buffer.Write(Channel);
buffer.Write(Volume);
buffer.Write(Offset);
buffer.Write(Length);
buffer.Write(Repeat);
buffer.Write(File);
}
}
}
19 changes: 19 additions & 0 deletions OpenDreamShared/Network/Messages/MsgSoundQuery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Lidgren.Network;
using Robust.Shared.Network;
using Robust.Shared.Serialization;

namespace OpenDreamShared.Network.Messages;

public sealed class MsgSoundQuery : NetMessage {
public override MsgGroups MsgGroup => MsgGroups.EntityEvent;

public int PromptId;

public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) {
PromptId = buffer.ReadVariableInt32();
}

public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) {
buffer.WriteVariableInt32(PromptId);
}
}
Loading
Loading